Fix memory-safety defects in the API hook handlers - #207
Draft
doomedraven wants to merge 1 commit into
Draft
doomedraven wants to merge 1 commit into
doomedraven wants to merge 1 commit into
Conversation
Ten handlers size, bound or dereference buffers using values the monitored process controls. Each one is independently reachable from a single hostile API call. hook_misc.c vDbgPrintExWithPrefixInternal strcpy() of the caller's Prefix into UCHAR Buffer[512]. A prefix longer than 512 bytes overflows the stack buffer, and cb then exceeds sizeof(Buffer) so the following sizeof(Buffer) - cb underflows to a huge size_t that _vsnprintf treats as unlimited. Replaced with strncpy_s/_vsnprintf_s bounded by _TRUNCATE. hook_file.c NtSetInformationFile The FileRenameInformation copy was bounded by the caller's FileNameLength alone. Neither the Length parameter (which bounds the structure the caller actually supplied) nor the 32768-wchar destination was consulted. Now validated against both. hook_reg.c RegEnumKeyW / RegEnumKeyExA / RegEnumKeyExW wcscpy_s(lpName, sizeof(lpName), ...) takes sizeof of a pointer: 4 on x86, 8 on x64. Every substituted name is longer than that, so on x86 the _s invalid-parameter handler terminates the process on any hit. RegEnumKeyW now uses cchName. For the Ex variants lpcName is in/out and holds the length written on return, so the capacity is captured into a local before the original call. hook_tls.c HexEncode writes 2*Length+1 bytes unconditionally. Call sites pass cbBuffer or cbDerivedKey straight from the caller into 65- and 97-byte stack buffers. Added a bounded wrapper and switched the four caller-controlled sites; the sites with literal lengths are unchanged. hook_socket.c alloc_combined_wsabuf and hook_crypto.c CryptHashMessage Both sum caller-controlled element lengths into a DWORD, then memcpy() the untruncated totals into the result. The sum wraps, the allocation is undersized, the copy is not. Both now accumulate in ULONGLONG, reject implausible totals, and clamp each chunk to the space that remains. hook_special.c CDocument_write Same overflow class over the BSTR lengths in the SAFEARRAY, plus wcslen() re-evaluated between the sizing pass and the copy pass, no NULL check on the SAFEARRAY or its elements, and buf leaked on every call. hook_process.c NtCreateUserProcess The attribute-walk bound is (TotalLength - sizeof(unsigned int)) / sizeof(PS_ATTRIBUTE). The subtrahend is the wrong width on x64 (the header field is SIZE_T) and the subtraction underflows whenever TotalLength < 4, producing a loop that walks off the end of the list. ValuePtr was then written through with no NULL check, no Size check and no SEH. All four are now checked. hook_services.c servicename_from_handle byteneeded holds a BYTE count from QueryServiceConfigW and was reused as the CHARACTER capacity for GetServiceKeyNameW, overstating the 0x1000-byte destination by 2x. Also frees the 8 KB servconfig, which leaked on every call from StartService/ControlService. hook_network.c get_ip_list The guard tested AddrCount instead of !AddrCount, so the body ran only for an empty list and produced a zero-byte calloc that was then logged as a C string. The write offset also never advanced, so each address overwrote the previous one. No behaviour change for well-formed input. The only observable differences are that oversized or malformed inputs are now skipped or truncated instead of corrupting memory, and get_ip_list starts producing the list it was always meant to produce. TAG=agy CONV=b3280e17-abe0-4fed-ad0b-c2e7f65da90f
This was referenced Sep 15, 2026
Contributor
Author
|
Correction to the CI note in the description above: the diagnosis there was wrong. The MSBuild workflow is On top of that it targets the retired #212 fixes the workflow file for all three. Re-enabling the workflow in Settings → Actions is the part that has to be done by hand. |
This was referenced Sep 15, 2026
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Second PR in the review series. #206 was the one-to-three-line logic defects; this one is memory safety in the API hook handlers.
Every defect here is reachable from a single API call made by the monitored process. The handler runs inside that process, so a sample that knows capemon is present can pick the argument. None of these require a chain.
Are these related? (the question #206 raised)
These ten are independent of each other. Each fix is local to one handler, each can be reverted alone, and no two share a data structure. They are in one PR because they are the same class of defect and one regression run covers all of them — not because they are coupled.
That matters because the next PR in the series is the opposite case. The
NDEBUGfix (Release configs never define it, soassert()is live in shipped builds) cannot be committed on its own:alloc.c:85creates the guard page insideassert(VirtualProtect(...)). DefineNDEBUGand the guard page silently stops being created.hooking_32.c:267,:378andhooking_64.c:542,:826useassert((p - pre_tramp) < MAX_PRETRAMP_SIZE)as the only overflow check on the 320-bytepre_trampbuffer. The x64 notail+stack stub already emits roughly 280 bytes.So "define NDEBUG" and "convert those asserts into real runtime checks" have to land in the same commit or the build regresses. That PR will say so explicitly. This one does not have that property.
The fixes
hook_misc.cvDbgPrintExWithPrefixInternalstrcpyof callerPrefixintoUCHAR Buffer[512];sizeof(Buffer) - cbthen underflows and_vsnprintftreats it as unlimitedhook_file.cNtSetInformationFileFileNameLength— not byLength, not by the destinationhook_reg.cRegEnumKeyW,RegEnumKeyExA/Wwcscpy_s(lpName, sizeof(lpName), ...)—sizeofof a pointerhook_tls.cHexEncodewrites2*Length+1into 65- and 97-byte stack buffers with a caller-suppliedLengthhook_socket.calloc_combined_wsabufDWORDsum ofWSABUFlengths wraps → undersizedmalloc, full-sizememcpyhook_crypto.cCryptHashMessagergcbToBeHashed[]hook_special.cCDocument_writewcslenre-measured between the sizing and copy passes, plusbufleaked every callhook_process.cNtCreateUserProcessValuePtrwritten through uncheckedhook_services.cservicename_from_handlehook_network.cget_ip_listWorth calling out individually
hook_reg.cis a live crash, not a theoretical one.sizeof(lpName)is 4 on x86.wcscpy_swith a destination size of 4 and a 7-character source invokes the_sinvalid-parameter handler, which terminates the process. Every substitution in theparent_keys/replace_subkeystable triggers it. On x86 this means any sample that enumerates those keys kills the monitored process and the analysis produces nothing.The Ex variants needed care:
lpcNameis in/out. On return it holds the length written, not the capacity, so reading it afterOld_RegEnumKeyEx*gives the wrong number. The capacity is captured into a local before the call.hook_network.cget_ip_listnever worked. The guard isif (!server_list || server_list->AddrCount) return NULL;— so the body executes only whenAddrCount == 0, which thencalloc(1, 0)s and returns a zero-byte allocation that the log layer reads as a C string. With the guard corrected the loop also needed the offset fix, since_snprintf_swas always writing at offset 0 and each address overwrote the last.hook_tls.c:HexEncodeis left alone. It is non-static and may be referenced elsewhere, and there is no local MSVC here to catch a signature break. Astatic HexEncodeBoundedwrapper was added instead and only the call sites with caller-controlled lengths were switched. The sites passing literal 32 or 48 are untouched.Overflow guards: the three summation fixes cap at 64 MB (
hook_socket.c,hook_crypto.c) and 32 MB (hook_special.c). A buffer past that is not going to be useful in a behaviour log anyway, and the cap makes the truncation explicit rather than implicit in a wrappedDWORD. If you would rather log them, say so and I will raise or drop the cap.Behaviour impact
Well-formed input is unaffected. Three observable differences:
get_ip_liststarts emitting the list it was always supposed to emit —DnsQuery_A/DnsQuery_Wrecords will gain aServerListvalue that was previously always absent.hook_reg.csubstitutions stop killing the process.Not verified by build
No MSVC on this machine. CI on #206 has not run either —
gh pr checks 206reports no checks on the branch. Two candidates: fork PRs need maintainer approval before workflows run, and.github/workflows/msbuild.ymltargetsruns-on: windows-2019, which GitHub has retired. If the runner is the problem I can send a prerequisite PR bumping it towindows-2022andactions/checkout@v4/upload-artifact@v4.Draft until it compiles and you have had a chance to run it.
Series
log.c,misc.c,config.c,capemon.c,YaraHarness.c)inside_hookranges,lookupbuckets, single backtrace walk)NDEBUG+ assert conversion +/O2 /Oy-, the coupled one)hook_apilock, arena free list, breakpoint lists)