概述
上一章进行了so的静态分析,很多时候由于加密逻辑非常复杂我们要依靠动态调试进行逻辑的判断,得到更有用的信息。本文将对上一章的so进行动态调试。
动态调试
在IDA目录下的dbgsrv目录下找到android_server,将它push到手机上面
adb push android_server /data/local/tmp
adb shell
su
cd /data/local/tmp/
chmod 777 android_server
./android_server
打开另一个cmd
#建立手机和电脑的连接 端口转发
adb forward tcp:23946 tcp:23946
模拟器里启动要调试的APP
启动IDA,打开debugger->attach->remote Armlinux/andoid debugger
填写hostname为127.0.0.1或localhost(推荐),端口确定为23946或你自定义转发的端口,其他默认,点击确定
弹出Choose process to attach to窗口,找到app的进程名,点击ok,等待分析后即可进入调试界面
此时虚拟机将被停住
android系统中libc是c层中最基本的函数库,libc中封装了io、文件、socket等基本系统调用。所有上层的调用都需要经过libc封装层。所以libc.so是最基本的,所以会断在这里,
而且我们还需要知道一些常用的系统so,比如linker;这个linker是用于加载so文件的模块,如何在.init_array处下断点;
还有一个就是libdvm.so文件,他包含了DVM中所有的底层加载dex的一些方法
定位函数
使用快捷键Ctrl+s打开segment窗口,选择so文件,这里可以使用Ctrl+f进行搜索;同时这里需要记下so文件的起始地址(8D673000)
image.png窗口里有多个so文件怎么办?应该选择哪一个?
其实这里并不是多个so文件,而是so文件对应的不同Segement信息被映射到内存中,包括代码段,数据段等,很明显,代码段会有执行权限的特点,所以我们选择带有X属性的so文件,即是我们想要调试的so文件。
用另一个IDA打开so文件,依照上一章操作找到对应函数的偏移位置,在下图可以看到偏移为:00000CA4
image.png
我们需要计算出函数在内存中的绝对地址,公式如下:
绝对地址=基址+偏移地址=8D673000+00000CA4=8D673CA4
按下快捷键G,输入8D673CA4即可跳转到正确的函数。
对比我们看到静态分析中的代码和动态调试中代码不一样
这与ELF文件加载有关。(下次讨论)
如果进入函数没有代码而是识别成数据,可以自己使用快捷键C也可以尝试进入对应lib的init方法
然后使用F2或者点击前面的小圆点 下一个断点
F9运行程序
在虚拟机输入密码后确定就被断在我们的断点了
在函数头部create function后,F5可进入到c/c++代码
image.png
可以依照上一章中添加结构体,增强代码可阅读性.
#需要将jni.h改成以下代码,因为后面的定义用到了stdarg.h的可变参数va_list
//#include <stdarg.h>
//#include <stdint.h>
typedef __builtin_va_list va_list;
转化后结果如下:
int __fastcall sub_66008CA4(_JNIEnv *a1, int a2, int a3)
{
_JNIEnv *v3; // r5@1
int v4; // r3@1
int v5; // r0@4
int v6; // r7@4
int v7; // r0@4
int v8; // r6@4
int v9; // r4@4
int v10; // r0@5
char v11; // r6@9
char *v12; // ST0C_4@9
char v13; // r7@9
char v14; // r2@9
int result; // r0@11
int v16; // r3@8
int v17; // [sp+10h] [bp-128h]@1
void *v18; // [sp+1Ch] [bp-11Ch]@1
int v19; // [sp+20h] [bp-118h]@1
int v20; // [sp+24h] [bp-114h]@1
char v21; // [sp+28h] [bp-110h]@1
char v22; // [sp+2Ch] [bp-10Ch]@10
int v23; // [sp+11Ch] [bp-1Ch]@1
v17 = a3;
v3 = a1;
v23 = _stack_chk_guard;
v18 = (void *)544830036;
v19 = 1767991137;
v20 = 8558;
//初始化内存为0
((void (__fastcall *)(char *, _DWORD, signed int))memset_0)(&v21, 0, 244);
v4 = dword_6600C020;
if ( !dword_6600C020 )
{
do
{
aWIlMjFxb[v4] ^= ~byte_6600A400[v4];
++v4;
}
while ( v4 != 12 );
dword_6600C020 = 1;
}
//得到安卓android/util/Base64 jclass类对象
v5 = ((int (__fastcall *)(_JNIEnv *, const char *))v3->functions->FindClass)(v3, "android/util/Base64");
v6 = v5;
//得到静态方法的id jmethodID
v7 = ((int (__fastcall *)(_JNIEnv *, int, const char *, const char *))v3->functions->GetStaticMethodID)(
v3,
v5,
"encode",
"([BI)[B");
//v8是访问数组的下标
v8 = 0;
//调用这个静态方法
//调用的方法原型: public static byte[] encode(byte[] input, int flags)
v9 = ((int (__fastcall *)(_JNIEnv *, int, int, int))v3->functions->CallStaticObjectMethod)(v3, v6, v7, v17);
//得到这个返回encode后返回值的大小判断是否大于11
if ( ((int (__fastcall *)(_JNIEnv *, int))v3->functions->GetArrayLength)(v3, v9) > 11 )
{
//获取数组指针(不能直接访问 Java 传递给 JNI 层的数组,必须选择合适的 JNI 函数来访问)
v10 = ((int (__fastcall *)(_JNIEnv *, int, _DWORD))v3->functions->GetByteArrayElements)(v3, v9, 0);
//aWIlMjFxb中存放的字符串就是VHJhY3lWNQ==
while ( (unsigned __int8)aWIlMjFxb[v8] == *(_BYTE *)(v10 + v8) )
{
//看下标是否到了12
if ( ++v8 == 12 )
{
v18 = &unk_46544343;
v16 = 0;
do
{
v11 = byte_6600A400[v16];
v12 = (char *)&v18 + v16;
v13 = byte_6600A400[v16 + 13];
v14 = aWIlMjFxb[v16++];
v12[4] = v13 ^ v14 ^ ~v11;
}
while ( v16 != 12 );
v22 = 0;
//到了12就跳出
break;
}
}
}
//此时v18存放的就是我们最后的verify结果 转成java的字符串,并返回
result = ((int (__fastcall *)(_JNIEnv *, void **))v3->functions->NewStringUTF)(v3, &v18);
if ( v23 != _stack_chk_guard )
result = ((int (__fastcall *)(int))unk_66008C2C)(result);
return result;
}
将VHJhY3lWNQ==经过解密得到:TracyV5
可以通过一些网站解密,也可以自己使用上面用到的android/util/Base64解密
在安卓中输入密码得到一下结果:
image.png
尾言
动态调试的基础就到这里了,后面都是需要靠着你ARM汇编阅读能力和多操作的经验积累,才能更加熟练。
网友评论