上一篇【为拆炸弹实验做的预习与准备】讲述了一些必须的知识
这个实验是给出可执行文件,要求用gdb反编译出汇编代码,一共有6个关卡,要求玩家输入6个输入,要通过分析汇编代码来找到何种输入不会触发炸弹,然后输入正确的输入信息以拆解炸弹
汇编原码及个人注释
0000000000400e70 <phase_1>:
400e70: 48 83 ec 08 sub $0x8,%rsp // 开栈
400e74: be f8 1a 40 00 mov $0x401af8,%esi // 函数的第二个参数是$0x401af8,猜测是常量字符串
400e79: e8 bf 03 00 00 callq 40123d <strings_not_equal>
400e7e: 85 c0 test %eax,%eax // 返回值等于0,即两个字符串相等,则结束否则bomb
400e80: 74 05 je 400e87 <phase_1+0x17>
400e82: e8 b6 07 00 00 callq 40163d <explode_bomb>
400e87: 48 83 c4 08 add $0x8,%rsp // 退栈
400e8b: c3
前三句是压栈,调用一个strings_not_equal
函数,根据英文阅读理解,判断这个函数是输入和一常量字符串比较判断是否相同,如果不同则返回true(1),如果相同则返回false(0)
然后根据返回值,用je进行跳转,如果eax即函数返回值为0则跳转成功,不会触发string_not_equal
的第二个参数,而第一个参数仍然是phase1调用的第一个参数,应该是输入值,所以我猜测这第二个参数是一个常量字符串的地址,于是gdb试一下,gdb查看对应内存地址的字符串
发现这个常量字符串是Science isn’t about why, it’s about why not? 结合上面的分析不难看出,输入串必须与其相等,方可通关!
答案:
Science isn't about why, it's about why not?
汇编源码及个人注释
0000000000400e8c <phase_2>:
400e8c: 48 89 5c 24 e0 mov %rbx,-0x20(%rsp) // 保存父函数数据,压栈
400e91: 48 89 6c 24 e8 mov %rbp,-0x18(%rsp)
400e96: 4c 89 64 24 f0 mov %r12,-0x10(%rsp)
400e9b: 4c 89 6c 24 f8 mov %r13,-0x8(%rsp)
400ea0: 48 83 ec 48 sub $0x48,%rsp // 开栈
400ea4: 48 89 e6 mov %rsp,%rsi // 栈顶指针给到第二个参数
400ea7: e8 97 08 00 00 callq 401743 <read_six_numbers> // 调用函数读数据
400eac: 48 89 e5 mov %rsp,%rbp // 保存栈指针到rbp
400eaf: 4c 8d 6c 24 0c lea 0xc(%rsp),%r13 // 栈指针+12保存到r13
400eb4: 41 bc 00 00 00 00 mov $0x0,%r12d // r12低32位清零
// ----------------------------------------------------- 循环 -------------------------------------------- //
400eba: 48 89 eb mov %rbp,%rbx // rbp的值保存到rbx,【下文跳转到这】
400ebd: 8b 45 0c mov 0xc(%rbp),%eax // M[rbp+12]保存到eax
400ec0: 39 45 00 cmp %eax,0x0(%rbp) // 看 M[rbp] 和 M[rbp+12] 是否相等
400ec3: 74 05 je 400eca <phase_2+0x3e> // 如果M[rbp]和M[rbp+12]不等则bomb
400ec5: e8 73 07 00 00 callq 40163d <explode_bomb>
400eca: 44 03 23 add (%rbx),%r12d // r12 += M[rbx]
400ecd: 48 83 c5 04 add $0x4,%rbp // rbp += 4,rbp为迭代指针
400ed1: 4c 39 ed cmp %r13,%rbp // r13和rbp如果不等则跳转
400ed4: 75 e4 jne 400eba <phase_2+0x2e>
// ---------------------------------------------------- 循环结束 ------------------------------------------ //
400ed6: 45 85 e4 test %r12d,%r12d // r12d是累加值,累加值为0则bomb
400ed9: 75 05 jne 400ee0 <phase_2+0x54>
400edb: e8 5d 07 00 00 callq 40163d <explode_bomb>
400ee0: 48 8b 5c 24 28 mov 0x28(%rsp),%rbx // 退栈
400ee5: 48 8b 6c 24 30 mov 0x30(%rsp),%rbp
400eea: 4c 8b 64 24 38 mov 0x38(%rsp),%r12
400eef: 4c 8b 6c 24 40 mov 0x40(%rsp),%r13
400ef4: 48 83 c4 48 add $0x48,%rsp
400ef8: c3 retq
可以清晰的看到,一开始先读取6个数(英文阅读理解可知),然后使用rbp作为迭代指针,rbp+12为迭代终点,一共迭代三次,而每次我们都将M[rbp] 和 M[rbp+12] 处的数据比对,如果不等则bomb,而且一个int刚好是4字节,三个就是12!结合刚刚的读取6个数字,可以大胆的猜测,这是在判断 a[i] 和 a[i+3] 是否相等!
r12之前清零,但是每次又 += M[rbx],猜测 是在计算累加和,根据后文的test $r12d $r12d,可以知道,如果三个数累加起来是0,即使a[i] = a[i+3] 也会炸,所以可以拆弹了!
因为1145141919810是好数字,不妨输入 114 514 1919810 114 514 1919810,可以满足条件!
答案:
114 514 1919810 114 514 1919810
a b c a b c 且 (a+b+c)!=0 即可
0000000000400ef9 <phase_3>:
400ef9: 48 83 ec 18 sub $0x18,%rsp // 开栈
400efd: 48 8d 4c 24 08 lea 0x8(%rsp),%rcx // rcx = rsp+8 第四个参数
400f02: 48 8d 54 24 0c lea 0xc(%rsp),%rdx // rdx = rsp+12 第三个参数
400f07: be be 1e 40 00 mov $0x401ebe,%esi // 第二个参数 gdb查找这个字符串的值是 "%d %d"
400f0c: b8 00 00 00 00 mov $0x0,%eax // 返回值低32字节置0
400f11: e8 9a fb ff ff callq 400ab0 <__isoc99_sscanf@plt> // 读数据
400f16: 83 f8 01 cmp $0x1,%eax // scanf返回不为1则bomb
400f19: 7f 05 jg 400f20 <phase_3+0x27>
400f1b: e8 1d 07 00 00 callq 40163d <explode_bomb>
400f20: 83 7c 24 0c 07 cmpl $0x7,0xc(%rsp) // 7 < M[rsp+12](第一个%d) 则跳转到bomb
400f25: 77 3c ja 400f63 <phase_3+0x6a>
400f27: 8b 44 24 0c mov 0xc(%rsp),%eax // M[rsp+12](第一个%d)存到eax
400f2b: ff 24 c5 60 1b 40 00 jmpq *0x401b60(,%rax,8) // 跳转到 M[0x401b60+ 8*rax] 地址
400f32: b8 17 02 00 00 mov $0x217,%eax // if rax=0则到这里,eax=535
400f37: eb 3b jmp 400f74 <phase_3+0x7b>
400f39: b8 d6 00 00 00 mov $0xd6,%eax // if rax=2则到这里,eax=214
400f3e: eb 34 jmp 400f74 <phase_3+0x7b>
400f40: b8 53 01 00 00 mov $0x153,%eax // if rax=3则到这里,eax=339
400f45: eb 2d jmp 400f74 <phase_3+0x7b>
400f47: b8 77 00 00 00 mov $0x77,%eax // if rax=4则到这里,eax=119
400f4c: eb 26 jmp 400f74 <phase_3+0x7b>
400f4e: b8 60 01 00 00 mov $0x160,%eax // if rax=5则到这里,eax=352
400f53: eb 1f jmp 400f74 <phase_3+0x7b>
400f55: b8 97 03 00 00 mov $0x397,%eax // if rax=6则到这里,eax=919
400f5a: eb 18 jmp 400f74 <phase_3+0x7b>
400f5c: b8 9c 01 00 00 mov $0x19c,%eax // if rax=7则到这里,eax=412
400f61: eb 11 jmp 400f74 <phase_3+0x7b>
400f63: e8 d5 06 00 00 callq 40163d <explode_bomb> // bomb
400f68: b8 00 00 00 00 mov $0x0,%eax
400f6d: eb 05 jmp 400f74 <phase_3+0x7b>
400f6f: b8 9e 03 00 00 mov $0x39e,%eax // if rax=1则到这里,eax=926
400f74: 3b 44 24 08 cmp 0x8(%rsp),%eax // switch转到这里,如果第二个%d不等于eax,则bomb
400f78: 74 05 je 400f7f <phase_3+0x86>
400f7a: e8 be 06 00 00 callq 40163d <explode_bomb>
400f7f: 48 83 c4 18 add $0x18,%rsp // 退栈
400f83: c3 retq
先关注scanf调用前,传入第三第四个参数了,他们存在rsp+8和rsp+12位置,从间距来看应该是int(4字节嘛),然后又向esi传入常量,推测是scanf格式确定的字符串,查看,果然,是“%d %d”,基本可以确定输入的是两个整数
然后注意到返回后判断了第一个数是否大于7,如果是则bomb,那么可以确定第一个数取值为0~7
然后有一个变址跳转,目的地址是M[0x401b60 + rax*8]
,rax是第一个%d,然后看看跳转之后的地址,在gdb中打印,分别对应第一个输入为0~7
时跳转的目的地址
跳转之后,为第一个输入的变量赋新值,然后判断第二个%d输入(rsp+8)和他是否相等,不等则bomb,那么思路很明确了,就是第一个值决定switch,然后判断第二个值是否对应,观察switch可知,这里有8个答案
答案(选一即可)
0 535
1 926
2 214
3 339
4 119
5 352
6 919
7 412
0000000000400fc1 <phase_4>:
400fc1: 48 83 ec 18 sub $0x18,%rsp // 开栈
400fc5: 48 8d 54 24 0c lea 0xc(%rsp),%rdx // rdx=rsp+12,设置第三个参数为rsp+12,即存放输入
400fca: be c1 1e 40 00 mov $0x401ec1,%esi // 根据上面几题的经验应该是scanf的格式字串,为%d
400fcf: b8 00 00 00 00 mov $0x0,%eax // eax置零
400fd4: e8 d7 fa ff ff callq 400ab0 <__isoc99_sscanf@plt>
400fd9: 83 f8 01 cmp $0x1,%eax // scanf返回值是否为1?不为1则bomb
400fdc: 75 07 jne 400fe5 <phase_4+0x24>
400fde: 83 7c 24 0c 00 cmpl $0x0,0xc(%rsp) // 进一步比较输入的int是否大于0
400fe3: 7f 05 jg 400fea <phase_4+0x29>
400fe5: e8 53 06 00 00 callq 40163d <explode_bomb> // 输入的int为0也bomb
400fea: 8b 7c 24 0c mov 0xc(%rsp),%edi // 如果输入int大于0则到这里,设置fun4第一个参数为输入的int
400fee: e8 91 ff ff ff callq 400f84 <func4>
400ff3: 83 f8 37 cmp $0x37,%eax // 返回值是否为55?
400ff6: 74 05 je 400ffd <phase_4+0x3c>
400ff8: e8 40 06 00 00 callq 40163d <explode_bomb> // 返回值不为55则bomb
400ffd: 48 83 c4 18 add $0x18,%rsp // 返回值为55则退栈
401001: c3 retq
同样观察参数,发现这次scanf只读一个参数,根据前面的经验不难看出,0x401ec1存储的是scanf的常字符串,那么gdb查看他,发现是一个int
然后往下看,读取的int必须大于0,再看,使用我们输入的int做了一个函数fun4的参数,如果fun4的返回值为55则退栈,否则bomb,看来问题聚焦在fun4上了,查看fun4:
0000000000400f84 <func4>:
400f84: 48 89 5c 24 f0 mov %rbx,-0x10(%rsp)
400f89: 48 89 6c 24 f8 mov %rbp,-0x8(%rsp)
400f8e: 48 83 ec 18 sub $0x18,%rsp // 开栈
400f92: 89 fb mov %edi,%ebx
400f94: b8 01 00 00 00 mov $0x1,%eax // 返回值置1
400f99: 83 ff 01 cmp $0x1,%edi // 如果第一个参数为1,直接返回(此时返回值是1)
400f9c: 7e 14 jle 400fb2 <func4+0x2e>
400f9e: 8d 7b ff lea -0x1(%rbx),%edi // 设置第一个参数 edi = rbx -1,再递归调用fun4
400fa1: e8 de ff ff ff callq 400f84 <func4>
400fa6: 89 c5 mov %eax,%ebp // 返回值保存到 ebp
400fa8: 8d 7b fe lea -0x2(%rbx),%edi // 设置第一个参数 edi = rbx -2,再递归调用fun4
400fab: e8 d4 ff ff ff callq 400f84 <func4>
400fb0: 01 e8 add %ebp,%eax // ebp += 返回值
400fb2: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx // 退栈
400fb7: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp
400fbc: 48 83 c4 18 add $0x18,%rsp
400fc0: c3 retq
Fun4一进来判断输入,如果是1返回1,如果不是,返回fun4(x-1)+fun4(x-2),显然这是斐波那契数列,那么fib的第几项等于55呢?应该是9,看:[1,1,2,3,5,8,13,21,55]
答案:
9
0000000000401002 <phase_5>:
401002: 48 83 ec 18 sub $0x18,%rsp // 开栈
401006: 48 8d 4c 24 08 lea 0x8(%rsp),%rcx // 第四个参数 += M[rsp+8]
40100b: 48 8d 54 24 0c lea 0xc(%rsp),%rdx // 第三个参数 += M[rsp+12]
401010: be be 1e 40 00 mov $0x401ebe,%esi // 第二个参数,gdb查看内容为"%d %d"
401015: b8 00 00 00 00 mov $0x0,%eax // eax清零
40101a: e8 91 fa ff ff callq 400ab0 <__isoc99_sscanf@plt>
40101f: 83 f8 01 cmp $0x1,%eax // 如果eax<=1那么bomb
401022: 7f 05 jg 401029 <phase_5+0x27>
401024: e8 14 06 00 00 callq 40163d <explode_bomb>
401029: 8b 44 24 0c mov 0xc(%rsp),%eax // eax = M[rsp+12],即输入的第一个int
40102d: 83 e0 0f and $0xf,%eax // eax &= f,即保留低4位
401030: 89 44 24 0c mov %eax,0xc(%rsp) // M[rsp+12] = eax ,即保留eax的低4位
401034: 83 f8 0f cmp $0xf,%eax // 如果eax = 15那么bomb
401037: 74 2c je 401065 <phase_5+0x63>
401039: b9 00 00 00 00 mov $0x0,%ecx // ecx清零
40103e: ba 00 00 00 00 mov $0x0,%edx // edx清零
// -------------------------------------------------- 循环 ---------------------------------------------------------//
401043: 83 c2 01 add $0x1,%edx // edx += 1 ,记录循环次数
401046: 48 98 cltq // 拓展字节
401048: 8b 04 85 a0 1b 40 00 mov 0x401ba0(,%rax,4),%eax // eax = M[0x401ba0+rax*4] (eax rax是一个寄存器)
40104f: 01 c1 add %eax,%ecx // ecx += eax,记录累加值
401051: 83 f8 0f cmp $0xf,%eax // 如果eax不等于15则继续
401054: 75 ed jne 401043 <phase_5+0x41>
// ------------------------------------------------ 循环结束 -------------------------------------------------------//
401056: 89 44 24 0c mov %eax,0xc(%rsp) // M[rsp+12] = eax,即=15
40105a: 83 fa 0c cmp $0xc,%edx // 如果edx(循环次数)不等于12则bomb
40105d: 75 06 jne 401065 <phase_5+0x63>
40105f: 3b 4c 24 08 cmp 0x8(%rsp),%ecx // M[rsp+8]即输入的第二个int,不等于ecx则bomb
401063: 74 05 je 40106a <phase_5+0x68>
401065: e8 d3 05 00 00 callq 40163d <explode_bomb>
40106a: 48 83 c4 18 add $0x18,%rsp // 退栈
40106e: c3 retq
一开始还是观察scanf的输入,两个lea语句将scanf的第三第四个参数确定,gdb esi的地址查看输入格式,发现是%d%d两个int
然后发现第一个输出是会被截断到0~15的,如果第一个输入等于15那么直接炸
关注之后的语句,发现有一个循环,edx每次++,eax每次指向内存中的一个地址,因为eax和rax是同一个,相当于 k = next[k]
这种数组跳转语句,而且偏移的单位是4字节,刚好是一个int,这更加坚定了这里出现数组跳转语句的判断了,gdb查看这个数组,果然
那么知道了跳转数组之后,发现跳转次数只能是12,而且,第二个输入的int必须等于每次跳转的目标下标的累加和,而且最后一次跳转的结果是15,所以画跳转路径:
要想跳转12次才到15,这里需要从15反推路径,推出了是从7下标开始,跳转12次即可到达15,而将他们加和起来,累加和是
11+13+9+4+8+0+10+1+2+14+6+15 = 93
答案
7 93
这题我没看懂,但是最后有一个比较,如果不等则bomb,我是通过gdb读取cmp的比较值,来猜输入是哪个数字的,属于投机取巧法
汇编源码及个人注释
00000000004010d9 <phase_6>:
4010d9: 48 83 ec 08 sub $0x8,%rsp // 开栈
4010dd: ba 0a 00 00 00 mov $0xa,%edx // 第三个参数=10,strtol转换基数为10
4010e2: be 00 00 00 00 mov $0x0,%esi // 第二个参数=0
4010e7: e8 94 fa ff ff callq 400b80 <strtol@plt> // 将字符串根据基数转换为长整型数
4010ec: 89 05 8e 16 20 00 mov %eax,0x20168e(%rip) # 602780 <node0> // M[20168e+rip] = eax(返回值)
4010f2: bf 80 27 60 00 mov $0x602780,%edi // edi为第一个参数,这个位置应该是输入的字符串
4010f7: e8 73 ff ff ff callq 40106f <fun6>
4010fc: 48 8b 40 08 mov 0x8(%rax),%rax // rax = M[rax+8] rax:返回值
401100: 48 8b 40 08 mov 0x8(%rax),%rax // rax = M[rax+8]
401104: 48 8b 40 08 mov 0x8(%rax),%rax // rax = M[rax+8]
401108: 8b 15 72 16 20 00 mov 0x201672(%rip),%edx # 602780 <node0> // edx = M[201672+rip]
40110e: 39 10 cmp %edx,(%rax) // 如果edx 不等于 rax则bomb
401110: 74 05 je 401117 <phase_6+0x3e>
401112: e8 26 05 00 00 callq 40163d <explode_bomb>
401117: 48 83 c4 08 add $0x8,%rsp // 退栈
40111b: c3 retq
这里一开始调用strtol,同时传入一个字符串,这个字符串的位置是0x602780
推测是让我们输入一个字符串,然后转换为long,然后这个long会传入fun6,一顿操作之后,附带几次寻址,比较 rdx 和M[rax] 的值,如果不相等则炸,那么关键点就在fun6了,查看fun6代码:
000000000040106f <fun6>:
40106f: 4c 8b 47 08 mov 0x8(%rdi),%r8 // 第五个参数 = M[rdi(第一个参数)+8]
401073: 48 c7 47 08 00 00 00 movq $0x0,0x8(%rdi) // M[rdi+8] = 0
40107a: 00
40107b: 48 89 f8 mov %rdi,%rax // rax = rdi
40107e: 48 89 f9 mov %rdi,%rcx // rcx = rdi
401081: 4d 85 c0 test %r8,%r8 // 第五个参数不等于0则跳转
401084: 75 40 jne 4010c6 <fun6+0x57>
401086: 48 89 f8 mov %rdi,%rax // rax = rdi(第一个参数)
401089: c3 retq // 退栈
// -------------------------------------------------- 循环 ---------------------------------------------------------//
40108a: 48 89 d1 mov %rdx,%rcx // rcx = rdx
40108d: 48 8b 51 08 mov 0x8(%rcx),%rdx // rdx = M[rcx+8],即rdx=M[rdx+8]
401091: 48 85 d2 test %rdx,%rdx // 如果rdx=0则跳转
401094: 74 09 je 40109f <fun6+0x30>
401096: 39 32 cmp %esi,(%rdx) // M[rdx]
401098: 7f f0 jg 40108a <fun6+0x1b>
// ------------------------------------------------ 循环结束 -------------------------------------------------------//
40109a: 48 89 cf mov %rcx,%rdi // rdi = rcx
40109d: eb 03 jmp 4010a2 <fun6+0x33>
40109f: 48 89 cf mov %rcx,%rdi // 如果rdx=0则到这rdi = rcx
// -------------------------------------------------- 循环 ---------------------------------------------------------//
4010a2: 48 39 d7 cmp %rdx,%rdi // 如果rdi = rdx
4010a5: 74 06 je 4010ad <fun6+0x3e>
4010a7: 4c 89 47 08 mov %r8,0x8(%rdi)
4010ab: eb 03 jmp 4010b0 <fun6+0x41>
4010ad: 4c 89 c0 mov %r8,%rax // 如果rdi=rdx则到这,rax=第五个参数
4010b0: 49 8b 48 08 mov 0x8(%r8),%rcx // rcx = M[r8+8]
4010b4: 49 89 50 08 mov %rdx,0x8(%r8) // M[r8+8] = rdx
4010b8: 48 85 c9 test %rcx,%rcx // 如果rcx=0则跳至结束
4010bb: 74 1a je 4010d7 <fun6+0x68>
4010bd: 49 89 c8 mov %rcx,%r8 // r8 = cax
4010c0: 48 89 c1 mov %rax,%rcx // rcx = rax
4010c3: 48 89 c7 mov %rax,%rdi // rdi = rax
4010c6: 48 89 ca mov %rcx,%rdx // 如果第五个参数不等于0那么到这 rdx=rcx
4010c9: 48 85 c9 test %rcx,%rcx
4010cc: 74 d4 je 4010a2 <fun6+0x33> // 如果rcx=0那么跳转到4010a2
// ------------------------------------------------ 循环结束 -------------------------------------------------------//
4010ce: 41 8b 30 mov (%r8),%esi // esi = M[r8]
4010d1: 39 31 cmp %esi,(%rcx)
4010d3: 7f b8 jg 40108d <fun6+0x1e> // 如果esi
4010d5: eb cb jmp 4010a2 <fun6+0x33>
4010d7: f3 c3 repz retq
看到fun6汇编的瞬间我双手离开键盘,我发现写完注释我还是看不懂,于是打算还是从判断语句反推
我通过在cmp语句设置断点(如下图),然后查看rdx 和 M[rax] 的值,发现rdx是我的输入(1234567),M[rax]是673,站在上帝视角,我猜测要想避免炸弹,输入必须等于673
于是直接投机取巧输入673 。。。。。。过了
答案:
673
复习到了很多知识,比如寄存器的功能,寻址方式,指令,gdb的使用等
还了解了调用者保存和被调用者保存,知了晓递归调用时如何保存状态,比如调用者保存就是父函数自己想办法保存数据,而被调用者保存则是子函数帮忙保存父函数的数据
其中最常用的就是 imm(rb) 这种,翻译过来就是 M[imm + R(rb)]
还复习了各种指令,用的最多的当属mov和add,jump指令
run 执行
si 单步执行
b 设置断点,可以在函数调用时中断,即
b func1
或者在指定地址处中断,比如
b *0x12f3de
在指定位置设置断点可以更好的判断触发bomb的条件,可以打印对应的寄存器看看值是多少,这样能够有效避免bomb,第六题我就是用这个技巧投机取巧
p 查看数据
p (char*)0x123fed 查看对应地址的字符串,在分析scanf的输入格式时很有效
还可以查看某个地址对应的数组,比如
p *0x30fed4@7,查看0x30fed4往后对应7个数字,查看数组在第五题尤其重要
查看寄存器
p $rdx
查看寻址结果
p *(0x3014fd) 或者 p *($rdx)