这是CSAPP的bomblab,对打pwn的新手补补基础还是非常有用的,尤其是各种汇编操作和IDA Pro里各种各样的奇妙语法,更是让我这个菜鸡大开眼界(还能这么坑……)
前五关非常的常规,我们通过汇编跟反汇编都看一下。
第六关我不行了,就通过反汇编的C代码走一走。
做了一个晚上加半个早上,终于搞定了,是我太菜……
phase 1
汇编
1 2 3 4 5 6 7 8 9
   | 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   
   | 
 
其中0x402400这个地址很奇妙,我们用gdb跟进去看一看:
这里的test跟je两个汇编语句是连接在一起的,一般就像是这样用的:
1 2
   | test %rax, %rax je 0x??????
   | 
 
test语句本质就是一个and,不过用test的话不会去改变%rax的值,而会直接放到下面来进行比较。
这两句汇编的意思就是%rax值等于0时就跳转,否则不跳转,执行下一条命令。
就是比较字符串相等就可以进入下一步了。
所以只需要保证输入的字符串是"Border relations with Canada have never been better.",就可以了。
IDA
用IDA的话一眼看出来,就不用分析了。
phase 2
汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
   | 0000000000400efc <phase_2>:   400efc:	55                   	push   %rbp   400efd:	53                   	push   %rbx   400efe:	48 83 ec 28          	sub    $0x28,%rsp   400f02:	48 89 e6             	mov    %rsp,%rsi   400f05:	e8 52 05 00 00       	callq  40145c <read_six_numbers>   400f0a:	83 3c 24 01          	cmpl   $0x1,(%rsp)   400f0e:	74 20                	je     400f30 <phase_2+0x34>   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   400f1a:	01 c0                	add    %eax,%eax   400f1c:	39 03                	cmp    %eax,(%rbx)   400f1e:	74 05                	je     400f25 <phase_2+0x29>   400f20:	e8 15 05 00 00       	callq  40143a <explode_bomb>   400f25:	48 83 c3 04          	add    $0x4,%rbx   400f29:	48 39 eb             	cmp    %rbp,%rbx   400f2c:	75 e9                	jne    400f17 <phase_2+0x1b>   400f2e:	eb 0c                	jmp    400f3c <phase_2+0x40>   400f30:	48 8d 5c 24 04       	lea    0x4(%rsp),%rbx   400f35:	48 8d 6c 24 18       	lea    0x18(%rsp),%rbp   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   
   | 
 
按照汇编来分析,stack frame的构造如下:
1 2 3 4 5 6 7 8 9
   | 0x00(rsp) 0x04(rbp) 0x08      rbp 0x1c          [5] 0x10          [4] 0x14          [3] 0x18          [2] 0x1c          [1] <- rbx 0x20 rsp  rsi [0] <- rax
   | 
 
在从rsp - 0x20到rsp - 0x08遍历的过程中,rax永远在栈上比rbx的地址小个4,也就是一个int的位置。每次check之后依次往后移一位。
我们需要满足的是两倍的rax等于rbx,也就是我们输入的数列是成倍增长的。
还有一个条件:读入到rsp - 0x20,也就是第一个数字,必须是1。
所以最终的输入就是1 2 4 8 16 32。
IDA
输入六个整数,需要符合里面的这个规则:
1 2 3 4 5 6 7 8
   | do {   result = (unsigned int)(2 * *((_DWORD *)v2 - 1));   if ( *(_DWORD *)v2 != (_DWORD)result )     explode_bomb();   v2 += 4; } while(v2 != v5);
   | 
这里需要注意:在第三行的代码里,v2先被强制类型转换为DWORD*,然后再执行减1的操作。
因为v2的指针类型在减1之前已经确定,所以实际上*((_DWORD *)v2 - 1)就相当于*(_DWORD *)(v2 - 4),也就是数组里面的上一个元素。
所以六个整数,只需要满足后一个是前一个的两倍,就可以了。
phase 3
IDA
非常简单,switch里面提供了8个配套选择,任选一个即可过关。
汇编
然而这个关卡的话看汇编会比较难看出来。这也是这一关的价值所在。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
   | 0000000000400f43 <phase_3>:   400f43:	48 83 ec 18          	sub    $0x18,%rsp   400f47:	48 8d 4c 24 0c       	lea    0xc(%rsp),%rcx   400f4c:	48 8d 54 24 08       	lea    0x8(%rsp),%rdx   400f51:	be cf 25 40 00       	mov    $0x4025cf,%esi   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>   400f65:	e8 d0 04 00 00       	callq  40143a <explode_bomb>   400f6a:	83 7c 24 08 07       	cmpl   $0x7,0x8(%rsp)   400f6f:	77 3c                	ja     400fad <phase_3+0x6a>   400f71:	8b 44 24 08          	mov    0x8(%rsp),%eax   400f75:	ff 24 c5 70 24 40 00 	jmpq   *0x402470(,%rax,8)   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   400fbe:	3b 44 24 0c          	cmp    0xc(%rsp),%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   
   | 
 
stack frame大概长这样:
1 2 3 4 5 6 7
   | 0x00(rsp) 0x04 0x08 0x0c rcx [1] 0x10 rdx [0] 0x14 0x18 rsp
   | 
发现了第一个奇妙地址0x4025cf,我们也用gdb看看:
害……
不过这里有另一个奇妙地址,其实这句话就是switch汇编实现的核心:
1
   | 400f75:	ff 24 c5 70 24 40 00 	jmpq   *0x402470(,%rax,8)
   | 
 
穿插复习下括号里两个数字和三个数字的表示法:
- (a, b) = a + b
 
- (a, b, c) = a + b * c
 
这种括号的表示方法不只在lea指令里面能用,在其他指令里也能见到。
再查一查0x402470这个地址的值,还有后面几个地址的值:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
   | (gdb) p/x *0x402470 $9 = 0x400f7c (gdb) p/x *0x402478 $10 = 0x400fb9 (gdb) p/x *0x402480 $11 = 0x400f83 (gdb) p/x *0x402488 $12 = 0x400f8a (gdb) p/x *0x402490 $13 = 0x400f91 (gdb) p/x *0x402498 $14 = 0x400f98 (gdb) p/x *0x4024a0 $15 = 0x400f9f (gdb) p/x *0x4024a8 $16 = 0x400fa6 (gdb) p/x *0x4024b0 $17 = 0x7564616d
   | 
 
可以发现,从0x402470开始储存的是一个指针数组,因为是64位,所以地址自然是8个字节8个字节间隔的。
并且,这个数组里的指针指向的值,都是phase_3函数的mov指令,即对应了switch语句中的不同分支。
说句题外话,之所以switch中每个case的最后一般都得加一个break,就是因为在底层就是这样实现的。如果不加break,在每一句执行后就不会jmp出这个switch的判断,在这里就可能%eax被多次赋值。所以该加break还是得加的哦!
一一对应后,可以梳理出能够通过的8个输出:
1 2 3 4 5 6 7 8
   | 0: 0xcf 1: 0x137 2: 0x2c3 3: 0x100 4: 0x185 5: 0xce 6: 0x2aa 7: 0x147
   | 
 
任选其一,就能通过第三关。
phase 4
IDA
这个部分我们需要保证第一个读入的整数v3小于等于14的同时,func4(v3, 0, 14)也等于0,第二个读入的整数v4也要等于0。
而要使这个函数的返回值为0,只需要让a1 = v3 = (14 - 0) / 2 + 0 = 7。
汇编
然而汇编并不像IDA反汇编出来的这样清晰,这一关一眼看上去可能眼花,认真看就好了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | 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>   401029:	83 f8 02             	cmp    $0x2,%eax   40102c:	75 07                	jne    401035 <phase_4+0x29>   40102e:	83 7c 24 08 0e       	cmpl   $0xe,0x8(%rsp)   401033:	76 05                	jbe    40103a <phase_4+0x2e>   401035:	e8 00 04 00 00       	callq  40143a <explode_bomb>   40103a:	ba 0e 00 00 00       	mov    $0xe,%edx   40103f:	be 00 00 00 00       	mov    $0x0,%esi   401044:	8b 7c 24 08          	mov    0x8(%rsp),%edi   401048:	e8 81 ff ff ff       	callq  400fce <func4>   40104d:	85 c0                	test   %eax,%eax   40104f:	75 07                	jne    401058 <phase_4+0x4c>   401051:	83 7c 24 0c 00       	cmpl   $0x0,0xc(%rsp)   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   
   | 
 
栈布局是这样的:
1 2 3 4 5 6 7
   | 0x00(rsp) 0x04 0x08 0x0c rcx [1] 0x10 rdx [0] 0x14 0x18 rsp
   | 
 
在这里需要满足的有:
0xe >= *(rsp + 0x8) 
0x0 == *(rsp + 0xc) 
func4(*(rsp + 0x8), 0, 0xe) == 0 
我们进入func4看看汇编:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
   | 0000000000400fce <func4>:   400fce:	48 83 ec 08          	sub    $0x8,%rsp   400fd2:	89 d0                	mov    %edx,%eax   400fd4:	29 f0                	sub    %esi,%eax   400fd6:	89 c1                	mov    %eax,%ecx   400fd8:	c1 e9 1f             	shr    $0x1f,%ecx   400fdb:	01 c8                	add    %ecx,%eax   400fdd:	d1 f8                	sar    %eax   400fdf:	8d 0c 30             	lea    (%rax,%rsi,1),%ecx   400fe2:	39 f9                	cmp    %edi,%ecx   400fe4:	7e 0c                	jle    400ff2 <func4+0x24>   400fe6:	8d 51 ff             	lea    -0x1(%rcx),%edx   400fe9:	e8 e0 ff ff ff       	callq  400fce <func4>   400fee:	01 c0                	add    %eax,%eax   400ff0:	eb 15                	jmp    401007 <func4+0x39>   400ff2:	b8 00 00 00 00       	mov    $0x0,%eax   400ff7:	39 f9                	cmp    %edi,%ecx   400ff9:	7d 0c                	jge    401007 <func4+0x39>   400ffb:	8d 71 01             	lea    0x1(%rcx),%esi   400ffe:	e8 cb ff ff ff       	callq  400fce <func4>   401003:	8d 44 00 01          	lea    0x1(%rax,%rax,1),%eax   401007:	48 83 c4 08          	add    $0x8,%rsp   40100b:	c3                   	retq   
   | 
 
没有什么栈的布局,就是些寄存器之间的计算,我们一个一个模拟一下:
(初始化:rdi = ?, rsi = 0, rdx = 0xe)
- eax = edx,  eax = 0xe
 
- eax -= esi, eax = 0xe
 
- ecx = eax,  ecx = 0xe
 
- ecx >>= 0x1f, ecx >>= 31, ecx = 0(注意是逻辑右移)
 
- eax += ecx, eax = 0xe
 
- eax >>= 1, eax = 0x7(注意是算术右移,且只有一个参数时默认右移1位)
 
- ecx = rax + rsi * 1 = 0x7 + 0 = 0x7
 
然后我们分析下后面跳转的流程:
- 如果%edi <= %ecx,就会跳转到0x400ff2去。
 
- 跳转完再来一个cmp,如果%edi >= %ecx,就可以调到0x401007结束函数了。
 
所以只需要%ecx和%edi一样大就可以了,所以rdi直接等于7就可以了。
所以我们直接输入7跟0就可以了。
所以最后复习下这些奇妙的汇编指令,以免我又忘了:
imul src, dest 乘法 
sal  src, dest 算术左移 
sar  src, dest 算术右移 
shl  src, dest 逻辑左移 
shr  src, dest 逻辑右移 
phase 5
汇编
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
   | 0000000000401062 <phase_5>:   401062:	53                   	push   %rbx   401063:	48 83 ec 20          	sub    $0x20,%rsp   401067:	48 89 fb             	mov    %rdi,%rbx   40106a:	64 48 8b 04 25 28 00 	mov    %fs:0x28,%rax   401071:	00 00    401073:	48 89 44 24 18       	mov    %rax,0x18(%rsp)   401078:	31 c0                	xor    %eax,%eax   40107a:	e8 9c 02 00 00       	callq  40131b <string_length>   40107f:	83 f8 06             	cmp    $0x6,%eax   401082:	74 4e                	je     4010d2 <phase_5+0x70>   401084:	e8 b1 03 00 00       	callq  40143a <explode_bomb>   401089:	eb 47                	jmp    4010d2 <phase_5+0x70>   40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx   40108f:	88 0c 24             	mov    %cl,(%rsp)   401092:	48 8b 14 24          	mov    (%rsp),%rdx   401096:	83 e2 0f             	and    $0xf,%edx   401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx   4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1)   4010a4:	48 83 c0 01          	add    $0x1,%rax   4010a8:	48 83 f8 06          	cmp    $0x6,%rax   4010ac:	75 dd                	jne    40108b <phase_5+0x29>   4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp)   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   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   
   | 
 
这个函数的stack frame是这样的:
1 2 3 4 5 6
   | phase 5 0x00 (rsp) 0x08 canary 0x10 rdi 0x18 0x20 rsp
   | 
 
同样有奇妙地址,我们查一查:
这个字符串打印出来之所以这样,是因为它最后一位不是\x00,所以就连续着把紧连着的下一个字符串也输出出来了。
最开始在call出string_length之前的这部分是用来初始化canary的。不用管。
字符串长度必须为6,才能跳转,不然会踩雷。
接下来从0x40108b开始,就是一个6次的循环,rax充当循环的counter,很容易看出来。
如果我们过完这个循环,最终要满足的是这个条件:
1 2 3 4 5 6
   | 4010ae:	c6 44 24 16 00       	movb   $0x0,0x16(%rsp) 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>
   | 
所以我们要做的,就是在跑完上面这次循环之后,让rsp + 0x10开始的字符串跟flyers一毛一样。
这段代码粘下来集中看一看:
1 2 3 4 5 6 7 8 9
   | 40108b:	0f b6 0c 03          	movzbl (%rbx,%rax,1),%ecx 40108f:	88 0c 24             	mov    %cl,(%rsp) 401092:	48 8b 14 24          	mov    (%rsp),%rdx 401096:	83 e2 0f             	and    $0xf,%edx 401099:	0f b6 92 b0 24 40 00 	movzbl 0x4024b0(%rdx),%edx 4010a0:	88 54 04 10          	mov    %dl,0x10(%rsp,%rax,1) 4010a4:	48 83 c0 01          	add    $0x1,%rax 4010a8:	48 83 f8 06          	cmp    $0x6,%rax 4010ac:	75 dd                	jne    40108b <phase_5+0x29>
   | 
 
开始模拟:
(初始化rbx指向的是最开始的rdi,也就是字符串的开始)
1 2 3 4 5 6
   | ecx = str[i] *rsp = cl (lower 4 digits of str[i]) rdx = *rsp = cl (lower 4 digits of str[i]) edx &= 0xf edx = array3449[cl] *(rsp + rax + 0x10) = dl (lower 4 digits of array3449[cl])
   | 
 
最后的这个(rsp + rax + 0x10)看上去不认识,但是参照下上面的栈结构,其实表示的就是字符串的第i位。
所以我们只需要去注意输入的6个字符中,每个字符的低4位在array3449中索引出来的值,这些值就会一个一个的,填到以rsp + 0x10为开始的字符串中。
手动数一数下标,就可以发现,要对应弄出flyers,我们依次需要下标是9 15 14 5 6 7。
所以我们只需要翻翻ASCII表,找到低4位是这些的字符,拼到一起就可以了。
我最终的答案是ionefg。答案不唯一。
IDA
主要是这句代码太具有迷惑性:
1
   | v3[i] = array_3449[*(_BYTE *)(a1 + i) & 0xF];
   | 
 
正确的解读是:
1
   | v3[i] = array_3449[a1[i] & 0xF];
   | 
 
在C里面,一个char所占据的大小恰好就是一个byte,所以_BYTE可以直接看成char。
这里我之所以迷糊,是因为IDA Pro反汇编说a1的类型是int64,然而事实上a1就是个字符串。
phase 6
最后一关,太复杂了!那我们就不分析汇编,直接上手看IDA Pro弄出来的代码。
其实弄出来的代码也不好看懂,一不小心也很容易晕!这里重新做一下记录。
IDA
反汇编出来的代码长这样,非常长,变量非常多。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94
   | __int64 __fastcall phase_6(__int64 a1) {   int *v1;    signed int v2;    signed int v3;    char *v4;    unsigned __int64 v5;    _QWORD *v6;    signed int v7;    int v8;    __int64 v9;    char *v10;    __int64 i;    __int64 v12;    signed int v13;    __int64 result;    int v15[6];    char v16;    __int64 v17;    char v18;    char v19; 
    v1 = v15;   read_six_numbers(a1, v15);   v2 = 0;   while ( 1 )   {     if ( (unsigned int)(*v1 - 1) > 5 )       explode_bomb(a1, v15);     if ( ++v2 == 6 )       break;     v3 = v2;     do     {       if ( *v1 == v15[v3] )         explode_bomb(a1, v15);       ++v3;     }     while ( v3 <= 5 );     ++v1;   }   v4 = (char *)v15;   do   {     *(_DWORD *)v4 = 7 - *(_DWORD *)v4;     v4 += 4;   }   while ( v4 != &v16 );   v5 = 0LL;   do   {     v8 = v15[v5 / 4];     if ( v8 <= 1 )     {       v6 = &node1;     }     else     {       v7 = 1;       v6 = &node1;       do       {         v6 = (_QWORD *)v6[1];         ++v7;       }       while ( v7 != v8 );     }     *(__int64 *)((char *)&v17 + 2 * v5) = (__int64)v6;     v5 += 4LL;   }   while ( v5 != 24 );   v9 = v17;   v10 = &v18;   for ( i = v17; ; i = v12 )   {     v12 = *(_QWORD *)v10;     *(_QWORD *)(i + 8) = *(_QWORD *)v10;     v10 += 8;     if ( v10 == &v19 )       break;   }   *(_QWORD *)(v12 + 8) = 0LL;   v13 = 5;   do   {     result = **(unsigned int **)(v9 + 8);     if ( *(_DWORD *)v9 < (signed int)result )       explode_bomb(a1, &v19);     v9 = *(_QWORD *)(v9 + 8);     --v13;   }   while ( v13 );   return result; }
   | 
 
首先我们画一画这个函数的栈:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
   | 0x00 rbp 0x08 0x10 0x18 0x20 0x28 char v19[0x28] 0ll 0x30               &node[v15[5]] 0x38               &node[v15[4]] 0x40               &node[v15[3]] 0x48               &node[v15[2]] 0x50 char v18      &node[v15[1]]  <- v10 0x58 long long v17 &node[v15[0]]  v9 0x60 char v16 0x64 v15[5] 0x68 v15[4] 0x6c v15[3] 0x70 v15[2] 0x74 v15[1] 0x78 v15[0]
   | 
 
这个栈的图片非常非常重要,首先先保证不会乱,因为后面还有跳出栈外的过程。
还有,在分析的过程中,时刻注意每一个变量到底是值,还是指针!千万不能错!
一步一步分析,不要急,一定要慢慢来:
最开始,从v15开始,读入6个int类型的整数,存在栈上。(v15是个指针)
第一个是嵌套循环,v1是当前遍历到的元素的指针,v2表示第几个元素(从1开始数),v3是循环变量。
每次遍历v1,都必须保证1 <= *v1 <= 6,关于强转unsigned int的知识点,在最后有总结。然后内层循环表示后面的元素都得跟前面的不一样,意思就是这6个数各不相同。
第二个是单个do-while循环。它做的就是把这6个数都运算一遍,把x变成了7-x,更改了这6个数。
第三个开始烧脑了!v8是循环中被遍历到的值,根据v8的数值大小,分别执行若干次从&node1开始的v8 - 1次地址跳转,最终把栈上原来数组的值重新写为跳转到最后的地址。
这里注意一下,v6 = (_QWORD *)v6[1];这句代码是伏笔!(为什么这个值可以强转为地址呢?)
我们点进node1,发现在data段,后面刚好延伸到node6结束,这是什么意思?
不懂,我们看到下一个代码部分:
一个for循环,从v17即rbp - 0x58开始,每次循环结束会跳转到v10的值。之所以可以直接迭代为v10的值,是因为这个数组在第三次操作的时候已经变成了指针数组了!
接下来又是一句意味深长的代码:*(_QWORD *)(i + 8) = *(_QWORD *)v10;
我们在IDA开始乱了,用gdb看一看有没有线索,毕竟还没有查过那段&node1的奇妙地址。结果非常的意外:
不知为什么,每一个node元素,他的第三个数字,恰好跟下一个node的地址一模一样!
其实突破点就出来了:
每一个node是一个struct类型!
node里面的第三个数字,代表着下一个元素的地址!
这就是链表的汇编!
其他的数字是啥意思呢?第一个数字对应节点的值,第二个数字是id,第三个数字是地址,然后怎么有空出来的0?
不是空出来的0,而是因为地址就是64位的!
在这里,结构体内的元素顺序不同,所占用的空间也会不同,这个在CSAPP中有提到过内存对齐的概念!
那为什么上面的那个伏笔,对应的下标是1呢?
因为v6就是一个QWORD类型,而node里面的数字都是int,只有32位呀!
接下来就非常简单了,最后一个循环所代表的,就是确保最终的数值是降序排列的。
所以最终的排序是924 > 691 > 477 > 443 > 332 > 168,即3 4 5 6 1 2。
别忘记了前面有一个x = 7 - x;,所以最终的答案就是4 3 2 1 6 5。
secret phase
待补充,今天晚点再做了补上。(咕咕咕)
Jan 15 upd:来补上secret phase了!
怎么进secret phase
secret_phase函数的入口其实在phase_defused里面。
懒得看汇编,直接用IDA Pro做了。其实反汇编出来的跟看汇编也差不多
这里看到一个num_input_strings,是个在bss段上的全局变量。同时,sscanf所读入的那个地址,也是在bss段上的,初始化都是0,不过可能会在函数执行的时候被修改。
那到底是什么时候被修改的?我们分别用gdb设断点看一看。
可以发现这个变量的意思就是记录现在是第几关。所以当第六关的时候就可以了。
可以发现是我们在打phase 4的时候,这个input_strings + 240所在的字符串就更改成了我们输入的内容。并且后面不会再更改。
所以我们只需要在第四阶段,在第三个位置上输入一个DrEvil,就可以在过完第六关之后触发了。
分析
要使这个func7返回2,并且输入的数字小于等于0x3e8 + 1,就可以通关了。
这里有一个&n1,点进去看看,又是在data段,跟前面的&node1很类似。并且,n1后面也紧跟着其他类似的东西,应该又是一个struct。
我们用gdb看一看:
可以发现,每个结构体储存了两个地址,我们做下笔记:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | n1(n21, n22)  36 n21(n31, n32) 8 n22(n33, n34) 50 n32(n43, n44) 22 n33(n45, n46) 45 n31(n41, n42) 6 n34(n47, n48) 107 n45 40 n41 1 n47 99 n44 35 n42 7 n43 20 n46 47 n48 1001
   | 
 
这种一对二的关系,其实就是二叉树:
1 2 3 4
   |                 n1       n21             n22   n31     n32     n33     n34 n41 n42 n43 n44 n45 n46 n47 n48
   | 
 
想让func7为2,首先要落向左边,然后落向右边,然后返回0,这样就能构造出2 * (2 * 0 + 1) = 2了。
最后的返回0,也可以走左边再返回0,所以n32和n43的值都是没问题的,即我们有20跟22两个答案。
终于通关了!芜湖起飞!