We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
https://leakbox258.github.io/C++%E6%80%8E%E4%B9%88UAF/
ps: 讨论场景为c++17
智能指针 智能指针是C++的常用特性之一, 用于解决C语言以及早期C++中的内存分配和释放过于复杂, 或者内存泄露的问题. 一般使用的智能指针有std::unique_ptr, std::shared_ptr, std::weak_ptr, std::weak_ptr可以看作是std::shared_ptr在特定情况下的补充. 需要#include . 然而, 即使有智能指针, 也不能高枕无忧, 因为内存问题是所有人都要面对的, 除非你是高贵的数据分析科学家, 或者你使用语言(yu’an)神Rust 下面简单列一下本人发现的可以对智能指针UAF的方法.
shared_ptr<> 内存回收方式 shared_ptr是比较常用的智能指针, 一块堆内存可以被多个若干个shared_ptr指向, 这块内存会记录被指向的数目(引用计数shared_count), 当引用归零时, 内存被释放. 表面上看是这样的. 实际上std::make_shared<>分配的内存有两个计数器, shared_count和weak_count(各4字节), 当然是为了配合weak_ptr使用 那么shared_ptr在内存中是什么样的的组织方式, 下面一个demo #include #include
int main(){ { std::shared_ptr<size_t> ptr1 = std::make_shared<size_t>(0x12345678); std::shared_ptr<size_t> ptr2 = ptr1; } return 0; }
上图是ptr2 = ptr1之后的栈空间 可以看到其实比较简单, shared_ptr对象本身是 地址(指针) + 一个虚表指针
上述指针(即.get()获取的裸指针)指向的一个chunk的中间部分, 对象的位置. 前面的是0x555555557cc8虚表地址, 和0x1weak_count, 0x2shared_count(use count). 值得一提的是weak_count返回的是0, 但实际上在内存中存的是1 作用域结束之后, 分别对两个std::shared_ptr对象进行析构 第一个析构之后, 堆肯定是没有释放的, 但是use count变成了0x1 然后追踪一下第二个析构
注意下面的调用栈, 现在在_M_release()中 movabs这一句, 0x100000001直接硬编码在指令里, 看来是有bear而来 cmp, rax里是堆块中use count + weak count的那一个字长的拷贝. 这里就是比较此时是不是两个count都只剩1了, 也就是该堆块只有当前正在析构的指针还在引用, 如果是的话, ZF标志位为1 sete al, 当equal(ZF为1)时, al被设置为1, 反之为0 test al, al, 经典按位与用来判断是不是0, 结果不是0, ZF变成0 je ..., 此时不跳转, 进入下面的堆块释放环节. 释放的环节调用了两个方法, _M_dispose, 和_M_destroy, 但是在这之前, use count和weak count被清零了, 如下图
然后分别call了_M_dispose, 和_M_destroy
在_M_destory中, 在~__allocator_ptr, 之后堆块释放. 更细节的调用没再追踪了
如果两个count不是1, 会进入下面的分支
shared_ptr的UAF 根据上面的分析, 得知两点 第一, 必须要让use count和weak count都是1, 才能触发堆块释放的操作 第二, 想要让一个堆块释放, 应该先进入一个shared_ptr的析构函数, 毕竟没人会在用了智能指针之后还手动delete. 如此, 一个简单的UAF思路产生了, 利用程序漏洞篡改两个count都是1, 然后触发析构, 如果此时还有别的shared_ptr没析构, 那么就成功UAF了, 不过这个UAF没法劫持虚表以及再次改两个count. 看另一个demo #include #include
int main(){
std::shared_ptr<size_t> ptr = std::make_shared<size_t>(0x12345678); { std::shared_ptr<size_t> ptr1 = ptr; // change use count: 2 -> 1 unsigned int *use_count = (unsigned int *)((unsigned long long)(ptr1.get()) - 8); *use_count = 1; // 生命周期结束, ptr1析构, 同时触发ptr和ptr1指向的堆块free } std::shared_ptr<size_t> ptr_new = std::make_shared<size_t>(114514); *ptr = 1919810; // UAF std::cout<< *ptr_new <<std::endl; return 0;
}
UAF大成功, 没用的知识又增加了 $ g++ test.cpp -g -o test $ ./test 1919810
weak_ptr 内存回收方式 首先了解一下weak_ptr的使用场景, 就是为了避免shared_ptr`之间的循环引用. 先看一个没有循环引用的demo, 结构体wrapper有一个成员ptr. #include #include
struct wrapper { std::shared_ptr ptr; };
{ ///@note wrapprx 应该叫做 wrapperx_ptr 才符合语义, 但是图都截了... std::shared_ptr<wrapper> wrapper1 = std::make_shared<wrapper>(); std::shared_ptr<wrapper> wrapper2 = std::make_shared<wrapper>(); std::shared_ptr<wrapper> wrapper3 = std::make_shared<wrapper>(); wrapper1->ptr = wrapper2; wrapper2->ptr = wrapper3; wrapper3->ptr = nullptr; } return 0;
如图, 在wrapper1析构之后, 对应的内存没有free. 事实上, 在wrapper3之后, 三个chunk才会一起释放. 但这个过程中, 三个wrapper指向的内存的引用计数在正确地减少. 注意作用域结束时, 析构的顺序是构造的顺序是相反的, v12是赋值是产生的copy, 可以不管
std::shared_ptr::~shared_ptr(wrapper1);析构wrapper1时, 析构了wrapper1->ptr, wrapper1->ptr指针析构时又其指向的对象(wrapper2), wrapper2析构时, 需要析构wrapper2->ptr, 析构wrapper2->ptr是析构了指向的对象*wrapper3 *wrapper3的chunk释放之后, 调用栈回溯, 逐个又free其他chunk 如果说话的方式简单点, 就是析构智能指针就会析构所指向的对象, 析构所指向的对象就会该对象使用的智能指针. 在这个过程中, 三块内存保存的use_count |析构状态|chunk1|chunk2|chunk3| |:—:|:—:|:—:|:—:| |没析构|1|2|2| |wrapper3析构|1|2|1| |wrapper2析构|1|1|1| |wrapper1析构|free|free|free|
进入正题, 有循环引用的demo #include #include
{ std::shared_ptr<wrapper> wrapper1 = std::make_shared<wrapper>(); std::shared_ptr<wrapper> wrapper2 = std::make_shared<wrapper>(); wrapper2->ptr = wrapper1; wrapper1->ptr = wrapper2; // 构成循环引用 } return 0;
如图, 当作用域结束之后, 两块wrapper的内存都没有释放 简单概括一下, 智能指针wrapper1析构时, 会析构wrapper对象(存在第一个chunk里), 然后析构ptr成员, 析构wrapper(存在第二个chunk里), 然后又ptr成员, 最后回去析构存在第一个chunk里的wrapper, 成功转了个圈 析构函数应该有什么检查机制(可能是检查地址), 因为这个循环递归地析构函数调用并不会卡死程序, 但是确实会让引用计数无法正确减少, 一直都是2, 对应的两个chunk永远无法free, 变成僵尸内存.
To be continued…
The text was updated successfully, but these errors were encountered:
No branches or pull requests
https://leakbox258.github.io/C++%E6%80%8E%E4%B9%88UAF/
ps: 讨论场景为c++17
智能指针
智能指针是C++的常用特性之一, 用于解决C语言以及早期C++中的内存分配和释放过于复杂, 或者内存泄露的问题. 一般使用的智能指针有std::unique_ptr, std::shared_ptr, std::weak_ptr, std::weak_ptr可以看作是std::shared_ptr在特定情况下的补充. 需要#include . 然而, 即使有智能指针, 也不能高枕无忧, 因为内存问题是所有人都要面对的, 除非你是高贵的数据分析科学家, 或者你使用语言(yu’an)神Rust 下面简单列一下本人发现的可以对智能指针UAF的方法.
shared_ptr<>
内存回收方式
shared_ptr是比较常用的智能指针, 一块堆内存可以被多个若干个shared_ptr指向, 这块内存会记录被指向的数目(引用计数shared_count), 当引用归零时, 内存被释放. 表面上看是这样的. 实际上std::make_shared<>分配的内存有两个计数器, shared_count和weak_count(各4字节), 当然是为了配合weak_ptr使用
那么shared_ptr在内存中是什么样的的组织方式, 下面一个demo
#include
#include
int main(){
{
std::shared_ptr<size_t> ptr1 = std::make_shared<size_t>(0x12345678);
std::shared_ptr<size_t> ptr2 = ptr1;
}
return 0;
}
上图是ptr2 = ptr1之后的栈空间
可以看到其实比较简单, shared_ptr对象本身是 地址(指针) + 一个虚表指针
上述指针(即.get()获取的裸指针)指向的一个chunk的中间部分, 对象的位置. 前面的是0x555555557cc8虚表地址, 和0x1weak_count, 0x2shared_count(use count). 值得一提的是weak_count返回的是0, 但实际上在内存中存的是1
作用域结束之后, 分别对两个std::shared_ptr对象进行析构
第一个析构之后, 堆肯定是没有释放的, 但是use count变成了0x1
然后追踪一下第二个析构
注意下面的调用栈, 现在在_M_release()中
movabs这一句, 0x100000001直接硬编码在指令里, 看来是有bear而来
cmp, rax里是堆块中use count + weak count的那一个字长的拷贝. 这里就是比较此时是不是两个count都只剩1了, 也就是该堆块只有当前正在析构的指针还在引用, 如果是的话, ZF标志位为1
sete al, 当equal(ZF为1)时, al被设置为1, 反之为0
test al, al, 经典按位与用来判断是不是0, 结果不是0, ZF变成0
je ..., 此时不跳转, 进入下面的堆块释放环节.
释放的环节调用了两个方法, _M_dispose, 和_M_destroy, 但是在这之前, use count和weak count被清零了, 如下图
然后分别call了_M_dispose, 和_M_destroy
在_M_destory中, 在~__allocator_ptr, 之后堆块释放. 更细节的调用没再追踪了
如果两个count不是1, 会进入下面的分支
shared_ptr的UAF
根据上面的分析, 得知两点
第一, 必须要让use count和weak count都是1, 才能触发堆块释放的操作
第二, 想要让一个堆块释放, 应该先进入一个shared_ptr的析构函数, 毕竟没人会在用了智能指针之后还手动delete.
如此, 一个简单的UAF思路产生了, 利用程序漏洞篡改两个count都是1, 然后触发析构, 如果此时还有别的shared_ptr没析构, 那么就成功UAF了, 不过这个UAF没法劫持虚表以及再次改两个count.
看另一个demo
#include
#include
int main(){
}
UAF大成功, 没用的知识又增加了
$ g++ test.cpp -g -o test
$ ./test
1919810
weak_ptr
内存回收方式
首先了解一下weak_ptr的使用场景, 就是为了避免shared_ptr`之间的循环引用.
先看一个没有循环引用的demo, 结构体wrapper有一个成员ptr.
#include
#include
struct wrapper
{
std::shared_ptr ptr;
};
int main(){
}
如图, 在wrapper1析构之后, 对应的内存没有free. 事实上, 在wrapper3之后, 三个chunk才会一起释放. 但这个过程中, 三个wrapper指向的内存的引用计数在正确地减少.
注意作用域结束时, 析构的顺序是构造的顺序是相反的, v12是赋值是产生的copy, 可以不管
std::shared_ptr::~shared_ptr(wrapper1);析构wrapper1时, 析构了wrapper1->ptr, wrapper1->ptr指针析构时又其指向的对象(wrapper2), wrapper2析构时, 需要析构wrapper2->ptr, 析构wrapper2->ptr是析构了指向的对象*wrapper3
*wrapper3的chunk释放之后, 调用栈回溯, 逐个又free其他chunk
如果说话的方式简单点, 就是析构智能指针就会析构所指向的对象, 析构所指向的对象就会该对象使用的智能指针.
在这个过程中, 三块内存保存的use_count
|析构状态|chunk1|chunk2|chunk3|
|:—:|:—:|:—:|:—:|
|没析构|1|2|2|
|wrapper3析构|1|2|1|
|wrapper2析构|1|1|1|
|wrapper1析构|free|free|free|
进入正题, 有循环引用的demo
#include
#include
struct wrapper
{
std::shared_ptr ptr;
};
int main(){
}
如图, 当作用域结束之后, 两块wrapper的内存都没有释放
简单概括一下, 智能指针wrapper1析构时, 会析构wrapper对象(存在第一个chunk里), 然后析构ptr成员, 析构wrapper(存在第二个chunk里), 然后又ptr成员, 最后回去析构存在第一个chunk里的wrapper, 成功转了个圈
析构函数应该有什么检查机制(可能是检查地址), 因为这个循环递归地析构函数调用并不会卡死程序, 但是确实会让引用计数无法正确减少, 一直都是2, 对应的两个chunk永远无法free, 变成僵尸内存.
To be continued…
The text was updated successfully, but these errors were encountered: