初见程序

附件下载:childRE.exe
运行程序后是能够输入的,但是当我们随机输入一些代码的时候,程序似乎卡住了

拖进ida中反编译:

int __fastcall main(int argc, const char **argv, const char **envp)
{
  __int64 v3; // rax
  __int64 v4; // rax
  const CHAR *v5; // r11
  __int64 v6; // r10
  int v7; // r9d
  const CHAR *v8; // r10
  __int64 v9; // rcx
  __int64 v10; // rax
  unsigned int v12; // ecx
  __int64 v13; // r9
  __int128 v14[2]; // [rsp+20h] [rbp-38h] BYREF

  memset(v14, 0, sizeof(v14));
  sub_140001080("%s");
  v3 = -1i64;
  do
    ++v3;
  while ( *((_BYTE *)v14 + v3) );
  if ( v3 != 31 )
  {
    while ( 1 )
      Sleep(0x3E8u);        // 就是这里,当输入字符串的长度不是31的时候sleep
  }
  v4 = XMMI2_FP_Emulation(v14);    //这个函数作用未知,跟踪后发现非常复杂!
  v5 = name;
  if ( v4 )                        // ?为什么要加一个判断
  {
    sub_1400015C0(*(_QWORD *)(v4 + 8));        //似乎是对数据的处理
    sub_1400015C0(*(_QWORD *)(v6 + 16));
    v7 = dword_1400057E0;
    v5[dword_1400057E0] = *v8;
    dword_1400057E0 = v7 + 1;
  }
  UnDecorateSymbolName(v5, outputString, 0x100u, 0);    //对c++名称去修饰化
  v9 = -1i64;
  do
    ++v9;
  while ( outputString[v9] );
  if ( v9 == 62 )                            // 去修饰化之后字符串长度应该是62
  {
    v12 = 0;
    v13 = 0i64;
    do
    {
      if ( a1234567890Qwer[outputString[v13] % 23] != a46200860044218[v13] )
        _exit(v12);
      if ( a1234567890Qwer[outputString[v13] / 23] != a55565653255552[v13] )
        _exit(v12 * v12);
      ++v12;                        // 这里的很容易解出
      ++v13;
    }
    while ( v12 < 0x3E );
    sub_140001020("flag{MD5(your input)}\n");        // 如果输入正确,md5一下就行
    return 0;
  }
  else
  {
    v10 = sub_1400018A0(std::cout);
    std::basic_ostream<char,std::char_traits<char>>::operator<<(v10, sub_140001A60);
    return -1;
  }
}

刚刚拿到反编译的结果,一上来就把所有东西都看穿是不现实的,需要耐心分析。不断的提出问题,猜测,写代码实践,查资料,一个一个的解决。

流程分析

ida反编译之后,可能无法准确地识别每个变量,导致会出现一些“冗余变量”,甚至会错误地把一个变量当成undefined的(比如这里的v5, v6, v8)。这种情况我们可以深入到汇编层级仔细研究,但是有时候我们暂时着眼于整体流程,忽略一些细节。在我们不能确定一个if存在的意义时,不妨先假设它会执行,如if(v4){}

我们可以大致的猜测出流程:
input -> XMMI2_FP_Emulation -> if(v4){加工数据} -> 去修饰化 -> 信息验证
正向分析到XMMI2_FP_Emulation就很困难了,但是我们可以通过变量的分布来揣测它的地位
注意这一行:

v4 = XMMI2_FP_Emulation(v14);
v5 = name;

v14是我们输入的数据,经过这个函数之后,v14就再也没有出现过了,所以v4和v5一定承载了我们的输入信息。这样,我们可以暂且跳过对这个复杂函数的分析,暂且将v4和v5当成我们的input
到这里,又引入一个问题:v5=name, 那么name是什么?它不是一个已经定义的全局变量,因为它的值在ida中显示的是???? 。 难道是编译器不能正确读取这一段数据吗?不,也可能是我们的思维太固执,或许name就是一段未使用的内存空间,v5仅仅是指向它,向name写数据,而不是读取name的数据呢!
那么我们就假设v4是input, v5是存储的buffer好了

接下来又遇到了这样的一个块:

if ( v4 )
  {
    sub_1400015C0(*(_QWORD *)(v4 + 8));
    sub_1400015C0(*(_QWORD *)(v6 + 16));
    v7 = dword_1400057E0;
    v5[dword_1400057E0] = *v8;
    dword_1400057E0 = v7 + 1;
  }

引入新的问题:为什么引用了两个值:v4+8和v6+16?
这个问题我们通过汇编层级的分析可以立刻解决:这是ida的双眼被蒙蔽了,实际上这两个都是v4 !
追踪sub_1400015c0:

__int64 __fastcall sub_1400015C0(unsigned __int8 *a1)
{
  int v2; // r8d
  __int64 result; // rax

  if ( a1 )
  {
    sub_1400015C0(*((_QWORD *)a1 + 1));    //这里有递归调用
    sub_1400015C0(*((_QWORD *)a1 + 2));    //
    v2 = dword_1400057E0;
    result = *a1;
    name[dword_1400057E0] = result;
    dword_1400057E0 = v2 + 1;
  }
  return result;
}

既然静态分析比较困难,我么就试着动态调试,用x64dbg打开,

在关键位置下断点,尝试输入abcdeabcdeabcdeabcdeabcdeabcde$,我们发现经过XMMI2_FP_Emulation处理之后的数据好像只是打乱了顺序,接受下面的未知递归函数处理。
我们试着跟踪v4 :

发现v4+8和v4+16都明显是一个地址,应该是指针。而前8个字节,通过代码分析,我们只需要第一个字节的字符,我们可以看到就是我们之前输入的字符串中的某个字符
随着指针跟踪下去,发现都是这样的结构

所以我们猜测这样的结构是一个node,可能有prev和next指针,也或许是一个treeNode,有left和right指针。如果是树的化,那上面的递归函数很可能就是后序遍历!!!这样一切都能解释清了。
我们继续调整输入,比如输入[a-z1-5]可以验证我们的猜想。

那么那个复杂的XMMI2_FP_Emulation函数其实就是通过输入的数据构造完全二叉树。输入的字符恰好有31个,刚好是一层一层排布的,然后后序遍历,输入到name部分。
逆向程序中识别数据结构是非常非常重要的!整个问题的形势从开始辨认出TreeNode的那一瞬间开始逆转了

UnDecorateSymbolName(v5, outputString, 0x100u, 0);

或许预示了数据处理到这里应该是一个符合c++ name mangle的字符串
然后就是数据判断部分,首先去修饰化之后长度应该是62,然后还要和内存中的一些数据比较。这一部分很清晰,可以直接用脚本还原

while ( outputString[v9] );
  if ( v9 == 62 )
  {
    v12 = 0;
    v13 = 0i64;
    do
    {
      if ( a1234567890Qwer[outputString[v13] % 23] != a46200860044218[v13] )
        _exit(v12);
      if ( a1234567890Qwer[outputString[v13] / 23] != a55565653255552[v13] )
        _exit(v12 * v12);
      ++v12;
      ++v13;
    }
    while ( v12 < 0x3E );
    sub_140001020("flag{MD5(your input)}\n");

不难知道我们需要让去修饰化之后的字符串是
private: char __thiscall R0Pxx::My_Aut0_PWN(unsigned char )

c++ name mangle学习

接下来的思路就很简单了,我们需要找到对上述函数修饰后的名称,而且长度必须是31个字符。
但是,不要忽视不同编译器的修饰方案存在或多或少的差异,我们需要使用msvc而不是mingw!
我找到了cl.exe进行实验,但是和结果还是有一点出入。
visual c++ name mangle wiki

c++名称解析
自己编写程序的obj中,可以找到这样的符号:
?myFunc@Parent@@AEAAPEADPEAE@Z
但解析后是:
private: char __ptr64__cdeclParent::myFunc(unsigned char __ptr64) __ptr64
第一个不同之处是调用约定,变成了__cdecl而不是__thiscall (尽管我在代码中写的是__thiscall),其次是多了很多__ptr64

通过学习visual c++的修饰方案,我们大致可以得到以下的信息(很难说清楚,还是得参考wiki):
以?开头
函数名@类名
@@ + ...(AccessLevel + CV-class-modifier FuncProperty)
其中AccessLevel指private、public等性质,CV指的是是否有const或者violate性质,
FuncProperty包含call-convention, return-val, arglist
call-convention,我们需要的是thiscall
return-val是一个Data类型,我们马上就说
arglist,是一系列Data类型,如果最后是@代表是定长参数,如果是Z则是变长
Data类型也有Accesslevel, datatype(int ,char等), CV-class-modifier
其中还有type修饰符,如P代表指针
?myFunc@Parent@@AAEPADPAE@Z是我们想要的,将函数名换一下,刚好是31位,md5后得到flag


StupidMagpie
6 声望0 粉丝