CTF比赛中C++的题越来越多,题目中经常出现string
,vector
等,而实际上手时发现常常迷失在"库函数"中,比如跟进了空间配置器相关函数
最近研究一下关于这些的底层机制与逆向,应该会写成一个系列
string
内存布局
visual studio
的调试实在是太好用了,因此用它举例
![5260f65da6b3d04edb355cc0998ee8e3.png](https://i-blog.csdnimg.cn/blog_migrate/71f3cc242180f3d8e71c854203deff5f.jpeg)
其中,size
是当前字符串长度,capacity
是最大的容量
可以发现,capacity
比size
大的多
而allocator
是空间配置器,可以看到单独的字符显示
原始视图中可以得知,字符串的首地址
![47fdf6202dbd6a37bbddd577f998e5a1.png](https://i-blog.csdnimg.cn/blog_migrate/b533ecabe03248b4fa3ea616aaa0a84f.jpeg)
可以看到,abcd
字符串在内存中也是以x00
结尾的
扩容机制
正是由于capacity
开辟了更多需要的空间,来具体研究一下它的策略
#include<iostream>
#include<string>
#include<stdlib.h>
#include<windows.h>
using namespace std;
int main(int argc, char** argv) {
string str;
for (int i = 0; i < 100; i++) {
str += 'a';
std::cout << "size : " << str.size() << " capacity : " << str.capacity() << std::endl;
}
system("pause");
return 0;
}
从输出结果发现,capacity
的变化为15 -> 31 -> 47 -> 70 -> 105
注意到15是二进制的1111
,而31是二进制的11111
,可能是设计成这样的?...
只有第一次变化不是1.5倍扩容,后面都是乘以1.5
当长度为15时,如下,两个0x0f
表示长度,而第一行倒数第三个0f
则表示的是当前的capacity
![e587f829ed028715447b9c0f51f7fc9f.png](https://i-blog.csdnimg.cn/blog_migrate/5f8725b73ba21b1bd078527b7348daf5.jpeg)
再次+='a'
![1cb811f8422bed1dcaaa2dcec20b433e.png](https://i-blog.csdnimg.cn/blog_migrate/bf958cfcc2a822f435505c9ed375d4e2.jpeg)
原先的capacity
已经从0x0f变成了0x1f,长度也变成了16
而原先存储字符串的一部分内存也已经被杂乱的字符覆盖了
新的字符串被连续存储在另一块地址
![52c3c71bdaf2be34b461d3101b3227cc.png](https://i-blog.csdnimg.cn/blog_migrate/c52428b69b9849e07524b0144c4d46e5.jpeg)
vs的调试中,红色代表刚刚改变的值
不过原先使用的内存里还有一些aaaa...
,可能是因为还没有被覆盖到
IDA视角
测试程序1
#include<iostream>
#include<string>
using namespace std;
int main(int argc, char** argv) {
string input;
cin >> input;
for (int i = 0; i < 3; i++) {
input += 'a';
}
for (int i = 0; i < 3; i++) {
input.append("12345abcde");
}
cout << input << endl;
return 0;
}
//visual studio 2019 x64 release
我用的IDA7.0,打开以后发现IDA似乎并没有对string
的成员进行适合读代码的命名,只好自己改一下
![24abf254554ae8d87823a2654de296a6.png](https://i-blog.csdnimg.cn/blog_migrate/3fbb40a44e2ab2714cb0385fc18c206f.jpeg)
第一块逻辑,当size>capacity
时,调用Rellocate_xxx
函数
否则,就直接在str_addr
后追加一个97,也就是a
![24abf254554ae8d87823a2654de296a6.png](https://i-blog.csdnimg.cn/blog_migrate/3fbb40a44e2ab2714cb0385fc18c206f.jpeg)
第二块逻辑,这次因为用的是append()
,每次追加10个字符,即使是一个QWORD
也无法存放,所以看到的是memmove_0
函数
最后是v9[10] = 0
,也是我们在vs中看到的,追加后,仍然会以x00
结尾
一开始我没想明白,+='a'
为什么没有设置x00结尾
后来才发现,*(_WORD*)&str_addr[_size] = 97;
这是一个WORD
,2个byte,考虑小端序,x00
已经被写入了
至于其中的Reallocate_xxx
函数,有点复杂...而且感觉也没必要深入了,刚刚已经在vs里了解扩容机制了
最后还有一个delete
相关的
![adf168c4666733bc11d9080af75455fe.png](https://i-blog.csdnimg.cn/blog_migrate/dd15187d48ae4da54557a76feed04875.jpeg)
之前在做题时经常分不清作者写的代码、库函数代码,经常靠动态调试猜,多分析之后发现清晰了不少
测试程序2
#include<iostream>
#include<string>
using namespace std;
int main(int argc, char** argv) {
string input1;
string input2;
string result;
std::cin >> input1;
std::cin >> input2;
result = input1 + input2;
std::cout << result;
return 0;
}
//g++-4.7 main.cpp
这次用g++编译,发现逻辑很简明,甚至让我怀疑这是C++吗...
![d5c0231ef73b7fa210d345db864b38fa.png](https://i-blog.csdnimg.cn/blog_migrate/b1e3fe2d0afaa456287550a3d30df561.jpeg)
调用了一次operator+
,然后operator=
赋值,最后输出
但是用vs编译,IDA打开就很混乱...下次再仔细分析一下
测试程序3
#include<iostream>
#include<string>
using namespace std;
int main(int argc, char** argv) {
string input1;
string input2;
std::cin >> input1;
std::cin >> input2;
//语法糖
for(auto c:input2){
input1 += c;
}
std::cout << input1;
return 0;
}
//g++-4.7 main.cpp -std=c++11
仍然是g++编译的,IDA打开后虽然没有友好的命名,需要自己改,但是逻辑很清晰
![1f20319bb9b3b74dccfbcbf5139cdd6f.png](https://i-blog.csdnimg.cn/blog_migrate/82c4f560da8ffcdb9c4367ece94de47b.jpeg)
for(auto c:input2)
这句是一个"语法糖",迭代地取出每一个字符,追加到input1
上
IDA中可以看到,迭代器begin和end
,通过循环中的operator!=
判断是否已经结束,再通过operator+=
追加,最后通过operator++
来改变迭代器input2_begin
的值
这里命名应该把input2_begin
改成iterator
更好一些,因为它只是一开始是begin
小总结
逆向水深...动态调试确实很容易发现程序逻辑,但是有反调试的存在
多练习纯静态分析也有助于解题,看得多了也就能分辨库函数代码和作者的代码了。
欢迎关注微信公众号【筑梦编程】,跟大家一起讨论交流。