内存分区
逻辑上划分(编译器划分)
- 代码区:存放代码,可读可执行
- 栈区:参数、局部变量、临时数据。可短可写
- 堆区:动态申请。可读可写
- 全局变量:可读可写
- 常量:只读
全局变量和常量
int g = 12;
int func(int a, int b) {
printf("test");
int c = a + g + b;
return c;
}
int main(int argc, char * argv[]) {
func(1, 2);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
对应的汇编:
可以看到
printf
函数的参数来源为:
0x104492140 <+20>: adrp x0, 1
0x104492144 <+24>: add x0, x0, #0xf81 ; =0xf81
X0
存储的是一个地址,为字符串常量区。那么以上两条指令是怎么获得0x0000000104493f81
这个地址的?
-
adrp
:Address Page 内存地址以页寻址。 -
0x104492140 <+20>: adrp x0, 1
:定位到某一页数据的开始(文件起始位置)
1.将1
的值左移12
位变成0x1000
。
2.当前pc
的值低12
位清零。0x104492140 -> 0x104492000
。
3.0x104492000 + 0x1000
得到0x104493000
。相当于pc
后3位置为0
,第4位加上x0后跟的值。 -
0x104492144 <+24>: add x0, x0, #0xf81
:偏移地址(当前代码偏移)
0x104493000 + 0xf81
得到0x104493f81
。
这样就得到了常量区字符串的地址了。
0x104493000
尾数为000
意味着000~fff -> 0~4095
大小为4096
也就是4k
。也就是定位到某一页数据的开始。mac
中pagesize 4k
iOS
中 pagesize 16k
。这里是兼容的4k * 4 = 16k
。
这里以pc
寄存器为参照,也就是当前代码段地址为参照。
继续执行:
通过上面的规则可以计算出
x9
最终的值为0x1044955d8
也就是全局变量g
的值。
⚠️全局变量和常量都是通过一个 基地址 + 偏移 获取。
汇编还原高级代码
先编译要还原的工程,进入.app
找到macho
文件并拖入Hopper
中
Hopper
分析完成后搜索要分析的函数
通过汇编我们可以还原高级语言的行为。
比如刚才func
分析出来汇编代码为:
_func:
000000010000612c sub sp, sp, #0x20 ; CODE XREF=_main+32
0000000100006130 stp x29, x30, [sp, #0x10]
0000000100006134 add x29, sp, #0x10
0000000100006138 stur w0, [x29, var_4]
000000010000613c str w1, [sp, #0x10 + var_8]
0000000100006140 adrp x0, #0x100007000 ; 0x100007f81@PAGE, argument "format" for method imp___stubs__printf
0000000100006144 add x0, x0, #0xf81 ; 0x100007f81@PAGEOFF, "test"
0000000100006148 bl imp___stubs__printf ; printf
000000010000614c ldur w8, [x29, var_4]
0000000100006150 adrp x9, #0x100009000 ; 0x1000095d8@PAGE
0000000100006154 add x9, x9, #0x5d8 ; 0x1000095d8@PAGEOFF, _g
0000000100006158 ldr w10, [x9] ; _g
000000010000615c add w8, w8, w10
0000000100006160 ldr w10, [sp, #0x10 + var_8]
0000000100006164 add w8, w8, w10
0000000100006168 str w8, [sp, #0x10 + var_C]
000000010000616c ldr w8, [sp, #0x10 + var_C]
0000000100006170 mov x0, x8
0000000100006174 ldp x29, x30, [sp, #0x10]
0000000100006178 add sp, sp, #0x20
000000010000617c ret
; endp
可以通过MachOView
查找0000000100007f81
:
00000001000095d8
:
分析完代码如下:
int global_g = 12;
int func2(int a, int b) {
//_func:
//000000010000612c sub sp, sp, #0x20 ; CODE XREF=_main+32
//0000000100006130 stp x29, x30, [sp, #0x10]
//0000000100006134 add x29, sp, #0x10
//4字节参数2个
//0000000100006138 stur w0, [x29, var_4]
//000000010000613c str w1, [sp, #0x10 + var_8]
//这里已经算好了adrp的结果 等价于 0000000100006140 adrp x0, #0x1
//0000000100006140 adrp x0, #0x100007000 ; 0x100007f81@PAGE, argument "format" for method imp___stubs__printf
//0000000100006144 add x0, x0, #0xf81 ; 0x100007f81@PAGEOFF, "test"
//这里可以算出地址为 0000000100007f81,在 MachOView 查找为 test
//0000000100006148 bl imp___stubs__printf ; printf
printf("test");
//000000010000614c ldur w8, [x29, var_4]
int w8 = a;
//0000000100006150 adrp x9, #0x100009000 ; 0x1000095d8@PAGE
//0000000100006154 add x9, x9, #0x5d8 ; 0x1000095d8@PAGEOFF, _g
//00000001000095d8 在 MachOView 中 为 0xC 也就是 12,定义全局变量 global_g = 12
//0000000100006158 ldr w10, [x9] ; _g
int w10 = global_g;
//000000010000615c add w8, w8, w10
w8 += w10;
//0000000100006160 ldr w10, [sp, #0x10 + var_8]
w10 = b;
//0000000100006164 add w8, w8, w10
w8 += w10;
//0000000100006168 str w8, [sp, #0x10 + var_C]
//000000010000616c ldr w8, [sp, #0x10 + var_C]
//0000000100006170 mov x0, x8
//0000000100006174 ldp x29, x30, [sp, #0x10]
//0000000100006178 add sp, sp, #0x20
//000000010000617c ret
// ; endp
return w8;
}
精简后:
int global_g = 12;
int func2(int a, int b) {
printf("test");
return a + global_g + b;
}
⚠️:从上往下还原,代码执行流程不关心,只关心结果和原始的一样。
if识别
int g = 12;
void func(int a, int b) {
if (a > b) {
g = a;
} else {
g = b;
}
}
0000000100006190 sub sp, sp, #0x10 ; CODE XREF=_main+32
0000000100006194 str w0, [sp, #0xc]
0000000100006198 str w1, [sp, #0x8]
000000010000619c ldr w8, [sp, #0xc]
00000001000061a0 ldr w9, [sp, #0x8]
a和b比较大小,cmp不影响 w8 和 w9 的值。做减法只影响标记寄存器的值。
00000001000061a4 cmp w8, w9
<= 跳转 loc_1000061c0,大于直接往下执行不跳转
00000001000061a8 b.le loc_1000061c0
//代码块1
00000001000061ac ldr w8, [sp, #0xc]
00000001000061b0 adrp x9, #0x100009000
00000001000061b4 add x9, x9, #0x5d0 ; _g
00000001000061b8 str w8, x9
这里b跳转loc_1000061d0,跳过了else的代码
00000001000061bc b loc_1000061d0
//代码块2
loc_1000061c0:
00000001000061c0 ldr w8, [sp, #0x8] ; CODE XREF=_func+24
00000001000061c4 adrp x9, #0x100009000
00000001000061c8 add x9, x9, #0x5d0 ; _g
00000001000061cc str w8, x9
loc_1000061d0:
00000001000061d0 add sp, sp, #0x10 ; CODE XREF=_func+44
00000001000061d4 ret
还原代码
int global = 12;
void func2(int a, int b) {
// 0000000100006190 sub sp, sp, #0x10 ; CODE XREF=_main+32
//两个参数
// 0000000100006194 str w0, [sp, #0xc]
// 0000000100006198 str w1, [sp, #0x8]
// 000000010000619c ldr w8, [sp, #0xc]
// 00000001000061a0 ldr w9, [sp, #0x8]
int w8 = a, w9 = b;
// a和b比较大小,cmp不影响 w8 和 w9 的值。做减法只影响标记寄存器的值。
// 00000001000061a4 cmp w8, w9
// <= 跳转 loc_1000061c0,大于直接往下执行不跳转
// 00000001000061a8 b.le loc_1000061c0
if (w8 > w9) {
// //代码块1
// 00000001000061ac ldr w8, [sp, #0xc]
w8 = a;
// 00000001000061b0 adrp x9, #0x100009000
// 00000001000061b4 add x9, x9, #0x5d0 ; _g
//MachOView中查到 00000001000095d0 为 0xC 4字节
// 00000001000061b8 str w8, x9
global = w8;
// 这里b跳转loc_1000061d0,跳过了else的代码
// 00000001000061bc b loc_1000061d0
} else {
// //代码块2
// loc_1000061c0:
// 00000001000061c0 ldr w8, [sp, #0x8] ; CODE XREF=_func+24
w8 = b;
// 00000001000061c4 adrp x9, #0x100009000
// 00000001000061c8 add x9, x9, #0x5d0 ; _g
// 00000001000061cc str w8, x9
global = w8;
}
// loc_1000061d0:
// 00000001000061d0 add sp, sp, #0x10 ; CODE XREF=_func+44
//ret前没有x8和x0的操作,返回void
// 00000001000061d4 ret
}
精简后:
int global = 12;
void func2(int a, int b) {
if (a > b) {
global = a;
} else {
global = b;
}
}
cmp(Compare)比较指令
cmp
把一个寄存器的内容和另一个寄存器的内容或立即数进行比较。但不存储结果,只是正确的更改标志(cpsr
)。
一般cmp
做完判断后会进行跳转,后面通常会跟上b
指令!
b 跳转指令
b
本身代表跳转,后面跟标号会有其他操作:
-
bl 标号
:跳转到标号处执行,并且影响lr寄存器的值。用于函数返回。 -
br 寄存器
:根据寄存器中的值跳转。 -
b.gt 标号
:比较结果是 大于(greater than) 执行标号,否则不跳转。 -
b.ge 标号
:比较结果是 大于等于(greater than or equal to) 执行标号,否则不跳转。 -
b.lt 标号
:比较结果是 小于(less than) 执行标号,否则不跳转。 -
b.le 标号
:比较结果是 小于等于(less than or equal to) 执行标号,否则不跳转。 -
b.eq 标号
标号:比较结果是 等于(equal) 执行标号,否则不跳转。 -
b.ne 标号
标号:比较结果是 不等于(not equal) 执行标号,否则不跳转。 -
b.hi 标号
标号:比较结果是 无符号大于 执行标号,否则不跳转。 -
b.hs 标号
:比较结果是 无符号大于等于 执行标号,否则不跳转。 -
b.lo 标号
:比较结果是 无符号小于 执行标号,否则不跳转。 -
b.ls 标号
:比较结果是 无符号小于等于 执行标号,否则不跳转。
⚠️:cmp
后跟的标号条件是else
。
循环
do-while
void func() {
int nSum = 0;
int i = 0;
do {
nSum = nSum + 1;
i++;
} while (i < 100);
}
while
void func() {
int nSum = 0;
int i = 0;
while (i < 100) {
nSum = nSum + 1;
i++;
}
}
for
void func() {
int nSum = 0;
for (int i = 0; i < 100; i++) {
nSum = nSum + 1;
}
}
⚠️
for
和while
的汇编代码相同。
Switch 选择指令
void func(int a) {
switch (a) {
case 1:
printf("case 1");
break;
case 2:
printf("case 2");
break;
case 3:
printf("case 3");
break;
default:
printf("case default");
break;
}
}
在case < 3
的情况下底层汇编就是if-else
。
修改代码让case
多于3
个:
void func(int a) {
switch (a) {
case 1:
printf("case 1");
break;
case 2:
printf("case 2");
break;
case 3:
printf("case 3");
break;
case 4:
printf("case 4");
break;
default:
printf("case default");
break;
}
}
int main(int argc, char * argv[]) {
func(2);
return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
}
分析汇编代码:
TestDemo`func:
0x102c220b4 <+0>: sub sp, sp, #0x20 ; =0x20
0x102c220b8 <+4>: stp x29, x30, [sp, #0x10]
0x102c220bc <+8>: add x29, sp, #0x10 ; =0x10
//参数入栈 2
0x102c220c0 <+12>: stur w0, [x29, #-0x4]
//参数存入w8
-> 0x102c220c4 <+16>: ldur w8, [x29, #-0x4]
//参数和最小case相减 这里是减1 给到 w8(这里相减有可能溢出)
0x102c220c8 <+20>: subs w8, w8, #0x1 ; =0x1
//x8的值存入x9
0x102c220cc <+24>: mov x9, x8
//从0~31取出来给x9,目的是把高32位清零。相当于拿到低32位数据。
0x102c220d0 <+28>: ubfx x9, x9, #0, #32
//比较 x9 和 0x3 的值。这里 0x3是 最大 case 和 最小 case 的差值。相当于 (参数 - 最小case - (最大case - 最小case))
0x102c220d4 <+32>: cmp x9, #0x3 ; =0x3
//x8低32位入栈
0x102c220d8 <+36>: str x9, [sp]
//比较结果无符号大于 跳转default。相当于不在差值区间也就是匹配不了case的情况下直接走default。
0x102c220dc <+40>: b.hi 0x102c22138 ; <+132> at main.m
//在区间中的情况下
//x8的值为 0x102c22150,对应的内存中值为 fffffffa8 -88
0x102c220e0 <+44>: adrp x8, 0
0x102c220e4 <+48>: add x8, x8, #0x150 ; =0x150
//这时候从栈中取数据给x11,栈中目前是x9。x9为(参数 - 最小case = 1)
0x102c220e8 <+52>: ldr x11, [sp]
//x8 + (x11 << 2)的值给到 x10。[x8内存地址 + 4] = 移到下一个位置(0x102c22154) 这里对应的值为-72。⚠️ret的地址为0x102c2214c + 4 = 0x102c22150,0x102c22150 + 4 = 0x102c22154
//这里计算完x10 = -72
0x102c220ec <+56>: ldrsw x10, [x8, x11, lsl #2]
//x8 + 偏移找到的负数 给到 x9 0x102c22150 - 72(0x48) = 0x102C22108
0x102c220f0 <+60>: add x9, x8, x10
//跳转寄存器的地址 0x102C22108 对应 case 2 的地址
0x102c220f4 <+64>: br x9
//case中语句
0x102c220f8 <+68>: adrp x0, 1
0x102c220fc <+72>: add x0, x0, #0xf5d ; =0xf5d
0x102c22100 <+76>: bl 0x102c22588 ; symbol stub for: printf
0x102c22104 <+80>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22108 <+84>: adrp x0, 1
0x102c2210c <+88>: add x0, x0, #0xf64 ; =0xf64
0x102c22110 <+92>: bl 0x102c22588 ; symbol stub for: printf
0x102c22114 <+96>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22118 <+100>: adrp x0, 1
0x102c2211c <+104>: add x0, x0, #0xf6b ; =0xf6b
0x102c22120 <+108>: bl 0x102c22588 ; symbol stub for: printf
0x102c22124 <+112>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22128 <+116>: adrp x0, 1
0x102c2212c <+120>: add x0, x0, #0xf72 ; =0xf72
0x102c22130 <+124>: bl 0x102c22588 ; symbol stub for: printf
0x102c22134 <+128>: b 0x102c22144 ; <+144> at main.m:48:1
0x102c22138 <+132>: adrp x0, 1
0x102c2213c <+136>: add x0, x0, #0xf79 ; =0xf79
0x102c22140 <+140>: bl 0x102c22588 ; symbol stub for: printf
0x102c22144 <+144>: ldp x29, x30, [sp, #0x10]
0x102c22148 <+148>: add sp, sp, #0x20 ; =0x20
0x102c2214c <+152>: ret
0x102c220e0 <+44>: adrp x8, 0
0x102c220e4 <+48>: add x8, x8, #0x150 ; =0x150
以上语句执行后x8
的值:
明显能够看出来是一个负数
0xfffffffa8
,在指向的地址中有一连串的负数。
(lldb) p *(int *)0x102c22150
(int) $5 = -88
(lldb) p (int)0xffffffa8
(int) $6 = -88
(lldb)
核心逻辑:
-
subs w8, w8, #0x1
:参数-最小case -
ubfx x9, x9, #0, #32
:从0~31
取出来给x9
,目的是把高32位清零
。相当于拿到低32位
数据。 -
cmp x9, #0x3
:0x3
为最大case - 最小case
-
b.hi 0x102c22138
: 大于为了判断(参数 - 最小case)是否在(最大case - 最小case)区间。无符号为了处理负数,否则负数永远小于,用无符号就永远大于直接排除掉了。 -
0x102c220e0 <+44>: adrp x8, 0
0x102c220e4 <+48>: add x8, x8, #0x150 ; =0x150
:
找到跳转表(jump table)地址,这张跳转表在编译的时候生成是连续的,永远在程序结束的后面。 -
ldrsw x10, [x8, x11, lsl #2]
:[跳转表地址 +(参数-最小case)<< 2] 得到跳转表中的偏移值。<<2
是因为表中数据是4个
字节。这里为差值 * 4
-
add x9, x8, x10
:上面的到的偏移值加上跳转表地址就得到了case
执行的地址。
计算过程
-
参数 - 最小case
得到表中index
。 -
index
与 (最大case - 最小case
)无符号比较判断是否在区间内。 - 不在区间内直接跳转
defalult
。 - 在区间内
表头地址 + index << 2
获取偏移地址(为负数)。 - 跳转
表头地址 + 偏移地址
执行对应case
逻辑。
⚠️:表中为什么不直接存地址? 1.地址过长 2.有ASLR
的存在
switch
语句的分支比较少的时候(< 3
的时候没有意义)没有必要使用表结构,相当于if
。各个分支常量的差值较大的时候,编译器会在效率还是内存进行取舍,这个时候编译器还是会编译成类似于
if-else
的结构。
比如:100、200、300、400
这种case
还是和if-else
相同,10、20、30、40
会生成一张表。所以在写switch
逻辑的时候最好使用连续的值。至于具体逻辑编译器会根据case
和差值
进行优化选择。case越多,差值越小,数值越连贯 编译器会生成跳转表,否则还是if-else
。在分支比较多的时候:在编译的时候会生成一个表(跳转表每个地址四个字节)。
跳转表中数量为
最大case - 最小case + 1
为一共有多少种可能性。switch
分支的代码是连续的。由于case
是连贯的用空间换时间。