主要说一下怎么逆向 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;
}

总结

  1. 整数或指针:如果使用32位或64位指令(如 mov),并且偏移量是4或8的倍数,这通常意味着访问的是整数或指针。
  2. 字符串或字符数组:当使用指针偏移方式(如 mov eax, [ebx+4] 后的 mov byte ptr [eax], 'A' )并且访问的是一个字节时,可能是字符或字符数组。
  3. 浮点数或大整数:如果程序使用 fldfstp 这样的浮点指令,可能意味着正在处理浮点数或结构体中的浮点类型成员。

总结一下,这玩意的重点还是在于细心和对上下文的合理推测,基本上把乱七八糟的指针引用弄干净了就算是逆向完成了。