for input string: 2 }_C++逆向学习(一) string

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

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

string

内存布局

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

5260f65da6b3d04edb355cc0998ee8e3.png

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

可以发现,capacitysize大的多

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

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

47fdf6202dbd6a37bbddd577f998e5a1.png

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

再次+='a'

1cb811f8422bed1dcaaa2dcec20b433e.png

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

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

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

52c3c71bdaf2be34b461d3101b3227cc.png
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

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

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

24abf254554ae8d87823a2654de296a6.png

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

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

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

调用了一次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

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

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

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

小总结

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

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

欢迎关注微信公众号【筑梦编程】,跟大家一起讨论交流。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值