Skip to content

Commit f3d7a17

Browse files
committed
[TEST:OUTPUTDEBUGSTRINGHOOK] Add OutputDebugStringA stub hook demo
Add a manual demo that verifies SlimDetours hooks the kernelbase implementation reached through the kernel32!OutputDebugStringA stub. Document MSDetours as the expected failing engine because it hooks only the kernel32 stub. Skip when kernel32!OutputDebugStringA is not the expected stub, keep detach cleanup best-effort so it does not mask the hook result, and run the demo in CI only on supported x64/ARM64 paths. Also tighten the jump decoder success annotation reported by code analysis.
1 parent 3560905 commit f3d7a17

6 files changed

Lines changed: 263 additions & 0 deletions

File tree

.github/workflows/Build_Publish.yml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ jobs:
3333
run: |
3434
.\Source\OutDir\${{matrix.platform}}\${{matrix.config}}\Demo.exe -Run
3535
.\Source\OutDir\${{matrix.platform}}\${{matrix.config}}\Demo.exe -Run DeadLock -Engine=SlimDetours
36+
- name: Run OutputDebugStringHook Test [x64/ARM64]
37+
if: ${{(matrix.os == 'windows-latest' && matrix.platform == 'x64') || (matrix.os == 'windows-11-arm' && matrix.platform == 'ARM64')}}
38+
run: |
39+
.\Source\OutDir\${{matrix.platform}}\${{matrix.config}}\Demo.exe -Run OutputDebugStringHook -Engine=SlimDetours
3640
- name: Build [NT5, x64/x86]
3741
if: ${{ matrix.platform == 'x64' || matrix.platform == 'x86' }}
3842
run: |
@@ -43,6 +47,10 @@ jobs:
4347
run: |
4448
.\Source\OutDir\${{matrix.platform}}\${{matrix.config}}\Demo.exe -Run
4549
.\Source\OutDir\${{matrix.platform}}\${{matrix.config}}\Demo.exe -Run DeadLock -Engine=SlimDetours
50+
- name: Run OutputDebugStringHook Test [NT5, x64]
51+
if: ${{ matrix.os == 'windows-latest' && matrix.platform == 'x64' }}
52+
run: |
53+
.\Source\OutDir\${{matrix.platform}}\${{matrix.config}}\Demo.exe -Run OutputDebugStringHook -Engine=SlimDetours
4654
Publish:
4755
if: ${{github.base_ref == '' && startsWith(github.event.head_commit.message, '[VERSION] ')}}
4856
needs: Build
@@ -76,10 +84,12 @@ jobs:
7684
run: |
7785
.\Source\OutDir\x64\Release\Demo.exe -Run
7886
.\Source\OutDir\x64\Release\Demo.exe -Run DeadLock -Engine=SlimDetours
87+
.\Source\OutDir\x64\Release\Demo.exe -Run OutputDebugStringHook -Engine=SlimDetours
7988
.\Source\OutDir\x86\Release\Demo.exe -Run
8089
.\Source\OutDir\x86\Release\Demo.exe -Run DeadLock -Engine=SlimDetours
8190
.\Source\OutDir\x64\Debug\Demo.exe -Run
8291
.\Source\OutDir\x64\Debug\Demo.exe -Run DeadLock -Engine=SlimDetours
92+
.\Source\OutDir\x64\Debug\Demo.exe -Run OutputDebugStringHook -Engine=SlimDetours
8393
.\Source\OutDir\x86\Debug\Demo.exe -Run
8494
.\Source\OutDir\x86\Debug\Demo.exe -Run DeadLock -Engine=SlimDetours
8595
- name: Create NuGet package

Source/Demo/Demo.vcxproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@
195195
<ClCompile Include="DelayHook.c" />
196196
<ClCompile Include="Instruction.c" />
197197
<ClCompile Include="Main.c" />
198+
<ClCompile Include="OutputDebugStringHook.c" />
198199
<ClCompile Include="TwiceSimpleHook.c" />
199200
</ItemGroup>
200201
<ItemGroup>

Source/Demo/Demo.vcxproj.filters

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<ClCompile Include="TwiceSimpleHook.c" />
88
<ClCompile Include="Instruction.c" />
99
<ClCompile Include="COMHook.c" />
10+
<ClCompile Include="OutputDebugStringHook.c" />
1011
</ItemGroup>
1112
<ItemGroup>
1213
<ClInclude Include="Demo.h" />

Source/Demo/Main.c

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
/* Manual demos */
66
TEST_DECL_FUNC(DeadLock);
7+
TEST_DECL_FUNC(OutputDebugStringHook);
78

89
/* Auto tests */
910
TEST_DECL_FUNC(COMHook);
@@ -15,6 +16,9 @@ TEST_DECL_FUNC(DelayHook);
1516

1617
CONST UNITTEST_ENTRY UnitTestList[] = {
1718
TEST_DECL_MANUAL_ENTRY(DeadLock),
19+
#if defined(_AMD64_) || defined(_ARM64_)
20+
TEST_DECL_MANUAL_ENTRY(OutputDebugStringHook),
21+
#endif
1822

1923
TEST_DECL_ENTRY(COMHook),
2024
TEST_DECL_ENTRY(TwiceSimpleHook),
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/*
2+
* This demo shows kernel32!OutputDebugStringA can be an architecture-specific stub before reaching
3+
* the actual implementation in kernelbase.dll.
4+
*
5+
* Run "Demo.exe -Run OutputDebugStringHook -Engine=MSDetours" will fail this test,
6+
* because the original Microsoft Detours hooks only the kernel32 stub.
7+
* Run "Demo.exe -Run OutputDebugStringHook -Engine=SlimDetours" will pass this test.
8+
*/
9+
10+
#include "Demo.h"
11+
12+
#if defined(_AMD64_) || defined(_ARM64_)
13+
14+
typedef
15+
VOID
16+
WINAPI
17+
FN_OutputDebugStringA(
18+
_In_opt_ LPCSTR lpOutputString);
19+
20+
static FN_OutputDebugStringA* g_pfnOutputDebugStringA = NULL;
21+
static LONG volatile g_lOutputDebugStringACount = 0;
22+
static CONST UNICODE_STRING g_usKernel32 = RTL_CONSTANT_STRING(L"kernel32.dll");
23+
static CONST UNICODE_STRING g_usKernelBase = RTL_CONSTANT_STRING(L"kernelbase.dll");
24+
static ANSI_STRING g_asOutputDebugStringA = RTL_CONSTANT_STRING("OutputDebugStringA");
25+
26+
static
27+
VOID
28+
WINAPI
29+
Hooked_OutputDebugStringA(
30+
_In_opt_ LPCSTR lpOutputString)
31+
{
32+
_InterlockedIncrement(&g_lOutputDebugStringACount);
33+
g_pfnOutputDebugStringA(lpOutputString);
34+
}
35+
36+
#if defined(_AMD64_)
37+
38+
static
39+
_Success_(return != FALSE)
40+
BOOL
41+
DecodeKernel32OutputDebugStringAStub(
42+
_In_ PBYTE pbCode,
43+
_Out_ PVOID * ppTarget)
44+
{
45+
if ((pbCode[0] & 0xf0) != 0x40 || pbCode[1] != 0xff || pbCode[2] != 0x25)
46+
{
47+
return FALSE;
48+
}
49+
50+
*ppTarget = *(UNALIGNED PVOID*)(pbCode + 7 + *(UNALIGNED INT32*) & pbCode[3]);
51+
return TRUE;
52+
}
53+
54+
#elif defined(_ARM64_)
55+
56+
static
57+
ULONG
58+
FetchArm64Opcode(
59+
_In_ PBYTE pbCode)
60+
{
61+
return *(UNALIGNED ULONG*)pbCode;
62+
}
63+
64+
static
65+
INT64
66+
SignExtend(
67+
_In_ UINT64 Value,
68+
_In_ UINT Bits)
69+
{
70+
UINT64 const Sign = 1ui64 << (Bits - 1);
71+
return (INT64)((Value & Sign) ? (Value | (~0ui64 << Bits)) : Value);
72+
}
73+
74+
static
75+
_Success_(return != FALSE)
76+
BOOL
77+
DecodeArm64ImportThunk(
78+
_In_ PBYTE pbCode,
79+
_Out_ PVOID * ppTarget)
80+
{
81+
ULONG const Opcode = FetchArm64Opcode(pbCode);
82+
ULONG const Opcode2 = FetchArm64Opcode(pbCode + 4);
83+
ULONG const Opcode3 = FetchArm64Opcode(pbCode + 8);
84+
UINT64 pageLow2;
85+
UINT64 pageHigh19;
86+
INT64 page;
87+
UINT64 offset;
88+
PBYTE pbTarget;
89+
90+
if ((Opcode & 0x9f00001f) != 0x90000010 ||
91+
(Opcode2 & 0xffe003ff) != 0xf9400210 ||
92+
Opcode3 != 0xd61f0200)
93+
{
94+
return FALSE;
95+
}
96+
97+
pageLow2 = (Opcode >> 29) & 3;
98+
pageHigh19 = (Opcode >> 5) & ~(~0ui64 << 19);
99+
page = SignExtend((pageHigh19 << 2) | pageLow2, 21) << 12;
100+
offset = ((Opcode2 >> 10) & ~(~0ui64 << 12)) << 3;
101+
pbTarget = (PBYTE)((ULONG_PTR)pbCode & ~(ULONG_PTR)0xfff) + page + offset;
102+
103+
*ppTarget = *(UNALIGNED PVOID*)pbTarget;
104+
return TRUE;
105+
}
106+
107+
static
108+
_Success_(return != FALSE)
109+
BOOL
110+
DecodeKernel32OutputDebugStringAStub(
111+
_In_ PBYTE pbCode,
112+
_Out_ PVOID * ppTarget)
113+
{
114+
ULONG const Opcode = FetchArm64Opcode(pbCode);
115+
INT64 branchOffset;
116+
117+
if ((Opcode & 0xfc000000) != 0x14000000)
118+
{
119+
return FALSE;
120+
}
121+
122+
// B <imm26>
123+
branchOffset = SignExtend((Opcode & 0x03ffffffULL) << 2, 28);
124+
return DecodeArm64ImportThunk(pbCode + branchOffset, ppTarget);
125+
}
126+
127+
#endif
128+
129+
static
130+
HRESULT
131+
RunOutputDebugStringAHook(
132+
_In_ DEMO_ENGINE_TYPE EngineType,
133+
_In_ FN_OutputDebugStringA* pfnKernel32,
134+
_In_ FN_OutputDebugStringA* pfnKernelBase)
135+
{
136+
HRESULT hr;
137+
138+
_InterlockedExchange(&g_lOutputDebugStringACount, 0);
139+
g_pfnOutputDebugStringA = pfnKernel32;
140+
141+
hr = HookTransactionBegin(EngineType);
142+
if (FAILED(hr))
143+
{
144+
return hr;
145+
}
146+
hr = HookAttach(EngineType, TRUE, (PVOID*)&g_pfnOutputDebugStringA, Hooked_OutputDebugStringA);
147+
if (FAILED(hr))
148+
{
149+
HookTransactionAbort(EngineType);
150+
return hr;
151+
}
152+
hr = HookTransactionCommit(EngineType);
153+
if (FAILED(hr))
154+
{
155+
return hr;
156+
}
157+
158+
pfnKernelBase("OutputDebugStringHook: kernelbase");
159+
pfnKernel32("OutputDebugStringHook: kernel32");
160+
161+
hr = HookTransactionBegin(EngineType);
162+
if (SUCCEEDED(hr))
163+
{
164+
hr = HookAttach(EngineType, FALSE, (PVOID*)&g_pfnOutputDebugStringA, Hooked_OutputDebugStringA);
165+
if (SUCCEEDED(hr))
166+
{
167+
hr = HookTransactionCommit(EngineType);
168+
} else
169+
{
170+
HookTransactionAbort(EngineType);
171+
}
172+
}
173+
return S_OK;
174+
}
175+
176+
TEST_FUNC(OutputDebugStringHook)
177+
{
178+
NTSTATUS Status;
179+
HRESULT hr;
180+
DEMO_ENGINE_TYPE EngineType;
181+
PVOID hKernel32, hKernelBase;
182+
FN_OutputDebugStringA *pfnKernel32OutputDebugStringA, *pfnKernelBaseOutputDebugStringA;
183+
PVOID pStubTarget;
184+
185+
if (FAILED(GetEngineTypeFromArgs(TEST_PARAMETER_ARGC, TEST_PARAMETER_ARGV, &EngineType)))
186+
{
187+
TEST_SKIP("Invalid engine type");
188+
return;
189+
}
190+
191+
Status = LdrGetDllHandle(NULL, NULL, (PUNICODE_STRING)&g_usKernel32, &hKernel32);
192+
if (!NT_SUCCESS(Status))
193+
{
194+
TEST_SKIP("LdrGetDllHandle for kernel32.dll failed with 0x%08lX", Status);
195+
return;
196+
}
197+
Status = LdrGetDllHandle(NULL, NULL, (PUNICODE_STRING)&g_usKernelBase, &hKernelBase);
198+
if (!NT_SUCCESS(Status))
199+
{
200+
TEST_SKIP("LdrGetDllHandle for kernelbase.dll failed with 0x%08lX", Status);
201+
return;
202+
}
203+
204+
Status = LdrGetProcedureAddress(hKernel32,
205+
&g_asOutputDebugStringA,
206+
0,
207+
(PVOID*)&pfnKernel32OutputDebugStringA);
208+
if (!NT_SUCCESS(Status))
209+
{
210+
TEST_SKIP("LdrGetProcedureAddress for kernel32!OutputDebugStringA failed with 0x%08lX", Status);
211+
return;
212+
}
213+
Status = LdrGetProcedureAddress(hKernelBase,
214+
&g_asOutputDebugStringA,
215+
0,
216+
(PVOID*)&pfnKernelBaseOutputDebugStringA);
217+
if (!NT_SUCCESS(Status))
218+
{
219+
TEST_SKIP("LdrGetProcedureAddress for kernelbase!OutputDebugStringA failed with 0x%08lX", Status);
220+
return;
221+
}
222+
if (!DecodeKernel32OutputDebugStringAStub((PBYTE)pfnKernel32OutputDebugStringA, &pStubTarget))
223+
{
224+
TEST_SKIP("kernel32!OutputDebugStringA is not the expected stub");
225+
return;
226+
}
227+
228+
if (pStubTarget != (PVOID)pfnKernelBaseOutputDebugStringA)
229+
{
230+
TEST_SKIP("kernel32!OutputDebugStringA stub does not target kernelbase!OutputDebugStringA");
231+
return;
232+
}
233+
234+
hr = RunOutputDebugStringAHook(EngineType,
235+
pfnKernel32OutputDebugStringA,
236+
pfnKernelBaseOutputDebugStringA);
237+
if (FAILED(hr))
238+
{
239+
TEST_FAIL("Hook OutputDebugStringA failed with 0x%08lX", hr);
240+
return;
241+
}
242+
243+
TEST_OK(g_lOutputDebugStringACount == 2);
244+
}
245+
246+
#endif

Source/KNSoft.SlimDetours/Instruction.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ detour_is_jmp_indirect_to(
178178
}
179179

180180
static
181+
_Success_(return != FALSE)
181182
BOOL
182183
detour_decode_jmp_indirect(
183184
_In_ PBYTE pbCode,

0 commit comments

Comments
 (0)