最近打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:23946tcp: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
      | 
      
       intsub_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(inta1) 
       { 
         intv1; //r0 
         __pid_t v2; //r0 
         intv4; //[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; 
             return1; 
           } 
           else 
           { 
             return0; 
           } 
         } 
         else 
         { 
           return0; 
         } 
       } | 
  
 
 
 使用isDebuggerConnected函数判断是否被调试,同样patch即可,不过多赘述。
再次运行发现还是无法进入check函数,只好仔细分析JNIOnload,发现在动态注册了之后会有另一个类似覆盖地址的操作
 
推测原地址被覆盖为了新的函数,点进可疑函数,发现同样是一个aes加密,并且使用了魔改base64,简单分析解密,成功得到明文。
 
  
   
   
 
  
 
更多【记一次完整的Android native层动态调试--使用avd虚拟机】相关视频教程:www.yxfzedu.com