初见程序
附件下载: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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。