该题唯一解是非预期。然后该题为蓝屏DUMP,需要分析内核,那必须搞一下。

下载地址:
https://pan.baidu.com/share/init?surl=Lo45kFwSwrXzm52GEv5zdw
取码(GAME)
预期解
其拿到手是个DMP文件,8G fulldump。
直接看一下堆栈情况,发现是一个从EXE 到 SYS中 并触发蓝屏的情况。

把栈帧切到驱动里面看一下

显然的他是故意触发蓝屏,解引用0指针。肯定要开始分析了,那么需要把驱动、应用,DUMP出来分析。
DUMP
显然可以直接.writemem来完成,但是会有问题。

问题就是,内存被换出了,并没有驻留,故此内存dump会缺页。
驱动并不明显,但是到了EXE这里就会出大问题,缺很多东西,那么需要换一种办法dump了。
我选择的是折磨自己的办法,写个windbg插件,这玩意AI竟然写不明白。只能自己摸索。
这里花了大量时间
代码如下:
function initializeScript()
{
return [new host.functionAlias(safeDumpMemory, "MDump")];
}
async function safeDumpMemory(filePath, startAddress, imageSize)
{
const pageSize = 4096;
let totalBytesWritten = 0;
let log = host.diagnostics.debugLog;
log(`[*] 开始安全转储内存...\n`);
log(` - 输出文件: ${filePath}.py\n`);
log(` - 起始地址: 0x${startAddress.toString(16)}\n`);
log(` - 总大小: 0x${imageSize.toString(16)} bytes\n`);
try
{
// 创建或覆盖目标文件
let file = host.namespace.Debugger.Utility.FileSystem.CreateFile(filePath + ".py", "CreateAlways");
var textWriter = host.namespace.Debugger.Utility.FileSystem.CreateTextWriter(file,'Utf8');
// 创建一个4KB的全零缓冲区,用于填充坏页
let nullBuffer = new ArrayBuffer(pageSize);
let nullView = new Uint8Array(nullBuffer);
textWriter.WriteLine(`# python ${filePath}.py
buffer = [
`);
// 逐页循环
for (let offset = host.Int64(0); offset.compareTo(imageSize) < 0; offset = offset.add(pageSize))
{
let currentAddress = startAddress.add(offset);
let bytesToWrite = (imageSize.subtract(offset).compareTo(pageSize) < 0) ? imageSize.subtract(offset) : host.Int64(pageSize);
try
{
// 尝试读取当前内存页
let buffer = host.memory.readMemoryValues(currentAddress,bytesToWrite);
textWriter.WriteLine(buffer);
textWriter.WriteLine(",");
}
catch(e)
{
log(`[!] 错误: ${e}\n`);
// 如果读取失败,就写入零
log(`[!] 无法读取地址: 0x${currentAddress.toString(16)}。正在写入0x${bytesToWrite.toString(16)}字节的0... \n`);
textWriter.WriteLine(nullView);
textWriter.WriteLine(",");
}
totalBytesWritten += bytesToWrite;
// 每转储1页打印一次进度
if (offset.bitwiseAnd(0xFFFFF) == 0 && offset.compareTo(0) > 0) {
log(` ... 已写入 0x${offset.toString(16)} bytes\n`);
}
}
textWriter.WriteLine(`]
f = open('${filePath}','wb')
f.write(bytearray(buffer))`);
file.Close();
log(`\n[+] 转储完成! 总共写入 0x${imageSize.toString(16)} 字节 请运行${filePath}.py\n`);
}
catch(e)
{
log(`[!] 发生严重错误: ${e}\n`);
}
}
驱动分析
驱动真正入口点很容易定位,无论是字符串还是什么。

可以往下分析一下。

稍微往下看一下就能发现其获取了ntoskrnl的基地址,并通过nt!HalPrivateDispatchTable表获取了HalTimerConvertAuxiliaryCounterToPerformanceCounter的地址(nt!HalPrivateDispatchTable + 0x398)并替换了指针,显然用于通信。
后面则是拿了全局的进程链表。
那么下一步就是看通信函数了。

其中需要恢复一下结构,其轻而易举。
struct CommuStruct
{
char sign[4];
int function;
__int64 pid;
__int64 *processCreateTime;
};
显然的,当function为1的时候就是蓝屏,当2的时候根据pid拿到该进程的CreateTime。
EPROCESS 的偏移如下:

观察一下所有函数,就可以发现此时驱动已经分析完了,并没有什么有用的函数了。

应用分析
首先查看一下进程列表,会发现有2个进程名字一样的进程(就是一个exe开了2),其中一个引发了蓝屏。

蓝屏的进程为ffffef063fbc1080(EPROCESS)

这里需要用我的插件dump出来并修复PE结构(我反正是手修的)。把2个EXE都dump出来,并把引发蓝屏的进程称之为A,另一个称之为B。
由于2个进程是一样的,这里选取A进程的dump着重分析。
当修好PE结构后,其导入表应该是都在的。

入口点一眼盯真发现其是创建了一个窗口程序,处理程序为sub_7FF611D11D10。直接点进去分析。
可以发现其创建了2按钮(一个是创建,一个是检查)2个输入的地方(一个是用户名,一个是秘密)。

创建逻辑分析

这里比较长,就直接讲是什么意思。
1.通过与R0通信,获取本进程的CreateTime
2.随机生成AESkey与IV
3.采用AES CBC + Padding的方式来加密输入的东西
4.把AESkey和IV与CreateTime进行亦或,本质就是加密一下AESkey和IV
5.创建邮件槽(IPC通信方式之一)并把密文写入其中。
6.创建共享内存(根据输入的用户名),把自己的Pid和邮件槽的句柄写入其中。
那么这一时刻又有老铁问了?为什么是AES。
其实FindCrypt已经给了答案。

检查逻辑分析

这里比较长,就直接讲是什么意思。
1.通过用户名读取共享内存,获取邮件槽句柄
2.查看邮件槽中的数据,并读出来(此时为密文)
3.通过与R0通信,获取本进程的CreateTime
4.通过CreateTime解密AESkey与IV
5.解密密文,并重新加密AESKey与IV
6.判断解密是否成功
7.成功则弹出信息框,不成功则把解密失败的内容写入邮件槽,并引发蓝屏
那么我们需要考虑其为什么蓝屏?
◆很显然是没解密出来正确的密文
那么为什么会出现这个问题?
◆需要动静结合阐述这个问题。
我们抓住一个点:其邮件槽的句柄是通过共享内存来获取的,共享内存则是根据用户名一一对应的。进而当2个进程输入一个同一个用户名则会导致混乱。
此时我们便可以进行现场还原,根据dumpfile。
在引起蓝屏的进程中查看句柄,情况真相大白。

不难看出mailslot_1359e83被引用了2次,其表明是A、B进程都引用了他,进而A进程用自己的密钥,解密B进程密钥加密的数据,导致与自身不匹配引发的蓝屏。
所以要解密其秘密数据,需要先用A进程的密钥加密回去,再用B进程的密钥解密。
(主要是他也不符合padding的结果,肯定是需要做其他操作)
而mailslot_e60a23e2是A进程自身的秘密数据,用A进程的密钥即可解密。
此时我们逻辑理清了,那么现在需要数据。Key、IV、CreateTime都可以直接拿到,难点则在邮件槽的数据。
邮件槽分析
邮件槽(mailslot)是一种跨进程通信的Windows服务,其基于文件。
具体的邮件槽原理在《Windows内核原理与实现》中写的很清楚,这里不在赘述,而是讲述一种分析思路。
我们从只有一个引用的邮件槽开始分析。
首先查看其对象类型,发现其是文件对象。

那么此时先按文件对象的结构进行解析。
此时需要关注其是什么驱动管理了该文件对象。

现在真相大白了,msfs驱动管理了邮件槽的对象,我们需要对其分析。

直接把本地的驱动丢IDA里看一下。
既然邮件槽的对象归msfs驱动管理,我们需要关注其IRP处理函数,就可以知道读写邮件槽时如何对其操作了。

此时转到MsFsdRead函数看一下。

此时可以发现,_FILE_OBJECT的FsContext成员为关键成员,其0x118处存储了数据。
进入MsReadDataQueue进一步分析。

其中会发现在0x18处是其数据的LIST_ENTRY,- 8则是原始数据结构的头部,+ 0x28则是数据指针,+ 0x20则是数据长度。
回归到具体数据上则是这样,0xffffe70b`7e814260是上文中的FsContext

解密
CreateTime只需要dt _EPROCESS的方式就能看到了。
此时我们理一下数据大致如下:
0298: Object: ffffe70b7f0673c0 GrantedAccess:00160089 (Protected) Entry:ffffa687a73fba60
Object: ffffe70b7f0673c0 Type:(ffffe70b77835850) File
ObjectHeader:ffffe70b7f067390 (new version)
HandleCount: 2 PointerCount:32768
Directory Object: 00000000 Name:\mailslot_1359e83 {Mailslot}
data:
ffffa687`a8d145d8 3c 5b 7d 22 a8 62 ff 7e-89 ef f9 6d 26 e3 d3 e6 <[}".b.~...m&...
ffffa687`a8d145e8 2f c3 9d ff 8a c5 4c 0b-e2 d1 02 47 39 45 96 83 /.....L....G9E..
ffffa687`a8d145f8 46 ce 20 68 f6 0a f0 65-2b 5b 85 be 4f dc f5 63 F. h...e+[..O..c
ffffa687`a8d14608 ee e2 74 9f 4e 07 71 29-03 94 7e 5a b0 bf 66 75 ..t.N.q)..~Z..fu
ffffa687`a8d14618 6c 4b 9c d7 c2 10 fd 45-4d a9 65 36 84 02 f9 8e lK.....EM.e6....
027c: Object: ffffe70b7f057380 GrantedAccess:00160089 (Protected) (Audit) Entry:ffffa687a73fb9f0
Object: ffffe70b7f057380 Type:(ffffe70b77835850) File
ObjectHeader:ffffe70b7f057350 (new version)
HandleCount: 1 PointerCount:1
Directory Object: 00000000 Name:\mailslot_e60a23e2 {Mailslot}
data:
ffffa687`a5efa7a8 95 c2 4e 06 41 92 cf 1c-52 18 01 32 86 4e 38 5e ..N.A...R..2.N8^
ffffa687`a5efa7b8 e2 ee 24 7b 68 0f dc be-7e 14 3b ee 47 fd 22 13 ..${h...~.;.G.".
ffffa687`a5efa7c8 b3 71 1c 1b 40 08 e2 b3-ff 78 34 eb e5 3e 7d 53 .q..@....x4..>}S
ffffa687`a5efa7d8 48 53 59 16 74 46 f5 8b-eb 9d 96 1c 57 13 5e bb HSY.tF......W.^.
ffffa687`a5efa7e8 2a 18 fa 30 98 29 23 db-99 73 d6 ac 4b 01 88 c5 *..0.)#..s..K...
ffffa687`a5efa7f8 7e 38 b0 a5 8d 3e 71 b0-fe c9 8e 7b d8 e3 0c 2c ~8...>q....{...,
ffffa687`a5efa808 27 07 b9 94 94 68 53 af-71 98 84 3b 89 ac 21 de '....hS.q..;..!.
ffffa687`a5efa818 56 67 7c c3 e6 cf 43 b8-d6 d5 39 61 5c 76 31 79 Vg|...C...9a\v1y
PROCESS ffffef063fbc1080 ->A(bsod) CreateTime: 0x01dc3fe6f02a77eb
SessionId: none Cid: 26f0 Peb: 9216e1a000 ParentCid: 14b0
DirBase: 1296dc000 ObjectTable: ffffa687a6dd3b40 HandleCount: 165.
Image: aPersonalVault.exe
unsigned char aes_key[] =
{
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
};
unsigned char iv[] =
{
0xAF, 0x40, 0xDF, 0x21, 0xDA, 0x73, 0x21, 0x01, 0x3A, 0xFA,
0x99, 0x1C, 0xE6, 0x56, 0x69, 0x00
};
PROCESS ffffef063fbe8080 ->B CreateTime: 0x01dc3fe6ed454439
SessionId: none Cid: 0fa8 Peb: 9f83185000 ParentCid: 14b0
DirBase: 840fe000 ObjectTable: ffffa687a7241740 HandleCount: 161.
Image: aPersonalVault.exe
unsigned char key[] =
{
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
};
unsigned char iv[] =
{
0xC9, 0x62, 0xDD, 0x9C, 0xF2, 0xEE, 0x60, 0x5E, 0xA0, 0x6B,
0x4C, 0xCF, 0xB5, 0xEF, 0x0D, 0x82
};
那么请注意,由于AESkey和IV都是亦或过自己的CreateTime,解密时仍然需要。
先看简单的,0x80长度的密文对应的时只有一个引用的(A进程)。
此时需要把AESkey和IV与A进程的CreateTime逐字节亦或并解密AES即可。

可以发现能解出来一句话,说明另一个才有flag。那么按照刚才的逻辑来捋一下。
1.需要用A的AESkey和IV先亦或一下CreateTime,得到真正的AESkey和IV
2.由于炸了之前把解密错误的文本写入了邮件槽,则需要把密文用A的key和IV加密
3.用B的AESkey和IV先亦或一下CreateTime,得到真的AESkey和IV,并解密刚刚加密的密文

此时成功解密:flag{Making_challenge_is_hard_manage_a_secure_vault_is_more_difficult}
看雪ID:moshuiD
https://bbs.kanxue.com/user-home-932553.htm
# 往期推荐
球分享
球点赞
球在看
点击阅读原文查看更多

