源码的情况下,对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里
注意包名那里如果不是1级包,就要全包名。
然后就可以调试了
jeb2 远程调试
jeb2.2.x版本支持apk调试,能够同时对java层和native层汇编代码进行调试
必须设置%ANDROID_SDK%
步骤:
1. Debugger菜单选择启动
2. 选择设备和被调试进程
3. 下断点调试
https://www.pnfsoftware.com/blog/jeb-android-debuggers/
名称混淆是通过一个再res里的proguard
一般拿到apk逆向,观察apk按钮弹窗,弹窗名字或者字符串。
然后找到关键函数Q(decompile)反编译成java,比如这里
然后为了方便观察吧一些变量重命名,另外findviewbyid的ID,想去找这个id,跟他关联起来,那这个id一定存在与我们的解包里的smali文件会用到。因为id这个是可见的,所以可以将解包后的文件托到编辑器里面做字符串搜索。数字可能变成16进制,如果没找到就找16进制的。
另外可以在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,就是要从确定按钮,找到校验用户名密码的逻辑。然后
然后观察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. 下断点调试
在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结构体
注意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,还有根据源码版本类型的枚举也可以加入如图
.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*,找到
.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所以加一)就是在内存中我们要定位函数的偏移
这里就是解密后的代码了。我们要让调试器知道这里是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里面输入密码确定,就断到这里了。
然后就可以分析这块解密代码具体了。