skip to content

天堂之门——西湖论剑 Dual personality 题解

一个天堂之门逆向题目, 然后还加入了神奇的 Windows 反调试机制, 不愧是西湖论剑啊

坐牢感想

md,因为要准备期末考试再加上买了新主机以后一直都在玩泰拉瑞亚,所以到现在才有题解。

以往 CTF 的 Windows RE 至少调试没啥问题,就算再怎么恶心上调试器就可以做出来了,结果这次 这个天堂之门直接不给你用常规的调试工具调试的。导致这次被卡了很久。

而且因为天堂之门 x86 和 x64 代码混杂,导致 IDA F5 是直接无法工作的。所以这次只能全程看汇编力。 还好出题人够仁慈没有在全程汇编的情况下塞个 AES RC4 啥复杂的算法。

而且我的 Mac 还是 Arm 的,以前 Windows Arm 可以直接运行 RE 题目程序,但是这次因为用了天堂之门 这个邪门的调试技术,导致我虚拟机直接废了,就算用 UTM 虚拟机直接硬装了个 X86 Windows 7 好像也运行不了, 真的吐了。

而且这次还有一个非常让人没想到的检测调试器的手段,导致我就算这几天做了很久也一直被卡。

天堂之门

天堂之门技术就是一个在 Windows 程序执行的时候通过类似 jmp 0x33:0x4012d0 这样的指令,让 CS 寄存器直接切换到 0x33,这样 Windows 就会让程序直接运行在 64 位环境。因为这个原因 x64dbg 这样的 普通调试工具直接就 GG 了。

后面我找到 flare on 5 有一个题目 Wow,一样也是天堂之门程序的逆向。

https://blog.attify.com/flare-on-5-writeup-part5/?utm_source=pocket_saves

在这个文章里面,作者是直接使用 windbg 64 位调试的。然后我尝试了一下,发现还真的可以,而且虽然 windbg 是 64 位的调试工具,但是似乎在程序刚开始在 32 位模式下工作的时候,也能正确识别汇编和下断点。

使用 windbg64 位调试

windbg 教程有不少,网上找找罢。我这里就列举几个比较常用的命令

  • bp 0x1234 在 0x1234 处下断点
  • bl 列出所有断点
  • lm 列出所有模块的内存地址
  • g 运行到断点

反汇编

在经过调试以后,我们可以知道 0x4011D0 ~ 0x4012E5 这个地址范围内的代码都是 64 位的 IDA 无法正常反汇编。 所以我们使用 capstone 这个 python 库来进行反汇编

from capstone import Cs
from capstone import CS_ARCH_X86
from capstone import CS_MODE_32
from capstone import CS_MODE_64
uc32 = Cs(CS_ARCH_X86, CS_MODE_32)
uc64 = Cs(CS_ARCH_X86, CS_MODE_64)
f = open('./prog.exe', 'rb')
file = f.read()
f.close()
text_offset = 0x400 # offset of text segement
base_addr = 0x400000 # base addr of prog
text_addr = 0x401000 # address of text enter point
start_addr = 0x4011D0
end_addr = 0x4012E5
code64 = file[text_offset + start_addr -
text_addr: text_offset + end_addr - text_addr]
code64_insn = list(uc64.disasm(code64, start_addr))
for i in code64_insn:
print("addr:{:7}|size:{:2}|{:12}{}".format(
hex(i.address), i.size, i.mnemonic, i.op_str))

经过上面的反调试可以输出一下汇编代码

addr:0x4011d0|size: 9|mov rax, qword ptr gs:[0x60]
addr:0x4011d9|size: 3|mov al, byte ptr [rax + 2]
addr:0x4011dc|size: 7|mov byte ptr [0x40705c], al
addr:0x4011e3|size: 2|test al, al
addr:0x4011e5|size: 2|jne 0x4011f5
addr:0x4011e7|size: 6|mov r12d, 0x5df966ae
addr:0x4011ed|size: 8|mov dword ptr [0x407058], r12d
addr:0x4011f5|size: 6|mov eax, 0x407000
addr:0x4011fb|size: 3|ljmp [rax]
addr:0x4011fe|size: 1|int3
addr:0x4011ff|size: 1|int3
addr:0x401200|size: 1|push rbp
addr:0x401201|size: 3|mov rbp, rsp
addr:0x401204|size: 9|movabs al, byte ptr [0x40705c]
addr:0x40120d|size: 2|test al, al
addr:0x40120f|size: 2|je 0x401245
addr:0x401211|size: 4|mov rax, qword ptr [rbp + 0x10]
addr:0x401215|size: 3|mov rbx, qword ptr [rax]
addr:0x401218|size: 4|rol rbx, 0x20
addr:0x40121c|size: 3|mov qword ptr [rax], rbx
addr:0x40121f|size: 4|mov rbx, qword ptr [rax + 8]
addr:0x401223|size: 4|rol rbx, 0x20
addr:0x401227|size: 4|mov qword ptr [rax + 8], rbx
addr:0x40122b|size: 4|mov rbx, qword ptr [rax + 0x10]
addr:0x40122f|size: 4|rol rbx, 0x20
addr:0x401233|size: 4|mov qword ptr [rax + 0x10], rbx
addr:0x401237|size: 4|mov rbx, qword ptr [rax + 0x18]
addr:0x40123b|size: 4|rol rbx, 0x20
addr:0x40123f|size: 4|mov qword ptr [rax + 0x18], rbx
addr:0x401243|size: 2|jmp 0x40127c
addr:0x401245|size: 4|mov rax, qword ptr [rbp + 0x10]
addr:0x401249|size: 3|mov rbx, qword ptr [rax]
addr:0x40124c|size: 4|rol rbx, 0xc
addr:0x401250|size: 3|mov qword ptr [rax], rbx
addr:0x401253|size: 4|mov rbx, qword ptr [rax + 8]
addr:0x401257|size: 4|rol rbx, 0x22
addr:0x40125b|size: 4|mov qword ptr [rax + 8], rbx
addr:0x40125f|size: 4|mov rbx, qword ptr [rax + 0x10]
addr:0x401263|size: 4|rol rbx, 0x38
addr:0x401267|size: 4|mov qword ptr [rax + 0x10], rbx
addr:0x40126b|size: 4|mov rbx, qword ptr [rax + 0x18]
addr:0x40126f|size: 4|rol rbx, 0xe
addr:0x401273|size: 4|mov qword ptr [rax + 0x18], rbx
addr:0x401277|size: 5|mov ebx, 0
addr:0x40127c|size: 5|mov ebx, 0
addr:0x401281|size: 3|xor rax, rax
addr:0x401284|size: 3|mov rsp, rbp
addr:0x401287|size: 1|pop rbp
addr:0x401288|size: 3|retf 8
addr:0x40128b|size: 1|int3
addr:0x40128c|size: 1|int3
addr:0x40128d|size: 1|int3
addr:0x40128e|size: 1|int3
addr:0x40128f|size: 1|int3
addr:0x401290|size: 3|xor rax, rax
addr:0x401293|size:10|movabs rax, 0x4014c5
addr:0x40129d|size: 7|mov dword ptr [0x407000], eax
addr:0x4012a4|size: 8|lea rax, [0x407014]
addr:0x4012ac|size: 2|mov bl, byte ptr [rax]
addr:0x4012ae|size: 3|mov cl, byte ptr [rax + 4]
addr:0x4012b1|size: 2|and bl, cl
addr:0x4012b3|size: 2|mov byte ptr [rax], bl
addr:0x4012b5|size: 3|mov bl, byte ptr [rax + 4]
addr:0x4012b8|size: 3|mov cl, byte ptr [rax + 8]
addr:0x4012bb|size: 2|or bl, cl
addr:0x4012bd|size: 3|mov byte ptr [rax + 4], bl
addr:0x4012c0|size: 3|mov bl, byte ptr [rax + 8]
addr:0x4012c3|size: 3|mov cl, byte ptr [rax + 0xc]
addr:0x4012c6|size: 2|xor bl, cl
addr:0x4012c8|size: 3|mov byte ptr [rax + 8], bl
addr:0x4012cb|size: 3|mov bl, byte ptr [rax + 0xc]
addr:0x4012ce|size: 2|not bl
addr:0x4012d0|size: 3|mov byte ptr [rax + 0xc], bl
addr:0x4012d3|size: 3|xor rax, rax
addr:0x4012d6|size: 7|jmp qword ptr [0x407050]
addr:0x4012dd|size: 1|int3
addr:0x4012de|size: 1|int3
addr:0x4012df|size: 1|int3
addr:0x4012e0|size: 1|push rbp
addr:0x4012e1|size: 2|mov ebp, esp
addr:0x4012e3|size: 1|pop rbp
addr:0x4012e4|size: 1|ret

反调试检测

这次的反调试检测手段真的很隐蔽,我是第一次见到。在内存地址为 0x4011d0 的地方有一个汇编指令:mov rax, qword ptr gs:[0x60],一开始我还不知道这是用来检测程序是否被调试器调试的 后面看了别人的题解才知道原来这个是用来反调试的。

gs:0x60 反调试

所以在读上面的汇编代码的时候要注意 0x40705c 这个地址存放的变量。如果这个变量为 0,则程序没有被调试。

这个题目还是有两个地方用到了这个变量的,就在上一步生成的 x64 汇编代码那里

解密算法

根据阅读汇编代码结合 windbg 调试,很快就能知道这个代码的算法了。这个地方我直接丢出来我的解密程序, 有需要的可以看看是哪个地方自己写错了。

#include <algorithm>
#include <iomanip>
#include <iostream>
using namespace std;
const int N = 32;
unsigned char data[] = {0x0AA, 0x4F, 0x0F, 0x0E2, 0x0E4, 0x41, 0x99, 0x54,
0x2C, 0x2B, 0x84, 0x7E, 0x0BC, 0x8F, 0x8B, 0x78,
0x0D3, 0x73, 0x88, 0x5E, 0x0AE, 0x47, 0x85, 0x70,
0x31, 0x0B3, 0x9, 0x0CE, 0x13, 0x0F5, 0x0D, 0x0CA};
unsigned int key0 = 0x5df966ae - 0x21524111; // 0x3ca7259d
unsigned char key1[] = {0x04, 0x77, 0x82, 0x4a};
unsigned long long ror(unsigned long long value, int shift) {
const int bits = sizeof(unsigned long long) * CHAR_BIT;
shift %= bits; // 防止移位数大于整数位数
// 对于移位数为 0,直接返回原值
if (shift == 0) {
return value;
}
// 将右移和左移分开计算,然后按位或运算得到结果
return (value >> shift) | (value << (bits - shift));
}
int main() {
// first
for (int i = 0; i < N; i++) {
data[i] ^= key1[i % 4];
}
// second
*((unsigned long long *)data) = ror(*((unsigned long long *)data), 0xc);
*((unsigned long long *)data + 1) =
ror(*((unsigned long long *)data + 1), 0x22);
*((unsigned long long *)data + 2) =
ror(*((unsigned long long *)data + 2), 0x38);
*((unsigned long long *)data + 3) =
ror(*((unsigned long long *)data + 3), 0xe);
// third
unsigned int cur_key;
for (int i = 0; i < 8; i++) {
cur_key = key0;
key0 ^= *((unsigned int *)data + i);
*((unsigned int *)data + i) =
(*((unsigned int *)data + i) - cur_key) & 0xffffffff;
}
for (int i = 0; i < N; i++) {
cout << (char)data[i];
}
cout << endl;
return 0;
}