Secured Personal Vault

题目考点

取证、进程&驱动逆向、msfs.sys逆向、Mailslot结构逆向

0x1 取证阶段

下载题目附件,得到MEMORY.DMP,是Windows的系统转储文件格式,使用Windbg打开该文件。

点击!analyze -v,进行dmp分析。

alt text

可以发现系统是触发了蓝屏,并且能看到蓝屏时保存的栈信息,是由aPersonalVault+0x2a4b开始调用,然后到最后personalVaultKernel+0x10f5代码触发蓝屏。

alt text

跳转到触发蓝屏的汇编代码,可以看到是xor清零了eax,然后后续又访问了[rax],导致空指针异常。

alt text

可以看到起始Call是在aPersonalVault.exe进程中,而触发蓝屏异常的是personalVaultKernel.sys,所以接下来就是Dump转储这两个文件进行分析。

alt text

使用!process 0 0命令遍历当前所有进程,发现当前运行的是存在两个aPersonalVault.exe进程。

1
2
3
4
5
6
7
8
9
PROCESS ffffef063fbe8080
SessionId: none Cid: 0fa8 Peb: 9f83185000 ParentCid: 14b0
DirBase: 840fe000 ObjectTable: ffffa687a7241740 HandleCount: 161.
Image: aPersonalVault.exe

PROCESS ffffef063fbc1080
SessionId: none Cid: 26f0 Peb: 9216e1a000 ParentCid: 14b0
DirBase: 1296dc000 ObjectTable: ffffa687a6dd3b40 HandleCount: 165.
Image: aPersonalVault.exe

alt text

直接通过Windbg的.writemem指令发现无法转储成功完整的进程和sys,所以尝试使用Volatility3进行直接原文件转储。

使用Volatility3命令python .\vol.py -f .\MEMORY.DMP windows.filescan遍历所有文件,找到对应的几个文件的地址,再使用python .\vol.py -f .\MEMORY.DMP windows.dumpfiles --virtaddr 地址进行转储即可得到原始文件。

alt text

0x2 逆向阶段

1. R0驱动逆向分析

找到关键入口点函数,发现这边是遍历了进程,寻找ntoskrnl.exe,获取到HalPrivateDispatchTable函数表,最后将函数表中的HalTimerConvertAuxiliaryCounterToPerformanceCounter替换成sub_1400010A0,并且获取到全局进程表保存起来。

alt text

这边提前提一下R3进程的部分代码,实际是驱动将该函数进行Hook,将NtConvertBetweenAuxiliaryCounterAndPerformanceCounter作为R3-R0通信函数,通信数据的结构通过R3代码可以分析出来。

1
2
3
4
5
6
7
8
9
struct ComData
{
DWORD MagicNum;
DWORD Signal;
DWORD64 Pid;
DWORD64 RetData;
CHAR PAD[216];
};

alt text

R0这边Hook替换的函数分析,可以看到Signal如果是1就会触发蓝屏部分代码,Signal为2则遍历进程链表,寻找R3通信传来的对应Pid的EProcess,然后获取CreateTime数据返回到R3。

alt text

上文EPROCESS结构通过Windbg的dt nt!_EPROCESS命令可以获得,可以通过偏移知道代码中实际对应是什么数据,若在0x1cb偏移前,则再需要dt nt!_KPROCESS,获取第一部分的结构。

alt text

alt text

实际用到的只有如下数据:

1
2
3
4
5
6
7
8
9
struct _EPROCESS
{
CHAR PAD[464];
DWORD64 UniqueProcessId; // 0x1d0
LIST_ENTRY *ActiveProcessLinks; // 0x1d8
CHAR PAD2[24];
DWORD64 CreateTime; // 0x1f8
char PAD3[1552];
};

至此R0驱动程序分析完毕。

2. R3程序逆向分析

main函数创建了一个窗口,并且获取了NtConvertBetweenAuxiliaryCounterAndPerformanceCounter函数作为通信函数使用,下一步分析WndProc函数。

alt text

存在两个输入框以及按钮,按下Create按钮则会获取输入的用户名以及Secret。

alt text

加密流程分析

调用该函数与R0驱动进行通信,传递Signal是2,传过去的Pid是GetCurrentProcessId,也就是当前进程ID,驱动会返回当前进程的CreateTime数据。

然后调用BCryptGenRandom随机初始化一个48字节的pbBuffer,用于后续的加密使用。

alt text

下面这部分是将随机的48字节作为两部分使用,前32字节作为Key,后16字节作为IV,对用户输入的Secret进行AES-CBC加密。

alt text

加密完,将48字节异或上驱动返回的CreateTime数据,加密了密钥和IV。

alt text

将输入的用户名进行Hash加密,作为Mailslot路径名和映射空间名的后缀,调用CreateMailslotA创建一个邮槽并将返回句柄存在全局的hObject内存位置,将加密的密文写入到邮槽中,再创建一个内存映射,将句柄保存的内存地址进行映射。

alt text

什么是邮件槽

邮槽(MailSlot)是Windows提供的一种单向进程间通信机制,只由客户端向服务端单向发送数据。特点是单向通信以及广播通信。

流程:

  1. 客户端通过CreateMailslotA创建一个邮槽对象。
  2. 客户端通过WriteFile函数写入消息到邮槽中。
  3. 服务端通过CreateMailslot创建邮槽,并读取相应数据。

命名格式是”.\mailslot\路径名”

由于邮槽是有自己的一个数据储存结构,后续会对这点进行逆向分析。

解密流程分析

点击Check Secret按钮,会调用以下部分代码。

获取当前的用户名,然后将用户名Hash作为映射空间名后缀,打开刚刚同用户名创建邮槽数据时映射的内存,拿到hObject数据,也就是Mailslot的句柄,再调用GetMailslotInfo获取邮槽中的数据大小,调用ReadFile获取邮槽中的密文数据。

alt text

与R0驱动进程通信再次获取当前进程的CreateTime数据,异或解密刚刚被异或加密的pbBuffer,也就是(Key+IV)共48字节数据。

alt text

使用解密的Key和IV对刚刚获取的密文进行AES-CBC解密,与加密是对称的,解密完毕后又将(Key+IV)48字节异或CreateTime进行数据加密。

alt text

最后红框处将解密文本与窗口输入的Secret文本进行比对,若匹配则弹出”Secret”信息框,不匹配则调用下面部分代码。

打开对应用户名的邮槽对象,将当前解密的数据写回邮槽中,并且设置Signal为1,与R0驱动进行通信,也就触发了R0驱动的那个[0]地址赋值异常,触发蓝屏。

alt text

3. 题目流程分析

已知题目DMP附件是由于驱动触发蓝屏时转储的,驱动触发蓝屏原因是:R3程序请求用户名获取的对应密文,解密后不匹配当前窗口中的Secret。

而取证阶段,我们通过!process 0 0遍历进程发现了存在两个aPersonalVault.exe,所以会存在一种情况造成蓝屏:

两个进程分别通过不同的原用户名创建了两份邮槽储存密文,然后其中一个进程误输错用户名,访问了另一个进程的邮槽,获取到了不是属于他密钥加密的密文,自然也无法解密出明文数据,比对失败后则通信Signal为1的消息,触发蓝屏。

整体流程:

  1. 两个进程分别用生成的不同随机密钥加密了两份Secret,储存到了两个邮槽当中。

  2. 进程1通过错误用户名访问到了进程2的邮槽,使用进程1的密钥进行了数据解密,匹配失败,将解密后数据写回到进程2的邮槽中,然后触发蓝屏。

逆向流程:

  1. 获取两个进程中被加密的48字节(Key+IV)数据,然后再获取两个进程的CreateTime,将48字节和CreateTime进行异或得到两份AES的密钥和IV。

  2. 获取两个进程创建的邮槽中的密文数据。

  3. 进程1的密文是可以直接通过进程1的密钥和IV进行AES-CBC解密出明文1。

  4. 进程2的密文由于被进程1解密过一次,并且重写覆盖到邮槽数据中,所以需要先使用进程1的密钥和IV进行AES-CBC加密一次,再用进程2的密钥和IV进行AES-CBC解密,最终得到明文2.

所以可以直接提取两个进程的被加密的(Key+IV),以及各自的CreateTime,从而解密出原始的两份(Key+IV)。

以及最重要的是接下来需要分析邮槽相关API以及数据结构,从而提取两份密文。

4. 密钥提取

可以从R3程序找到(Key+IV)储存的偏移,即exe+0x26BA8

alt text

以下是刚刚遍历得到的两个进程数据,Eprocess分别是ffffef063fbe8080、ffffef063fbc1080。

1
2
3
4
5
6
7
8
9
PROCESS ffffef063fbe8080
SessionId: none Cid: 0fa8 Peb: 9f83185000 ParentCid: 14b0
DirBase: 840fe000 ObjectTable: ffffa687a7241740 HandleCount: 161.
Image: aPersonalVault.exe

PROCESS ffffef063fbc1080
SessionId: none Cid: 26f0 Peb: 9216e1a000 ParentCid: 14b0
DirBase: 1296dc000 ObjectTable: ffffa687a6dd3b40 HandleCount: 165.
Image: aPersonalVault.exe

需要先使用.process /r /p Eprocess地址,切换到对应进程,才能通过当前进程Base地址+偏移读取到目标程序中的数据。

alt text

再通过lm vm aPersonalVault命令查看进程模块信息,获取到进程基地址,两个进程都是同一个基地址,所以需要切换进程才能访问对应进程的数据。

alt text

db 00007ff611d10000+0x26BA8查询字节,得到48字节被加密的(Key+IV),切换到另一个进程同样操作,即可以得到两份被加密的(Key+IV)。

alt text

alt text

可以使用.writemem C:\Users\admin\Desktop\Key_and_IV.dat 00007ff611d36ba8 L30命令导出目标地址0x30长度的字节到文件中。

5. 进程CreateTime获取

通过获取到的两个进程的Eprocess地址,转成_EPROCESS结构查看,即可找到CreateTime数据。

dt nt!_EPROCESS ffffef063fbe8080

alt text

alt text

1
2
0x01dc3fe6ed454439
0x01dc3fe6f02a77eb

6. 邮槽逆向分析

前置分析

注意到R3程序创建邮槽的句柄是保存在自身进程的全局地址中,所以可以直接像上文提取密钥相关数据一样操作,切换进程,读取对应偏移数据,得到两个进程的邮槽句柄。

alt text

alt text

获取到两个句柄分别是0x27C和0x280。

ffffef063fbc1080:

alt text

ffffef063fbe8080:

alt text

使用!handle 27c f ffffef063fbc1080命令获取该进程0x27C句柄的信息。

可以看到Name是\mailslot_e60a23e2,也就是R3看到的创建的邮槽的名字,后面的是用户名的Hash值,Object地址是ffffe70b7f057380

alt text

此类型的句柄都是_FILE_OBJECT结构类型,使用dt nt!_FILE_OBJECT ffffe70b7f057380将目标地址转成指定结构查看。

alt text

点击DeviceObject,发现设备是Msfs驱动,也就是msfs.sys,是Windows R0层控制邮槽操作的驱动,所以接下来就对msfs.sys进行逆向分析。

alt text

msfs.sys逆向分析

由于从DMP中Dump出来的msfs.sys IAT有点小问题,所以这边使用自己主机中的msfs.sys进行逆向分析。

DriverEntry函数,这边初始化了各种IRP函数,主要看的是ReadFile对应回调的MsFsdRead,R3层通过ReadFile获取邮槽中的数据,因为只需要获取到邮槽中的数据,所以只需要分析MsFsdRead函数,得到邮槽结构,即可寻址找到邮槽数据。

alt text

MsFsdRead调用了MsCommonRead,然后再调用了MsReadDataQueue,先从当前的邮槽FileObject获取FsContext指针,加上0x88传入函数。

后续UserBuffer和Length就是要返回的数据指针以及读取大小。

alt text

可以从memcpy的传参返回去看,可以找到对应的数据指针以及数据长度的寻址计算方法,所以就可以通过上文得到的两个FileObject地址,寻址找到两个邮槽密文数据,以及数据长度。

pData = *(void**)(*(QWORD*)(FileObject->FsContext + 0x88 + 0x18) - 8 + 0x28)

Datalength = *(DWORD*)(*(QWORD*)(FileObject->FsContext + 0x88 + 0x18) - 8 + 0x20)

alt text

注意一点,上面是对主机的msfs.sys进行逆向分析,不是目标DMP系统中的msfs.sys,所以相关偏移可能不太一样,需要同样使用Volatility3进行Dump出msfs.sys,用ida分析。

发现这边实际是FsContext+0x118,并且MsReadDataQueue内的的寻址是和上文一致,只需要修改FsContext添加的偏移即可。

alt text

alt text

最终数据和数据长度寻址如下:

pData = *(void**)(*(QWORD*)(FileObject->FsContext + 0x118 + 0x18) - 8 + 0x28)

Datalength = *(DWORD*)(*(QWORD*)(FileObject->FsContext + 0x118 + 0x18) - 8 + 0x20)

7. 密文提取

通过上文获取的两个邮槽的FileObject,获取FsContext,寻址得到邮槽数据以及数据长度。

alt text

Eprocess ffffef063fbc1080:

alt text

Eprocess ffffef063fbe8080:

alt text

使用命令导出密文到文件即可。

.writemem C:\Users\admin\Desktop\EncData_1.dat ffffa687a5efa7a8 L80

.writemem C:\Users\admin\Desktop\EncData_2.dat ffffa687a8d145d8 L50

0x3 解密

通过两份CreateTime值和两份(Key+IV)值异或,得到两份AES密钥和IV。

解密代码:

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <iostream>
#include <Windows.h>

int main()
{
uint8_t EncKeyIV_1080[]{0x45, 0x71, 0x9c, 0x96, 0xf3, 0x11, 0x80, 0x2e, 0x9d, 0xac, 0xf2, 0x94, 0x64, 0x4e, 0xc7, 0xe5, 0x52, 0xa4, 0x46, 0x35, 0x5e, 0x86, 0x72, 0x75, 0xa9, 0xb7, 0xbf, 0x80, 0x52, 0xd0, 0x06, 0xa2, 0xc9, 0x62, 0xdd, 0x9c, 0xf2, 0xee, 0x60, 0x5e, 0xa0, 0x6b, 0x4c, 0xcf, 0xb5, 0xef, 0x0d, 0x82};
uint64_t CreateTime_1080 = 0x01dc3fe6ed454439;

uint8_t EncKeyIV_8080[]{0x20, 0x51, 0xb5, 0x07, 0x07, 0x70, 0xb8, 0x0e, 0xfc, 0xa3, 0x9c, 0x30, 0x54, 0x92, 0xd6, 0x44, 0x9d, 0x08, 0xe2, 0x02, 0xfe, 0x81, 0xd1, 0xf6, 0x70, 0xb6, 0x86, 0x35, 0x20, 0xb4, 0xa6, 0x6e, 0xaf, 0x40, 0xdf, 0x21, 0xda, 0x73, 0x21, 0x01, 0x3a, 0xfa, 0x99, 0x1c, 0xe6, 0x56, 0x69, 0x00};
uint64_t CreateTime_8080 = 0x01dc3fe6f02a77eb;

for (int i = 0; i < 48; i += 8)
{
*(uint64_t *)(EncKeyIV_1080 + i) ^= CreateTime_1080;
}

for (int i = 0; i < 48; i += 8)
{
*(uint64_t *)(EncKeyIV_8080 + i) ^= CreateTime_8080;
}

printf("AES Key_1(1080): ");
for (int i = 0; i < 32; i++)
{
printf("%02X ", EncKeyIV_1080[i]);
}
printf("\n");

printf("AES IV_1(1080): ");
for (int i = 0; i < 16; i++)
{
printf("%02X ", EncKeyIV_1080[32 + i]);
}
printf("\n");

printf("AES Key_2(8080): ");
for (int i = 0; i < 32; i++)
{
printf("%02X ", EncKeyIV_8080[i]);
}
printf("\n");

printf("AES IV_2(8080): ");
for (int i = 0; i < 16; i++)
{
printf("%02X ", EncKeyIV_8080[32 + i]);
}
printf("\n");
return 0;
}

Output:

1
2
3
4
AES Key_1(1080): 7C 35 D9 7B 15 2E 5C 2F A4 E8 B7 79 82 71 1B E4 6B E0 03 D8 B8 B9 AE 74 90 F3 FA 6D B4 EF DA A3 
AES IV_1(1080): F0 26 98 71 14 D1 BC 5F 99 2F 09 22 53 D0 D1 83
AES Key_2(8080): CB 26 9F F7 E1 4F 64 0F 17 D4 B6 C0 B2 AD 0A 45 76 7F C8 F2 18 BE 0D F7 9B C1 AC C5 C6 8B 7A 6F
AES IV_2(8080): 44 37 F5 D1 3C 4C FD 00 D1 8D B3 EC 00 69 B5 01

上文流程分析可知,一个密文可以被其中一对密钥+IV直接解密,而另一个密文需要被一对密钥+IV加密后再用另一对密钥+IV解密,直接尝试配对解密即可。

可直接解密密文:

alt text

被解密过的密文:

alt text

flag{Making_challenge_is_hard_manage_a_secure_vault_is_more_difficult}