源码的情况下,对APK的动态调试主要分为两种:
smali汇编动态调试
arm汇编动态调试

Smali汇编动态调试

对smali汇编的动态调试主要分为两种:
使用ida进行调试
使用IDE + apktool进行调试
Eclipse + apktool
Android studio + apktool
Idea + apktool

使用jeb2.2以后版本调试

IDA 调试smali

步骤:

1.设置APP的debug选项
修改APP AndroidManifest.xml中Application标签包含属性android:debuggable=true,重新打包APP,这种动了签名可能过不了签名校验
第二种使用setpropex设置
setpropex ro.secure 0
setpropex ro.debuggable 1
第三种修改/default.prop配置文件,重刷boot.img

2.设置调试选项

Dalvik debugger里,设置adb路径,包名,activity(minifest 里的actibity)。

就是ida的Debugger的Debugger Options的Set specific options里

android release包 调试 调试apk_动态调试

注意包名那里如果不是1级包,就要全包名。

然后就可以调试了

jeb2 远程调试

jeb2.2.x版本支持apk调试,能够同时对java层和native层汇编代码进行调试
必须设置%ANDROID_SDK%
步骤:
1. Debugger菜单选择启动
2. 选择设备和被调试进程
3. 下断点调试

https://www.pnfsoftware.com/blog/jeb-android-debuggers/

android release包 调试 调试apk_android release包 调试_02


 

名称混淆是通过一个再res里的proguard

一般拿到apk逆向,观察apk按钮弹窗,弹窗名字或者字符串。

然后找到关键函数Q(decompile)反编译成java,比如这里

android release包 调试 调试apk_动态调试_03

然后为了方便观察吧一些变量重命名,另外findviewbyid的ID,想去找这个id,跟他关联起来,那这个id一定存在与我们的解包里的smali文件会用到。因为id这个是可见的,所以可以将解包后的文件托到编辑器里面做字符串搜索。数字可能变成16进制,如果没找到就找16进制的。

android release包 调试 调试apk_xml_04

另外可以在layout里的activity_main.xml看到布局里有两个check和扫描的button

 <public type="id" name="button" id="0x7f0d004a" />
    <public type="id" name="btnScan" id="0x7f0d004b" />

。从public.xml里button和btnScan的id就可以确定jeb的id分别各是哪个button,从而而确定了setOnclickListener哪个是check的button的。然后修改名字为checkListener,然后对于这个demo,就是要从确定按钮,找到校验用户名密码的逻辑。然后

android release包 调试 调试apk_xml_05

然后观察checkListener的逻辑。

class checkListener implements View$OnClickListener {
    checkListener(MainActivity arg1) {
        this.a = arg1;
        super();
    }

    public void onClick(View arg5) {
        String v0 = this.a.editText.getText().toString();
        String v1 = this.a.editText2.getText().toString();
        if(v0.length() < 6 || v0.length() > 20) {
            this.a.s.setMessage("Name too short(<6) or too long(>20)!");
        }
        else if(1 == this.a.NativeCheckRegister(v0, v1)) {
            this.a.s.setMessage("Check Success!");
        }
        else {
            this.a.s.setMessage("Check Fail!");
        }

        this.a.s.create().show();
    }
}

这里差不多就看到了,真正校验就在NativeCheckRegister

然后如果不知道逻辑,需要动态调试

步骤:
1. Debugger菜单选择启动
2. 选择设备和被调试进程
3. 下断点调试

android release包 调试 调试apk_android release包 调试_06

在Jeb调试窗口中,比较重要的就是观察变量的窗口,另外类型可以根据情况自己修改类型。

IDA远程调试APK文件

调试步骤:
1. 上传android_server到安卓手机
2. 开启app调试
3. 切换到root用户,启动android_server
4. 端口转发adb forward tcp:23946 tcp:23946
5. 打开ida,选择Debugger,选择attach,选择调试器:
Remote ARM Linux/Android debugger
6. 连接配置
127.0.0.1 port:默认23946即可
.  选择目标进程
8.  选择目标地址,下断点
9.  转发jdb
adb forward tcp:8899 jdwp:pid
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8899

一般打开两个ida端口。一个静态观察,一个动态调试。

注意第一次断下是进程被挂起的断下,并不是我们的断点。

寻找下断点的位置,可以开启Debugger Windows的Module list ,查找到我们的模块,然后找到符号,然后在想下断点的位置下断。

apk逆向流程

拿到apk,首先要做的事安装体验apk的功能。
获取apk的一些基本信息:
1. 浏览apk安装时所申请的权限,特别关注一些敏感权限(比如去看minifest)
打电话、收发短信、读取通信录等等
2. 熟悉apk的界面,界面的布局,界面的切换(比如layout文件,xml里等)
3. 关注界面的名称,控件的名称
4. 关注控件所触发的事件,对apk的功能有初步认识
5. 关注logcat信息,是否存在一些敏感的debug信息
把玩的目的:
对apk的功能有初步认识,掌握apk的全局信息,便于模块化分析,后续可通过资源文件来辅助定位关键信息

apk逆向分析

对apk分析的方式和其他平台的bin分析方式类似,分为静态和动态分析
由于apk存在应用层和native层,故需要分层分析
静态分析的目的:
对apk的功能架构有全面的认识
明确apk各个功能模块

动态分析的目的:
对局部关注的目的进行调试,关注模块的具体细节,关注执行流程、算法等等

根据apk文件的组成模块,依次分析
AndroidMannifest.xml
class.dex
结合资源文件夹
lib目录
确定Native库
搜索raw目录,一般存放一些重要文件,如可执行文件、加密数据等等

分析内容
Activity
Activity名称、数量
启动Activity
BroadcastReceive
静态广播类型名称、数量、权限设置
Service
后台任务进程名称、数量、权限设置
Content Provider
数据共享权限设置、接口
确定apk申请的权限,过滤出一些敏感权限
确定入口点,确定是否使用了第三方加固技术
 

然后来看下demo的例子,着重看一下arm

class checklistener implements View$OnClickListener {
    checklistener(MainActivity arg1) {
        this.a = arg1;
        super();
    }

    public void onClick(View arg5) {
        String v0 = this.a.editText.getText().toString();
        String v1 = this.a.editText2.getText().toString();
        if(v0.length() < 6 || v0.length() > 20) {
            this.a.s.setMessage("Name too short(<6) or too long(>20)!");
        }
        else if(1 == this.a.NativeCheckRegister(v0, v1)) {
            this.a.s.setMessage("Check Success!");
        }
        else {
            this.a.s.setMessage("Check Fail!");
        }

        this.a.s.create().show();
    }
}

这里看到nativeCheckRegister传入了2个java的string。

因为第一次参数是env那第二个参数是jobj,jstring,jstring所以分别对应r0,r1,r2,r3

另外在IDAoption->general->number of opcode bytes 可以改opcode分配大小,改成4,

.text:00001758 F7 B5                       PUSH    {R0-R2,R4-R7,LR}
.text:0000175A 01 68                       LDR     R1, [R0]        ; //*env
.text:0000175C 17 1C                       MOVS    R7, R2          ; 根据jeb那看出这里r2是name
.text:0000175E A9 22 92 00                 MOVS    R2, #0x2A4
.text:00001762 1D 1C                       MOVS    R5, R3          ; 密码
.text:00001764 8B 58                       LDR     R3, [R1,R2]     ; 从*env之后加个值给r3后面发现blx r3这里其实就是(*env)->Funx(0x2A4)
.text:00001766 39 1C                       MOVS    R1, R7
.text:00001768 00 22                       MOVS    R2, #0
.text:0000176A 04 1C                       MOVS    R4, R0          ; env给r4

然后*env是个结构体,我们没有把这个结构导入,可以在Structures界面,点Edit插入结构体。高版本IDA已经支持导入jni结构体

android release包 调试 调试apk_xml_07

注意jni里env结构体JNINativeInterface* C_JNIEnv;所以导入srearch的是

然后右键将其变成JNINativeInterface的偏移这里就显示是

.text:0000175E A9 22 92 00                 MOVS    R2, #JNINativeInterface.GetStringUTFChars

00001768 00 22                       MOVS    R2, #0
.text:0000176A 04 1C                       MOVS    R4, R0          ; env给r4
.text:0000176C 98 47                       BLX     R3              ; r1,r2根据上面r7,#0所以这里函数就是(*env->)GetStringUTFChars(env,name,0)
.text:0000176E 21 68                       LDR     R1, [R4]
.text:00001770 A9 22 92 00                 MOVS    R2, #JNINativeInterface.GetStringUTFChars
.text:00001774 06 1C                       MOVS    R6, R0          ; r0是上面函数返回值,暂存r6,为char* name
.text:00001776 8B 58                       LDR     R3, [R1,R2]
.text:00001778 20 1C                       MOVS    R0, R4
.text:0000177A 29 1C                       MOVS    R1, R5
.text:0000177C 00 22                       MOVS    R2, #0
.text:0000177E 98 47                       BLX     R3              ; GetStringUTFChars(env,passwd,0)
.text:00001780 00 90                       STR     R0, [SP,#0x20+p_passwd]
.text:00001782 00 99                       LDR     R1, [SP,#0x20+p_passwd]
.text:00001784 30 1C                       MOVS    R0, R6
.text:00001786 FF F7 55 FF                 BL      checkLogical    ; 参数至少char*,char*

然后进入checkLogical发现r3被赋值r4,说明参数有两个没多的。所以可以在函数流程里右键settype设置类型

int checkLogical(char* name,char* passwd)

所以后面代码,改type之类

00001786 FF F7 55 FF                 BL      checkLogical
.text:0000178A 21 68                       LDR     R1, [R4]
.text:0000178C AA 22 92 00                 MOVS    R2, #JNINativeInterface.ReleaseStringUTFChars
.text:00001790 8B 58                       LDR     R3, [R1,R2]
.text:00001792 01 90                       STR     R0, [SP,#0x20+var_1C]
.text:00001794 39 1C                       MOVS    R1, R7
.text:00001796 32 1C                       MOVS    R2, R6
.text:00001798 20 1C                       MOVS    R0, R4
.text:0000179A 98 47                       BLX     R3
.text:0000179C 21 68                       LDR     R1, [R4]
.text:0000179E AA 22 92 00                 MOVS    R2, #JNINativeInterface.ReleaseStringUTFChars
.text:000017A2 8B 58                       LDR     R3, [R1,R2]
.text:000017A4 20 1C                       MOVS    R0, R4
.text:000017A6 29 1C                       MOVS    R1, R5
.text:000017A8 00 9A                       LDR     R2, [SP,#0x20+p_passwd]
.text:000017AA 98 47                       BLX     R3
.text:000017AC 01 98                       LDR     R0, [SP,#0x20+var_1C]

发现F5之后跟实际差距较大,因为结构体不对, 所以可以给这个验证native代码加入settype

int Java_com_tencent_tencent2016a_MainActivity_NativeCheckRegister(JNIEnv *,void *,jstring *,jstring *)

int __cdecl Java_com_tencent_tencent2016a_MainActivity_NativeCheckRegister(JNIEnv *a1, void *a2, jstring *a3, jstring *a4)
{
  jstring *v4; // r7@1
  jstring *v5; // r5@1
  JNIEnv *v6; // r4@1
  char *v7; // r6@1
  char *p_passwd; // ST00_4@1
  int v9; // ST04_4@1

  v4 = a3;
  v5 = a4;
  v6 = a1;
  v7 = (char *)(*a1)->GetStringUTFChars(a1, a3, 0);
  p_passwd = (char *)((int (__fastcall *)(_DWORD, _DWORD, _DWORD))(*v6)->GetStringUTFChars)(v6, v5, 0);
  v9 = checkLogical(v7, p_passwd);
  ((void (__fastcall *)(_DWORD, _DWORD, _DWORD))(*v6)->ReleaseStringUTFChars)(v6, v4, v7);
  ((void (__fastcall *)(_DWORD, _DWORD, _DWORD))(*v6)->ReleaseStringUTFChars)(v6, v5, p_passwd);
  return v9;
}

这样发现就比较接近

下面我们来看下如果函数末尾没识别如何处理,比如我们自己的函数,前面学习jni编写的函数。

比如设置函数settype,bad declaration,这时候可以对着名字点右键p,create function,因为这里JavaVM也是JNI结构体所以也可以导入结构体。JNIInvokeInterface,还有根据源码版本类型的枚举也可以加入如图

android release包 调试 调试apk_动态调试_08

android release包 调试 调试apk_R3_09

 

.text:000006C8 ; int __cdecl JNI_OnLoad(JavaVM *, void *)
.text:000006C8                 EXPORT JNI_OnLoad
.text:000006C8 JNI_OnLoad
.text:000006C8
.text:000006C8 env             = -0x18
.text:000006C8 var_14          = -0x14
.text:000006C8 var_10          = -0x10
.text:000006C8
.text:000006C8                 PUSH            {R4-R7,LR}
.text:000006CA                 ADD             R7, SP, #0xC
.text:000006CC                 STR.W           R11, [SP,#0xC+var_10]!
.text:000006D0                 SUB             SP, SP, #8
.text:000006D2                 LDR             R1, ='8
.text:000006D4                 LDR             R4, =JNI_VERSION_1_4 ; 版本,这里是个枚举,可以在enums添加
.text:000006D6                 ADD             R1, PC ; __stack_chk_guard_ptr
.text:000006D8                 LDR             R6, [R1] ; __stack_chk_guard
.text:000006DA                 MOV             R2, R4
.text:000006DC                 LDR             R1, [R6]
.text:000006DE                 STR             R1, [SP,#0x18+var_14]
.text:000006E0                 MOVS            R1, #0
.text:000006E2                 STR             R1, [SP,#0x18+env]
.text:000006E4                 LDR             R1, [R0]
.text:000006E6                 LDR             R3, [R1,#JNIInvokeInterface.GetEnv]
.text:000006E8                 MOV             R1, SP
.text:000006EA                 BLX             R3
.text:000006EC                 CBZ             R0, loc_708
.text:000006EE
.text:000006EE loc_6EE                                 ; CODE XREF: JNI_OnLoad+44j
.text:000006EE                                         ; JNI_OnLoad+56j ...
.text:000006EE                 MOV.W           R4, #0xFFFFFFFF
.text:000006F2
.text:000006F2 loc_6F2                                 ; CODE XREF: JNI_OnLoad+6Cj
.text:000006F2                 LDR             R0, [SP,#0x18+var_14]
.text:000006F4                 LDR             R1, [R6]
.text:000006F6                 SUBS            R0, R1, R0
.text:000006F8                 ITTTT EQ
.text:000006FA                 MOVEQ           R0, R4
.text:000006FC                 ADDEQ           SP, SP, #8
.text:000006FE                 LDREQ.W         R11, [SP+0x10+var_10],#4
.text:00000702                 POPEQ           {R4-R7,PC}
.text:00000704                 BLX             __stack_chk_fail
.text:00000708 ; ---------------------------------------------------------------------------
.text:00000708
.text:00000708 loc_708                                 ; CODE XREF: JNI_OnLoad+24j
.text:00000708                 LDR             R5, [SP,#0x18+env]
.text:0000070A                 CMP             R5, #0
.text:0000070C                 BEQ             loc_6EE
.text:0000070E                 LDR             R0, [R5]
.text:00000710                 LDR             R1, =(aComExampleAppl - 0x718)
.text:00000712                 LDR             R2, [R0,#JNINativeInterface.FindClass]
.text:00000714                 ADD             R1, PC  ; "com/example/applicationandjni/MainActiv"...
.text:00000716                 MOV             R0, R5
.text:00000718                 BLX             R2
.text:0000071A                 MOV             R1, R0
.text:0000071C                 CMP             R0, #0
.text:0000071E                 BEQ             loc_6EE
.text:00000720                 LDR             R0, [R5]
.text:00000722                 MOVS            R3, #1
.text:00000724                 LDR.W           R12, [R0,#JNINativeInterface.RegisterNatives]
.text:00000728                 MOV             R0, R5
.text:0000072A                 LDR             R2, =(off_4004 - 0x730)
.text:0000072C                 ADD             R2, PC ; off_4004
.text:0000072E                 BLX             R12
.text:00000730                 CMP.W           R0, #0xFFFFFFFF
.text:00000734                 BGT             loc_6F2
.text:00000736                 B               loc_6EE
.text:00000736 ; End of function JNI_OnLoad
.text:00000736
.text:00000736 ; ---------------------------------------------------------------------------
.text:00000738 dword_738       DCD JNI_VERSION_1_4     ; DATA XREF: JNI_OnLoad+Cr
.text:0000073C dword_73C       DCD '8                ; DATA XREF: JNI_OnLoad+Ar
.text:00000740 off_740         DCD aComExampleAppl - 0x718 ; DATA XREF: JNI_OnLoad+48r
.text:00000740                                         ; "com/example/applicationandjni/MainActiv"...
.text:00000744 off_744         DCD off_4004 - 0x730    ; DATA XREF: JNI_OnLoad+62r
.text:00000748 ; ---------------------------------------------------------------------------
.text:00000748

 

所以根据上面是registerNatives

.text:0000072A                 LDR             R2, =(off_4004 - 0x730)这里是注册函数,因为是显示注册所以指针是没有导出的,要逆这一块。

static int registerNativeMethods(JNIEnv* env, const char* className,
        JNINativeMethod* gMethods, int numMethods)
{
	jclass clazz = env->FindClass(className);
	if (clazz == NULL) {
		return JNI_FALSE;
	}
	if (env->RegisterNatives(clazz, gMethods, numMethods) < 0) {
		return JNI_FALSE;
	}
	return JNI_TRUE;
}

 所以在RegisterNatives,env是r0,clazz是r1,gMethods是r2,所以这里r2是个数组,参考

.text:0000072C                 ADD             R2, PC ; off_4004

r2是跟pc相关的一个偏移,双击off,发现是个三元组,函数名,参数,指针

.data:00004004 off_4004        DCD aTestc              ; DATA XREF: JNI_OnLoad+64o
.data:00004004                                         ; .text:off_744o
.data:00004004                                         ; "testC"//函数名
.data:00004008                 DCD aLjavaLangStr_0     ; "()Ljava/lang/String;"//参数
.data:0000400C                 DCD sub_8D4+1//指针
.data:0000400C ; .data         ends

所以双击sub_8D4,这里是我们的test,改名test

.text:000008D4             test                                    ; DATA XREF: .data:0000400Co
.text:000008D4 02 68                       LDR             R2, [R0]
.text:000008D6 02 49                       LDR             R1, =(aHelloFromMetho - 0x8E0)
.text:000008D8 D2 F8 9C 22                 LDR.W           R2, [R2,#0x29C]
.text:000008DC 79 44                       ADD             R1, PC  ; "Hello from Method C"
.text:000008DE 10 47                       BX              R2
.text:000008DE             ; End of function test

所以如果我们下断点,要下的是这里。

所以我们通过静态分析找到了他。

然后再开一个窗口动态调试,attch之后,暂停下,moudule里搜ver*,找到

android release包 调试 调试apk_android release包 调试_10


 


.text:00000CA4 ; __unwind {
.text:00000CA4                 MOVLTS  R4, #0xF000
.text:00000CA8                 LDCVSL  p15, c4, [R11,#0xD8]!
.text:00000CAC                 LDRLSB  R11, [R11,R3,LSL#23]
.text:00000CB0                 MRCLE   p3, 7, LR,c15,c10, 7
.text:00000CB4                 UBFXPL  R9, R12, #0xF, #0x16
.text:00000CB8                 LDCVS   p13, c13, [R8],#0x2C
.text:00000CBC                 LDCVSL  p4, c11, [R8],#0x2F0
.text:00000CC0                 LDCVSL  p4, c11, [R7],#0x2F0
.text:00000CC4                 LDCVSL  p4, c11, [R6],#0x2F0
.text:00000CC8                 SUBNES  R0, R5, R0,LSL#16
.text:00000CCC                 BLVS    0xFFEADFC8
.text:00000CD0                 STRLSB  R11, [R4,R4,LSL#23]!


 

 

这里在内存基地址base是随机的,静态都是从零开始,所以这里base加上静态的偏移CA5(thumb所以加一)就是在内存中我们要定位函数的偏移

android release包 调试 调试apk_android release包 调试_11

 

这里就是解密后的代码了。我们要让调试器知道这里是tumb指令,不能直接C,所以alt+g切t指令,value这里写0x1

libverify.so:B381FCA4 CODE16
libverify.so:B381FCA4 DCW 0xB5F0
libverify.so:B381FCA6 DCW 0x4C48
libverify.so:B381FCA8 DCW 0xB0C9
libverify.so:B381FCAA DCW 0x9204
libverify.so:B381FCAC DCB 0x7C ; |
libverify.so:B381FCAD DCB 0x44 ; D
libverify.so:B381FCAE DCB 0x24 ; $
libverify.so:B381FCAF DCB 0x68 ; h

他就识别是code16了,然后对着data按c转化成code

ibverify.so:B381FCA4 CODE16
libverify.so:B381FCA4 PUSH            {R4-R7,LR}
libverify.so:B381FCA6 LDR             R4, =(off_B3822FA8 - 0xB381FCB0)
libverify.so:B381FCA8 SUB             SP, SP, #0x124
libverify.so:B381FCAA STR             R2, [SP,#0x10]
libverify.so:B381FCAC ADD             R4, PC                  ; off_B3822FA8
libverify.so:B381FCAE LDR             R4, [R4]                ; __stack_chk_guard
libverify.so:B381FCB0 MOVS            R5, R0
libverify.so:B381FCB2 MOVS            R1, #0
libverify.so:B381FCB4 LDR             R3, [R4]
libverify.so:B381FCB6 ADD             R0, SP, #0x28
libverify.so:B381FCB8 MOVS            R2, #0xF4
libverify.so:B381FCBA STR             R3, [SP,#0x11C]
libverify.so:B381FCBC LDR             R3, =0x20797254
libverify.so:B381FCBE STR             R3, [SP,#0x1C]
libverify.so:B381FCC0 LDR             R3, =0x69616761
libverify.so:B381FCC2 STR             R3, [SP,#0x20]
libverify.so:B381FCC4 LDR             R3, =0x216E
libverify.so:B381FCC6 STR             R3, [SP,#0x24]
libverify.so:B381FCC8 BLX             memset_0
libverify.so:B381FCCC LDR             R3, =(dword_B3823020 - 0xB381FCD4)
libverify.so:B381FCCE STR             R4, [SP,#0x14]
libverify.so:B381FCD0 ADD             R3, PC                  ; dword_B3823020
libverify.so:B381FCD2 LDR             R3, [R3]
libverify.so:B381FCD4 CMP             R3, #0
libverify.so:B381FCD6 BNE             loc_B381FCF8

就变成了真正的代码了。

然后在这里下个断点,全速运行,app里面输入密码确定,就断到这里了。

android release包 调试 调试apk_android release包 调试_12

 然后就可以分析这块解密代码具体了。