前言

这次比赛做了d3kernel和locked door这两题,也是都拿下了一血。这两题都是强对抗类型,和传统的加解密类型题目有区别。kernel是内核逆向&反调试对抗;第二题是vmp对抗+real world类型,模仿的场景是商业软件许可证伪造逆向。这两题都挺有意思的,质量也都很高。

locked door

分析

查壳

查壳发现是vmp 3.5.1+的壳,

alt text

绕过反调试

由于vmp 3.5.1+的检测很严,scyllahide插件已经没办法过了,这边赛中用的是下面这个付费调试器动调的,这边提供另一个方案。titanhide驱动来处理vmp调试。

alt text

使用VKD工具的target64中的vminstall在虚拟机中运行安装,会多出来一个引导启动,重启电脑选择新的引导启动就可以,他会进入内核调试模式,禁止驱动强制签名以及关闭PG,也就可以让我们加载titanhide驱动。

alt text

在github上下载titanhide编译版本。

https://github.com/mrexodia/TitanHide/releases

直接拖入TitanHide.sys加载运行。

alt text

将titanhide中的x64插件这两个文件拖入dbg的plugins文件夹即可。

alt text

现在直接打开x64dbg,拖入程序运行就可以进行愉快的调试了。

alt text

脱壳

vmp找OEP的思路如下,断点QueryPerformanceCounter或GetSystemTimeAsFileTime都可以,可能执行一次不太够,要执行到返回地址的第二个栈顶地址是在.text段范围内的,此时第二个栈顶地址就是ope了。

alt text

alt text

很明显的看出确实是ope的代码

alt text

f4让程序走到这个oep下面的这个jmp,这时直接dump即可。

alt text

IDA反编译,字符串搜索key1定位到核心main函数。

alt text

可以在oep这个jmp跳过来后的代码段下面找到核心main函数的调用,接下来就是调试就可以了。

alt text

流程分析

main函数发现反编译不完全,发现是sub_7FF73E5EC840里面调用的一个call导致的,可以从传参+函数具体实现发现这应该是一个读取key文件的函数。

alt text

alt text

暂时将两处调用读取key文件的函数相关代码nop,让我们的反编译可以看到完整代码。

alt text

以下是完整反编译代码,第一个if应该就是校验Key1的函数, sub_7FF73E5EC900跟进看发现是一堆加密代码,且其中调用的sub_7FF73E797910似乎存在虚拟化,没办法看代码,只能进行调试看看具体是什么功能。

alt text

alt text

调试走到sub_7FF73E5EC900调用处发现参数一传入的就是key1.bin的256字节数据指针。

alt text

步入跟进函数,走到sub_7FF73E797910函数调用处,发现参数二传入的是key1.bin数据。

alt text

步过函数执行,跳转到刚刚参数一的地址,发现只是把参数二的数据复制了一份到参数一,那么这个函数实际上的作用和memcpy是一样的。

sub_7FF73E5EC900这函数功能就是将key.bin输入数据进行一次加密。

alt text

if那边调用的函数是对加密后的key数据进行一次EVP签名校验,可以通过进入各个函数里面,看到一些文本就可以知道是openssl的EVP代码。

alt text

而参数一就是要校验的目标文本,让key1.bin加密后的数据和”Welcome”进行签名校验。

alt text

可以看到下面调用函数结构和上面key1差不多,都是读入key2.bin然后进行sub_7FF73E5EC900的一次加密,最后与”Here is the key”进行签名校验,不过key2的EVP校验函数被vmp保护了,没办法查看代码。

alt text

从题目的描述可知key1和key1校验逻辑完全相同,那我们就可以从这个点出发进行伪造绕过校验。

alt text

伪造绕过签名校验

上文可知key1和key2校验逻辑是完全一样,由于key1校验是通过的,那我们就可以复制一份key1.bin改名覆盖key2.bin,让程序读key2.bin的时候实际读入的是key1的数据,然后断点在key2的EVPCheck函数,将参数一字符串地址改成’Welcome”字串的地址,最终让程序校验成功。

以下实操:

修改完key2.bin后,断点在Check2处,将参数一改成”Welcome”的地址。

alt text

alt text

步过执行Check2后,key2校验成功,flag就从Check2保护函数中输出出来了。

alt text

d3ctf{Y0u_0p3n_7h3_d00r!!!}

d3kernel

R3分析

调试client.exe,发现单步走会触发除0异常,最后跳到这一个函数,发现里面全都是r3层单纯的io交互和校验,并没用用到驱动通信。

alt text

alt text

调试,随便输入点东西,在这两处strncmp可以找到两个目标对应字符串,但这个password是fake的,名字后面还能用的到。

alt text

alt text

alt text

汇编界面发现上层的这个call存在异常,将箭头处jmp nop即可看到完整的代码。

alt text

alt text

发现这边有个反调试,然后走了不同代码,如果检测到被调试,就走我们上面分析的那个纯r3的交互,如果没有被检测到,就走真实的R0通信交互。

alt text

要想调试真实的代码部分,我们就要在反调试和检测驱动handle的地方做一下处理,因为我们目前还没有运行驱动,找不到驱动句柄的。

alt text

反调试这边直接将返回值rax改成0就可以绕过反调试。

alt text

createFile处我们也可以从参数得到”\\.\d3ctf”,mov r15,rax处将rax改成1就可以绕过这边的合法句柄校验。

alt text

alt text

经过调试可以发现这边是解密出一些字符串,然后输出输入。

alt text

alt text

然后将name和password指针放到一个结构体里面({name,password}),通信传到R0,然后再通信一次得到返回数据,再解密输出。控制码分别是0x222000和0x222004。到此整体R3流程分析完毕。

alt text

R0分析

代码流程分析

DriverEntry进来,发现函数这边存在反调试检测以及对R3程序进程保护,反调试检测到会直接将windbg调试剥离。

alt text

alt text

alt text

直接将红框处代码全部nop,就可以去掉所有的反调试和保护了,主要分析就在这边的MajorFunction,sub_1400011A0进行分析。

alt text

alt text

我们确实也可以在这里面找到两个控制码的判断和执行代码。

alt text

alt text

在控制码0x222004下面,有两处文本解密,可以模拟代码,手动解密出来为这两个字符串。

alt text

alt text

alt text

解密代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <windows.h>

void DecText(unsigned char *enc, int len)
{
int v45{};
char v47{}, v48{};

for (int i = 0; i < len; i++)
{
v47 = 52 * (v45 / 52);
v48 = v45++;
enc[i] ^= v48 - v47 + 55;
}
}

int main()
{
unsigned char unk_140004270[29] = {
0x46, 0x4F, 0x48, 0x16, 0x1B, 0x4B, 0x55, 0x47, 0x1E, 0x61, 0x60, 0x63, 0x62, 0x64, 0x28, 0x3F,
0x67, 0x3B, 0x2C, 0x29, 0x39, 0x29, 0x39, 0x6F, 0x6E, 0x71, 0x70, 0x73, 0x53};
unsigned char unk_140004290[16] = {
0x4D, 0x59, 0x52, 0x55, 0x17, 0x1C, 0x49, 0x4C, 0x46, 0x60, 0x20, 0x25, 0x22, 0x2D, 0x2B, 0x46};
DecText(unk_140004270, 29);
DecText(unk_140004290, 16);

printf("%.29s\n", unk_140004270);
printf("%.16s\n", unk_140004290);
return 0;
}

那么就可以知道这部分就是对加密后的name和password进行校验,校验成功输出”qwq, why!!!!! my secret!!!!!”,校验失败,跳转到下面输出”zako, try again”。

这部分代码我们也可以看到外层校验了28个字节,内层校验了144个字节,分别除以4就是7和36,分别对应上了r3层的name和password的长度。

alt text

在上面代码部分也可以看到有password和name的数据加载,一个36循环一个7循环就可以分辨出来。

alt text

alt text

这边有调用到的sub_14000270C是一个虚拟机代码,目前不知道执行了什么。接下来就先在两处的数据加载动调分析入手,看看password和name都做了什么处理。

alt text

动调分析

这边实际调试发现,这边扫描nt模块地址时候会导致蓝屏,所以最好在箭头处断点,手动赋值上nt模块地址,d3ctf+0x1CC4。

alt text

先用sxe ld d3ctf命令拦截断点该驱动的加载。

alt text

加载驱动断下后,设置d3ctf+0x1CC4的断点。

alt text

断下后lm vm nt查询nt的地址得到fffff804`75e00000,手动给d3ctf+0x5400赋值即可。

alt text

alt text

alt text

在我们刚刚要动调的36循环处下断点,运行成功断下。

alt text

alt text

这边可以发现要给eax赋值的数组处就是我们r3输入传进来的password,单步执行完call,可以发现参数一是r14赋值给rcx的,查看执行完函数的r14,发现是一个两个DWORD64地址的结构体。

alt text

alt text

直接pa d3ctf+0x1596让代码执行到此处,也就是for循环结束后的代码。

再次查看r14结构的第二个地址,发现这循环实际作用就是将输入的36 password字节都转换成DWORD类型,存入到一个数组。

alt text

单步执行完循环下面的两个VM后,发现是给这个数组前面加了个0x24数据

alt text

pa d3ctf+0x19E2,跳到name那边循环赋值的循环结束地址,同样查看参数一,这边是r15,发现是和password初始操作一样,将输入字符串转成DWORD数组存入。

alt text

接下来我们先不管中途对password和name的一些加密操作,直接跳到Check处,这边是28字节Check,对应name的DWORD长度。

pa d3ctf+0x1A99

alt text

alt text

查看两个数组的数据,发现前七个DWORD,也就是前28字节是完全一样的,说明name确实是r3假流程那边得到的”mitsuha”。

alt text

再pa d3ctf+0x1AC4,执行到校验password的地方。

alt text

alt text

查询将要对比校验的两个数组数值,上面是我们password加密后的密文,下面是目标密文。

我们输入的password是”123456abcdef123456abcdef123456abcdef”,发现加密后的密文很有规律性和重复性,猜测是简单加密。

alt text

并且发现第一个数据0x15 ^ ‘1’ = 0x24,也就是刚刚上面出现的VM函数给password数组添加的第一个0x24数据。

alt text

发现第一个0x15 ^ 0x24 = ‘1’,然后’1’ ^ 0x3 = ‘2’,依次下去,就解密出来了原输入的password。

说明检验前password的加密如下:插入0x24到数组头,依次往后两两相邻数值异或。

那么这边直接提取37个DWORD的密文,进行两两异或解密即可。

解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>

int main()
{
// 密文前面插入0x24用于解密
uint8_t Enc[]{0x24, 0x45, 0x57, 0xe, 0x5c, 0x2, 0x4, 0x52, 0x6, 0x1b, 0x1a, 0xe, 0x1, 0x5e, 0x4b, 0x19, 0x56, 0x6, 0x55, 0x1c, 0x14, 0x5c, 0x5d, 0x9, 0x1c, 0x1d, 0x1, 0x0, 0x50, 0x0, 0x4, 0x6, 0x52, 0x0, 0x2, 0x55, 0x56, 0x6A};

for (int i = 0; i < 37; i++)
{
Enc[i + 1] ^= Enc[i];
}

// 跳过第一个0x24,输出后36 password明文
printf("d3ctf{%.36s}\n", Enc + 1);
return 0;
}

d3ctf{a68dfb06-798f-4bd1-9e81-011aaec113f0}