题目描述
//人死后会去向何方?
大一时候的一道题,当时学长发了天堂之门的相关文章给我,没有复现,回来再复现这道题,发现可以学到蛮多知识的,关于异常处理和天堂之门,最近也是遇到了其他异常处理的题目,发现自己也的确不是很熟悉。
DubheCTF-Destination&Moon | Clovershrub
DubheCTF 2024 Reverse – lrhtony 的小站
DUBHEctf_Destination | fishjump’s blog
题目描述和题目本身其实已经提示了天堂之门的考点
主函数直接下断点调试会直接跑飞,但是主函数里面翻了一下没有反调试,我们就在主函数执行之前的下断点,发现程序运行到0x418279的j__initterm函数时会退出,那这个就是反调试了,搜索相关资料可以发现该函数接受两个指针,指向一个函数指针数组的起始和结束位置,然后依次执行数组中每个非空函数指针指向的函数。
从0x41E000到0x41E318,数组很大,但是大部分是NULL,先出现?pre_cpp_initialization@@YAXXZ,后续再出现三个反调试的相关函数。
出题人给了源码,反调试写的挺复杂的,绕过方法很简单,把j__initterm函数nop或者地址区间的值全部改成0就行。
然后进入主函数,主函数反编译代码很简单
for ( i = 0; i < 45; ++i )
*((_BYTE *)&dword_4234A8 + i) = getchar();
__debugbreak();
for ( j = 0; j < 48; ++j )
{
if ( *((unsigned __int8 *)&dword_4234A8 + j) == (unsigned __int8)byte_423038[j] )
++v6;
}
if ( v6 == 48 )
sub_411122("Good answer! You will be taken to HEAVEN!\n", v4);
else
sub_411122("Bad answer! You will be taken to HELL!\n", v4);
system("PAUSE");
return 0;
只有输入和判断部分,但是运行会发现,最后判断时输入会被加密,并且这里布过 __debugbreak()函数会触发一个软件断点异常。
那么就需要看汇编界面了,这里发现了一个int 3指令触发了异常,并且注册了异常处理的函数
这里的sub_4113D4其实就是调用sub_4140D7函数,所以两者等同,这里触发异常后我们在sub_4140D7打个断点进入sub_4140D7函数,然后就发现函数有很多花指令。
参考别人文章,也利用IDA的Instruction tracing功能跟踪一下程序
该函数有很多循环,,高亮大部分执行的指令后,发现花指令主要包含两种,并且有些分支会连在一起
第一种是call和retn组成的无用花指令,第二种就是永真跳转,手动nop的话有点麻烦,我们可以看看出题人给的idc去花脚本
#include <idc.idc>
static main()
{
auto last_eip;
auto eip = 0x4140D7;
auto offset;
auto sum = 1;
auto just_jump = 0;
while(Byte(eip) != 0xC3) //遇到retn指令停止
{
if(Byte(eip) == 0xE8 && Byte(eip+1) == 0x00 && Byte(eip+1) == 0x00 && Byte(eip+1) == 0x00 && Byte(eip+1) == 0x00)
{ //当遇到无效call花指令
PatchByte(eip + 0, 0x90);
PatchByte(eip + 1, 0x90);
PatchByte(eip + 2, 0x90);
PatchByte(eip + 3, 0x90);
PatchByte(eip + 4, 0x90);
PatchByte(eip + 5, 0x90);
PatchByte(eip + 6, 0x90);
PatchByte(eip + 7, 0x90);
PatchByte(eip + 8, 0x90);
PatchByte(eip + 9, 0x90);
//去除call和retn组成的花指令
eip = eip + 10;
continue;
}
else if(Byte(eip) == 0xE9 && just_jump == 0)
{ //当遇到长跳转jmp指令
offset = Byte(eip+1);
offset = Byte(eip+2) * 256 + offset;
offset = Byte(eip+3) * 256 * 256 + offset;
offset = Byte(eip+4) * 256 * 256 * 256 + offset;
eip = eip + offset;
eip = eip + 5;
eip = eip & 0xffffffff;
print(eip);
//根据opcode计算跳转地址
sum = sum + 1;
just_jump = 1;
continue;
}
else if(Byte(eip) == 0x0F && Byte(eip+1) == 0x84 && just_jump == 0)
{ //当遇到永恒跳转花指令
last_eip = eip;
offset = Byte(eip+2);
offset = Byte(eip+3) * 256 + offset;
offset = Byte(eip+4) * 256 * 256 + offset;
offset = Byte(eip+5) * 256 * 256 * 256 + offset;
eip = eip + offset;
eip = eip + 6;
eip = eip & 0xffffffff;
//根据opcode计算跳转地址
print(eip);
sum = sum + 1;
just_jump = 1;
PatchByte(last_eip, 0x90);
PatchByte(last_eip+1, 0xE9);
//将永恒跳转花指令替换为jmp
continue;
}
else if(Byte(eip) == 0xEB && just_jump == 0)
{ //当遇到短跳转jmp指令
offset = Byte(eip+1);
eip = eip + offset;
eip = eip + 2;
// //根据opcode计算跳转地址
print(eip);
sum = sum + 1;
just_jump = 1;
continue;
}
else if(Byte(eip) == 0x74 && just_jump == 0)
{ //这里应该同理,但是翻了一下没找到这个对应花指令,去的差不多就行了
last_eip = eip;
offset = Byte(eip+1);
eip = eip + offset;
eip = eip + 2;
print(eip);
sum = sum + 1;
just_jump = 1;
PatchByte(last_eip, 0xEB);
continue;
}
eip = eip + 1;
just_jump = 0;
}
print(sum);
return 0;
}
去玩花指令之后我就卡在这里了,应该不知道什么原因,没有恢复好函数,IDA里面无法反编译生成伪代码了,一开始以为是大量的垃圾数据的原因,因为出题人除了设置了以上提到的两种花指令之外,在每个代码块直接还夹杂了大量没用的数据,如图
把这些数据nop掉要花很多时间,所以已经怀疑这些数据并不用处理了,最后nop完之后还是反编译不出伪代码。
后面和学长讨论研究了一下,发现指令处有一些这样的情况,如图
这里的jmp指令没有被识别在函数的指令块当中,我们需要对着jmp指令按E,就可以让IDA把他包括在指令块当中。还有一些指令块实际被执行,但是却没有被识别属于sub_4140D7函数,一样对着按E就可以自动分析归属到sub_4140D7函数,可以反编译之后可能会有红色的JUMPOUT错误,这个错误会指示哪里还没修好。
修好后就可以得到伪代码了。
上面这种是我通过这道题学习到的常规方法,去掉比较明显的花指令,IDA再进行trace,将多余的指令和执行的指令分隔,然后一步步把函数恢复好,IDA反编译。
当然还有硬搓汇编的方法,因为对于这种运算特征比较鲜明的加密算法,直接拿x32dbg进行trace,得到执行过的汇编指令之后也不难看出是执行两次XXTEA加密,而且我不止在这道题看到过这样的方法,其他题目也见过,而且这种方法目前我见得还更多。顺便一提,x64dbg的trace似乎会比IDA的trace功能更准确些。
sub_4140D7函数代码如下
void sub_4140D7()
{
unsigned int v1; // [esp-10Ch] [ebp-110h]
unsigned int v2; // [esp-10Ch] [ebp-110h]
unsigned int sum; // [esp-44h] [ebp-48h]
unsigned int v4; // [esp-38h] [ebp-3Ch]
int e; // [esp-20h] [ebp-24h]
unsigned int i; // [esp-14h] [ebp-18h]
int rounds; // [esp-8h] [ebp-Ch]
rounds = 50;
sum = 0;
v4 = flag[11];
do
{
sum -= 0x5B4B9F9E;
e = (sum >> 2) & 3;
for ( i = 0; i < 11; ++i )
{
v2 = (((v4 ^ key[e ^ i & 3]) + (flag_1[i] ^ sum)) ^ (((16 * v4) ^ (flag_1[i] >> 3)) + ((4 * flag_1[i]) ^ (v4 >> 5))))
+ flag[i];
flag[i] = v2;
v4 = v2;
}
v1 = (((v4 ^ key[e ^ i & 3]) + (flag[0] ^ sum)) ^ (((16 * v4) ^ (flag[0] >> 3)) + ((4 * flag[0]) ^ (v4 >> 5))))
+ flag[11];
flag[11] = v1;
v4 = v1;
}
while ( --rounds );
}
可以看出这是个XXTEA加密算法,然后这里动态调试会发现,这个函数会执行两次,出题人给了这个链接,说是异常处理的机制,但是反正我是没看懂,埋个坑,不过这个可以通过动调发现,所以不知道似乎也不影响。
Windows-SEH学习笔记(2) – 云之君’s Blog
这里发现我们输入经过两次XXTEA加密之后,执行后面的jmp far loc_4142A7指令后,数据又被加密
但是这里0x4142A7地址处的数据转成汇编指令并不是很对。
得知这里运用了天堂之门的的技术,通过修改CS寄存器实现32位到64位代码的切换,用于反调试
天堂之门(WoW64技术)总结及CTF中的分析-CTF对抗-看雪-安全社区|安全招聘|kanxue.com
[原创]天堂之门 (Heaven’s Gate) C语言实现-软件逆向-看雪-安全社区|安全招聘|kanxue.com
我们可以用x32dbg再打开程序
可以看到实际的指令其实是jmp far 0x33:413F77,这里将cs寄存器的修改为0x33从而让32位程序执行64位代码,0x413F77处的代码如下
void sub_413F60()
{
int *v1; // ebp
int v2; // ecx
int v3; // eax
unsigned int v4; // esi
unsigned int v5; // edi
int v6; // ecx
unsigned int v7; // esi
int savedregs; // [esp+CCh] [ebp+0h] BYREF
v1 = &savedregs;
v2 = 2;
v3 = -858993461;
while ( 1 )
{
v1 = (int *)((char *)v1 - 1);
v4 = 0;
do
{
v5 = v4 >> 31;
v6 = v2 - 3;
if ( v4 >> 31 == 1 )
v7 = (2 * v4) ^ 0x84A6972F;
else
v7 = 2 * v4;
v4 = v7 + 1;
v2 = v6 - 2;
}
while ( v4 != 32 );
*(_DWORD *)&flag[4 * v5] = 32;
v3 -= 2;
if ( v5 == 11 )
__asm { retf }
}
}
这里逻辑32位的IDA会识别有误,从而成功通过天堂之门的技术影响了IDA分析,比如汇编指令这里明明出现了两次flag变量,但是伪代码却只出现了一次。
我们这里把opcode dump出来然后再放到IDA64位当中,得到以下结果
void sub_0()
{
__int64 flag; // rdi
unsigned __int64 v1; // rsi
__int64 i; // r14
flag = 0i64;
while ( 1 )
{
v1 = *(unsigned int *)(4 * flag + 0x4234A8);
for ( i = 0i64; i != 32; ++i )
{
if ( v1 >> 31 == 1 )
v1 = (2 * (_DWORD)v1) ^ 0x84A6972F;
else
v1 = (unsigned int)(2 * v1);
}
*(_DWORD *)(4 * flag++ + 0x4234A8) = v1;
if ( flag == 12 )
__asm { retfq }
}
}
这里是判断数据符号位来决定是否进行异或,并且汇编指令最后
在跳转回0x4179F3的时候又将cs寄存器转为了0x23,此后继续执行32位代码。
那么解题脚本参考上面三位师傅
#include <iostream>
#include <cstring>
using namespace std;
#define DELTA 0xa4b46062
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
int main()
{
// unsigned char flag[100] = {67, 149, 84, 158, 72, 179, 124, 94, 47, 74, 168, 217, 222, 153, 235, 133, 132, 88, 130, 182, 161, 78, 247, 196, 138, 130, 177, 34, 150, 114, 13, 41, 115, 228, 142, 25, 41, 181, 85, 150, 106, 25, 172, 56, 54, 98, 43, 25};
unsigned char flag[100] = { 214, 250, 144, 167, 119, 162, 200, 232, 250, 132, 3, 207, 215, 127, 108, 46, 139, 150, 51, 109, 39, 194, 87, 91, 94, 166, 60, 101, 252, 241, 198, 133, 119, 37, 243, 225, 118, 174, 215, 212, 196, 109, 175, 63, 140, 157, 89, 13 };
unsigned int f;
int i, j;
for (i = 0; i < 12; i++)
{
f = *((unsigned int*)flag + i);
for (j = 0; j < 32; j++)
{
if (f & 1)
{
f ^= 0x84a6972f;
f >>= 1;
f |= 0x80000000;
}
else
{
f >>= 1;
f &= 0x7fffffff;
}
}
*((unsigned int*)flag + i) = f;
}
unsigned int rounds, sum, e, p, y, z;
unsigned int key[4] = { 0x6b0e7a6b, 0xd13011ee, 0xa7e12c6d, 0xc199aca6 };
rounds = 0x32;
sum = rounds * DELTA;
y = ((unsigned int*)flag)[0];
do
{
e = (sum >> 2) & 3;
for (p = 11; p > 0; p--)
{
z = ((unsigned int*)flag)[p - 1];
y = ((unsigned int*)flag)[p] -= MX;
}
z = ((unsigned int*)flag)[11];
y = ((unsigned int*)flag)[0] -= MX;
sum -= DELTA;
} while (--rounds);
rounds = 0x32;
sum = rounds * DELTA;
y = ((unsigned int*)flag)[0];
do
{
e = (sum >> 2) & 3;
for (p = 11; p > 0; p--)
{
z = ((unsigned int*)flag)[p - 1];
y = ((unsigned int*)flag)[p] -= MX;
}
z = ((unsigned int*)flag)[11];
y = ((unsigned int*)flag)[0] -= MX;
sum -= DELTA;
} while (--rounds);
for (i = 0; i < 48; i++)
{
printf("%c", flag[i]);
}
return 0;
}
//输出DubheCTF{82e1e3f8-85fe469f-8499dd48-466a9d60}
所有flag就是DubheCTF{82e1e3f8-85fe469f-8499dd48-466a9d60}
这道题学习到很多知识,而且值得更加深入的去研究一下,比如这里的异常处理机制,还有天堂之门技术,我目前只是处于一个初步了解的状态。
https://vitz.ru/forums/index.php?autocom=gallery&req=si&img=4866
http://terios2.ru/forums/index.php?autocom=gallery&req=si&img=4608