不知道你有没有发现,在我们电脑的C盘里的windows文件夹下有一个SysWoW64,看起来像是一个开发者的感叹,他的功能是保存在64位电脑上仍需要运行的32位程序或者依赖。
这就引出了一个在动态调试层面恐怕是最有难度的逆向反调试--天堂之门(Heaven's Gate)
这一篇文章已经写了如何在C语言中使用天堂之门技术
https://bbs.kanxue.com/thread-270153.htm
所以在此我不会过多赘述实现机制,而是在反汇编层面进行解析
天堂之门的实现主要依靠操作系统提供的在不同位数CPU进行跨架构的指令调用,这使得32位和64位的指令环境可以放在同一个程序中,但目前的调试器几乎没有能够跨架构的,所以程序能够标准的在操作系统中进行操作,但是在调试器中,当走到跨架构的指令时,就会因为指令无法识别而跳飞。
而在代码层面实现之后,在反汇编层面会有一些比较明显的指令,其中分为从64位跳转到32位,该种指令如下:
jmp far 33:地址
或者
push 0x33
call $+5
add dword [esp], 5
retf
我们稍微了解一下这些指令
首先jmp far和jmp的区别是jmp far会比jmp多执行一个指令,即在修改ip的值之外还会修改CS的值,修改的值就是far后面跟的数字,在硬编码层面表现如下:
EA 77 3F 41 00 33 00
可以看见,这个指令就是在jmp一个地址的硬编码后面加上四字节的数字,这个数字就是CS修改为多少,32位寄存器CS值是0x22,64位CS寄存器值是0x33。
在ida中,修改的值不会直接显示,我们需要设置option->general->Number of opcode bytes,将后面的框中的数字改为9或者其他值(这个数字的意思是每一行能够同时指令的几个硬编码)
下图是实例
这样就能在ida里面看见每条指令对应的硬编码了。
一旦理解了jmp far指令,push 0x33``call $+5
和add dword [esp], 5
这三条指令理解起来就比较方便了
push 0x33 在栈中压入0x33,作为CS寄存器的新值。
call $+5 下一条指令的地址入栈,并继续执行下一条指令
add dword [esp], 5 栈顶的返回地址加5,指向retf的下一条指令
retf, 返回到下一条指令继续执行,同时会pop ip和pop cs
32位转成64位的如下
call $+5``mov dword [rsp + 4], 0x23``add dword [rsp], 0xD``retf
原理和上面的一样,利用retf将CS寄存器的值修改,这样就可以达到在程序实现32位代码和64位代码之间的转换。
下载附件,打开IDA,首先就发现一片数据和标红
在上面还调用了其他的函数,有一个函数是用来修改内存的,调试查看修改了什么
从f5反汇编之后得到的代码来看,这段代码不仅修改了返回地址,还修改了开辟出来的地址,返回地址,即call这个函数的下一个指令的地址在被修改之后变成了如下
这里是IDA反汇编错误,根据硬编码来看,应该是
jmp far 33:0x4011d0
所以这里天堂之门真正跳转的地址是0x4011d0,而4011d0也是我们刚才修改过内存的地址,我们直接G跳过去发现这段地址ida在乱编,显然没识别出来是什么指令
因为一开始用的是IDA32位,现在转到64位的环境无法反汇编,所以我们需要对程序的十六进制修改
红色框住的是PE结构的magic魔术字
说明文件类型:10B 32位下的PE文件 20B 64位下的PE文件
修改魔术字为20B,然后放到64位IDA中,修改基址为0x40000,会自动修复基址,之后直接G跳转到11d0
看上去就很正确了,这里有个反调试,gs:60h是PEBeginDebugging反调试位,如果检测到正在调试,该位为1,反之为0,这段代码在没有调试的情况下会把0x5DF966AE赋值给dword_407058。
之后跳转到0x1E0000,从这里在跳转回去减去0x21524111得到一个加秘密钥
得到key之后跳转到下面进行第一次加密
很容易分析并逆向出解密脚本
相同的跳转总的有3个
第二个
到40700地址上的值,直接返回去看32位程序的40700
跳转到了401200,而我们刚才就在64位的程序上看见了401200,P生成函数进行f5,就可以看见加密函数
这里也进行了PEBeginDebugging反调试,检测到调试器和没有检测到调试器的加密会不同,如下
第三个对应的跳转到401290,到64位上反汇编如下
所以总的解密代码如下
灵感、解密脚本来源 https://kamasammohana.github.io/
从西湖论剑的题型可以看出,天堂之门这个反调试技术单独使用还是比较容易看得出来,还需要结合其他混淆和反调试技术一起使用,由于跨架构指令不能直接动调,所以逆向难度会极大的上升。
这个题就采用了SEH异常处理反调试+天堂之门的手法,隐藏掉天堂之门的入口指令和没有进入天堂之门前的一次加密,极大的提升了题目的难度。
打开附件,在主函数上有一个很明显的SEH异常处理函数
如果我们这个时候进行f5反编译,那么会是这样
但是根据汇编层面的观察,不可能只有这么点代码,往上面观察,可以看出程序一开始就加载了异常处理句柄
在接收了字符串之后马上写入了一个了try块,保存完关键地址后就int 3造成中断异常
异常之后当然是进行异常处理,而回调函数地址就是被push进栈中的loc_4140d7,这个地址被引用到异常处理结构体中
我们跳转到4140d7观察,看到很多数据,做题时以为是移动返回地址,复现时看了出题人的出题笔记才知道是花指令
下个硬断单步跟,但是首先要把其他隐藏在程序里面的花指令去掉,使用AntiDebuggerSeeker查就行
这一处是加载ntdll,直接把eax改成0
这一处是要把调用ZwSetInformationThread反调试的调用号给去掉,改成0
保存整个程序,利用OD进行调试,打开OD,f9到0x4140D7,快捷键Alt+O打开调试选项面板,在异常中去掉勾选忽略(传递给程序)以下异常:INT 3中断
这样int 3中断之后不会跳飞,这个时候再下一个断点到异常处理回调函数4140d7上,就可以单步调试代码了
调试几步后发现压入了一些数,0x32在后面可以知道是循环次数,0x5B4B9F9E很明显是一个常数,这个时候就可以猜测是TEA族
这里是将输入按照四字节分组压入栈
左移5位
左移2位
进行异或
看到这里就已经知道是XXtea了,到这里需要找到密钥key,重新打开IDA,在密文下面翻得到
由于栈展开之后会回调两次异常处理函数,所以这里的xxtea也会执行两次
到这里总算算是把入天堂之门前的加密代码解析成功,之后回调函数回来时执行except块,也就是
这里就是典型的门了,直接转x32Dbg,打开sharpOD,把能勾选的反反调试全勾上
然后一路调试到jmp far这个指令,直接跳转进jmp far后面的地址
将该地址用scylla dump下来放进ida就可以看到门后的加密了
可以看出是crc算法
所以加密算法是两次xxtea+一次crc,直接解密就行,exp如下
for
(
int
i
=
0
; i <
8
;
+
+
i){
DWORD bak
=
ans[i];
ans[i]
=
(ans[i]
-
key) &
0xFFFFFFFF
;
key ^
=
bak;
}
BYTE
*
p
=
(BYTE
*
)ans;
for
(
int
i
=
0
;i<
0x20
;
+
+
i)
printf(
"%c"
,
*
(p
+
i));
for
(
int
i
=
0
; i <
8
;
+
+
i){
DWORD bak
=
ans[i];
ans[i]
=
(ans[i]
-
key) &
0xFFFFFFFF
;
key ^
=
bak;
}
BYTE
*
p
=
(BYTE
*
)ans;
for
(
int
i
=
0
;i<
0x20
;
+
+
i)
printf(
"%c"
,
*
(p
+
i));
def
xor_reverse(data, key):
for
i
in
range
(
len
(data)):
data[i] ^
=
key
return
data
def
rol_reverse(data, offset):
tmp_arr
=
[]
for
i
in
range
(
4
):
tmp_arr.append((data[i
*
2
+
1
] <<
32
)
+
data[i
*
2
])
for
i
in
range
(
4
):
tmp_arr[i]
=
((tmp_arr[i] >> offset[i]) | ((tmp_arr[i] << (
64
-
offset[i])) &
0xffffffffffffffff
)) &
0xffffffffffffffff
for
i
in
range
(
4
):
data[
2
*
i]
=
tmp_arr[i] &
0xffffffff
data[
2
*
i
+
1
]
=
tmp_arr[i] >>
32
return
data
def
deffusion_reverse(data, factor):
for
i
in
range
(
len
(data)):
tmp
=
data[i]
data[i]
=
(data[i]
-
factor) &
0xffffffff
factor ^
=
tmp
return
data
def
main():
cmp_data
=
[
0xE20F4FAA
,
0x549941E4
,
0x7E842B2C
,
0x788B8FBC
,
0x5E8873D3
,
0x708547AE
,
0xCE09B331
,
0xCA0DF513
]
final_key
=
0x4a827704
rol_offset
=
[
0xc
,
0x22
,
0x38
,
0xe
]
diffusion_factor
=
0x3CA7259D
flag_arr
=
xor_reverse(cmp_data, final_key)
flag_arr
=
rol_reverse(flag_arr, rol_offset)
flag_arr
=
deffusion_reverse(flag_arr, diffusion_factor)
print
(
'DASCTF{'
, end
=
'')
for
i
in
flag_arr:
print
(
int
.to_bytes(i,
4
,
'little'
).decode(),end
=
'')
print
(
'}'
)
if
__name__
=
=
"__main__"
:
main()
def
xor_reverse(data, key):
for
i
in
range
(
len
(data)):
data[i] ^
=
key
return
data
def
rol_reverse(data, offset):
tmp_arr
=
[]
for
i
in
range
(
4
):
tmp_arr.append((data[i
*
2
+
1
] <<
32
)
+
data[i
*
2
])
for
i
in
range
(
4
):
tmp_arr[i]
=
((tmp_arr[i] >> offset[i]) | ((tmp_arr[i] << (
64
-
offset[i])) &
0xffffffffffffffff
)) &
0xffffffffffffffff
for
i
in
range
(
4
):
data[
2
*
i]
=
tmp_arr[i] &
0xffffffff
data[
2
*
i
+
1
]
=
tmp_arr[i] >>
32
return
data
def
deffusion_reverse(data, factor):
for
i
in
range
(
len
(data)):
tmp
=
data[i]
data[i]
=
(data[i]
-
factor) &
0xffffffff
factor ^
=
tmp
return
data
def
main():
cmp_data
=
[
0xE20F4FAA
,
0x549941E4
,
0x7E842B2C
,
0x788B8FBC
,
0x5E8873D3
,
0x708547AE
,
0xCE09B331
,
0xCA0DF513
]
final_key
=
0x4a827704
rol_offset
=
[
0xc
,
0x22
,
0x38
,
0xe
]
diffusion_factor
=
0x3CA7259D
flag_arr
=
xor_reverse(cmp_data, final_key)
flag_arr
=
rol_reverse(flag_arr, rol_offset)
flag_arr
=
deffusion_reverse(flag_arr, diffusion_factor)
print
(
'DASCTF{'
, end
=
'')
for
i
in
flag_arr:
print
(
int
.to_bytes(i,
4
,
'little'
).decode(),end
=
'')
print
(
'}'
)
if
__name__
=
=
"__main__"
:
main()
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
#define DELTA 0x5B4B9F9E
void
crcbreak(uint32_t* flagbuf,
int
key) {
for
(
int
i = 0; i < 12; i++) {
uint32_t enc_num = *(flagbuf + i);
for
(
int
j = 0; j < 32; j++) {
if
(enc_num & 1)
{
enc_num ^= key;
enc_num /= 2;
enc_num |= 1 << 31;
}
else
enc_num /= 2;
}
*(flagbuf + i) = enc_num;
}
}
void
btea(uint32_t* v,
int
n, uint32_t
const
key[4])
{
uint32_t y, z, sum;
unsigned p, rounds, e;
if
(n > 1)
{
rounds = 6 + 52 / n;
sum = 0;
z = v[n - 1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for
(p = 0; p < n - 1; p++)
{
y = v[p + 1];
z = v[p] += MX;
}
y = v[0];
z = v[n - 1] += MX;
}
while
(--rounds);
}
else
if
(n < -1)
{
n = -n;
rounds = 0x32;
sum = rounds * ((~DELTA) + 1);
y = v[0];
do
{
e = (sum >> 2) & 3;
for
(p = n - 1; p > 0; p--)
{
z = v[p - 1];
y = v[p] -= MX;
}
z = v[n - 1];
y = v[0] -= MX;
sum -= ((~DELTA) + 1);
}
while
(--rounds);
}
}
int
main() {
uint32_t flagbuf[] = {
0xA790FAD6, 0xE8C8A277, 0xCF0384FA, 0x2E6C7FD7, 0x6D33968B, 0x5B57C227, 0x653CA65E, 0x85C6F1FC,
0xE1F32577, 0xD4D7AE76, 0x3FAF6DC4, 0x0D599D8C
};
int
key = 0x84A6972F;
uint32_t
const
k[4] = { 0x6B0E7A6B, 0xD13011EE, 0xA7E12C6D, 0xC199ACA6 };
crcbreak(flagbuf, key);
btea(flagbuf, -12, k);
btea(flagbuf, -12, k);
puts
((
const
char
*)flagbuf);
printf
(
"\n\n"
);
return
0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))
#define DELTA 0x5B4B9F9E
void
crcbreak(uint32_t* flagbuf,
int
key) {
for
(
int
i = 0; i < 12; i++) {
uint32_t enc_num = *(flagbuf + i);
for
(
int
j = 0; j < 32; j++) {
if
(enc_num & 1)
{
enc_num ^= key;
enc_num /= 2;
enc_num |= 1 << 31;
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!