2016年9月17日星期六

ANDROID 逆向学习笔记 (三)- IDA 动态调试 so


前面几篇所说的均是对安卓进行 smali 静态分析的内容,本节探讨一下动态的调试和分析,将会使用到的是 IDA Pro (金山 leak)和 Android Studio 的部分工具(模拟器,ddms,adb 等)。

Android Studio 的官方网址是: https://developer.android.com/studio/intro/index.html,可以找到不同系统的版本,下载并安装好了之后在 ~/Library/android/sdk/tools 路径下可以找到安卓开发调试所需要的各种小工具。
安装好之后启动 Android Studio 找到 AVD Manager 按钮,即可建立自己的 AVD(Android Virtual Device),创建的过程中根据自己需要选择对应的参数吧,创建好之后如下图所示,此时可以直接点击绿色箭头运行,或者是终端输入来运行虚拟机。

emulator -netdelay none -netspeed full -avd $AVD_NAME
从图中可以看到我创建了名为 4_2_AMD 和 4_2_X86 的两个 AVD,X86 相对于 AMD 来说速度比较快,但是有部分软件和程序不能在 X86 上模拟只能使用 AMD。

在 IDA 的安装目录的 dgbsrv 文件夹下有调试需要用到的 android_server 二进制文件,只有在安卓机器上运行了这个文件之后 IDA 才可以正常 attach,然后这里有一个坑,X86 的模拟器运行这个是会出错的,所以需要换 AMD 的模拟器来调试,执行命令如下:

adb push IDA/android_server  /data/local/tmp
adb shell
cd /data/local/tmp
chmod 755 android_server
./android_server

没有出错的情况下会提示在监听 23946 端口,这个时候使用 adb forward 将 AVD 的端口映射到本地,如果是本机运行的 IDA 即可直接 attach。不过,如果要是其他主机需要 attach 呢,我这里使用的命令如下。

adb forward tcp:23946 tcp:23946
socat -v tcp-listen:12344,fork tcp-connect:localhost:23946

IDA -> Debugger -> Attach -> remote ARMLinux/Android debugger,按要求填写即可。

如果上述步骤都成功的话会进入这个界面,点击需要调试的进程即可进行动态调试。下面使用 MSC 的第二题来做详细说明。

MSC Challenge # 2

先反编译 crackme1.apk,看一下 smali 代码,在 com/yaotong/crackme/MainActivity.smali 中可以看到按钮绑定的 onClick() 事件调用了 securityCheck() 来检查输入是否正确,而该函数定义含有 native 属性,可知该函数是在 .so 文件中定义的,尝试静态分析 .so 文件,直接把 libcrackme.so 拉入 IDA 中进行分析,关键代码如下所示。

  v5 = (_BYTE *)((int (__fastcall *)(JNIEnv *, int, _DWORD))(*v3)->GetStringUTFChars)(v3, v4, 0);
  v6 = off_628C;
  while ( 1 )
  {
    v7 = (unsigned __int8)*v6;
    if ( v7 != *v5 )
      break;
    ++v6;
    ++v5;
    v8 = 1;
    if ( !v7 )
      return v8;
  }

可以看到使用用户传入的字符串跟 off_628C 位置的数据进行了比较,双击进入 off_628C 的位置,如下所示,可以看到静态分析得到的 key 是 [wojiushidaan],输入 apk 后并不正确,说明这个字符串在动态运行的时候做了变换,所以还需要使用 IDA 进行动态的分析。

.data:0000628C ; .data         ends                    ; "wojiushidaan"

这里静态的分析有一个小 trick,使用 IDA 的时候可以通过 shift + F12 来显示所有定义的字符串内容,本例中通过 shift + F12 可以看到,检测到了 “wojiushidaan” 这个字符串,不过这里并不是真正的答案。



先启动 crackme.apk 之后再用 IDA 进行 attach,IDA 闪退,说明程序做了反调试机制,检测到程序的处于被调试状态则退出,所以猜测时 .so 文件加载时进行了特殊的处理,来看一下 .so 文件的运行情况。

因为很多情况 .so 文件在 apk 运行的开始就会进行加载,所以如果直接运行软件然后再去 IDA 找对应的进程进行 attach 这样很有可能就会直接错过 .so 文件加载时的运行情况,所以这里使用

adb shell am start -D -n com.yaotong.crackme/.MainActivity

这样会启动 crackme 但是不会往下运行,因为添加了 -D 参数,说明等待调试器连接启动程序之后刮起,后面的 com.yaotong.crackme/.MainActivity 对应着上文中的 smali 目录结构和主 Activity 的名称。

这样启动的程序就可以用 IDA 来 attach 了,然后再在 .so 文件载入时下一个断点,这样就能够实时的分析 .so 文件加载时的运行情况了。下断点的方式为,Debugger -> Debugger setup -> suspend on library load/unload。另外这里值得一提的是,因为程序处于挂起状态,如果直接操作 IDA 的话没有任何反应,所以这里需要使用 jdb 恢复运行。

ddms
jdb -connect com.sun.jdi.SocketAttach:hostname=127.0.0.1,port=8700

看到执行 jdb 命令之前首先执行了 ddms(即 Dalvik Debug Monitor Service),jdb 的运行在 OS X 上需要 ddms 为启动状态,在之前提到的 sdk/tools 文件夹中可以找到 ddms,启动即可。此时点击 IDA 的绿色小箭头运行即可。

此时正常运行,到加载 .so 文件时会弹出两个框,ESC 取消掉即可,然后即可进行实时调试,找出反调试的位置,去掉反调试之后继续调试找到变换后的 key。系统加载 .so,在完成装载、映射和重定向以后,就首先执行 .init 和 .init_array 段的代码,之后如果存在 JNI_OnLoad 就调用该函数。对一个 .so 进行分析,需要先看看有没有 .init_array section 和 .init section ,.so加壳一般会在初始化函数进行脱壳操作。

F8 单步调试后发现运行到 JNI_OnLoad 伪代码 33 行的这一段之后调试器会闪退,F7 跟进函数看看具体的操作。

v5 = dword_4BE442B4(v8, 0, &unk_4BE3F6A4, 0);

F7 指示跳转到 pthread_create 函数,所以猜测这里使用了 phtread_create 来检查函数是否有调试器 attach 的操作,如果存在即退出,这是一种反调试的手段,知道上述情况之后再开一个 IDA 来进行 .so 的静态分析,找到 JNI_OnLoad 函数的 33 行。

v5 = dword_62B4(handle, 0, sub_16A4, 0);

故这里的 dword_64B4 即为 pthread_create,根据 pthread_create 定义,可知第三个参数为新线程的起始地址,即新开了一个线程执行 sub_16A4 操作,并且在这个操作执行的过程中调试器闪退。刚开始看的时候可能会有一个疑问,为什么不直接静态分析 dword_64B4 位置的内容呢,因为静态分析时这个位置的内容还没有被赋值,所以直接看的话会是这样的。

.bss:000062B4 dword_62B4  % 4   # 只有在动态调试时的这个 % 4 会变成相应的地址,才可以继续分析

sub_16A4 在 F5 之后的伪代码如下所示,可以看到循环调用了 sub_130C 和 dword_62B0(3) 两条指令,猜测是不断对程序进行调试器检测的操作。

void __noreturn sub_16A4()
{
  while ( 1 )
  {
    sub_130C();
    dword_62B0(3);
  }
}

不过直接看 sub_16A4 的代码显得比较复杂,这时可以在 libc.so 文件的 fopen 函数下断点,因为检测是否 attach 调试器一般都是通过 fopen 打开对应的 pid 文件 [/proc/$pid/status],然后获取对应 TracerPid,如果该值不为 0 则说明存在 attach 的调试器。

下断点的方式:在对应函数处右键 add breakpoint, 或者是在函数的起始行将行首的小圆点,点亮。下好断点之后,点击 IDA 的运行,PC 指针会执行到断点处后停止,如图所示。
这时将 Hex View 设置与 R0 同步可以看到 R0 为 /proc/1182/status,即当前程序的 Pid 为 1182。接下来看看是在什么地方调用的 fopen 函数,将 IDA View 设置与 LR (返回地址)同步,可以看到如下。
可以看到 LR 处于 libcrackme.so 里,且相对位移为 4BE3F420 - 4BE3E000 = 1420,那么在静态分析中找到偏移为 1420 位置,可以发现其实是在 sub_130C 里,而这个 sub_130C 正是 sub_16A4 内调用的函数,故推测 sub_130C 函数为调试检测函数,将 sub_16A4 内的 [BL sub_130C] 指令替换成 [MOVS R0,R0] 即可取消该调试函数。

而之所以使用 [MOVS R0, R0](指令码:A0 E1)而不是 NOP(指令码:C0 46),是因为 ARM 的平台中并不像 X86 一样拥有 NOP 指令。

然后保存 libcrackme.so 文件,重新打包进 apk 文件、签名,此时已经可以随意 attach 调试器并且没有调试器检测,动态调试断点到 Java_com_yaotong_crackme_MainActivity_securityCheck 函数的 v6 查看结果即可。

Kill Bypass反调试

使用 [kill -19 $Pid ]来达到挂起程序的目的,然后 attach IDA 的调试器,此时因为程序处于挂起状态,反调试不起作用,直接分析即可,详情见 伪·MSC解题报告 - QEver

其他参考资料:

安卓动态调试七种武器之孔雀翎 – Ida Pro -蒸米

*文中分析的 apk 文件和部分工具可以在 AndroidRevStudy 找到