Thumb 不是一个完整的指令体系结构,必须和 ARM 指令集结合才能正常工作。安卓逆向总是由 CODE32 声明的 ARM 指令集开始,执行的过程中可以通过 BX 指令切换到 CODE16 声明的 Thumb 指令集。
ARM 程序分析(I)
源码:
#include#include void (*func)(); void sub() { void *p = dlopen("libxxx.so", RTLD_NOW); if (!p) { return; } else { // myfn是libxxx.so的一个函数,作用是打印"call myfn...!" func = (void (*)())dlsym(p,"myfn"); func(); } } int main() { printf("RTLD_NOW=%d\n", RTLD_NOW); sub(); return 0; }
反汇编之后:
sub: 1. 84a0: 4808 ldr r0, [pc, #32] ; (84c4) ; r0 = [pc + 32 + 4] = [0x84a0 + 0x24] = [0x84c4] = 0x48 ; 这里之所以多 +4 是因为 Thumb 涉及 PC 时需要 +4,而 ARM 计算涉及 PC 时需要 +8 2. 84a2: 2100 movs r1, #0 ; r1 = 0 3. 84a4: b510 push {r4, lr} ; r4 和 lr 入栈,r4 属于局部变量而 lr 保存了 sub 函数的返回地址 4. 84a6: 4c08 ldr r4, [pc, #32] ; (84c8 ) ; r4 = [pc + 32 + 4] = [0x84a6 + 0x24] = [0x8aca] = [0x84c8] = 0x0c26 ; 这里的 [0x8aca] 不属于标准起始,必须对齐到 (0, 4, 8, c),所以 [0x84ca] 变成 [0x84c8] 5. 84a8: 4478 add r0, pc ; r0 = r0 + pc = 0x48 + 0x84a8 + 4 = 0x84f4 -> [0x84f4] = 'libxxx.so' 6. 84aa: 447c add r4, pc ; r4 = r4 + pc = 0x0c26 + 0x84aa + 4 = 0x90d4 7. 84ac: f7ff efc8 blx 8440 ; 执行 dlopen 函数,可以看到此时的指令长度为 (0x84ac ~ 0x84b0) = 4 bytes = 32 bit 为 ARM 指令 ; 而之前指令长度可以看到尾 2 bytes = 16 bit,为 Thumb 指令 8. 84b0: b138 cbz r0, 84c2 ; r0 不为 0 则跳转执行 0x84c2 位置的 printf 函数,此时 r0 为上一条指令 dlopen 的返回结果 9. 84b2: 4906 ldr r1, [pc, #24] ; (84cc ) ; r1 = [0x84b2 + 24 + 4] = [0x84b2 + 0x1c] = [0x84ce] = [0x84cc] = 0x46 10. 84b4: 4479 add r1, pc ; r1 = 0x46 + 0x84b4 + 4= 0x84fe -> [0x84fe] = 'myfn' 11. 84b6: f7ff efca blx 844c ; 切换到 ARM 指令执行 dlsym 12. 84ba: 4905 ldr r1, [pc, #20] ; (84d0 ) ; r1 = [pc + 20 + 4] = [0x84ba + 0x18] = [0x84d2] = [0x84d0] = 0xfffffffc 13. 84bc: 5863 ldr r3, [r4, r1] ; r3 = [r4 + r1] = [0x90d4 + 0xfffffffc] = [0x90d4 - 0x04] = [0x90d0] = 0x90f0 ; 图中没有给出 [0x90d0] 的内容,原文中截图截少了,这里的 0x90f0 即是 myfn 函数的地址,下面传入 r0 并执行 14. 84be: 6018 str r0, [r3, #0] 15. 84c0: 4780 blx r0 16. 84c2: bd10 pop {r4, pc}
ARM 程序分析(II)
#include <stdio.h> #include <string.h> int main(int argc,char *argv[]) { char name[]="helloworld"; int keys[]={0xb,0x1f,0x19,0x19,0x49,0xb,0xb,0xb,0x31,0x53}; char Thekeys[11]; int i; for(i=0;i<10;i++) { keys[i]^=7; keys[i]=keys[i]/6; keys[i]+=22; keys[i]-=24; keys[i]^=name[i]; } for(i=0;i<10;i++) { Thekeys[i]=keys[i]; } Thekeys[i]=0; if(!strcmp(Thekeys,argv[1])) printf("Good Work,you have Successed!"); else printf("NO,you are failed!"); return 0; }
反编译之后,流程图下所示,可以看到起点是 start 函数,但是没有发现名称为 main 的函数。那怎么确定改名之后的 main 函数呢?可以通过搜索 “__libc_init” ,定位到调用位置,代码类似如下:
.text:0000847C start .text:0000847C .text:0000847C var_14 = -0x14 .text:0000847C var_10 = -0x10 .text:0000847C var_C = -0xC .text:0000847C var_8 = -8 .text:0000847C .text:0000847C LDR R12, =(_GLOBAL_OFFSET_TABLE_ - 0x8498) .text:00008480 STMFD SP!, {R11,LR} .text:00008484 LDR R3, =(off_AF98 - 0xAFCC) .text:00008488 ADD R11, SP, #4 .text:0000848C SUB SP, SP, #0x10 .text:00008490 ADD R12, PC, R12 ; _GLOBAL_OFFSET_TABLE_ .text:00008494 LDR R3, [R12,R3] ; unk_AE90 .text:00008498 STR R3, [R11,#var_14] .text:0000849C LDR R3, =(off_AF9C - 0xAFCC) .text:000084A0 ADD R0, R11, #4 .text:000084A4 LDR R3, [R12,R3] ; unk_AE88 .text:000084A8 STR R3, [R11,#var_10] .text:000084AC LDR R3, =(off_AFA0 - 0xAFCC) .text:000084B0 MOV R1, #0 .text:000084B4 LDR R3, [R12,R3] ; unk_AE80 .text:000084B8 STR R3, [R11,#var_C] .text:000084BC LDR R3, =(off_AFA4 - 0xAFCC) .text:000084C0 LDR R3, [R12,R3] ; unk_AE98 .text:000084C4 STR R3, [R11,#var_8] .text:000084C8 LDR R3, =(off_AFA8 - 0xAFCC) .text:000084CC LDR R2, [R12,R3] ; sub_850C .text:000084D0 SUB R3, R11, #-var_14 .text:000084D4 BL __libc_init .text:000084D8 SUB SP, R11, #4 .text:000084DC LDMFD SP!, {R11,PC} .text:000084DC ; End of function start
根据 __libc_init 函数的定义,可以知道调用时保存在寄存器 R2 中的地址即使 main 函数的地址,此时根据 “LDR R2, [R12, R3] ; sub_850C” 这一行即可知道 main 函数为 sub_850C,跟进 F5 看看。
int __fastcall sub_850C(int a1, int a2) { int *v2; // r3@1 char *v3; // r0@1 int v4; // t1@2 signed int *v5; // r3@3 char *v6; // r2@3 signed int v7; // t1@4 const char *v8; // r1@5 int result; // r0@7 int v10; // [sp+0h] [bp-60h]@3 int v11; // [sp+4h] [bp-5Ch]@1 signed int v12; // [sp+8h] [bp-58h]@1 signed int v13; // [sp+Ch] [bp-54h]@1 signed int v14; // [sp+10h] [bp-50h]@1 signed int v15; // [sp+14h] [bp-4Ch]@1 signed int v16; // [sp+18h] [bp-48h]@1 signed int v17; // [sp+1Ch] [bp-44h]@1 signed int v18; // [sp+20h] [bp-40h]@1 signed int v19; // [sp+24h] [bp-3Ch]@1 signed int v20; // [sp+28h] [bp-38h]@1 int v21; // [sp+2Ch] [bp-34h]@1 signed int v22; // [sp+30h] [bp-30h]@1 __int16 v23; // [sp+34h] [bp-2Ch]@1 char v24; // [sp+36h] [bp-2Ah]@1 char v25; // [sp+37h] [bp-29h]@3 char v26; // [sp+38h] [bp-28h]@5 char v27; // [sp+42h] [bp-1Eh]@5 int v28; // [sp+44h] [bp-1Ch]@1 v21 = 1819043176; v22 = 1919907695; v23 = 'dl'; v24 = 0; v11 = 11; v12 = 31; v13 = 25; v14 = 25; v15 = 73; v16 = 11; v17 = 11; v18 = 11; v2 = &v11; v28 = _stack_chk_guard; v19 = 49; v20 = 83; v3 = (char *)&v20 + 3; do { v4 = (unsigned __int8)(v3++)[1]; *v2 = ((*v2 ^ 7) / 6 - 2) ^ v4; ++v2; } while ( v2 != &v21 ); v5 = &v10; v6 = &v25; do { v7 = v5[1]; ++v5; (v6++)[1] = v7; } while ( v5 != &v20 ); v8 = *(const char **)(a2 + 4); v27 = 0; if ( !strcmp(&v26, v8) ) printf("Good Work,you have Successed!"); else printf("NO,you are failed!"); result = 0; if ( v28 != _stack_chk_guard ) _stack_chk_fail(0); return result; }
可以看到做了一系列变换之后进行判断,不涉及其他位置的函数了,不详细分析下去了。
参考链接:
* IDA 如何识别 ARM 的 main 函数
* ARM 指令和 Thumb 指令的区别
* 关于__stack_chk_guard_ptr的理解
* ARM 汇编版 CRACKME 分析