最近打dsctf,第一道Android题用到了动态调试(其实也可以不用)arm架构的so库,经过不懈努力,终于搭好了一个像样的环境,在此记录一下完整的过程。
1.反编译并分析java层代码
把apk包丢进jadx(或jeb)即可,找到MainActivity分析
发现关键函数是check,显然存在于so库。使用apktool解包apk,得到so库并使用ida分析。
apktool解包命令:
1
|
apktool d catchme.apk
-
o catchme
|
(正常情况下应该是java -jar apktool,我将此命令打包成了批处理文件执行)
2.分析so库文件代码
在ida里找到接口函数JNIOnload
直接查找check没有结果,说明函数是动态注册产生且函数名经过了混淆处理,在ida中载入jni.h,以便找到registerclass函数
使用ctrl+f9或file->load file->Parse C header file即可载入,jni.h我会放在附件里。载入后对变量重新设置类型(如JNIEnv *)即可将指针偏移直接转化为结构体成员。
实践发现,此函数为动态注册函数,传入的第二个值(a2)就是函数地址,我们定位到对应的函数中
使用findcrypt插件会发现存在aes算法,分析函数可得具体加密过程,但发现解密根本得不到明文,后面经过动调会发现,这是一个假的check,程序根本不会经过这里。
3.搭建avd环境动态调试
因为so文件是基于arm架构,故不能在一般的模拟器上调试,只能在Android Studio中下载arm架构虚拟机(推荐各位有root真机最好还是用真机,不仅流畅程度高不少,某些要检测环境的app也可以避免去绕),安装好虚拟机后,使用adb push将ida中的android_server、android_server64放入虚拟机(我放的位置是/data/local/tmp),接着给予执行权限并执行,然后进行调试的四个步骤
1.以调试模式启动app
1
|
adb shell am start
-
D
-
n com.ctf.catchme
/
.MainActivity
|
2.端口转发
1
|
adb forward tcp:
23946
tcp:
23946
|
3.ida附加,并更改调试选项
打开IDA,选择菜单Debugger -> Attach -> Remote ARM Linux/Android debugger,访问本地的23946端口。连接后更改调试选项,至少将载入库断点加上,也就是Suspend on library load/unload。
4.jdb连接
一定要打开ddms,并且是在以调试模式启动那一个步骤前(也就是第一步之前)打开,往往调试端口就是8700
1
|
jdb
-
connect com.sun.jdi.SocketAttach:hostname
=
127.0
.
0.1
,port
=
8700
|
然后就可以查看当前载入库,不断运行(F9)当发现我们要的so库被载入时,即可点击查看函数了
对JNIOnload和我们怀疑的check函数断点,发现在JNIOnload开头成功断下,但再次执行后程序直接脱离调试器,这让我一度认为我的调试步骤有问题。后面经过学长点拨,推测是有反调试机制,于是进入JNIOnload不断步过,成功定位到使程序中止的函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
int
sub_A394()
{
FILE
*
stream;
/
/
[sp
+
30h
] [bp
-
820h
]
__pid_t v2;
/
/
[sp
+
3Ch
] [bp
-
814h
]
char s1[
10
];
/
/
[sp
+
44h
] [bp
-
80Ch
] BYREF
char v4[
1014
];
/
/
[sp
+
4Eh
] [bp
-
802h
] BYREF
char s[
1036
];
/
/
[sp
+
444h
] [bp
-
40Ch
] BYREF
v2
=
getpid();
sprintf(s, byte_1F168, v2);
stream
=
fopen(s, byte_1F178);
if
( stream )
{
while
( fgets(s1,
1024
, stream) )
{
if
( !strncmp(s1, aZikmzxal,
9u
) )
{
if
( atoi(v4) )
{
fclose(stream);
sub_89C8(v2,
9
);
LOBYTE(dword_0)
=
99
;
}
break
;
}
}
fclose(stream);
}
return
_stack_chk_guard;
}
|
其中函数sub_89C8就是关闭程序的罪魁祸首,而这整个函数就是使用了一种
简单的patch掉if执行条件后,再次运行依然不正常退出,再次定位出现问题的点,发现还有一个反调试函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
int
__fastcall sub_A208(
int
a1)
{
int
v1;
/
/
r0
__pid_t v2;
/
/
r0
int
v4;
/
/
[sp
+
18h
] [bp
-
18h
]
sub_9F20(a1);
if
( a1 )
{
v4
=
sub_A020(a1, &unk_1F130);
v1
=
sub_A04A(a1, v4, &unk_1F150, &unk_1F164);
if
( (unsigned __int8)sub_A2A0(a1, v4, v1) )
{
v2
=
getpid();
sub_89C8(v2,
9
);
LOBYTE(dword_0)
=
99
;
return
1
;
}
else
{
return
0
;
}
}
else
{
return
0
;
}
}
|
使用isDebuggerConnected函数判断是否被调试,同样patch即可,不过多赘述。
再次运行发现还是无法进入check函数,只好仔细分析JNIOnload,发现在动态注册了之后会有另一个类似覆盖地址的操作
推测原地址被覆盖为了新的函数,点进可疑函数,发现同样是一个aes加密,并且使用了魔改base64,简单分析解密,成功得到明文。
更多【记一次完整的Android native层动态调试--使用avd虚拟机】相关视频教程:www.yxfzedu.com