Zj_W1nd's BLOG

STL容器逆向——逆向工程实验STLWP

2024/09/19

什么是STL?

STL(Standard Template Library)是C++库为我们实现好的一系列数据结构和方法,是一些常用结构的标准实现。他们的数据结构都被定义好了,在编程的时候可以直接调用。

那么对于逆向来说,我们主要是要识别出对应的STL容器类以及对他们操作的函数功能(常见的类方法),识别相关容器的内存结构能起到很大作用。简单记录一下,其实这个没有Go这种逆向难。

本题中STL容器的内存结构和分析

参考https://www.52pojie.cn/thread-1720719-1-1.html
参考https://zhuanlan.zhihu.com/p/604288453,这篇文章写了一个LLVM PASS来分析底层结构注意,以下的结构并不是“严格”遵循的,比如本题中红黑树就有细小的差别,应该与版本等有关,重要的是识别出在干嘛然后方便我们猜测程序的功能。

String

C++的String结构包含一个指向内容的指针,一个长度和一个联合体:

1
2
3
4
5
6
7
8
9
10
class.std::__cxx11::basic_string {
struct.std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_Alloc_hider {
uint8_t* ptr;
} dataplus;
uint64_t string_length;
union.anon {
uint64_t allocated_capacity;
uint8_t local_buf[8];
};
} field_0;

如果字符串长度较小(小于8),会直接将字符串的内容和String结构放在一起,第一个字段就指向localbuf,也就是说对于局部变量字符串就会放在栈上,我们能看到。如果字符串长,字符串内容就会分配在堆上,那么这个字段就会存储堆上分配的空间大小。

对于String,结合本题来看,动调确定主流程中的输入模块之后分析参数可以看出是String,这时候可以先输入8个或以下字符跟进看一下,在栈上就大概率是了。

Vector

Vector容器看着功能多,实际上底层就三个指针,第一个存放数据开始,第二个存放数据结束,第三个存放当前vector容量的结束位置。

1
2
3
4
5
6
7
8
9
10
11
class.std::vector<string> {
struct.std::_Vector_base {
struct.std::_Vector_base<std::__cxx11::basic_string<char>, std::allocator<std::__cxx11::basic_string<char> > >::_Vector_impl {
struct.std::_Vector_base<std::__cxx11::basic_string<char>, std::allocator<std::__cxx11::basic_string<char> > >::_Vector_impl_data {
class.std::__cxx11::basic_string* start;
class.std::__cxx11::basic_string* finish;
class.std::__cxx11::basic_string* end_of_storage;
} field_0;
} field_0;
} field_0;
} field_0;

内存中的结构就是三个相连的指针。如果vector是一直随输入变化增长的没有事先申请额外的空间(一般可能都是这样),那么finish字段的内容和end_of_storage将会指向同一个地址。如果内存中有一个开始地址,连续两个结束地址而且跟进看发现是数据,那么大概率是vector了。

rb_tree

红黑树,我并不了解红黑树相关的算法思想,这里先简单介绍一下:

红黑树就是一个自平衡的搜索二叉树,和一般二叉树不同的是,它为每个节点标注夜色(红/黑),并遵循一定的规则。根节点黑色开始,叶子结点(NIL)黑色结束。红色节点的子节点必须都是黑色,且任意节点到每个叶子结点的所有路径都包含相同数量的黑色节点。总之就是通过某些人为的染色规定,保证二叉树的相对平衡,令其在最坏情况下有O(logn)的复杂度。

我们并不关心它的算法实现,STL为我们实现好了,我们关心的是他的内存视图。他的内存视图有两种,header和node。header是一个代表一整颗树的变量,node是真正的结点。

对于一个header,它应该包含以下信息(本题中一个header占6个机器字长)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class.std::map<string,string> {
class.std::_Rb_tree.6 {
struct.std::_Rb_tree<std::__cxx11::basic_string<char>, std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> >, std::_Select1st<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > >, std::less<std::__cxx11::basic_string<char> >, std::allocator<std::pair<const std::__cxx11::basic_string<char>, std::__cxx11::basic_string<char> > > >::_Rb_tree_impl {
struct.std::_Rb_tree_key_compare {
struct.std::less {
uint8_t value;
} key_compare;
} compare;
struct.std::_Rb_tree_header {
struct.std::_Rb_tree_node_base {
uint32_t color; // 不知道为什么做题的时候这里header是8个字节
struct.std::_Rb_tree_node_base* parent;
struct.std::_Rb_tree_node_base* left;
struct.std::_Rb_tree_node_base* right;
} node;
uint64_t node_count;
} field_1;
} field_0;
} field_0;
} field_0;

简单说,第一个字段不重要(也没懂是干嘛的),后面包含color,parent,left,right的三个指针,以及一个节点总数。对于rbtree,color只取0和1,0黑1红。

header的这三个指针,在树为空的时候parent为0,left和right指向自己,树初始化后,paretn指向根节点,left和right则指向这个树最左边和最右边的结点。同时header和真正的根节点root他们的parent互相是对方。

node

一个节点存储的信息就好说了,只有color(0和1,int32),parent,left,right三个指针,以及一个存储真正数据的element data域。

总之,红黑树的关键就是那个color,识别到有这种0和1的color域就可能是红黑树。

Others

STL容器还有很多,deque,基于红黑树的map等等,这里不再细说,了解这三种足够我们做这道题了。

题目分析

拿到手IDA打开,主函数很清晰,输入正确会显示flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
v18 = sub_4E0940;
v19 = &off_5010FC;
v20 = &v45;
v21 = &loc_401ACF;
v22 = v13;
sub_40CB20(&v13[64], &loc_401ACF, envp);
sub_40B470();
sub_4C17C0(&a1);
sub_4BCE40(&vector1);
v28[0] = 0xA7398D39;
v28[1] = 0x7EA7887E;
v28[2] = 0xF3DE7E39;
v28[3] = 0xB5F37E12;
v28[4] = 0x733AF388;
v28[5] = 0x7EDE73F3;
v28[6] = 0xA7DE8DAA;
v28[7] = 0xA7DEA739;
v28[8] = 0x8D7EB57E;
ssm0.m128i_i64[0] = (__int64)v28; // 128位16字节,第一个存数组指针第二个存数组长度
ssm0.m128i_i64[1] = 9LL;
nop2((__int64)&v29);
ssm00 = _mm_load_si128(&ssm0);
v17 = 1;
init_vec(&vector2, ssm00.m128i_i64, (__int64)&v29);
nop((__int64)&v29);
memcpy(ascii_db0, ascii_db, sizeof(ascii_db0));
ssm1.m128i_i64[0] = (__int64)ascii_db0;
ssm1.m128i_i64[1] = 256LL;
nop3((__int64)&v32);
ssm00 = _mm_load_si128(&ssm1);
v17 = 2;
tree_create(&rb_tree, ssm00.m128i_i64, (__int64)&v31, (__int64)&v32);
nop4((__int64)&v32);
v17 = 3;
String_append(qword_4E4740, &a1, v3);
if ( get_lenth(&a1) != 36 )
exit(0);
for ( i = 0; ; i += 4 )
{
ssm1.m128i_i64[0] = i;
lenth = get_lenth(&a1);
if ( ssm1.m128i_i64[0] >= lenth )
break;
v17 = 3;
chr = (unsigned __int8 *)String_get_idx(&a1, i);
enc0 = enc(*chr);
v43 = *(_BYTE *)sub_4B7D90((__int64)&rb_tree, &enc0);
idx = (unsigned __int8 *)String_get_idx(&a1, i + 1);
v34 = enc(*idx);
v42 = *(_BYTE *)sub_4B7D90((__int64)&rb_tree, &v34);
v7 = (unsigned __int8 *)String_get_idx(&a1, i + 2);
v35 = enc(*v7);
v41 = *(_BYTE *)sub_4B7D90((__int64)&rb_tree, &v35);
v8 = (unsigned __int8 *)String_get_idx(&a1, i + 3);
v36 = enc(*v8);
v40 = *(_BYTE *)sub_4B7D90((__int64)&rb_tree, &v36);
v37 = (v41 << 8) | (v42 << 16) | (v43 << 24) | v40;
vector_pushback((__int64)&vector1, &v37);
}
ssm0.m128i_i64[0] = chr_memcpy2stack(&vector2);
ssm1.m128i_i64[0] = sub_4BCC10((__int64)&vector1);
v9 = chr_memcpy2stack(&vector1);
v17 = 3;
if ( (unsigned __int8)sub_4D8000(v9, ssm1.m128i_i64[0], ssm0.m128i_i64[0]) )
{
sub_4DB950(v39, "you find it, flag is vmc{", &a1);
v17 = 4;
sub_4DB900(v38, v39, "}");
v17 = 5;
v10 = sub_4DB310(&unk_4E4AA0, v38);
((void (__fastcall *)(__int64))sub_4D7EE0)(v10);
sub_4C1B00(v38);
sub_4C1B00(v39);
}
else
{
v17 = 3;
v11 = sub_4DB0D0(&unk_4E4AA0, "try again");
((void (__fastcall *)(__int64))sub_4D7EE0)(v11);
}
sub_4B7D70(&rb_tree);
sub_4BCEA0(&vector2);
sub_4BCEA0(&vector1);
sub_4C1B00(&a1);
ssm1.m128i_i32[0] = 0;
sub_40CB80(&v16);
return ssm1.m128i_i32[0];

这里用了STL容器但是把符号全扣了,我们先来找一下输入函数,动调单步,输入函数看不懂太复杂,确定输入是这个a1,而且输入8个字符放在栈上,结构也符合,a1应该是一个String类来接受我们的输入。确定是String类后根据String的结构还原一下结构体,也就找到了getlenth的函数,可以看到我们要输入36个字符。

中间的ssm和m128i这种代表用了simd指令集,用到了那个128位的xmm寄存器,操作这个寄存器可以按64,32位去放数据。允许两个机器字长。

加密逻辑就是主函数的循环,一次跑4个字节,跑9轮到长度就退出。还原结构体后可以看到中间是按i去从String内容里按下标取出字符,然后调用一个位运算操作:

1
2
3
4
5
6
7
8
9
char *__fastcall String_get_idx(String_class *a1, __int64 a2)
{
return &a1->ptr[a2];
}

__int64 __fastcall enc(unsigned __int8 a1)
{
return ~((16 * a1) | (unsigned int)((int)a1 >> 4));
}

最关键的,就是这个左移右移处理过后的字符传入的这个函数4b7d90,我们可以看到最后是将4个字节拼凑成一个然后做了一些操作,那么这个4b7d90的加密就很重要。

关键函数分析

开始入手的时候试图分析这个函数,但是跟进发现一拖史,调用套调用最后看不懂,而且相当多功能简单(直接返回,返回arg+8)的函数没有复用都是新的符号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
__int64 __fastcall sub_4B7D90(struct my_rbtree_header *rb_tree, char *enc_byte)
{
_BYTE *off32; // rax
__int64 enc_byte2; // rax
__int64 v5; // r8
__int64 v7; // [rsp+38h] [rbp-48h] BYREF
__int64 off8_ptr; // [rsp+40h] [rbp-40h] BYREF
char v9; // [rsp+4Eh] [rbp-32h] BYREF
char v10; // [rsp+4Fh] [rbp-31h] BYREF
_BYTE v11[8]; // [rsp+50h] [rbp-30h] BYREF
_QWORD v12[3]; // [rsp+58h] [rbp-28h] BYREF

v7 = sub_4B7B00((__int64)rb_tree, (__int64)enc_byte);// 0x10返回
off8_ptr = get_off8_ptr_((__int64)rb_tree);
if ( isderefArg1_eq_derefArg2(&v7, &off8_ptr)
|| (sub_429160(rb_tree), off32 = (_BYTE *)get_off32(&v7), isArg2_below_arg3((__int64)&v9, enc_byte, off32)) )
{
enc_byte2 = just_ret3((__int64)enc_byte);
sub_4D5BA0((__int64)v11, enc_byte2, v5);
sub_4B79E0(v12, &v7);
v7 = sub_4CD350((_DWORD)rb_tree, v12[0], (unsigned int)&unk_4E6000, (unsigned int)v11, (__int64)&v10);
}
return get_off32(&v7) + 1;
}

看不懂在干嘛,看STL相关结构的同时决定动调。

我们能看到这个函数接收了两个参数,这第一个参数就很重要。回到主函数的开头,这个变量参与了这样一个函数(当然名字是做出来之后才有的):

1
tree_create(&rb_tree, ssm00.m128i_i64, (__int64)&v31, (__int64)&v32);

这个函数的内容我们也看不懂,但是不重要,它接受的第二个参数很有意思,在前面的初始化过程中,它存储的是一段神秘数据的指针。检查的时候这段数据是从0-FF依次对应另一个字节,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  memcpy(ascii_db0, ascii_db, sizeof(ascii_db0));
ssm1.m128i_i64[0] = (__int64)ascii_db0;
ssm1.m128i_i64[1] = 256LL;
nop3((__int64)&v32);
ssm00 = _mm_load_si128(&ssm1);

// 类似于这样:
.data:00000000004E2020 ascii_db my_bytes < 0, 2Eh> ; 0
.data:00000000004E2020 ; DATA XREF: main+186↑o
.data:00000000004E2022 my_bytes < 1, 7Dh> ; 1
.data:00000000004E2024 my_bytes < 2, 0C5h> ; 2
.data:00000000004E2026 my_bytes < 3, 1Dh> ; 3
.data:00000000004E2028 my_bytes < 4, 96h> ; 4
.data:00000000004E202A my_bytes < 5, 0BAh> ; 5
.data:00000000004E202C my_bytes < 6, 4Ah> ; 6
... //连续存储

这个东西程序一拿到手就修了,但是不知道在哪用的。但是很重要的是,我们的输入函数还在靠后的位置,前面初始化的这些东西都和输入无关,代表他们都是死的,动调到输入之前看看初始化的都是什么,如果是一些加密用的东西直接dump出来就不用分析函数了。抱着这个心态,发现了红黑树:

动调之后函数一过:

1
2
3
4
5
6
Stack[00006BA8]:000000000071FA20                 dq offset unk_A31E80
Stack[00006BA8]:000000000071FA28 dq 0
Stack[00006BA8]:000000000071FA30 dq offset unk_B54690
Stack[00006BA8]:000000000071FA38 dq offset unk_A31EF0
Stack[00006BA8]:000000000071FA40 dq offset unk_B58E90
Stack[00006BA8]:000000000071FA48 dq 100h

6个指针,跟进去看结构,

1
2
3
4
5
6
debug030:0000000000B54690 dword_B54690    dd 1                    ; DATA XREF: Stack[00006BA8]:000000000071FA30↑o
debug030:0000000000B54694 dd 0BAADF00Dh
debug030:0000000000B54698 dq offset qword_71FA28
debug030:0000000000B546A0 dq offset unk_A32A90
debug030:0000000000B546A8 dq offset unk_B55E90
debug030:0000000000B546B0 db 3Fh ; ?

parent指向header,左右指针,红黑树是八九不离十了。这时候我们就可以猜测这个函数是建立了一棵树,结合我们前面看到的那个下标-字符数组,我们就可以大胆猜测是按照那个数组建立的,加密函数是按照某种逻辑(对应下标)从这个树上搜一个值出来。

黑盒测试,前面经过动调已经知道输入特定字符加密过后是不变的了,这时候我们去看那个数组的0x61看是不是a加密后的3a,发现不是。动调之后再猜测,这棵树是一个S盒,我们是把加密后的那个字符输入然后返回一个代换字符,确定不了,把这个数组dump出来转成S盒还原一下加密逻辑,结果和动调获得的一致,加密逻辑就分析出来了:按照索引建立一个红黑树,本质上是一个S盒代换

最后就是将4个字节拼起来放在一个vector里(动调看内存确定的)。前面栈上那9个初始化的32位变量就是被放进了vector,最后的比较也是传两个vector,那么最后肯定是我们的输入加密后和那个初始化的比较了

1
2
3
4
//vector
Stack[00006BA8]:000000000071FA50 dq offset unk_A31E90 //start
Stack[00006BA8]:000000000071FA58 dq offset unk_A31EB4 //end
Stack[00006BA8]:000000000071FA60 dq offset unk_A31EB4 //capacity_end

exp

逆向s盒不会写,按字节也就256个,按字节爆破和结果比对出flag就行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
# a(0x61)-0x3a b-0x4e c-0xa7 d-0x88 连在一起
#
# 首先将输入的ascii进行enc后取后两位索引红黑树查找,db事先写好在程序
# 一轮4个,拿到4个字节按顺序拼在一起装入vector
# 猜测最后要和栈上的比较
# 尝试
ascii_db =[
0, 46, 1, 125, 2, 197, 3, 29, 4, 150,
5, 186, 6, 74, 7, 174, 8, 17, 9, 179,
10, 33, 11, 230, 12, 110, 13, 187, 14, 27,
15, 205, 16, 203, 17, 52, 18, 221, 19, 102,
20, 183, 21, 91, 22, 148, 23, 65, 24, 90,
25, 54, 26, 137, 27, 122, 28, 233, 29, 96,
30, 70, 31, 251, 32, 19, 33, 16, 34, 134,
35, 220, 36, 191, 37, 114, 38, 95, 39, 24,
40, 7, 41, 237, 42, 216, 43, 162, 44, 164,
45, 243, 46, 25, 47, 32, 48, 242, 49, 169,
50, 38, 51, 72, 52, 97, 53, 107, 54, 101,
55, 84, 56, 234, 57, 212, 58, 225, 59, 5,
60, 12, 61, 43, 62, 14, 63, 176, 64, 47,
65, 99, 66, 48, 67, 199, 68, 85, 69, 210,
70, 133, 71, 82, 72, 214, 73, 228, 74, 156,
75, 139, 76, 0, 77, 23, 78, 145, 79, 40,
80, 198, 81, 178, 82, 50, 83, 79, 84, 103,
85, 106, 86, 51, 87, 2, 88, 45, 89, 75,
90, 215, 91, 238, 92, 154, 93, 184, 94, 226,
95, 173, 96, 208, 97, 211, 98, 73, 99, 135,
100, 6, 101, 36, 102, 192, 103, 161, 104, 15,
105, 194, 106, 92, 107, 10, 108, 170, 109, 111,
110, 56, 111, 249, 112, 223, 113, 138, 114, 9,
115, 132, 116, 146, 117, 63, 118, 94, 119, 67,
120, 155, 121, 68, 122, 62, 123, 3, 124, 181,
125, 41, 126, 190, 127, 26, 128, 151, 129, 13,
130, 118, 131, 188, 132, 207, 133, 105, 134, 189,
135, 60, 136, 80, 137, 163, 138, 88, 139, 4,
140, 18, 141, 209, 142, 244, 143, 34, 144, 253,
145, 49, 146, 147, 147, 182, 148, 123, 149, 61,
150, 20, 151, 22, 152, 157, 153, 141, 154, 204,
155, 64, 156, 159, 157, 28, 158, 152, 159, 158,
160, 98, 161, 89, 162, 124, 163, 239, 164, 168,
165, 231, 166, 255, 167, 131, 168, 227, 169, 57,
170, 86, 171, 247, 172, 100, 173, 248, 174, 109,
175, 172, 176, 165, 177, 246, 178, 241, 179, 121,
180, 83, 181, 218, 182, 177, 183, 93, 184, 112,
185, 136, 186, 144, 187, 236, 188, 126, 189, 180,
190, 142, 191, 37, 192, 195, 193, 224, 194, 160,
195, 59, 196, 143, 197, 185, 198, 196, 199, 113,
200, 120, 201, 167, 202, 35, 203, 235, 204, 115,
205, 66, 206, 108, 207, 229, 208, 245, 209, 1,
210, 53, 211, 117, 212, 171, 213, 166, 214, 39,
215, 250, 216, 81, 217, 78, 218, 140, 219, 130,
220, 104, 221, 31, 222, 69, 223, 217, 224, 175,
225, 193, 226, 11, 227, 128, 228, 77, 229, 153,
230, 254, 231, 42, 232, 149, 233, 58, 234, 55,
235, 119, 236, 116, 237, 200, 238, 129, 239, 240,
240, 76, 241, 219, 242, 201, 243, 8, 244, 202,
245, 21, 246, 206, 247, 87, 248, 30, 249, 127,
250, 213, 251, 44, 252, 222, 253, 232, 254, 252,
255, 71
]

Sbox = [0]*256

# 将原本代码中的红黑树搜索转化成Sbox代换
for i in range(0, len(ascii_db), 2):
index = ascii_db[i]
value = ascii_db[i + 1]
Sbox[index] = value

def enc_chr (c):
return ~((c<<4)|(c>>4))

result = [
0xA7,0x39,0x8D,0x39,
0x7E,0xA7,0x88,0x7E,
0xF3,0xDE,0x7E,0x39,
0xB5,0xF3,0x7E,0x12,
0x73,0x3A,0xF3,0x88,
0x7E,0xDE,0x73,0xF3,
0xA7,0xDE,0x8D,0xAA,
0xA7,0xDE,0xA7,0x39,
0x8D,0x7E,0xB5,0x7E
]

def enc(chr):
chr = int(chr)
idx = enc_chr(chr) & 0xFF
crypt = Sbox[idx]
return crypt
flag=b''

for r in result:
for c in range(256):
if enc(c) == r:
flag += bytes([c])

print(flag)
# result = enc_chr(int(0x64)) & 0xFF
# print(hex(inv_Sbox[Sbox[int(result)]]))
1
2
3
PS D:\学校相关文件\逆向工程\stl> .\stl.exe
cefe4cd4-04e8-473a-d403-c0f9c0cef484
you find it, flag is vmc{cefe4cd4-04e8-473a-d403-c0f9c0cef484}

总结

函数还是太简单了,只要分析一个STL的数据结构就行,没有控制流的难度很多功能太好猜,扣符号真的恶心。感觉可以改改之后放l3h招新(纯粹数据结构还原)。

CATALOG
  1. 1. 什么是STL?
  2. 2. 本题中STL容器的内存结构和分析
    1. 2.1. String
    2. 2.2. Vector
    3. 2.3. rb_tree
      1. 2.3.1. header
      2. 2.3.2. node
    4. 2.4. Others
  3. 3. 题目分析
  4. 4. 关键函数分析
  5. 5. exp
  6. 6. 总结