上次找到了两个关键的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机器码查询