android逆向奇技淫巧十八:x音so层代码花指令防护分析(三)

  上次找到了两个关键的so:sscronet和metasec_ml,本想着用jni trace看看jni函数的加载顺序、参数、地址等关键信息,结果大失所望:一个都没有....... 仔细想想原因:要么是没用到,要么是加密了!

  

  继续用ida打开mestasec_ml:发现导出函数列表发现大量的函数名都加密了(还有几个明显没加密的函数名大家都看到了么? man are you sss bbb? 字节的同学真幽默( ̄▽ ̄)")!

   

   很多字符串也被加密:

   

   init_array发现大量函数:

  

   1、好奇心驱使我挨个点进去查看,第一个就吃了闭门羹: 试图F5查看源码的时候,直接弹窗报”3BC6E: positive sp value has been found“错误,和我上次F5 jni_onload遇到的问题一毛一样;仔细看代码,发现这里有问题,如下:

  (1)开始的时候还很正常:入栈保存寄存器,然后通过sub sp,sp 0x6c开辟栈空间来存放参数、局部变量等;

.text:0003BC10 000                 PUSH            {R4-R7,LR}
.text:0003BC12 014                 ADD             R7, SP, #0xC
.text:0003BC14 014                 PUSH.W          {R8-R11}
.text:0003BC18 024                 SUB             SP, SP, #0x6C
.text:0003BC1A 090                 MOV.W           R0, #0x172

  但是到了下一个分支就是这样的了:突然一个add指令让sp大幅增加,而且分支中根本没用到栈空间,分支结束时也没回复栈平衡

text:0003BC6C     loc_3BC6C                               ; CODE XREF: sub_3BC10+3CC↓j
.text:0003BC6C 090                 ADD             SP, SP, #0x180
.text:0003BC6E -F0                 NOP
.text:0003BC70 -F0                 LDR             R0, =0x5F1D4716
.text:0003BC72 -F0                 B               loc_3BFD6
.text:0003BD42     loc_3BD42                               ; CODE XREF: sub_3BC10+3FA↓j
.text:0003BD42 -F0                 ADD             SP, SP, #0xDC
.text:0003BD44 -1CC                LDR             R0, =0xBDA61CFA
.text:0003BD46 -1CC                B               loc_3BFD6
text:0003BD72     loc_3BD72                               ; CODE XREF: sub_3BC10+40A↓j
.text:0003BD72 -1CC                ADD             SP, SP, #0x15C
.text:0003BD74 -328                MOV             R0, R2
.text:0003BD76 -328                B               loc_3BFD6
.text:0003BDA0     loc_3BDA0                               ; CODE XREF: sub_3BC10+41A↓j
.text:0003BDA0 -328                ADD             SP, SP, #0x15C
.text:0003BDA2 -484                NOP
.text:0003BDA4 -484                LDR             R0, =0x743ECA69
.text:0003BDA6 -484                B               loc_3BFD6
.text:0003BDA8     loc_3BDA8                               ; CODE XREF: sub_3BC10+422↓j
.text:0003BDA8 -484                ADD             SP, SP, #0x104
.text:0003BDAA -588                NOP
.text:0003BDAC -588                MOV             R0, R9
.text:0003BDAE -588                B               loc_3BFD6
.text:0003BDC8     loc_3BDC8                               ; CODE XREF: sub_3BC10+432↓j
.text:0003BDC8 -588                ADD             SP, SP, #0xF4
.text:0003BDCA -67C                NOP
.text:0003BDCC -67C                LDR             R0, =0x2E70D3EB
.text:0003BDCE -67C                B               loc_3BFD6

  直到整个函数结束前还在add sp的值,最后pop了函数刚开始时入栈的寄存器值,始终未通过sub sp让栈重新平衡!

.text:0003C08E -67C                BNE             loc_3BFD6
.text:0003C090 -67C                ADD             SP, SP, #0x6C
.text:0003C092 -6E8                POP.W           {R8-R11}
.text:0003C096 -6F8                POP             {R4-R7,PC}
.text:0003C096     ; End of function sub_3BC10

  所以这里总结一下这个libmetasec_ml.so的防护方式之一:

  • 增加一些无用的分支,在分支中破坏栈平衡,然后跳转到原本有用的分支继续执行

 (2)继续看init_array的其他函数,从第二个函数开始都能顺利F5反编译了,就第一个不行,这里又是欲盖弥彰:肯定很重要,所以才防护,第一个函数有必要好好跟踪一下!

   通过代码分析,发现这些add sp的分支在其他代码中有被引用,但都是在cmp条件中引用的,而这些条件都是不成立的,换句话说:这些add sp的分支都不会被执行的,存粹是为了反IDA搞的鬼!仔细想想也是:正经的编译器是不会干这种事的,干这种事的都不是正经的编译器!为了重新平衡栈,这里借助010editor把额外add的地方都NOP掉,方式如下:

  

   010Editor比较贴心,把我手动改的地方全都标红了:一共改了6处,全都NOP掉了!

   

  把这些add sp代码全量nop掉后,第一个函数能正常F5了,部分代码片段(代码太长了,放不下)如下:发现又是OLLVM混淆;

signed int sub_3BC10()
{
  int v0; // r1
  signed int result; // r0
  int v2; // r1
  bool v3; // zf
  signed int v4; // r1
  char v5; // nf
  int v6; // r1
  int v7; // r1
  int v8; // r1
  char v9; // r11
  int v10; // r1
  char v11; // r0
  int v12; // r1
  int v13; // r1
  signed int v14; // r1
  char v15; // [sp+61h] [bp-27h]
  char v16; // [sp+63h] [bp-25h]
  int v17; // [sp+64h] [bp-24h]
  char v18; // [sp+6Bh] [bp-1Dh]

  sub_82B38(547604, 19);
  if ( v0 )
    LOBYTE(v0) = 1;
  v15 = v0;
  result = -224184235;
  do
  {
    while ( 1 )
    {
      do
      {
        while ( 1 )
        {
          while ( 1 )
          {
            while ( 1 )
            {
              do
              {
                while ( 1 )
                {
                  while ( 1 )
                  {
                    while ( 1 )
                    {
                      while ( 1 )
                      {
                        while ( 1 )
                        {
                          while ( 1 )
                          {
                            while ( 1 )
                            {
                              while ( 1 )
                              {
                                while ( 1 )
                                {
                                  do
                                  {
                                    while ( 1 )
                                    {
                                      while ( 1 )
                                      {
                                        while ( 1 )
                                        {
                                          v14 = result;
                                          if ( result != -1786743035 )
                                            break;
                                          result = 1595754262;
                                        }
                                        if ( result != -1766343261 )
                                          break;
                                        *(_DWORD *)((char *)R2bC6xH3fE6sH5rZ6gG
                                                  + ((((~(unsigned int)sub_3BC10 | 0xA021040) & 0xA061440)
                                                    + ((unsigned int)sub_40400 & (unsigned int)sub_3BC10 | 0x1010104)) ^ 0xF5F5F57C)) = 563;
                                        sub_82B38(49730, 7);
                                        result = 1701695347;
                                      }
                                      if ( result != -1732828906 )
                                        break;
                                      sub_82B38(53825, 7);
                                      v3 = v2 == 0;
                                      v4 = -1113187078;
                                      result = -964039141;
                                      if ( !v3 )
                                        result = -1113187078;
                                      v5 = 1;

   先找些函数点进去看看都是干啥的,发现有个sub_40400的函数F5也是报同样的错,这里只能继续把add sp指令NOP掉(甚至连pop代码也要去掉,因为函数的入口就没有push),如下:

    

   这次栈是平衡了,F5反编译还是出错;回过头来想:这么加安全防护,不担心改变业务以往的逻辑么?继续往上回溯代码,发现这个分支也是在cbz条件内部,但这个条件根本不成立,所以这个分支永远不会被执行!和上面的花指令方式如出一辙!

.text:00040D7E 000 30 46                       MOV             R0, R6
.text:00040D80 000 00 21                       MOVS            R1, #0
.text:00040D82 000 CE F7 BB FF                 BL              sub_FCFC
.text:00040D86 000 B0 B3                       CBZ             R0, loc_40DF6
................................................................................... .text:00040DB6
000 30 46 MOV R0, R6 .text:00040DB8 000 00 21 MOVS R1, #0 .text:00040DBA 000 CE F7 F3 FE BL sub_FBA4 .text:00040DBE 000 D0 B1 CBZ R0, loc_40DF6

  除了init_array,还有另外一个重要的函数Jni_onload,用了同样的sp不平衡的方式反IDA的F5,即使我在函数末尾改sp为0,还是报同样的错!而且我也不知道还有多少地方都是这样的,挨个去找太费劲了!

.text:0003A76C 108 0D 9A                       LDR             R2, [SP,#0x100+var_CC]
.text:0003A76E 108 12 68                       LDR             R2, [R2]
.text:0003A770 108 51 1A                       SUBS            R1, R2, R1
.text:0003A772 108 02 BF                       ITTT EQ
.text:0003A774 108 39 B0                       ADDEQ           SP, SP, #0xE4
.text:0003A776 024 BD E8 00 0F                 POPEQ.W         {R8-R11}
.text:0003A77A 014 F0 BD                       POPEQ           {R4-R7,PC}
.text:0003A77C 000 00 BF                       NOP
.text:0003A77E 000 00 BF                       NOP
.text:0003A77E                 ; END OF FUNCTION CHUNK FOR JNI_OnLoad

  至此: 静态分析基本上到头了!原因有两个:(1)很多地方都人为让栈不平衡反ida的F5,挨个去找很麻烦,我个人没那么多时间,耗不起!  (2)就算F5成功了,还面临OLLVM的控制流混淆、字符串加密,这种情况下静态分析根本没辙!

  有同学又会问了:既然静态分析不行,就动态调试呗! 刚开始我确实也是这样想的,尝试后发现ida经常弹窗说捕获异常(如下图),让我选择怎么处理,导致我连指令或block的trace都不行(个人猜测可能是估计加了反动态调试)

  

 

   好吧,截至目前静态分析不行,动态调试也不行,我就只剩这么条路可以走了:

  •   frida 去hook关键字符串,看看内存中解密后的字符串都是啥
  •        unicorn、androidNativeEmu、unidbg这些模拟器来运行so了
  •        魔改artMethod类的registerNative、prettyMethod、JniMethodStart等方法trace函数的执行顺序(https://www.cnblogs.com/theseventhson/p/14952092.html)

    未完待续,下周继续更新!

  

 心得:

1、调试器、模拟器、解释器、虚拟机等没本质区别,核心功能都是一样的!

2、一旦编译成汇编语言,C++相比C,本质就是个“大号”的结构体(C++类名义上有成员函数,但编译后函数在代码段,对象在栈或堆上,调用成员函数时第一个参数是this指针)!

      

 参考:

1、https://armconverter.com/ arm机器码查询

         

 

posted @ 2021-07-25 20:28  第七子007  阅读(5657)  评论(5编辑  收藏  举报