- A+
Bomb Lab
引言:主要任务是“拆炸弹”。所谓炸弹,其实就是一个二进制的可执行文件,要求输入六个字符串,每个字符串对应一个phase。如果字符串输入错误,系统就会提示BOOM!!!
解决这次实验需要将二进制文件反汇编,通过观察理解汇编语言描述的程序行为来猜测符合条件的字符串。可以看出该可执行程序要求从命令行或者文件以 行 为单位读入字符串,每行字符串对应一个phase的输入。如果phase执行完毕,会调用phase_defused 函数表明该 phase 成功搞定。实验共有6个 phase,难度是逐级提升,考点也不尽相同。首先执行命令objdump -d bomb > bomb.txt
得到反汇编代码。
Phase1
考察点:字符串的传递方式
查看bomb.txt
文件的反汇编代码,如下所示,首先栈顶指针向下移动了8个字节,在64位机器下就是一格,然后将0x402400
传递给了esi
寄存器(保存函数参数的寄存器),在0x400ee9
处调用了string_not_equal
函数,调用返回后如果eax
寄存器的值为0的话,我们就会跳转到phase_1 + 0x17 = 400ef7
的位置,否则的话调用explode_bomb
函数就失败了,显然,我们需要让其判断相等,利用gdb
查看0x402400
处的字符串,
0000000000400ee0 <phase_1>: 400ee0: 48 83 ec 08 sub $0x8,%rsp 400ee4: be 00 24 40 00 mov $0x402400,%esi 400ee9: e8 4a 04 00 00 callq 401338 <strings_not_equal> 400eee: 85 c0 test %eax,%eax 400ef0: 74 05 je 400ef7 <phase_1+0x17> 400ef2: e8 43 05 00 00 callq 40143a <explode_bomb> 400ef7: 48 83 c4 08 add $0x8,%rsp 400efb: c3 retq
我们按 s 单步执行时也可以看到这个字符串
我们gdb bomb
时,将上面的字符串输入,可以看到第一关就过了,Border relations with Canada have never been better.
Phase2
考察点:汇编代码中数组的表示
还是首先查看汇编代码
0000000000400efc <phase_2>: 400efc: 55 push %rbp # 保存rbp 400efd: 53 push %rbx # 保存rbx 400efe: 48 83 ec 28 sub $0x28,%rsp # 扩大栈空间,扩大0x28即40个字节 400f02: 48 89 e6 mov %rsp,%rsi # 保存栈顶元素到rsi寄存器 # 对应的C语言格式汇编代码 rsi = rsp; callq read_six_number; if (*rsp == 1) goto 400f30; else callq explode_bomb; goto 400f30; 400f05: e8 52 05 00 00 callq 40145c <read_six_numbers> # 读入六个数字 400f0a: 83 3c 24 01 cmpl $0x1,(%rsp) # 比较rsp必须为1 400f0e: 74 20 je 400f30 <phase_2+0x34> # 如果m[rsp] = 1则跳转到0x400f30 400f10: e8 25 05 00 00 callq 40143a <explode_bomb> # 显然不能执行这条指令 400f15: eb 19 jmp 400f30 <phase_2+0x34> 400f17: 8b 43 fc mov -0x4(%rbx),%eax # 下面是一段循环 eax = M[rbx - 4] # 400f17 - 400f25: eax = *(rbx-4); # 每次取出M[rbx - 4]的值给eax eax += eax; # eax每次都会变为 *(ebx - 4)的二倍 if (eax == *rbx) # rbx为存放的第二个元素的值 即上一个元素的二倍必须等于下一个元素的值 goto 400f25; else callq explode_bomb; 400f1a: 01 c0 add %eax,%eax # eax = eax + eax 400f1c: 39 03 cmp %eax,(%rbx) # if (eax == m[rbx]) goto 0x400f25 400f1e: 74 05 je 400f25 <phase_2+0x29> # 跳过下面的bomb 显然我们需要让eax = m[rbx] 400f20: e8 15 05 00 00 callq 40143a <explode_bomb> 400f25: 48 83 c3 04 add $0x4,%rbx # rbx = rbx + 4 # 400f25 - 400f2e: rbx += 4; # 下一个元素,下一次的 rbx - 4就相当于这一次的rbx了 # 不难看出,下面是一个以rbx为搜索指针,以rbp为结尾信号的循环 if (rbx != rbp) # 只要rbx还没有到rbp rbx其实就相当于for循环的i rbp为6 goto 400f17; # 循环 else goto 400f3c; 400f29: 48 39 eb cmp %rbp,%rbx # if (rbx-rbp!=0) goto 0x400f17,回到循环开始 400f2c: 75 e9 jne 400f17 <phase_2+0x1b> 400f2e: eb 0c jmp 400f3c <phase_2+0x40> # rbx==rbp的话就会到这里 0x400f3c 400f30: 48 8d 5c 24 04 lea 0x4(%rsp),%rbx # rbx = rsp + 4 lea指令传递的是寄存器的内容 400f35: 48 8d 6c 24 18 lea 0x18(%rsp),%rbp # rbp = rsp + 24 400f3a: eb db jmp 400f17 <phase_2+0x1b> # 接着循环 400f3c: 48 83 c4 28 add $0x28,%rsp 400f40: 5b pop %rbx 400f41: 5d pop %rbp 400f42: c3 retq # 六个数分别存放到 rsp rsp+0x4 rsp+0x8 rsp+0xc rsp+0x # read_six_numbers代码 需要我们输入6个数字然后进行比较这里还有如果数字不满足6个的健壮性判断 注意rdi和rsi寄存器已经被用来保存read_six_numbers的两个参数了, 000000000040145c <read_six_numbers>: 40145c: 48 83 ec 18 sub $0x18,%rsp # 6个数, 4 * 6 = 24 = 0x18 401460: 48 89 f2 mov %rsi,%rdx # 在上面的函数中我们将rsp存储到了rsi中 401463: 48 8d 4e 04 lea 0x4(%rsi),%rcx # rsp + 4的地址,存放输入的第二个数 401467: 48 8d 46 14 lea 0x14(%rsi),%rax # 用rax暂存输入的第六个数(rsp + 0x14) 40146b: 48 89 44 24 08 mov %rax,0x8(%rsp) # rsp + 8 = rax = rsi(之前的rsp) + 0x14 401470: 48 8d 46 10 lea 0x10(%rsi),%rax # 存放第五个数,存放到了rax寄存器中 401474: 48 89 04 24 mov %rax,(%rsp) # rsp = rsi(之前的rsp) + 0x10(多的参数存到内存) 401478: 4c 8d 4e 0c lea 0xc(%rsi),%r9 # 存放第四个数 这时候六个寄存器已经用完了 40147c: 4c 8d 46 08 lea 0x8(%rsi),%r8 # 存放第三个数 401480: be c3 25 40 00 mov $0x4025c3,%esi # 给rsi 赋值为 0x4025c3 401485: b8 00 00 00 00 mov $0x0,%eax 40148a: e8 61 f7 ff ff callq 400bf0 <__isoc99_sscanf@plt> # 调用 sscanf 函数读取输入 40148f: 83 f8 05 cmp $0x5,%eax # 比较上面函数的返回值 如果大于5,说明读取的合法 401492: 7f 05 jg 401499 <read_six_numbers+0x3d> 401494: e8 a1 ff ff ff callq 40143a <explode_bomb> # 否则执行炸弹bomb 401499: 48 83 c4 18 add $0x18,%rsp # 恢复堆栈 40149d: c3 retq
这次汇编代码比较长了,分析的结果都写在注释里了,下面通过gdb
动态调试一下,首先b phase_2
然后run
,可以看到四个寄存器均保存了我们的输入
查看寄存器的内容i reg
或者p $eax
,接着查看内存中该地址的内容,/s表示以字符形式显示。可以看到我们输入的内容都是以字符串格式先保存的,然后通过sscanf
格式化输出为了6个整数
执行到调用read_six_numbers
函数之前,我们可以看到该函数的第一个参数传递给了rdi
,即我们输入的字符串,然后将rsi
寄存器置为0,注意这里的反汇编第一个操作数是目的操作数,rsi
保存的是提升堆栈后的rsp
的值,用来保存数组的起始地址。
使用 f 可以查看当前栈信息,利用 bt
指令可以查看函数调用栈之间的关系
一步步执行下去,直到上面不是很懂的mov $0x4025c3, %esi
指令,可以看到该地址的内容如下,其实就是作为sscanf
函数的参数,rdi
寄存器的内容始终都没有被修改,这里也可以看出端倪,输入的字符串保存在rdi
中,此时作为sscanf
函数的第一个参数
调用下面这个函数将rax
寄存器的值设置为6,从而可以下面可以cmp $0x5, %eax
使eax
的值大于5,直接跳转回phase_2
函数。回到phase_2
函数,之后执行的就是一个循环判断了,判断存进去的数是否满足后一个数是前一个数的二倍,rbx
保存的地址是从2开始的,格式化后的6个数字存储到了从ebp
开始的连续的内存空间,查看可见下图
这里的汇编代码比较好理解,第一行rbx = rsp + 4
,第二行rbp = rsp + 0x18
,保存循环结束位置(6个数,每个数4字节,共16+8=24字节),将M[rbx - 4]赋值给eax
,此时eax
保存的即为输入的第一个数 1,然后add eax, eax
是eax
保存的数变为原来的二倍,接着比较eax
保存的值与当前内存中M[rbx]
是否相等,相等的话接下来让rbx + 4
(这里的+4其实就对应数组元素的+1),看是否满足循环终止条件,不满足就跳到上面phase_2+27
继续执行。
动态执行完后就可以看到过掉了
Phase3
考察点:switch语句,索引表的汇编表示
0000000000400f43 <phase_3>: 400f43: 48 83 ec 18 sub $0x18,%rsp ; 首先提升堆栈 400f47: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx ; rcx = rsp + 0xc 400f4c: 48 8d 54 24 08 lea 0x8(%rsp),%rdx ; rdx = rsp + 0x8 保存的是参数 400f51: be cf 25 40 00 mov $0x4025cf,%esi ; 猜测这里与上面一样 是sscanf用到的参数 %% 400f56: b8 00 00 00 00 mov $0x0,%eax ; 用作返回值 400f5b: e8 90 fc ff ff callq 400bf0 <__isoc99_sscanf@plt> ;这个函数与上面的一样,先输入字符串再格式化 400f60: 83 f8 01 cmp $0x1,%eax ; 上面的函数返回值 400f63: 7f 05 jg 400f6a <phase_3+0x27> ; 0x400f6a 跳过爆炸的函数,返回值需要大于1 400f65: e8 d0 04 00 00 callq 40143a <explode_bomb> 400f6a: 83 7c 24 08 07 cmpl $0x7,0x8(%rsp) # rsp + 8存储第一个参数,rsp+c存储第二个 400f6f: 77 3c ja 400fad <phase_3+0x6a>;0x400fad 会爆炸,所以rsp+8<7,用ja当a[0]<0是也no 400f71: 8b 44 24 08 mov 0x8(%rsp),%eax ; eax = rsp + 8 < 7 将第一个数存到eax 400f75: ff 24 c5 70 24 40 00 jmpq *0x402470(,%rax,8) ; 跳转到 M[0x402470+rax*8] 处其实就是400fb9 400f7c: b8 cf 00 00 00 mov $0xcf,%eax 400f81: eb 3b jmp 400fbe <phase_3+0x7b> 400f83: b8 c3 02 00 00 mov $0x2c3,%eax 400f88: eb 34 jmp 400fbe <phase_3+0x7b> 400f8a: b8 00 01 00 00 mov $0x100,%eax 400f8f: eb 2d jmp 400fbe <phase_3+0x7b> 400f91: b8 85 01 00 00 mov $0x185,%eax 400f96: eb 26 jmp 400fbe <phase_3+0x7b> 400f98: b8 ce 00 00 00 mov $0xce,%eax 400f9d: eb 1f jmp 400fbe <phase_3+0x7b> 400f9f: b8 aa 02 00 00 mov $0x2aa,%eax 400fa4: eb 18 jmp 400fbe <phase_3+0x7b> 400fa6: b8 47 01 00 00 mov $0x147,%eax 400fab: eb 11 jmp 400fbe <phase_3+0x7b> 400fad: e8 88 04 00 00 callq 40143a <explode_bomb> 400fb2: b8 00 00 00 00 mov $0x0,%eax 400fb7: eb 05 jmp 400fbe <phase_3+0x7b> 400fb9: b8 37 01 00 00 mov $0x137,%eax ; 如果 a[0]=1时会跳转到这里 eax=0x137 400fbe: 3b 44 24 0c cmp 0xc(%rsp),%eax ; 比较第二个参数与eax的值是否相同 相同的话就过了 400fc2: 74 05 je 400fc9 <phase_3+0x86> 400fc4: e8 71 04 00 00 callq 40143a <explode_bomb> 400fc9: 48 83 c4 18 add $0x18,%rsp 400fcd: c3 retq
下面利用gdb
动态调试,在进入sscanf
函数之前,查看0x4025cf
处存储的要传入sscanf
的字符串,所以可以知道sscanf
这次要读取的是两个整数,不用跟进去猜测sscanf
的作用就知道,它将输入的标准字符串格式化为了两个整数,
一步一步执行下去,知道进入sscanf
函数之前,可以看到与该函数有关的信息如下所示:
退出sscanf
函数后,可以查看该函数将格式化后的数字存储到了哪里,其中0x7fffffffe3d0
为栈指针rsp
的地址,rsp + 4
存储返回地址,rsp + 8
存储输入的第一个数,rsp + c
存放输入的第二个数,
读取堆栈的数据 --两种方式 入栈(edx为栈顶,ebx为栈底) 1、base加偏移 栈底为高地址 读第一个压入的数据:mov esi,dword ptr ds:[ebx-4] 读第四个压入的数据:mov esi,dword ptr ds:[ebx-0x10] 2.top加偏移 栈顶为低地址 读第二个压入的数据:mov edi,dword ptr ds:[edx+8] 读第三个压入的数据:mov edi,dword ptr ds:[edx+4]
rsp和rbp
寄存器不用我们指定内容,是由编译器确定的,接下来是比较rsp + 8 和 0x7
的大小,需要满足rsp + 8 < 0x7
,即第一个参数小于7, 注意这里的ja
指令可以同时处理输入的a[0] > 7
和a[0] < 0
的情况,之后会做一个无条件的jmp *0x402470(,%rax,8)
,根据rax
的值去找对应的语句,猜测是一个以rax
为索引的索引表,类比switch
语句,对于我们输入的每一对数,都会根据第一个数的值去确定第二个数的值。查看以地址0x402470
为基址的索引表的信息如下所示,我们输入的是1,所以取0x400fb9
的地址寻找
当我们跳转到指定地址后可以看到(这里输入的第一个参数为1),将eax
赋值为0x137
,然后比较我们输入的第二个数与这个数是否相等,即我们可以输入的有1 311或者其他六种其他的数。
对应的C形式的伪代码就如下所示:
void phase_3(char* output) { int x, y; if(sscanf(output, "%d %d", &x, &y) <= 1) explode_bomb(); if(x > 7) explode_bomb(); int num; switch(x) { case 0: num = 207; break; case 1: num = 311; break; case 2: num = 707; break; case 3: num = 256; break; case 4: num = 389; break; case 5: num = 206; break; case 6: num = 682; break; case 7: num = 327; } if (num != y) explode_bomb(); return; }
Phase4
考察点:递归函数的参数及返回值
000000000040100c <phase_4>: 40100c: 48 83 ec 18 sub $0x18,%rsp 401010: 48 8d 4c 24 0c lea 0xc(%rsp),%rcx 401015: 48 8d 54 24 08 lea 0x8(%rsp),%rdx 40101a: be cf 25 40 00 mov $0x4025cf,%esi 40101f: b8 00 00 00 00 mov $0x0,%eax 401024: e8 c7 fb ff ff callq 400bf0 <__isoc99_sscanf@plt> # 同样调用了sscanf这个函数 401029: 83 f8 02 cmp $0x2,%eax # 如果上面函数的返回值与2不相等的话就bomb了 40102c: 75 07 jne 401035 <phase_4+0x29> # 跳到 0x401035 即bomb函数 40102e: 83 7c 24 08 0e cmpl $0xe,0x8(%rsp) # 这里需要满足 M[rsp+8] <= 0xe 这样才能跳过bomb 401033: 76 05 jbe 40103a <phase_4+0x2e> # jbe是小于等于 401035: e8 00 04 00 00 callq 40143a <explode_bomb> 40103a: ba 0e 00 00 00 mov $0xe,%edx # edx = 0xe 下面三行应该都是 func4函数的参数 40103f: be 00 00 00 00 mov $0x0,%esi # esi = 0x0 401044: 8b 7c 24 08 mov 0x8(%rsp),%edi # edi = a[0](我们输入的第一个参数的值) 401048: e8 81 ff ff ff callq 400fce <func4> # 这里又调用了一个函数 40104d: 85 c0 test %eax,%eax # 判断 eax 是否为0,即func4函数的返回值是否为0 40104f: 75 07 jne 401058 <phase_4+0x4c> # 如果不为0的话跳转到 bomb,所以需要使eax为0 401051: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) # 比较输入的第二个数和0是否相等 不相等会bomb 401056: 74 05 je 40105d <phase_4+0x51> 401058: e8 dd 03 00 00 callq 40143a <explode_bomb> 40105d: 48 83 c4 18 add $0x18,%rsp 401061: c3 retq ; func4函数 0000000000400fce <func4>: 400fce: 48 83 ec 08 sub $0x8,%rsp # 栈空间扩大8个字节 这里的 0x1就代表地址空间可以多存储一个字节 400fd2: 89 d0 mov %edx,%eax # eax作为sscanf的返回值一直没有修改 edx为第三个参数0xe 400fd4: 29 f0 sub %esi,%eax # eax = eax - esi = 0xe - 0 = 0xe 400fd6: 89 c1 mov %eax,%ecx # ecx = eax = 0xe 400fd8: c1 e9 1f shr $0x1f,%ecx # shr逻辑右移指令 ecx = ecx >> 0x1f = 0 400fdb: 01 c8 add %ecx,%eax # eax = eax + ecx = 0xe 400fdd: d1 f8 sar %eax # sar 算术右移指令 省略了一个操作数 gdb中显示为 1 1110>> 1 =111=7 到这里就可以看出端倪了:eax = (eax + eax >> 0x1f) >> 1 其中 eax = edx - esi = 0xe 400fdf: 8d 0c 30 lea (%rax,%rsi,1),%ecx # ecx = rax + rsi * 1 = 7 + 0 = 7 400fe2: 39 f9 cmp %edi,%ecx # 将我们输入的第一个参数与 7 比较 400fe4: 7e 0c jle 400ff2 <func4+0x24> # 如果7 <= a[0] 跳转到 0x400ff2 执行 400fe6: 8d 51 ff lea -0x1(%rcx),%edx # 否则 a[0] < 7 edx = rcx - 1 = 6 400fe9: e8 e0 ff ff ff callq 400fce <func4> # 递归调用 func4 函数 400fee: 01 c0 add %eax,%eax # 2 * func() 400ff0: eb 15 jmp 401007 <func4+0x39> 400ff2: b8 00 00 00 00 mov $0x0,%eax # eax = 0 400ff7: 39 f9 cmp %edi,%ecx # if(ecx(7) >= edi(a[0])) goto 401007; else func4(); 400ff9: 7d 0c jge 401007 <func4+0x39> 400ffb: 8d 71 01 lea 0x1(%rcx),%esi 400ffe: e8 cb ff ff ff callq 400fce <func4> # 这里如果 a[0] > 7的话也会进行递归 a[0] = 7就是边界条件 401003: 8d 44 00 01 lea 0x1(%rax,%rax,1),%eax # eax = rax + rax + 1 递归调用 401007: 48 83 c4 08 add $0x8,%rsp 40100b: c3 retq 递归函数其实就是 int func(int x, int a, int b) (edi esi edx) { int c = b - a;(c存储在 ecx b在edx里) c = (c + c >> 31) >> 1; 这里c又存储到了 eax 里 int d = c + a; (rax + rsi(用来传递第二个参数)) d 存储在 ecx if (d <= x) goto 0x400ff2 { if (d >= x) return 0; ; 递归调用 注意第二个参数变了 lea 0x1(%rcx),%esi esi = d + 1 return 2 * func4(x, d + 1, b) + 1 } goto 0x400fe6 lea -0x1(%rcx),%edx b = b - 1 return 2 * func(x, a, b - 1); 只有第三个参数变了 别的都没变 }
由上面的分析可知,输入的第二个数一定为0,第一个数作为func4
函数的第一个参数进行了运算,需要满足func4
函数的返回值为0。下面利用gdb
动态调试,可以看到地址0x4025cf
处存储的是% %
,所以我们要输入的参数个数是两个
由上面汇编的分析可知,我们输入的第一个参数需要小于等于14,第二个参数一定为0。执行到调用func4
函数时界面如下,可以看到func4
函数有四个参数,第一个参数就是我们输入的第一个数。分析可知,func4
函数是一个递归函数,递归终止条件为 a[0] >=7
,且下面还有一个判断如果a[0] > 7
也会递归调用函数func4
,所以我们令第一个参数为7即可,如第二张图。
仔细分析后可以得知,func4
函数这个递归函数的代码如下所示
int func4(int x, int a, int b) { int num = b - a; num = (num + num >> 31) / 2; // 31就是 0x1f int c = num + a; if (c <= x) { if (c >= x) return 0; return 2 * func4(x, num+1, b) + 1; } return 2 * func4(x, a, num-1); }
Phase5
考察点:字符数组,循环,ASCII,与运算
0000000000401062 <phase_5>: 401062: 53 push %rbx 401063: 48 83 ec 20 sub $0x20,%rsp # 开辟 32 字节的栈空间 401067: 48 89 fb mov %rdi,%rbx # rbx = rsi 40106a: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax # %fs:0x28保存的是 输入的值 401071: 00 00 401073: 48 89 44 24 18 mov %rax,0x18(%rsp) # rsp + 0x18 = rax(存放的是我们输入的参数) 401078: 31 c0 xor %eax,%eax # eax = 0 40107a: e8 9c 02 00 00 callq 40131b <string_length> # 获取输入的字符串长度(包括空格) 40107f: 83 f8 06 cmp $0x6,%eax # 如果输入的字符串长度不为6 会爆炸 401082: 74 4e je 4010d2 <phase_5+0x70> # 跳转到 0x4010d2 401084: e8 b1 03 00 00 callq 40143a <explode_bomb> 401089: eb 47 jmp 4010d2 <phase_5+0x70> ; 下面这段指令的含义:遍历输入字符串的每一个字符,然后逐次将每个字符与0xf与操作,得到的值做为0x4024b0处字符串的下标 40108b: 0f b6 0c 03 movzbl (%rbx,%rax,1),%ecx # movzbl零扩展指令 move zero byte to double word ; rax此时为0 ecx = rax + rbx (零扩展后再传送) 一般用于使用小字节变量给大字节变量赋值 40108f: 88 0c 24 mov %cl,(%rsp) # M[rsp] = cl(ecx的低8位) = 0x31(1的ASCII码) 401092: 48 8b 14 24 mov (%rsp),%rdx # rdx = M[rsp] = 0x31 401096: 83 e2 0f and $0xf,%edx # edx = edx & 1111 = 110001 & 1111 = 1 401099: 0f b6 92 b0 24 40 00 movzbl 0x4024b0(%rdx),%edx # edx = M[rdx + 0x4024b0] 根据上面与的结果去内存寻找 4010a0: 88 54 04 10 mov %dl,0x10(%rsp,%rax,1) # 将edx的第八位存到后面指定的内存地址 4010a4: 48 83 c0 01 add $0x1,%rax # rax = rax + 1 4010a8: 48 83 f8 06 cmp $0x6,%rax # if(rax!=6) goto 40108b; else goto 4010ae 需要执行6次 4010ac: 75 dd jne 40108b <phase_5+0x29> # 回到上面继续循环 4010ae: c6 44 24 16 00 movb $0x0,0x16(%rsp) # 6次循环结束后 执行到这里 M[rsp+0x16] = 0 4010b3: be 5e 24 40 00 mov $0x40245e,%esi # 函数的参数 4010b8: 48 8d 7c 24 10 lea 0x10(%rsp),%rdi 4010bd: e8 76 02 00 00 callq 401338 <strings_not_equal> 4010c2: 85 c0 test %eax,%eax 4010c4: 74 13 je 4010d9 <phase_5+0x77> 4010c6: e8 6f 03 00 00 callq 40143a <explode_bomb> 4010cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1) 4010d0: eb 07 jmp 4010d9 <phase_5+0x77> 4010d2: b8 00 00 00 00 mov $0x0,%eax # eax = 0 4010d7: eb b2 jmp 40108b <phase_5+0x29> # 又跳转到上面了 4010d9: 48 8b 44 24 18 mov 0x18(%rsp),%rax 4010de: 64 48 33 04 25 28 00 xor %fs:0x28,%rax 4010e5: 00 00 4010e7: 74 05 je 4010ee <phase_5+0x8c> 4010e9: e8 42 fa ff ff callq 400b30 <__stack_chk_fail@plt> 4010ee: 48 83 c4 20 add $0x20,%rsp 4010f2: 5b pop %rbx 4010f3: c3 retq
打开gdb
进行动态调试,首先看到我们输入的长度为6的字符串如下所示
前面的指令都很简单,我们直接看movzbl
这条指令,是一个带零扩展的数据传送指令,在gdb
中查看该指令是如下形式,明确给出了byte
类型,此时rax = 0
,rbx = 0x6038c0(我们输入的字符串的地址)
,执行完这条指令后rcx = 0x31 = 49(1的ASCII码)
,
可以看到0x6038c0
处存储的内容如下,存储的是我们输入的123456
的ASCII码,
这里还发现了python
中对变量做and
运算时的一些有意思的点,python
中所有变量的位操作都是通过强制转换成bool
实现的,严格遵循短路逻辑,只有and
,如果每个表达式都不为假,返回第二个,只有or
,从左往右有一个不为假就返回这个值。
下一条指令mov %cl,(%rsp)
是将ecx
寄存器的低八位赋值给M[rsp]
,存放到栈指针指向的地址,0x7f
开头的往往就是栈所在地址
中间经过一些处理后此时rdx = 49 & 0xf = 1
,然后又是一条零扩展指令movzbl 0x4024b0(%rdx),%edx
,根据上面相与的结果取内存中寻找对应的值赋值给edx
,可以看到这里是0x61
,可以看到内存中存储的字符串为下面的maduiersnfotvbyl
接下来gdb
中的指令更容易理解,mov byte ptr [rsp + rax + 0x10], dl
,将0x61
存储到rsp+0x10
开始的内存地址(即存储变化后的字符到一个栈中新开辟的字符数组里),rax
此时仍为0,先查看未执行前,该地址存储的数为:0x10
,执行之后就变成了了0x61
,
下面首先rax = rax + 1
,然后判断rax != 6
的话回到上面循环之前的操作,可知这里是一个6次的循环,下一次循环,rbx
存储的还是我们输入的字符串的地址,但是rax
就变成1了,取到的字符由之前的0x31
变为了0x32
,直到遍历完6个长度的字符串
总结一下,这一段循环的意义是遍历输入的每一个字符,将每一个字符的ASCII码与0xf
相与,与后的结果作为索引去指定内存地址0x4024b0
处找对应的字符存储起来。循环结束后,我们再往下看,下面就是传递参数,然后调用了strings_not_equal
这个函数
该函数的第一个参数为我们输入的字符串的每一个字符与上0xf
后作为索引去内存中找到的maduiersnfotvbyl
这个字符串的子串,第二个参数为内存中存储的正确结果flyers
,显然我们需要让这两个字符串相等,这样这个函数才会返回0,才会跳过下面的explode_bomb
下面要做的就很清晰了,找到所有与上0xf
后的索引为flyers
在0x4024b0
为起始地址的索引表中的位置即可,索引依次为9 15 14 5 6 7
,我们需要找到与上0xf
后为以上索引的字符,x & 1111 = 1001 x = 1001001或者111001或者1111001
,可以看出后四位即为索引,我们先尝试第一个1001001(73)
,对应的输入为IONEFG
,第二种,对应的输入为9?>567
,第三种输入y(112+15=127)
不是可打印字符。
因此,关键步骤用C语言来写就是
const char g_str[16] = "maduiersnfotvbyl"; void phase_5(char* input) { char str[7]; if (string_length(input) != 6) { explode_bomb(); } // x & 0xf = 9 15 14 5 6 7 // I O N E F G 或 9 ? > 5 6 7 for (int i = 0; i != 6; i++) { str[i] = g_str[input[i] & 0xf]; } str[7] = ' '; if(string_not_equal(str, "flyers") != 0) { explode_bomb(); } }
至此,第五关也就过了
Phase6
考察点:多重循环,链表,结构体,
eax
比较数值时只会比较低32位,冗长的汇编
00000000004010f4 <phase_6>: 4010f4: 41 56 push %r14 4010f6: 41 55 push %r13 4010f8: 41 54 push %r12 4010fa: 55 push %rbp 4010fb: 53 push %rbx ; 传入参数 为read_six_numbers做准备 4010fc: 48 83 ec 50 sub $0x50,%rsp # 提供80字节的栈空间 401100: 49 89 e5 mov %rsp,%r13 # r13 = rsp 401103: 48 89 e6 mov %rsp,%rsi # rsi = rsp 第二个参数 401106: e8 51 03 00 00 callq 40145c <read_six_numbers> # 读取六个数字,这个函数在 p2 见过 40110b: 49 89 e6 mov %rsp,%r14 # r14 = rsp 此时rsp根进去之前其实还是一样的 所以r14=r13 40110e: 41 bc 00 00 00 00 mov $0x0,%r12d # r12d = 0 401114: 4c 89 ed mov %r13,%rbp # rbp = r13 = rsp 401117: 41 8b 45 00 mov 0x0(%r13),%eax # eax = M[r13] = M[rsp] M[rsp]=0x200000001 因为eax为32位寄存器,只能存储下来 0x200000001 的低4字节 即 00000001 所以此时 eax = 0x1 40111b: 83 e8 01 sub $0x1,%eax # eax = eax - 1 40111e: 83 f8 05 cmp $0x5,%eax # eax需要 < 5 401121: 76 05 jbe 401128 <phase_6+0x34> 401123: e8 12 03 00 00 callq 40143a <explode_bomb> 401128: 41 83 c4 01 add $0x1,%r12d # r12d += 1 每次循环加1 40112c: 41 83 fc 06 cmp $0x6,%r12d # 循环终止条件 r12d = 6 401130: 74 21 je 401153 <phase_6+0x5f> 401132: 44 89 e3 mov %r12d,%ebx # rbx = r12d 循环变量暂存到rbx中 401135: 48 63 c3 movslq %ebx,%rax # 符号位扩展,l->q 字到双字, rax = ebx(符号位扩展) 正数用0 401138: 8b 04 84 mov (%rsp,%rax,4),%eax 40113b: 39 45 00 cmp %eax,0x0(%rbp) 40113e: 75 05 jne 401145 <phase_6+0x51> # *rbp 不能等于 eax 401140: e8 f5 02 00 00 callq 40143a <explode_bomb> 401145: 83 c3 01 add $0x1,%ebx 401148: 83 fb 05 cmp $0x5,%ebx 40114b: 7e e8 jle 401135 <phase_6+0x41> # ebx <= 5 继续循环 40114d: 49 83 c5 04 add $0x4,%r13 401151: eb c1 jmp 401114 <phase_6+0x20> ; 上面是一个循环 phase_6(rdi) ; 我们输入的字符串传入到 rdi 中 { r13 = rsp; rsi = rsp; read_six_numbers(rdi, rsi); rdi 为我们输入的字符串, rsi为 %%%%%% r14 = rsp; for (r12 = 0 r12 != 6 r12++) { rbp = r13; eax = *r13; 去内存中找 eax -= 1; if (eax > 5) explode_bomb(); for (ebx = r12 + 1 ebx <= 5 ebx++) { rax = ebx; 符号位扩展 e->r ebx为正数用0填充高位 为负数用1填充高位 eax = *(rsp + rax * 44); if (*rbp == eax) explode_bomb(); } r13 += 4; } } 401153: 48 8d 74 24 18 lea 0x18(%rsp),%rsi 401158: 4c 89 f0 mov %r14,%rax 40115b: b9 07 00 00 00 mov $0x7,%ecx 401160: 89 ca mov %ecx,%edx 401162: 2b 10 sub (%rax),%edx 401164: 89 10 mov %edx,(%rax) 401166: 48 83 c0 04 add $0x4,%rax 40116a: 48 39 f0 cmp %rsi,%rax # rax != rsi 的话继续循环 40116d: 75 f1 jne 401160 <phase_6+0x6c> ; 这里也是一个循环 单独写在这里 rsi = rsp + 0x18; rax = r14; ecx = 0x7; for (rax = r14 rax != rsi rax += 4) { edx = ecx; edx = edx - *rax; *rax = edx; } 40116f: be 00 00 00 00 mov $0x0,%esi 401174: eb 21 jmp 401197 <phase_6+0xa3> 401176: 48 8b 52 08 mov 0x8(%rdx),%rdx 40117a: 83 c0 01 add $0x1,%eax 40117d: 39 c8 cmp %ecx,%eax 40117f: 75 f5 jne 401176 <phase_6+0x82> 401181: eb 05 jmp 401188 <phase_6+0x94> 401183: ba d0 32 60 00 mov $0x6032d0,%edx # ebx = 0x6032d0 401188: 48 89 54 74 20 mov %rdx,0x20(%rsp,%rsi,2) # *(rsp + rsi*2) = rdx 40118d: 48 83 c6 04 add $0x4,%rsi # rsi += 4 401191: 48 83 fe 18 cmp $0x18,%rsi 401195: 74 14 je 4011ab <phase_6+0xb7> # rsi = 0x18的话 就跳到下面了 ; 因为这是最外层开始的循环 所以也可以通过这条语句跳转到的地址确定本次循环的层数,即最内层循环的语句在哪里结束 401197: 8b 0c 34 mov (%rsp,%rsi,1),%ecx 40119a: 83 f9 01 cmp $0x1,%ecx # ecx <= 1 40119d: 7e e4 jle 401183 <phase_6+0x8f> 40119f: b8 01 00 00 00 mov $0x1,%eax 4011a4: ba d0 32 60 00 mov $0x6032d0,%edx 4011a9: eb cb jmp 401176 <phase_6+0x82> ; 小tips 怎么看循环到哪里结束呢 找下面最远的跳到上面的指令往往就是最内层循环 for (esi = 0 rsi != 0x18 rsi += 4) { ecx = *(rsp + rsi); if (ecx <= 1) { edx = 0x6032d0; *(rsp + rsi * 2 + 0x20) = rdx; 这句话两个分支 都会跳转到那里执行 } else { edx = 0x6032d0; for (eax = 1 eax != ecx eax++) { rdx = *(rdx + 8); } *(rsp + rsi * 2 + 0x20) = rdx; } } 4011ab: 48 8b 5c 24 20 mov 0x20(%rsp),%rbx 4011b0: 48 8d 44 24 28 lea 0x28(%rsp),%rax 4011b5: 48 8d 74 24 50 lea 0x50(%rsp),%rsi 4011ba: 48 89 d9 mov %rbx,%rcx 4011bd: 48 8b 10 mov (%rax),%rdx 4011c0: 48 89 51 08 mov %rdx,0x8(%rcx) 4011c4: 48 83 c0 08 add $0x8,%rax 4011c8: 48 39 f0 cmp %rsi,%rax 4011cb: 74 05 je 4011d2 <phase_6+0xde> 4011cd: 48 89 d1 mov %rdx,%rcx 4011d0: eb eb jmp 4011bd <phase_6+0xc9> ; 又是一个循环 rbx = *(rsp + 0x20); rsi = rsp + 0x50; rcx = rbx; for (rax = rsp + 0x28 rax != rsi rax += 8) { rdx = *rax; *(rcx + 0x8) = rdx; rcx = rdx; } 4011d2: 48 c7 42 08 00 00 00 movq $0x0,0x8(%rdx) 4011d9: 00 4011da: bd 05 00 00 00 mov $0x5,%ebp 4011df: 48 8b 43 08 mov 0x8(%rbx),%rax 4011e3: 8b 00 mov (%rax),%eax 4011e5: 39 03 cmp %eax,(%rbx) # *rbx 需要大于 eax 4011e7: 7d 05 jge 4011ee <phase_6+0xfa> 4011e9: e8 4c 02 00 00 callq 40143a <explode_bomb> 4011ee: 48 8b 5b 08 mov 0x8(%rbx),%rbx 4011f2: 83 ed 01 sub $0x1,%ebp 4011f5: 75 e8 jne 4011df <phase_6+0xeb> 4011f7: 48 83 c4 50 add $0x50,%rsp 4011fb: 5b pop %rbx 4011fc: 5d pop %rbp 4011fd: 41 5c pop %r12 4011ff: 41 5d pop %r13 401201: 41 5e pop %r14 401203: c3 retq *(rdx + 0x8) = 0; for (ebp = 0x5 ebp != 0x1 ebp -= 1) { rax = *(rbx + 0x8); eax = *rax; if (*rbx < eax) explode_bomb(); rbx = *(rbx + 0x8); }
代码太长,这里我考虑直接用gdb
分析,前面入栈的六个寄存器的值如下图所示
前面的指令没什么好说的,注意此时r13
和rsi
中保存的都是栈指针rsp
的内容,调试到调用read_six
之前,这个函数需要两个参数,第一个是我们输入的字符串,存储在寄存器rdi
中,第二个返回值的6个int
型元素数组的首地址,存储在寄存器rsi
中,
这个函数内部同样调用了sscanf
,在次就不再详细展开
从该函数退出之后,mov r14, rsp
将rsp
的值又赋给了r14
,注意rsp
进入read_six
函数之后又回来栈是被平衡了的,所以此时 r13 = r14 = rsp
,下一步是mov r12d, 0
,很有意思,r12d
是r12
寄存器的??,接着将r13
中保存的rsp
地址又传给了rbp
,将M[rsp]
传给了eax
,这里要注意,M[rsp] = 0x200000001
,但是eax
寄存器是32位寄存器,只能存储低四个字节,即0x00000001
,之后eax -= 1 变成了0
,
整理一下汇编代码,其对应的C风格如下,分成了以空行间隔的五段代码,
phase_6(rdi) // 我们输入的字符串传入到 rdi 中 { r13 = rsp; rsi = rsp; read_six_numbers(rdi, rsi); // rdi 为我们输入的字符串, rsi为 %%%%%% r14 = rsp; // 总结一下 这个循环的含义:输入6个1-6的数,且不能重复 for (r12 = 0; r12 != 6; r12++) // r12d猜测应该是 r12 的低32位 { rbp = r13; // 这里 rbp = r13 =rsp rsp 存储的就是我们输入的字符串格式化后的数字 eax = *r13; // 去内存中找 第一次 eax = 1 第二次eax= 2 eax -= 1; // eax -= 1 = 0 这里限制了输入的数字必须为 1-6 if (eax > 5) explode_bomb(); for (ebx = r12 + 1; ebx <= 5; ebx++) // 初始 ebx = r12+1 = 1 { rax = ebx; // 符号位扩展 e->r ebx为正数用0填充高位 为负数用1填充高位 rax = ebx 这里是整数 rax = 0x1 eax = *(rsp + rax * 44); // eax = *(rsp + i * 4) 依次遍历 1 2 3 4 5 6 初始rax=1 所以eax = 2 // 之后 *rbp 是不变的,始终是1 但是 eax 会依次遍历所有 2 3 4 5 6 都不会相等 所以最终ebx = 5 eax=6退出循环 if (*rbp == eax) // 2 != 1(*rbp) explode_bomb(); } r13 += 4; // r13 = rsp + 4 相当于下一次循环 rbp + 4 取下一个数判断是否有与它相同的数 } // 下面这段循环的含义:将 a[i] 变为 7 - a[i] 存储到原先a[i]所在的位置 即 esp + 4*i rsi = rsp + 0x18; // 刚好是我们输入的6个字符的下一个位置 24个字节 其实是我们输入的字符数组的 ' ' rax = r14; // rax = r14 = rsp ecx = 0x7; for (rax = r14; rax != rsi; rax += 4) // 遍历所有字符数组 { edx = ecx; // edx = 7 edx = edx - *rax; // edx = 7 - a[i] 同样也是 1-6 的数 *rax = edx; // *rax = rdx 存回内存 } // 下面含义: for (esi = 0; rsi != 0x18; rsi += 4) // 遍历所有字符数组 0x18很明显 遍历7次 刚好到' '结束循环 { ecx = *(rsp + rsi); // 取出对应的字符数组的值 输入的是123456 经过上面变换后成了 654321 if (ecx <= 1) // 只有 输入的为 6 时才会执行 { edx = 0x6032d0; // 此地址处是一个结构体 // 下面的含义:将edx存储的结构体信息 存储到rsp + rsi * 2 + 0x20 处的地址 就是 rsp + 8*i + 0x20 //*(rsp + rsi * 2 + 0x20) = rdx; // 这句话 无论进入哪个分支都会跳转到那里执行 可以写到外面 } else // 只要 输入的 不为6 { edx = 0x6032d0; // 与上面一样 for (eax = 1; eax != ecx; eax++) // 第一个 ecx = 6 循环6次 最终7 - 1存储到了 node6 { // 循环一次 对应node1 循环两次 对应node2 即 node{7-a[i]} rdx = *(rdx + 8); // 从 6032d0(node1) -> 6032e0(node2) } //*(rsp + rsi * 2 + 0x20) = rdx; } *(rsp + rsi * 2 + 0x20) = rdx; // 第一次 rcx=7-1=6 rdx指向node6 rsp+0x20 = node6 } // 这段好像没什么用 rbx = *(rsp + 0x20); // 距离栈指针最近的 node 对应输入的第一个数 node的编号即为 7-a[i] node6 rsi = rsp + 0x50; // node 的结束地址 rcx = rbx; // 保存输入 for (rax = rsp + 0x28; rax != rsi; rax += 8) // rax 从 第二个node 开始遍历 node5 { // 典型的交换操作 rdx = *rax; // 暂存遍历到的node rdx = node5 rdx = node4 *(rcx + 0x8) = rdx; // node5 = node6 node4=node5 rcx = rdx; // node6 = node5 } // 分析: node[7-input[i]]->data >= node[7-input[i+1]]->data *(rdx + 0x8) = 0; // 此时 rdx 保存最后一个node for (ebp = 0x5; ebp != 0x1; ebp -= 1) // 循环5次 { rax = *(rbx + 0x8); // rbx 仍指向距离栈指针最近的node rax = node eax = *rax; // 取出node的值(注意eax,取得是低32位) 只看低32位 node的大小顺序为 3 4 5 6 1 2 if (*rbx < eax) // 如果第一个node的值小于下一个 就会爆炸 所以需要保证输入的数对应的node是降序排列在栈中的 // 即 node6 node5 .. node1 只看低32位 node的大小顺序为 3 4 5 6 1 2 所以我们输入的应该为 4 3 2 1 6 5 (7-a) explode_bomb(); rbx = *(rbx + 0x8); } }
首先确认我们输入的数据的位置,可以看到在rsp rsp + 4
处依次存放着格式化后的数字 1 2 ..,然后根据gdb
看上面的分析即可
到第三段代码时,可以看到程序会将edx
设置为0x6032d0
,查看该地址处信息可知,猜测这里应该是一个结构体node1
,在gdb
中也明确地告诉了我们
执行完mov rdx, qword ptr [rdx + 8]
这条指令后,rdx
存储的内容由 node1
变成了node2
,接着循环又会变成node3
、node4
一直到node
,然后执行qword ptr [rsp + rsi*2 + 0x20], rdx
指令,将node6
存储到了栈上我们输入的字符串的上面。同样,循环6次,找到每一个变化后的 7- a[i] 对应的node,并存储到栈的对应位置。
循环结束后,可以看到栈的情况(右侧的数字即为node->data
):
查看六个node
结构体的信息
经过分析,得知最后一段代码的作用是将结构体的data
按非升序排列,每一个node的data如上图所示,注意我们比较时用的是eax
来存储结构体node->data
,只能存储低32位,所以按node->data
的低32位排序,可以得到降序排列为node 3,4,5,6,1,2
,而7-input[i]
刚好与node
的编号是一一对应的,所以我们的输入为4 3 2 1 6 5
。 终于完成了?