前置任务
- 对bomb进行反汇编
1 | objdump -d bomb > bomb.txt |
查看反编译后的结果可以知道每个阶段输入答案的指针都存入了%rdi(函数第一个参数),并将该参数传入phase_1~6函数
Phase 1
将0x402400移入%esi并调用string_not_equal函数,而string_not_equal函数用于判断两个字符串是否相等,若相等则返回0,否则返回1
349行string_not_equal比较输入字符和0x402400的内容
350~351行将函数的返回值相与并判断结果是否为0,由于只有0&0的结果才为0,所以%rax不为0就会爆炸,即输入字符一定要和0x402400内的内容相等
调用gdb查看0x402400:
答案即为Border relations with Canada have never been better.
Phase 2
360行栈顶指针赋值给%rsi,361行调用read_six_numbers函数,而read_six_number函数读入输入的六个数字并压入栈中
362~364行比较栈顶元素(输入的第一个数)是否和1相等,如果不相等则爆炸,所以第一个数一定为1,跳到0x400f30:
375~377行栈中第二个元素的指针赋值给%rbx,并将栈中第7个元素的指针(终止条件)赋值给%rbp,接着跳回0x400f17进入循环:
%rbx本存着栈中第二个元素的指针,366行将指针向顶部移动,即将栈顶元素赋值给%eax,
367行将%eax元素乘2,即%eax元素变为2
gdb断点测试证实推测:
368行将%eax和栈中第二个元素比较,如果不相等则爆炸,所以输入的第二个元素应该为2
跳到0x400f25:
%rbx中的指针向栈底移动,并和终止条件比较,如果没有到第六个元素则重新返回0x400f17
归纳可得循环中:
1.%eax存着栈中前一个元素的值
2.%eax乘以二
3.%eax和栈中后一个元素比较,如果相等则继续循环直到结束,否则直接爆炸
答案即为1 2 4 8 16 32
Phase 3
385~386行将栈中第个4元素的指针赋值给%rcx(函数第四个参数),第3个元素的指针赋值给%rdx(函数第三个参数)
387~389行将0x4025cf赋值给%esi(函数第二个参数),将%eax置0,并调用sscanf函数
sscanf函数的第一个参数表示输入源,第二个参数表示format参数,返回输入源按照format参数进行匹配的个数,并将匹配的字符串依次放入第三和第四个参数中
调用gdb查看0x4025cf:
390~392行如果函数返回值大于1(等于2)则不会爆炸,所以答案应为两个整数
393行将栈中第三个元素(输入的第一个数字)和7进行比较,如果大于则爆炸,说明第一个数字范围在0~7
395行将输入的第一个数字存入%eax,396行进入跳转表,跳转到0x402470+%rax*8
调用gdb查看跳转表:
根据跳转表查看汇编代码:
%rax从0到7都有一一对应的跳转代码,说明有8种答案
以第一个数字为0为例:
根据跳转表跳转到397行,而在397行将%eax赋值为0xcf,并跳转到0x400fbe,比较%eax(0xcf)是否和栈中第四个元素相等,不相等则爆炸,所以当第一个数字为0时,第二个数字为207
根据跳转表可以推断出其余7个答案:
1 311
2 707
3 256
4 389
5 206
6 682
7 327
Phase 4
前一部分和phase_3完全相同,输入应为两个整数,且放于栈中第三个位置和第四个位置中
455行判断输入的第一个数字是否小于等于14且非负(无符号),否则爆炸
457~460行%edx(第三个参数)赋值为14,%esi(第二个参数)赋值为0,%edi(第一个参数)赋值为输入的第一个数字,%rcx(第四个参数)之前赋值为输入的第二个数字,调用func4函数
先分析func4后的代码:
461~462行判断func4函数的返回值是否为0,不为0则爆炸
463行判断输入的第二个数字是否为0,不为0则爆炸,所以输入的第二个数字一定为0
由于func4函数中包含递归,所以选择反汇编为c语言:
1 | int a = 7, b = 0, c = 14, d = 0, rax; |
发现第一次进入函数中时d的值为7,而当输入的数也为7时为最简单的一种情况,即既不会进入(d>a)的递归,也不会进入(d<a)的递归,直接返回0
除了最简单的7 0这一种情况,第一个数也可以为3或1或0
Phase 5
472行将输入字符串的指针赋值给%rbx,476行清空%eax,477行调用string_length函数,478~480行如果函数返回值不为6则爆炸,说明答案为长度为6的字符串
跳到0x4010d2将%eax清空,跳回0x40108b
%rax等于0,所以482行%ecx为输入字符串的第一个字符,483行将第一个字符存入栈顶,484行~485行将第一个字符存入%rdx并与上0xf(相当于mod16),486行将%rdx+0x4024b0重新赋值给%rdx
调用gdb查看0x4024b0:
即%rdx中存放着maduiersnfotvbyl的第(输入的第一个数mod16)位的字符
487行~490行将%rdx重新存入栈中向下0x10位,%rax加1并和6比较,如果不等于6则跳回0x4024b0进入循环
归纳可得循环后:从栈中向下0x10位起起每个字节(共6个字节)存放着maduiersnfotvbyl的第(输入的第n数mod16)位的字符
491行将0x10起第7个字节置0,492~496行将0x40245e赋值给%esi(第二个参数),将栈向下0x10位地址存入%rdi(第一个参数)并调用strings_not_equal函数,如果返回值不等于0则爆炸
调用gdb查看0x40245e:
说明栈中存放的内容应该为flyers,推得输入的字符串mod16后应该为12,18,17,6,7,8,通过ascii码反推可得输入的答案应该为ionefg
Phase 6
518~519行将栈指针赋值给%r13和%rsi,并调用read_six_numbers函数,所以答案应为6个整数
read_six_number函数将输入的数字依次存入栈中
521~528行将栈指针赋值给%r14和%rbp,%r12d赋值为0(计数器),将栈顶第一个元素(输入的读一个数字)赋值给%eax,将%eax减1并和5比较,如果比5大就爆炸,说明输入的第一个数字不能大于6,且必须大于等于1(jbe比较的是无符号整数)
529~531行将计数器%r12d加上1并判断是否等于6,如果等于6则跳出大循环进入0x401153
如果不等于6则继续进入下一步:
将计数器赋值给%ebx(第二个计数器)和%rax,并将栈中第%rax个元素重新赋值给%eax并和栈顶元素比较,如果相等则爆炸,如果不相等则进入下一步:
538~542行%ebx加上1并和5比较,如果小于等于5则跳回0x401135重复比较栈中第%rax个元素和栈顶元素,如果大于5则将%r13加上4,跳会循环开始0x401114
由此可以归纳上述二重循环代码:
大循环比较输入的每个数字是否在1到6之间,小循环比较大循环中中第i位(i表示大循环了几轮)输入和之后输入的数字是否相等
即输入的每个数字是否在1到6之间且互不相等
c代码:
1 | int input[6] = { 1,2,3,4,5,6 }; |
543~548行将栈中第七个元素存入%rsi(循环终止条件),将%r14(栈指针)赋值给%rax,将7赋值给%ecx,并将(7-栈顶元素)重新赋值回栈中,%rax向栈底移动一个元素,如果%rax不等于%rsi(终止条件)则循环上述操作(输入的整数取相反数并加7)
循环跳出后将%esi(大循环的计数器)清零并跳到0x401197:
%ecx首先存入栈顶元素,并和1比较,如果小于等于1则跳到0x401183:
561~562行将0x6032d0存入栈顶向下0x20个字节的位置,563行将%rsi加上1并和0x18比较,如果相等则跳出循环,否则567行%ecx变为栈中第二个元素,继续和1比较
而若栈中目前遍历到的元素大于1,则跳过跳转代码向下运行
%eax(小循环的计数器,同样用作比较)置1,同样将0x6032d0存入%eax,并跳转到0x401176:
555行%rdx加上0x8并将指针相对应的元素重新赋值给%rdx
调用gdb查看0x6032d0:
发现其为链表,而%rax加上0x8并将指针相对应的元素重新赋值相当于%rdx+0x10
计数器%eax加1并和%ecx(栈中目前遍历到的元素)比较,如果不相等则跳回0x401176进行循环直到计数器%eax和%ecx相等,此时%rdx应该等于0x6032d0+0x10*(栈中目前遍历到的数字-1),跳到0x401188
将%rdx的值赋值到栈中%rsi*2+0x20位置,并将%rsi(大循环计数器)加上0x4,并和0x18比较,如果不相等(没有遍历完六个元素)则继续循环,否则跳出循环进入0x4011ab
由此可以归纳上述二重循环代码:
每次将0x6032d0+0x10*(栈中目前遍历到的数字-1)赋值到栈中(栈中目前遍历到第几个数)*8+0x20位置
即栈中从栈顶起向下(1)0x20,(2)00x28,(3)0x30,(4)0x38,(5)0x40,(6)0x48分别保存了0x6032d0+0x10*(6-ai)
573~575行将栈顶起向下0x20的元素存入%rbx,将栈顶起向下0x28的地址存入%rax,将栈顶起向下0x50的地址存入%rsi(循环终止条件)
576~578行将和%rcx栈中遍历到的指针所对应的节点的next地址(%rcx+0x8)置换为栈中遍历到的后一个指针(%rdx),%rax向后移动,并将%rax和终止条件比较,如果不相等则%rcx也向后移动并继续循环,否则跳到0x4011d2将最终的节点的指针置空
(根据栈中存储的指针对链表进行重排)
586行%ebp置5(计数器),%rax置为栈中第一个元素指针指向的节点的下一个节点指针,%rbx为栈中第一个元素指针,并将(%rax)低四字节和(%rbx)比较,如果(%rbx)比(%rax)小则爆炸,否则继续循环直到计数器为0,即从重排序链表的第二个节点开始依次比较前节点和该节点的低四位大小
为了不爆炸,重排序链表应为降序,由此可反推:
第1个数——>(924排第3位)——>-3+7=4
第2个数——>(691排第4位)——>-4+7=3
第3个数——>(477排第5位)——>-5+7=2
第4个数——>(443排第6位)——>-6+7=1
第5个数——>(332排第1位)——>-1+7=6
第6个数——>(168排第2位)——>-2+7=5
Secret Phase
分析phase_defused代码:
891行清空%rax,892行判断0x603760是否和6相等,如果不相等则跳过了secret_phase函数,所以0x603760一定要等于6
调用gdb在第一阶段结束后加断点,并查看0x603760:
0x603760中存放的数字为1,所以0x603760存放了输入答案的次数,只有通过了phase_6才会进入secret_phase
899行调用sscanf,900行函数返回值如果不等于3就会跳过secret_phase,说明%edi一定要和%esi匹配
调用gdb查看0x402619(%esi)和0x603870(%edi):
0x603870中存放了第四阶段的输入,所以第四阶段应再输入一个字符串
902行将0x402622赋值给%esi,903行将栈中第五个位置指针赋值给%rdi,904~906行调用strings_not_equal函数,比较%rdi和%esi所指向的内容字符串是否相等,不相等则跳过secret_phase函数,所以两者必须相等
调用gdb查看0x402622:
所以第四阶段答案应为7 0 DrEvil
进入隐藏关卡:
查看secret_phase函数代码:
622行将输入的字符串转成相应的10进制长整型,623行将长整型赋值给%rbx,说明应输入一个整数
624~627行如果输入的数字减一后大于1000则爆炸,说明输入的数字应该小于等于1001
628行将%ebx,即输入的数字赋值给第二个参数,将0x6030f0赋值给第一个参数,并调用fun7
先看fun7之后的代码:
631~633行如果%rax不等于2就会爆炸,说明函数返回值一定是2
fun7函数代码:
首先传入的第一个参数如果等于0则会跳到0x401238将%eax赋值为0xffffffff,所以第一个参数不能等于0
由于fun7函数中包含递归,所以选择反汇编为c语言:
1 | int x[]{36,8,50,22,45,6,107,40,1,99,35,7,20,47,1001}; |
由于函数最终%rax必须等于2,所以根据c语言代码确定%rax的最简单的递归顺序应该为0->1(2*rax+1)->2(rax=rax*2)
调用gdb查看%rdi内存放的内容:
从0x6030f0起内存被分成了15块节点,每个节点分成三个部分,第一个部分为数值,第二、三部分为指针
可以推断出第一次节点初值为36,进入rdx > input部分,地址+0x8变为0x603110,值变为8,输入数字小于36
第二次进入rdx < input部分,地址+0x10变为0x603150,值变为22,输入数字大于8
第三次进入rdx=input部分,即输入数字等于22,且22大于8且小于36,所以22为答案
22并不是唯一的答案,20也能通过隐藏关卡