C++逆向学习(一) string

2019-04-24 约 1720 字 预计阅读 4 分钟

声明:本文 【C++逆向学习(一) string】 由作者 ret2nullptr 于 2019-04-24 08:58:00 首发 先知社区 曾经 浏览数 131 次

感谢 ret2nullptr 的辛苦付出!

CTF比赛中C++的题越来越多,题目中经常出现stringvector等,而实际上手时发现常常迷失在"库函数"中,比如跟进了空间配置器相关函数

最近研究一下关于这些的底层机制与逆向,应该会写成一个系列

string

内存布局

visual studio的调试实在是太好用了,因此用它举例

定义一个string类,字符串为abcd,内存布局如下

其中,size是当前字符串长度,capacity是最大的容量

可以发现,capacitysize大的多

allocator是空间配置器,可以看到单独的字符显示

原始视图中可以得知,字符串的首地址

可以看到,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

再次+='a'

原先的capacity已经从0x0f变成了0x1f,长度也变成了16

而原先存储字符串的一部分内存也已经被杂乱的字符覆盖了

新的字符串被连续存储在另一块地址

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的成员进行适合读代码的命名,只好自己改一下

第一块逻辑,当size>capacity时,调用Rellocate_xxx函数

否则,就直接在str_addr后追加一个97,也就是a

第二块逻辑,这次因为用的是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相关的

之前在做题时经常分不清作者写的代码、库函数代码,经常靠动态调试猜,多分析之后发现清晰了不少

测试程序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++吗...

调用了一次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打开后虽然没有友好的命名,需要自己改,但是逻辑很清晰

for(auto c:input2)这句是一个"语法糖",迭代地取出每一个字符,追加到input1

IDA中可以看到,迭代器begin和end,通过循环中的operator!=判断是否已经结束,再通过operator+=追加,最后通过operator++来改变迭代器input2_begin的值

这里命名应该把input2_begin改成iterator更好一些,因为它只是一开始是begin

小总结

逆向水深...动态调试确实很容易发现程序逻辑,但是有反调试的存在

多练习纯静态分析也有助于解题,看得多了也就能分辨库函数代码和作者的代码了

关键词:[‘安全技术’, ‘二进制安全’]


author

旭达网络

旭达网络技术博客,曾记录各种技术问题,一贴搞定.
本文采用知识共享署名 4.0 国际许可协议进行许可。

We notice you're using an adblocker. If you like our webite please keep us running by whitelisting this site in your ad blocker. We’re serving quality, related ads only. Thank you!

I've whitelisted your website.

Not now