前言

最近几年安卓的题也开始火了,不过有点杂,所以写篇文章总结下。注:以安卓4.4为准。

四大组件

app比较关键的就这四个组件:活动(Activity)、服务(Service)、广播接收器(Broadcast Receiver)、内容提供器(Content Provider)。下面一一详解。

活动

简介

活动是一种可以包含用户界面的控件,主要用于和用户进行交互。一个应用程序中可以包含零个或多个活动。

活动需要在在AndroidManifest.xml文件中注册,另外如果在其activity标签内部加入标签,并且加入和这两句声明这会把该活动当做主活动,从而在程序启动时即开始该活动。另外,如果应用程序中没有声明任何一个活动作为主活动,这个程序仍然是可以正常安装的,只是无法在启动器中看到或者打开这个程序。

在活动中切换可以使用intent,包括显示intent和隐式intent。

活动的生命周期

Andorid是使用任务(Task)来管理活动的,一个任务就是一组存放在栈里的活动的集合,这个栈也被称作返回栈。栈是一种后进先出的数据结构,在默认情况下,每当我们启动了一个新的活动,它会在返回栈中入栈,并处于栈顶位置。而每当我们按下Back键或调用finish()方法去销毁一个活动时,处于栈顶的活动就会出栈,这时前一个栈的活动就会重新处于栈顶的位置。系统总是会显示处于栈顶的活动给数据。

活动状态

每个活动在其生命周期中最多可能会有4中状态:运行状态、暂停状态、停止状态、销毁状态。

Activity类中的7个回调方法

Activity类中定义了7个回调方法,覆盖了生命活动周期的每一个环节:

  • onCreate():这个方法会在活动第一次被创建的时候调用。在这个活动中应完成初始化操作,比如加载布局、绑定事件等。
  • onStart():这个方法在活动由不可见变为可见的时候调用。
  • onResume():这个方法在活动准备好和用户进行交互的时候调用,此时的活动一定位于返回栈的栈顶,并且处于运行状态。
  • onPause():这个方法在系统准备去启动或者恢复另一个活动的时候调用,我们通常会在这个方法中将一些消耗CPU资源释放掉,以及保存一些关键数据,但这个方法的执行速度一定要快,不然会影响到新的栈顶活动的使用。
  • onStop():这个方法在活动完全不可见的时候调用。它和onPause()区别在于如果启动的新活动是一个对话框式的活动,那么onPause()方法会得到执行,而onStop()方法并不会执行。
  • onDestory():这个方法在活动被销毁之前调用,之后活动的状态将变为销毁状态。
  • onRestart():这个方法在活动由停止状态变为运行状态之前调用,也就是活动被重新启动了。
活动被回收后

当A去启动B 后,A被回收了,而B按下back键返回A时,还是会正常显示A,但是这时并不会执行onRestrat(),而是执行onCreate(),因为活动A在这种情况下会被重新创建一次。

活动的启动模式

启动模式一共有四种,分别是standard、singleTop、singleTask和singleInstance,可以在标签指定android:launchMode属性来选择启动模式。

广播

简介

Android提供了一套完整的API,允许应用程序自由地发送和接收广播。发送广播的方法既即Intent,而接收广播的方法则是用广播接收器。

Android中的广播主要可以分为两种类型:标准广播和有序广播(有序广播中低优先级的接收器可以被高优先级的接收器截断广播)。另外还有仅限当前程序可接收的本地广播。

广播接收器

广播接收器可以通过动态注册或者静态注册,动态注册的广播必须要在程序启动后才能开始接收广播。

内容提供器

简介

内容提供器主要用于在不同的应用程序之间实现数据共享的功能,并且保证被访问数据的安全性。

访问其他程序中的数据

内容提供器的用法一般有两种,一种是使用现有的内容提供器来读取和操作相应程序中的数据;另一种是创建自己的内容提供器给我们程序的数据提供外部访问接口。

ContentResolver提供了一系列方法用于对数据进行CRUD操作,可以用Context中的getContentResolver()方法获取该类的实例

ContentResolver中的增删改查方法都是不接收表名参数的,而是用一个Uri参数代替,这个参数被称为内容URI。

内容URI给内容提供器中的数据建立了唯一标识符,它主要由两部分组成:authority和path。authority用于对不同的应用程序做区分,path则是用于对同一应用程序中不同的表做区分,通常会添加到authority后面。内容URI最标准的格式写法如下:

1content://com.example.app.provide/table1

服务

简介

服务是Android中实现后台运行的解决方案。

服务并不是运行在一个独立的进程中,而是依赖于创建服务时所在的应用程序进程。当某个应用程序进程被杀掉时,所有依赖于该进程的服务也会停止运行。

另外,服务并不会自动开启线程,所有的代码都是默认运行在主线程当中的。

服务的生命周期

一个服务的生命周期从onCreate()到onStartCommand(),最终再到onDetory()。另外也可以通过bind()绑定一个服务,并进行通信,但需要相应的unbind()解绑后服务才会被销毁,并且也不会经过onStartCommand()过程。

逆向过程中adb常用命令

adb是android调试时常用的工具之一,了解一些常用命令是非常必要的。

非shell命令

需要提前用adb shell命令运行的命令叫做shell命令,直接用adb shell运行的命令叫做非shell命令。

  • adb shell dumpsys activity top 可以查看当前应用的activity信息。
  • adb shell dumpsys 同上,但会打印四大组件的信息。
  • adb shell dumpsys package [pkgname] 可以查询指定包名应用的详细信息(相当于应用的AndroidManifest.xml中的内容)。
    adb shell dumpsys meminfo [pname/pid] 可以查看指定进程名或进程id的内存信息。
  • adb shell dumpsys dbinfo [packagename] 可以查看指定包名应用的数据库存储信息(包括存储的SQL语句)。
  • adb install [apk] 安装指定应用宝apk文件,-r参数相当于升级。
  • adb uninstall [packagename] 卸载应用。
  • adb pull [设备目录文件] [本地目录] 下载设备上的文件到本地。
  • adb pull [本地目录] [设备目录文件] 上传本地文件到目录。
  • adb shell screencap -p [路径] 截屏。
  • adb shell screenrecord [路径] 录屏。
  • adb shell input text [content] 输入文本内容。
  • adb forward [远程端协议:端口号] [设备端协议:端口号] 设备的端口转发。
  • adb jdwp 查看设备中可以被调试的应用的进程号。
  • adb logcat 查看当前日志信息。

shell命令

  • run-as [packagename] 可以在非root设备中查看指定debug模式的包名应用沙盒数据。
  • ps 查看进程信息。
  • pm clear [packagename] 清空指定包名应用的数据
  • pm install [apk] 安装apk文件。
  • pm uninstall [packagename] 卸载应用。
  • am start -n [packagename]/[packagename].[Activity] 启动一个指定活动。debug方式需加-D参数。
  • am startservice -n [packagename]/[packagename].[service] 启动一个服务。
  • am broadcast -a [广播动作] 发送一个广播。
  • netcfg 查看设备的ip地址。
  • netstat 查看设备的端口号信息。
  • app_process [运行代码目录] [运行主类] 运行java代码。
  • dalvikvm -cp [dex文件] [运行主类] 运行一个dex文件。
  • top 查看当前应用的CPU消耗信息。
  • getprop [属性值名称] 查看系统属性值

操作apk命令

  • aapt dump xmltree [apk包名] [需要查看的资源文件xml] 查看apk中的信息以及编辑apk程序包。
  • dexdump [dex文件路径] 查看dex文件的详细信息。

xml、dex、resource.arsc、so文件格式分析

这部分内容比较琐碎,建议可自行搜索,大体与elf差不多。

逆向调试

反编译

一般常用jeb、jadx来分析apk源码。so文件则先提取出来再用ida进行逆向即可。

修改

修改方面,so文件可以直接用ida的patch,java层方面则可以通过hook技术,或者用apktool反编译—>修改—>编译—>签名,具体过程后面会有。

动态调试

主要用到两种工具:jeb和ida。jeb主要用于调试java层,而ida用于调试so文件。

jeb
  1. 在jeb中载入app,
  2. 运行app,
  3. 点击jeb菜单栏中的调试器中的开始按钮,attach上去即可

(注:需开启adb)

ida

ida的用法与在linux下的调试类似。

  1. 在ida目录下的dbgsrv文件夹中找到对应的elf文件,如32位x86的即android_x86_server
  2. 将android_x86_server用adb的push指令移动到android上
  3. 赋予android_x86_server运行权限并运行,可用-p参数修改端口
  4. 运行adb forward tcp:远程设备端口 tcp:本地端口命令,以映射端口
  5. 在ida中打开要调试的so文件,并attach上去

hook技术

Xposed

Xposed框架的原理

它部署在ROOT后的安卓手机上,通过替换/system/bin/app_process程序控制zygote进程,使得app_process在启动过程中会加载XposedBridge.jar这个jar包,从而完成对Zygote进程及其创建Dalvik虚拟机的劫持。

编写步骤
新建项目(xposedtry1)并编辑AndroidManifest.xml

在标签内添加:

1<meta-data2    android:name="xposedmodule"3    android:value="true" />
4<meta-data5    android:name="xposeddescription"6    android:value="My First Xpoesd" />
7<meta-data8    android:name="xposedminversion"9    android:value="53" />
修改app下的build.gradle
1...
2repositories{
3    jcenter()
4}
5dependencies {
6    compileOnly 'de.robv.android.xposed:api:82'
7    compileOnly 'de.robv.android.xposed:api:82:sources'
8    ...
新建一项目(xposedtest)作为靶场

添加按钮,并且修改MainActivity代码:

1public class MainActivity extends AppCompatActivity {
 2
 3    @Override
 4    protected void onCreate(Bundle savedInstanceState) {
 5        super.onCreate(savedInstanceState);
 6        setContentView(R.layout.activity_main);
 7        Button check = (Button) findViewById(R.id.check);
 8        check.setOnClickListener(new View.OnClickListener(){
 9            @Override
10            public void onClick(View v){
11                Toast.makeText(MainActivity.this, toastmsg(), Toast.LENGTH_LONG).show();
12            }
13        });
14    }
15    private String toastmsg(){
16        return "我未被劫持";
17    }
18}
回到xposedtry1项目中新建类HOOK_function
1package com.example.xposedtry1;
 2
 3import de.robv.android.xposed.IXposedHookLoadPackage;
 4import de.robv.android.xposed.XC_MethodHook;
 5import de.robv.android.xposed.XposedBridge;
 6import de.robv.android.xposed.XposedHelpers;
 7import de.robv.android.xposed.callbacks.XC_LoadPackage;
 8
 9public class HOOK_function implements IXposedHookLoadPackage {
10    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable{
11        if(loadPackageParam.packageName.equals("com.example.xposedtest")){
12            XposedBridge.log("has Hooked!");
13            Class clazz = loadPackageParam.classLoader.loadClass("com.example.xposedtest.MainActivity");
14            XposedHelpers.findAndHookMethod(clazz, "toastmsg", new XC_MethodHook() {
15                protected void beforeHookedMethod(MethodHookParam param) throws Throwable{
16                    super.beforeHookedMethod(param);
17                }
18                protected void afterHookedMethod(MethodHookParam param) throws Throwable{
19                    param.setResult("你被劫持了");
20                }
21            });
22        }
23    }
24}
添加入口点

new->Folder->Assets Folder新建assets文件夹,新建xposed_init文件(text类型),写入:

1com.example.xposedtry1.HOOK_function
安装

直接在编译器里将app安装上去,然后xposed的model界面内勾上该模块,重启即可看到效果。

frida

简介

Frida是个轻量级so级别的hook框架,它可以帮助逆向人员对指定的进程的so模块进行分析。它主要提供了功能简单的python接口和功能丰富的js接口,使得hook函数和修改so编程化,值得一提的是接口中包含了主控端与目标进程的交互接口,由此我们可以即时获取信息并随时进行修改。使用frida可以获取进程的信息(模块列表,线程列表,库导出函数),可以拦截指定函数和调用指定函数,可以注入代码,总而言之,使用frida我们可以对进程模块进行手术刀式剖析。

它主要的工作方式是将脚本库注入到目标进程,在目标进程执行脚本。这里需要注意的是,它是将脚本库注入到已经启动的进程,但并不是说,对于进程初始化所做的动作,frida无能为力,frida提供了一个接口spawn,可以启动并暂时挂起进程,然后待我们布置好hook代码后再恢复进程运行。

除了用于脚本编程的接口外,frida还提供了一些简单的工具,比如查看进程列表,追踪某个库函数等。分别是:frida-trace,frida-ps,frida,frida-discover。(相应源码地址:https://github.com/frida/frida-python)

环境
win10
python 3.8
pip version 19.2.3


Frida源码:https://github.com/frida

Frida下载:https://github.com/frida/frida/releases

安装

首先安装python和pip,并且安装:

1pip install frida-tools

下载frida,这里因为是用x86的32位模拟器,所以下载frida-server-12.9.7-android-x86.xz。然后将其用adb push进手机,赋予运行权限并运行。

测试:运行后在windows下的dos界面输入frida-ps -U显示当前进程则完成安装。

例子

出处

一个简单地石头剪刀布app,需要赢1000次才能获得flag。jeb中代码如下:

1public class MainActivity extends Activity implements View$OnClickListener {
 2    class com.example.seccon2015.rock_paper_scissors.MainActivity$1 implements Runnable {
 3        com.example.seccon2015.rock_paper_scissors.MainActivity$1(MainActivity arg1) {
 4            MainActivity.this = arg1;
 5            super();
 6        }
 7        public void run() {
 8            View v0 = MainActivity.this.findViewById(0x7F0C0052);
 9            if(MainActivity.this.n - MainActivity.this.m == 1) {
10                ++MainActivity.this.cnt;
11                ((TextView)v0).setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
12            }
13            else if(MainActivity.this.m - MainActivity.this.n == 1) {
14                MainActivity.this.cnt = 0;
15                ((TextView)v0).setText("LOSE +0");
16            }
17            else if(MainActivity.this.m == MainActivity.this.n) {
18                ((TextView)v0).setText("DRAW +" + String.valueOf(MainActivity.this.cnt));
19            }
20            else if(MainActivity.this.m this.n) {
21                MainActivity.this.cnt = 0;
22                ((TextView)v0).setText("LOSE +0");
23            }
24            else {
25                ++MainActivity.this.cnt;
26                ((TextView)v0).setText("WIN! +" + String.valueOf(MainActivity.this.cnt));
27            }
28            if(1000 == MainActivity.this.cnt) {
29                ((TextView)v0).setText("SECCON{" + String.valueOf((MainActivity.this.cnt + MainActivity.this.calc()) * 107) + "}");
30            }
31            MainActivity.this.flag = 0;
32        }
33    }
34    Button P;
35    Button S;
36    int cnt;
37    int flag;
38    private final Handler handler;
39    int m;
40    int n;
41    Button r;
42    private final Runnable showMessageTask;
43    static {
44        System.loadLibrary("calc");
45    }
46    public MainActivity() {
47        super();
48        this.cnt = 0;
49        this.handler = new Handler();
50        this.showMessageTask = new com.example.seccon2015.rock_paper_scissors.MainActivity$1(this);
51    }
52    public native int calc() {
53    }
54    public void onClick(View arg11) {
55        int v9 = 3;
56        int v8 = 2;
57        if(this.flag != 1) {
58            this.flag = 1;
59            this.findViewById(0x7F0C0052).setText("");
60            View v2 = this.findViewById(0x7F0C0050);
61            View v3 = this.findViewById(0x7F0C0051);
62            this.m = 0;
63            this.n = new Random().nextInt(v9);
64            String[] v1 = new String[v9];
65            v1[0] = "CPU: Paper";
66            v1[1] = "CPU: Rock";
67            v1[v8] = "CPU: Scissors";
68            ((TextView)v3).setText(v1[this.n]);
69            if(arg11 == this.P) {
70                ((TextView)v2).setText("YOU: Paper");
71                this.m = 0;
72            }
73            if(arg11 == this.r) {
74                ((TextView)v2).setText("YOU: Rock");
75                this.m = 1;
76            }
77            if(arg11 == this.S) {
78                ((TextView)v2).setText("YOU: Scissors");
79                this.m = v8;
80            }
81            this.handler.postDelayed(this.showMessageTask, 1000);
82        }
83    }
84    protected void onCreate(Bundle arg2) {
85        super.onCreate(arg2);
86        this.setContentView(0x7F040018);
87        this.P = this.findViewById(0x7F0C004D);
88        this.S = this.findViewById(0x7F0C004F);
89        this.r = this.findViewById(0x7F0C004E);
90        this.P.setOnClickListener(((View$OnClickListener)this));
91        this.r.setOnClickListener(((View$OnClickListener)this));
92        this.S.setOnClickListener(((View$OnClickListener)this));
93        this.flag = 0;
94    }
95}

不难得出cnt是赢的次数,n-m==1即算我们赢。

这样只需要修改cnt为999,并且让n-m==1成立,即可打印出flag。

hook代码如下:

1import frida, sys
 2def on_message(message, data):
 3    if message['type'] == 'send':
 4        print("[*] {0}".format(message['payload']))
 5    else:
 6        print(message)
 7
 8jscode = """ 9Java.perform(function () {10  // 获取想hook的活动11  var MainActivity = Java.use('com.example.seccon2015.rock_paper_scissors.MainActivity');1213  // 获取onClick方法14  var onClick = MainActivity.onClick;15  onClick.implementation = function (v) {16    // 显示正在hook onClick方法17    send('onClick');18    //调用原本的onClick函数19    onClick.call(this, v);20    //修改次数,由于是多线程,所以在原本调用onClick中与flag输出相关的函数其参数已被修改,从而输出flag。21    this.m.value = 0;22    this.n.value = 1;23    this.cnt.value = 999;24    console.log('Done:' + JSON.stringify(this.cnt));25  };26});27"""
28
29process = frida.get_usb_device().attach('com.example.seccon2015.rock_paper_scissors')
30script = process.create_script(jscode)
31script.on('message', on_message)
32print('[*] Running CTF')
33script.load()
34sys.stdin.read()

直接python运行该脚本即可。

关于Frida的api介绍可翻阅官方文档。

两道例题

前面干讲了一大堆理论,下面就以攻防世界中的两道题为例进行下安卓的逆向。

ill-intentions

用jeb分析,发现3个主要活动,都是通过广播发送消息(以ThisIsTheRealOne为例):

1Intent v3 = new Intent();
2v3.setAction("com.ctf.OUTGOING_INTENT");
3v3.putExtra("msg", ThisIsTheRealOne.this.orThat(ThisIsTheRealOne.this.getResources().getString(0x7F030006) + "YSmks", Utilities.doBoth(ThisIsTheRealOne.this.getResources().getString(0x7F030002)), Utilities.doBoth(this.getClass().getName())));
4ThisIsTheRealOne.this.sendBroadcast(v3, "ctf.permission._MSG");

直接考虑写个程序拦截广播:

1public class MainActivity extends AppCompatActivity {
 2    private IntentFilter intentFilter;
 3    private Receiver receiver;
 4    @Override
 5    protected void onCreate(Bundle savedInstanceState) {
 6        super.onCreate(savedInstanceState);
 7        setContentView(R.layout.activity_main);
 8        intentFilter = new IntentFilter();
 9        intentFilter.addAction("com.ctf.OUTGOING_INTENT");
10        receiver = new Receiver();
11        registerReceiver(receiver, intentFilter);
12    }
13
14    @Override
15    protected void onDestroy() {
16        super.onDestroy();
17        unregisterReceiver(receiver);
18    }
19
20    class Receiver extends BroadcastReceiver {
21        @Override
22        public void onReceive(Context context, Intent intent){
23            String a = intent.getStringExtra("msg");
24            Log.d("ctf", a);
25        }
26    }
27}

这样就能直接拦截到发送到广播。

另外因为广播设置了权限,这里不太清楚怎么赋予权限,就直接改代码去掉权限限制:

首先反编译出源码:

1apktool d ill-intentions.apk

修改其中的DefinitelyNotThisOne$.smaliIsThisTheRealOne$1.smaliThisIsTheRealOne$1.smali的文件,将其中sendBroadcast相应代码改为:

1invoke-virtual {v4, v3}, 
2Lcom/example/application/ThisIsTheRealOne;->sendBroadcast(Landroid/content/Intent;)V

这里直接去除了原本设置权限的参数,然后打包:

1apktool b ill-intentions

再进行签名:

1keytool -genkey -alias abc.keystore -keyalg RSA -validity 20000 -keystore abc.keystore
2jarsigner -verbose -keystore abc.keystore -signedjar ill-intentions1.apk ill-intentions.apk abc.keystore

最后用adb运行对应的活动,点击按钮后就可以在logcat中看到相应的广播。

1adb shell am start -n com.example.hellojni/com.example.application.ThisIsTheRealOne
2adb shell am start -n com.example.hellojni/com.example.application.IsThisTheRealOne
3adb shell am start -n com.example.hellojni/com.example.application.DefinitelyNotThisOne

Flag_system

这道题感觉出的有点问题。。。脑洞太大,所以这里主要是搞下前面恢复数据以及获取key的部分。

用文本打开文件,可以看到是以下数据开头的:

1ANDROID BACKUP
24
30
4none

可以判断出这是安卓的备份文件。

首先需要修复格式:修改其中的4为2,0为1。(应该是默认格式吧。。感觉这题脑洞特别多)

接着使用abe插件即可恢复:

1java -jar abe-all.jar unpack 12856d4b6b6c4c7d916c054dfb53708c 1

这样就可以得到一个压缩包,通过解压即可看到apps文件夹,其中com.example.mybackup即是我们的目标程序。

在com.example.mybackup/a目录下可以找到相应的apk包。

用jeb分析可以得知flag是存放在BOOKS.db数据库中的(在com.example.mybackup/db目录下可以找到。实际中并没有找到flag,可能是攻防世界上的环境设置问题或者是文件本身搞错了),并且数据库是加密的。

数据库采用的是SQLite,相应的类为BooksDB,其构造函数如下:

1public BooksDB(Context arg4) {
2    super(arg4, "BOOKS.db", null, 1);
3    this.k = Test.getSign(arg4);
4    this.db = this.getWritableDatabase(this.k);
5    this.dbr = this.getReadableDatabase(this.k);
6}

可以看到密钥即是文件的签名。这里考虑用xposed通过hook来获取Test.getSign(arg4)的返回值,代码如下(其他部分配置参考前面的内容):

1import android.app.Application;
 2import android.content.Context;
 3import android.util.Log;
 4import android.widget.Toast;
 5
 6import de.robv.android.xposed.IXposedHookLoadPackage;
 7import de.robv.android.xposed.XC_MethodHook;
 8import de.robv.android.xposed.XposedBridge;
 9import de.robv.android.xposed.XposedHelpers;
10import de.robv.android.xposed.callbacks.XC_LoadPackage;
11
12public class HOOK_function implements IXposedHookLoadPackage {
13    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable{
14        if(loadPackageParam.packageName.equals("com.example.mybackup")){
15            XposedBridge.log("has Hooked!");
16            Class clazz = loadPackageParam.classLoader.loadClass("com.example.mybackup.Test");
17            XposedHelpers.findAndHookMethod(clazz, "getSign", Context.class, new XC_MethodHook() {
18                protected void beforeHookedMethod(MethodHookParam param) throws Throwable{
19                    super.beforeHookedMethod(param);
20                    XposedBridge.log("ctf" + param.args[0]);
21                }
22                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
23                    Object result = param.getResult();
24                    XposedBridge.log("ctf" + result);
25                }
26            });
27        }
28    }
29}

在Xposed中启用该插件,重启android系统后再运行app,即可在Xposed的logs中看到密钥了。

然后剩下的部分就不进行了,需要脑洞加密方式以及flag格式的猜测什么的 ,与技术无关。

总结

总的来说安卓逆向的内容与其他逆向题目是类似的,就是多了像四大组件之类的概念,并且用到一些新的工具;而so文件等则类似于常规的elf文件,可以跟以往一样用ida等工具来进行调试;至于类似加壳、混淆加密等防护手段,则需要更深入的研究才能体会到了。