主要说一下怎么逆向 Pwn 题里常见的各种数据结构。
0x01. 数组
最简单的一种就是逆向数组,因为数组的形式在经过反编译之后会呈现出指针偏移的形式,e.g. *(p + offset)
。
比如说下面这份代码:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+4h] [rbp-Ch]
__int64 v5; // [rsp+8h] [rbp-8h]
for ( i = 0; i <= 9; ++i )
{
*(4LL * i + v5) *= i * i;
printf("%d\n", *(4LL * i + v5));
}
return 0;
}
看到这种 *(p + index * offset)
的形式,大概率这里的 v5 是个数组指针,稍微尝试一下,发现可以恢复成如下形式:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int i; // [rsp+4h] [rbp-Ch]
int *v5; // [rsp+8h] [rbp-8h]
for ( i = 0; i <= 9; ++i )
{
v5[i] *= i * i;
printf("%d\n", v5[i]);
}
return 0;
}
这里的技巧是根据初始的定义 v5 是 8 字节的整型,但是偏移是 4 字节跳转的,所以实际上数组元素的大小是 4 字节,赋个 __int32
之类的就可以。
结构体
结构体的逆向,一个前置知识点在于如何计算结构体字节数。
我这里用一道题目的某个函数来举例子。
int __fastcall feed(_QWORD *a1, const char *a2, __int64 a3)
{
unsigned __int64 v3; // rax
char *s1; // [rsp+20h] [rbp-20h]
unsigned __int64 v7; // [rsp+28h] [rbp-18h]
unsigned __int64 v8; // [rsp+30h] [rbp-10h]
unsigned __int64 i; // [rsp+38h] [rbp-8h]
v3 = sub_12C9(a2);
v8 = v3;
if ( v3 )
{
v3 = 10 * (v3 / 0xA);
v7 = v8 % 0xA;
for ( i = 0LL; i <= 0xA; ++i )
{
s1 = (*a1 + 24 * i + 96 * v7 + 80);
if ( !strncmp(s1, a2, 0x10uLL) )
{
*(s1 + 2) += a3; // Pin!
LODWORD(v3) = printf("It %s has weight %zu\n", s1, *(s1 + 2));
return v3;
}
v3 = *(*a1 + 8 * v7);
if ( i == v3 )
{
strncpy(s1, a2, 0x10uLL);
*(s1 + 2) += a3; // Pin!
if ( a2 != "useless" )
printf("It %s has weight %zu\n", s1, *(s1 + 2));
++*(*a1 + 8 * v7);
LODWORD(v3) = sub_133C(&qword_4060, "useless");
return v3;
}
}
}
return v3;
}
注意一下反编译得到的代码中被 Pin 的两行,可以看到这就是前面提到的数组结构,但是它的偏移是个固定值…? 这很不合理。
根据上下文,,我们知道这个 printf
分别打印了名称和重量,而且数据的格式也可以通过格式化字符串看出来,所以 IDA 创建一个结构体给 s1 用着先:
struct map_entry
{
char name[16];
__int32 weight;
};
然后发现得到了 *&s1->weight
这种东西,想一想,应该是 weight 的字节数不对,改成 __int64
就可以了。
结构体套数组套结构体、二维数组
根据上面两节的内容我们大概可以恢复成下面这个样子。
int __fastcall feed(_QWORD *a1, const char *name, __int64 size)
{
unsigned __int64 v3; // rax
map_entry *s1; // [rsp+20h] [rbp-20h]
unsigned __int64 index; // [rsp+28h] [rbp-18h]
unsigned __int64 v8; // [rsp+30h] [rbp-10h]
unsigned __int64 i; // [rsp+38h] [rbp-8h]
v3 = sub_12C9(name);
v8 = v3;
if ( v3 )
{
v3 = 10 * (v3 / 0xA);
index = v8 % 0xA;
for ( i = 0LL; i <= 0xA; ++i )
{
s1 = (*a1 + 24 * i + 96 * index + 80);
if ( !strncmp(s1->name, name, 0x10uLL) )
{
s1->weight += size;
LODWORD(v3) = printf("It %s has weight %zu\n", s1->name, s1->weight);
return v3;
}
v3 = *(*a1 + 8 * index);
if ( i == v3 )
{
strncpy(s1->name, name, 0x10uLL);
s1->weight += size;
if ( name != "useless" )
printf("It %s has weight %zu\n", s1->name, s1->weight);
++*(*a1 + 8 * index);
LODWORD(v3) = sub_133C(&qword_4060, "useless");
return v3;
}
}
}
return v3;
}
接下来看 a1,注意到 s1 = (*a1 + 24 * i + 96 * index + 80);
这一行,由于是给 *a1
进行加偏移的操作访问内存,说明 a1 应该有个指针套指针。
struct map {
map_data *data;
};
再者,由于这里同时存在两个偏移量,说明是二维数组,形式如 i * offset1 + j * offset2
,其中所乘的 offset
大说明相应的下标在前面,这里就是 index 在前面,i 在后面,而 gcd(offset1, offset2)
就是这个数组里元素的大小,这里是 24,说明是个自定义类型。
最后面有个 80
常数,说明这个数据结构的数组前面常驻 80 字节的偏移,而 ++*(*a1 + 8 * index);
这一句正是在操作这 80 字节的内部,所以有
struct map_data {
__int64 bins_size[10];
map_entry bins[10][96/24]; // 前标的范围是根据上下文得到的,后面的范围是用大的 offset 除以小的 offset
};
最后就逆的很完美了,也很容易看出来对应的漏洞。
int __fastcall feed(map *a1, const char *name, __int64 size)
{
unsigned __int64 v3; // rax
map_entry *s1; // [rsp+20h] [rbp-20h]
unsigned __int64 index; // [rsp+28h] [rbp-18h]
unsigned __int64 v8; // [rsp+30h] [rbp-10h]
unsigned __int64 i; // [rsp+38h] [rbp-8h]
v3 = sub_12C9(name);
v8 = v3;
if ( v3 )
{
v3 = 10 * (v3 / 0xA);
index = v8 % 0xA;
for ( i = 0LL; i <= 0xA; ++i )
{
s1 = &a1->data->bins[index][i];
if ( !strncmp(s1->name, name, 0x10uLL) )
{
s1->weight += size;
LODWORD(v3) = printf("It %s has weight %zu\n", s1->name, s1->weight);
return v3;
}
v3 = a1->data->bins_size[index];
if ( i == v3 )
{
strncpy(s1->name, name, 0x10uLL);
s1->weight += size;
if ( name != "useless" )
printf("It %s has weight %zu\n", s1->name, s1->weight);
++a1->data->bins_size[index];
LODWORD(v3) = gift(&qword_4060, "useless");
return v3;
}
}
}
return v3;
}
总结
- 整数或指针:如果使用32位或64位指令(如 mov),并且偏移量是4或8的倍数,这通常意味着访问的是整数或指针。
- 字符串或字符数组:当使用指针偏移方式(如
mov eax, [ebx+4]
后的mov byte ptr [eax], 'A'
)并且访问的是一个字节时,可能是字符或字符数组。 - 浮点数或大整数:如果程序使用
fld
、fstp
这样的浮点指令,可能意味着正在处理浮点数或结构体中的浮点类型成员。
总结一下,这玩意的重点还是在于细心和对上下文的合理推测,基本上把乱七八糟的指针引用弄干净了就算是逆向完成了。