A brief walkthrough of how I identified an obfuscated API resolve routine in a Windows driver and reproduced the decryption to reveal the target function.
Target Function (Decompiled)
Relevant excerpt (condensed):
1// Resolve GetProcAddress(GetModuleHandleA("ntdll.dll"), <obfuscated>)
2qmemcpy(ProcName, "NuFhznilQxMz", 12);
3*(DWORD *)&ProcName[12] = 2122350970;
4*(DWORD *)&ProcName[16] = 2138789756;
5*(WORD *)&ProcName[20] = 5497;
6
7// byte[i] -= i for i in [0, 0x16)
8BYTE *p = ProcName; unsigned v = 0;
9do { *p++ -= v++; } while (v < 0x16);
10
11qword_1801FB9F0 = GetProcAddress(GetModuleHandleA("ntdll.dll"), ProcName);Identification
- What stands out:
GetModuleHandleA("ntdll.dll")followed byGetProcAddresssuggests resolving anNt*export at runtime.- A small buffer named
ProcNameis built from a string literal plus integers, then immediately transformed in a tight loop.
- Length clue: the loop runs while
v < 0x16→ exactly 22 bytes are transformed. That usually means an ANSI API name with a trailing\x00is in play. - Hypothesis: a position-dependent Caesar-style deobfuscation of an export name.
Step-by-step Decryption
Step 1 — Recreate the exact bytes
The code writes two DWORDs and one WORD into the buffer. On Windows/x86-64, these assignments store values in little-endian order.
1import struct
2
3buf = bytearray(b"NuFhznilQxMz") # 12 bytes
4buf += struct.pack("<I", 2122350970) # DWORD @ +12 (little-endian)
5buf += struct.pack("<I", 2138789756) # DWORD @ +16 (little-endian)
6buf += struct.pack("<H", 5497) # WORD @ +20 (little-endian)
7
8# Sanity: we must have 22 bytes
9assert len(buf) == 22
10print(buf.hex())Why little-endian? The assignment *(DWORD *)&ProcName[12] = 2122350970; writes the integer as raw bytes in native order.
Step 2 — Understand the transform
The loop is byte[i] = byte[i] - i for i in [0, 22). In other words, each position is decremented by its index.
i = 0:'N' - 0 = 'N'i = 1:'u' - 1 = 't'i = 2:'F' - 2 = 'D'
This already hints at NtD... which matches common Nt* exports.
Mathematically: out[i] = (in[i] - i) & 0xFF. The mask keeps values in byte range.
Step 3 — Apply it safely
1decrypted = bytes((b - i) & 0xFF for i, b in enumerate(buf[:0x16]))
2print(decrypted)
3print(decrypted.rstrip(b"\x00").decode("ascii"))You should see b'NtDeviceIoControlFile\x00' and the ASCII string NtDeviceIoControlFile.
Step 4 — Wrap it into a helper you can reuse
1def decrypt_export_name(raw_bytes: bytes, length: int = 0x16) -> str:
2 part = raw_bytes[:length]
3 out = bytes((b - i) & 0xFF for i, b in enumerate(part))
4 return out.rstrip(b"\x00").decode("ascii")
5
6assert decrypt_export_name(buf) == "NtDeviceIoControlFile"Step 5 — Validate against the binary behavior
The result is fed to GetProcAddress using the ntdll.dll handle, consistent with resolving the NtDeviceIoControlFile export.
Why This Works
- The per-index subtraction is a cheap, position-dependent Caesar variant that survives naive
stringsscans. - Splitting the name across a literal and integer stores hides long ASCII runs in the data section.
- Restricting the transform to 22 bytes preserves any subsequent buffer contents.
Surrounding Behavior (Context)
After resolving the function pointer, the routine:
- Builds a small header from
{ a1, GetCurrentThreadId(), GetTickCount(), ... }followed by the caller-provided payload. - Obfuscates the combined buffer with an 8-bit rotate-right by 3 and an XOR with the total length:
dst[i] = len ^ ROR8(src[i], 3)wherelen = a3 + 24. - Invokes the resolved routine with arguments matching
NtDeviceIoControlFileslots and lengths.
Troubleshooting (for first-time attempts)
- Wrong endianness: use
"<I"and"<H"when rebuilding integers. - Off-by-one length: the loop uses
0x16(22). Decrypt exactly 22 bytes. - Unicode vs ANSI: this code resolves an ANSI export name; decoding as UTF-16 will produce garbage.
- Trailing NUL: strip a final
\x00when converting to text.
Takeaways
- Look for short arithmetic loops over small constants near
GetProcAddressto spot deobfuscation. - Reconstruct buffers exactly (sizes, offsets, endianness) before applying transforms.
- Validate outputs early by checking for plausible
Nt*names and usingrstrip("\x00").