Attack lab
void test()
{
int val;
val = getbuf();
printf("No exploit. Getbuf returned 0x%x\n", val);
}
正常执行的话是调用getbuf,然后从屏幕中输入字符串,如果正常退出的话,则会执行
printf("No exploit. Getbuf returned 0x%x\n", val)`;
而这个lab就是不让它正常退出。
这个lab总共有5个phase,前三个phase是一种方法,后俩是另一种。
level1
level1要求输入字符串使程序跳转到函数touch1
touch1
里面没啥东西,所以我们要做的只是使程序跳转。
要使程序跳转到touch1,我们需要将test栈帧顶部存放的地址改写成touch1
的起始地址
void touch1() {
vlevel = 1;
printf("Touch!: You called touch1()\n");
validate(1);
exit(0);
}
通过反汇编工具得到touch1
的地址如下
可以看到touch1
的地址为0x4017c0
接下来需要做的是填充getbuf分配的空间,查看getbuf
的反汇编如下:
可以看到%rsp
减了40
,故我们需要填满这40个字节的缓冲区,然后在接下来的8个字节中,填入touch1
的地址0x4017c0
。
这里我选择用字节00
填充。
综上所述,输入的字节序列为:
最后再验证答案正确性
level2
level2要求输入字符串使程序跳转到函数touch2
void touch2(unsigned val){
vlevel = 2;
if (val == cookie){
printf("Touch2!: You called touch2(0x%.8x)\n", val);
validate(2);
} else {
printf("Misfire: You called touch2(0x%.8x)\n", val);
fail(2);
}
exit(0);
}
首先看看函数touch2
要干嘛,它判断参数val
是否等于cookie
,要等于才算过关。
所以我们不仅要使程序跳转到touch2
,还得保证传给touch2
的参数val
必须与cookie
相等。
其中cookie
是十六进制表示的无符号整数,值为0x59b997fa
,
level2与level1的差别之一在于,level2在跳转之前需要将0x59b997fa
传给touch2
,这一步可以由汇编指令
movq 0x59b997fa, %rdi
完成。
那么如何使CPU执行这条指令呢?
我们可以将指令写入getbuf
分配的空间中,并将test栈帧中的return address
改写成getbuf
的栈顶(注入字节序列的起始地址),这样一来,当getbuf
执行ret
的时候,CPU会跳转到getbuf
的栈顶,执行我们写入的指令。
而getbuf的栈顶可以通过反汇编得到:
这里显示的是执行完第一条sub
指令后的状态,这时候的%rsp
即为我们注入字符串的首地址=0x5561dc78
为了使程序执行touch2
,需要将touch2
的地址push
到栈中,然后执行ret
指令
使用反汇编得到touch2
的地址为:0x4017ec
故我们的指令为
movq $0x59b997fa, %rdi
pushq 0x4017ec
ret
故栈的目标状态如下:
最后需要做的就是将汇编指令转化成字节序列
故最后得到的字节序列为:
最后再验证答案正确性
level3
level3要求输入字符串使程序跳转到函数touch3
/* Compare string to hex represention of unsigned value */
int hexmatch(unsigned val, char *sval) {
char cbuf[110];
/* Make position of check string unpredictable */
char *s = cbuf + random() % 100;
sprintf(s, "%.8x", val);
return strncmp(sval, s, 9) == 0;
}
void touch3(char *sval) {
vlevel = 3; /* Part of validation protocol */
if (hexmatch(cookie, sval)) {
printf("Touch3!: You called touch3(\"%s\")\n", sval);
validate(3);
} else {
printf("Misfire: You called touch3(\"%s\")\n", sval);
fail(3);
}
exit(0);
}
首先看看函数touch3
要干嘛,它调用了函数hexmatch
,要使它返回true才算过关。
那么看看hexmatch
函数要干嘛,它首先分配了一个110字节的缓冲区,然后在这个缓冲区里面随机取一个地址作为指针s
的值,sprintf
将参数val
存放在s
所指的缓冲区内,最后比较参数sval
所指的字符串与s
所指的字符串是否相等。
所以为了使hexmatch
返回true,我们需要使转化成字符串后的cookie
,与字符串sval
相等。
其中cookie
是十六进制表示的无符号整数,值为0x59b997fa
,
使用man ascii
指令查询cookie
的ascii码表示为:
35 39 62 39 39 37 66 61 00
现在我们知道了sval
字符串的内容,还要解决一个问题,就是该把它放在哪里?
当调用hexmatch
和strncmp
时,他们会把数据压入到栈中,有可能会覆盖getbuf
栈帧的数据,所以不要放到getbuf
的栈帧里面,要放到test
的栈帧里。那么就放在test栈帧里返回地址的上面就好了。
使用反汇编工具,查看getbuf函数的汇编代码如下图:
现在的状态是刚进入函数,还没执行第一条语句,所以rsp指向的是test栈帧的顶部,里面存放的是本该回到的地址,
rsp
指向0x5561dca0
,由于地址占8个字节,所以我们的sval字符串应该放在0x5561dca0+8=0x5561dca8
,这个位置。
总的来说,栈的初始状态很简单,如果没有buffer overflow,那么执行完getbuf函数,CPU将回到test函数中紧跟着getbuf()的语句继续执行;这里我们的目的就是使CPU执行完getbuf后,跳转到touch3函数,为了达到这个目的,我们需要把原本应该回到的地址改成我们注入字符串的首地址,如此一来,getbuf函数ret后并不会回到test,而是跳转到我们getbuf的栈顶,开始执行我们写入的指令,这些指令的任务就是使CPU去执行函数touch3。
(1)为了使程序跳转到touch3
,需要把touch3
的起始地址push
到栈中
使用disas touch3
得到函数touch3的反汇编代码:``
可知touch3的起始地址为0x4018fa
。
(2)在执行touch3
之前,需要把之前得到的与cookie
相等的字符串sval的首地址作为参数1传递给touch3
,
而这个首地址我们在之前已经算出来是0x5561dca8
(3)最后只剩下注入字符串的首地址未知了,我们直接从栈顶注入字符串,所以首地址就是getbuf
的栈顶,即执行完第一条汇编指令后%rsp
所指的地址。如下图:
这时候的%rsp
即为我们注入字符串的首地址=0x5561dc78
结合(1)(2)我们可以得出存放在getbuf
栈帧中的注入代码为
movq $0x5561dca8, %rdi
pushq 0x4018fa
ret
再结合(3),得到
目标状态具体为
当然,这段指令需要转化成字节表示,
具体做法如下
那么得到最终的输入字节序列就出来了
最后再验证答案正确性
phase 4
phase4
其实就是用另一种方法实现phase2
的工作。
要将cookie
作为参数传入touch2
,可以用以下指令完成
popq %rax
movq %rax, %rdi
接下来从栈的状态图一步步来分析,这些指令究竟都做了什么
在getbuf ret之前,%rsp指向popq %rax ret
指令所在的地址
当getbuf ret
后,popq %rax ret
指令所在的地址被pop到%rip,指示CPU去执行语句popq %rax
,并且将%rsp
加8,使其指向栈中的前一个值。
popq %rax
这句话相当于
movq (%rsp) %rax
addq $8 %rsp
ret
后, movq %rax, %rdi
所在的地址被pop到%rip,指示CPU去执行语句 movq %rax, %rdi
,并且将%rsp
加8,使其指向栈中的前一个值。
这时我们把前一个值改写成cookie
,那么%rsp
指向的地址内存放的就是cookie
的值。
即(%rsp)==cookie
,这样一来,语句popq %rax
将cookie
的值赋给%rax
,并将%rsp
加8指向 movq %rax, %rdi
所在的地址。
movq %rax, %rdi
把%rax
赋给%rdi
,即将要调用的函数touch2
的第一个参数,再通过ret
指令跳转到touch2
。
接下来要做的就是找到这两个gadget
的地址了。
popq %rax
对应的 字节是 58
,
ret
是 c3
movq %rax, %rdi
是48 89 c7
所以我们的任务就是将farm.c
变成字节序列,然后从中找到58 c3
以及48 89 c7 c3
这两个序列,记下他们的地址。
搜索58
发现么有58 c3
,只有58 90 c3
,90
其实代表的是指令no operation,就是啥都不做,相当于跳过的意思。
所以这里用58 90 c3
效果是一样的。记下地址为0x5c
同理记下地址0x51
由此得到输入字节序列为
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00
5c 00 00 00 00 00 00 00
fa 97 b9 59 00 00 00 00
51 00 00 00 00 00 00 00
ec 17 40 00 00 00 00 00
level 5
在这一阶段中,我们需要做的就是把字符串的起始地址传送到%rdi,然后调用touch3函数。
因为每次栈的位置是随机的,所以无法直接用地址来索引字符串的起始地址,只能用栈顶地址 + 偏移量来索引字符串的起始地址。从farm中我们可以获取到这样一个gadget
:lea (%rdi,%rsi,1),%rax
,这样就可以把字符串的首地址传送到%rax。
解题思路:
(1)首先获取到%rsp的地址,并且传送到%rdi
(2)其二获取到字符串的偏移量值,并且传送到%rsi
(3)lea (%rdi,%rsi,1),%rax, 将字符串的首地址传送到%rax, 再传送到%rdi
(4)调用touch3函数
(1) 第一步,获取到%rsp的地址
movq %rsp, %rax
的指令字节为:48 89 e0, 所以这一步的gadget地址为:0x1c5
(2) 第二步,将%rax的内容传送到%rdi
movq %rax, %rdi
的指令字节为:48 89 c7,所以这一步的gadget地址为:0x1a
(3) 第三步,将偏移量的内容弹出到%rax
popq %rax
的指令字节为:58, 其中90为nop指令, 所以这一步的gadget地址为:0x5c
(4) 第四步,将%eax的内容传送到%edx
movl %eax, %edx
的指令字节为:89 c2
, 所以这一步的gadget地址为:0x79
(5) 第五步,将%edx的内容传送到%ecx
movl %edx, %ecx
的指令字节为:89 d1
,所以这一步的gadget地址为:0x164
(6) 第六步,将%ecx的内容传送到%esi
movl %ecx, %esi
的指令字节为:89 ce
, 所以这一步gadget地址为:0xcf
(7) 第七步,将栈顶 + 偏移量得到字符串的首地址传送到%rax
这一步的gadget地址为:0x6a
(8) 将字符串首地址%rax传送到%rdi
movq %rax, %rdi
的指令字节为:48 89 c7
,所以这一步的gadget地址为:0x51
综上,我们可以得到字符串首地址和返回地址之间隔了9条指令,所以偏移量为72
个字节,也就是0x48
,可以的到如下字符串的输入: