hookie 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- hookie-0.1.0/ARCHITECTURE.md +360 -0
- hookie-0.1.0/LICENSE +21 -0
- hookie-0.1.0/MANIFEST.in +11 -0
- hookie-0.1.0/PKG-INFO +90 -0
- hookie-0.1.0/README.md +64 -0
- hookie-0.1.0/examples/README.md +36 -0
- hookie-0.1.0/examples/linux/getpid_payload/hook.py +27 -0
- hookie-0.1.0/examples/linux/puts_payload/hook.py +27 -0
- hookie-0.1.0/examples/windows/beep_payload/hook.py +30 -0
- hookie-0.1.0/examples/windows/messagebox_payload/hook.py +32 -0
- hookie-0.1.0/hookie/__init__.py +19 -0
- hookie-0.1.0/hookie/__main__.py +4 -0
- hookie-0.1.0/hookie/arch/__init__.py +0 -0
- hookie-0.1.0/hookie/arch/x64.py +42 -0
- hookie-0.1.0/hookie/arch/x86.py +34 -0
- hookie-0.1.0/hookie/core.py +300 -0
- hookie-0.1.0/hookie/disassembler.py +90 -0
- hookie-0.1.0/hookie/injector.py +153 -0
- hookie-0.1.0/hookie/memory.py +321 -0
- hookie-0.1.0/hookie/packaging.py +1006 -0
- hookie-0.1.0/hookie/stubs/x64-linux/hookie_stub.c +541 -0
- hookie-0.1.0/hookie/stubs/x64-windows/hookie_stub.c +392 -0
- hookie-0.1.0/hookie/stubs/x86-linux/hookie_stub.c +541 -0
- hookie-0.1.0/hookie/stubs/x86-windows/hookie_stub.c +392 -0
- hookie-0.1.0/hookie/trampoline.py +227 -0
- hookie-0.1.0/hookie.egg-info/PKG-INFO +90 -0
- hookie-0.1.0/hookie.egg-info/SOURCES.txt +53 -0
- hookie-0.1.0/hookie.egg-info/dependency_links.txt +1 -0
- hookie-0.1.0/hookie.egg-info/entry_points.txt +2 -0
- hookie-0.1.0/hookie.egg-info/requires.txt +1 -0
- hookie-0.1.0/hookie.egg-info/top_level.txt +1 -0
- hookie-0.1.0/pyproject.toml +55 -0
- hookie-0.1.0/requirements.txt +1 -0
- hookie-0.1.0/setup.cfg +4 -0
- hookie-0.1.0/tests/README.md +50 -0
- hookie-0.1.0/tests/linux/_common.py +26 -0
- hookie-0.1.0/tests/linux/test_lifecycle.py +35 -0
- hookie-0.1.0/tests/linux/test_loop_reloc.py +37 -0
- hookie-0.1.0/tests/linux/test_native_basic.py +30 -0
- hookie-0.1.0/tests/linux/test_native_original.py +31 -0
- hookie-0.1.0/tests/linux/test_native_relative.py +46 -0
- hookie-0.1.0/tests/linux/test_packaging_posix.py +59 -0
- hookie-0.1.0/tests/linux/test_relocation_matrix.py +94 -0
- hookie-0.1.0/tests/linux/test_safety.py +62 -0
- hookie-0.1.0/tests/linux/test_stub_runtime_policy.py +144 -0
- hookie-0.1.0/tests/linux/test_transaction.py +231 -0
- hookie-0.1.0/tests/run_linux.py +42 -0
- hookie-0.1.0/tests/run_windows.py +38 -0
- hookie-0.1.0/tests/test_packaging.py +177 -0
- hookie-0.1.0/tests/windows/test_direct_destroy.py +31 -0
- hookie-0.1.0/tests/windows/test_lifecycle.py +59 -0
- hookie-0.1.0/tests/windows/test_native_relative.py +84 -0
- hookie-0.1.0/tests/windows/test_stub_runtime_policy.py +113 -0
- hookie-0.1.0/tests/windows/test_winapi_basic.py +47 -0
- hookie-0.1.0/tests/windows/test_winapi_original.py +51 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
# Hookie Architecture
|
|
2
|
+
|
|
3
|
+
This document describes how Hookie works internally. `README.md` intentionally
|
|
4
|
+
stays small; this is the technical reference.
|
|
5
|
+
|
|
6
|
+
## 1. Mental model
|
|
7
|
+
|
|
8
|
+
Hookie has two layers:
|
|
9
|
+
|
|
10
|
+
1. **Hook engine** — patches a native function in the process where Python is
|
|
11
|
+
already running.
|
|
12
|
+
2. **Payload bootstrap** — gets Python + Hookie into another process, then runs
|
|
13
|
+
the user's payload entrypoint there.
|
|
14
|
+
|
|
15
|
+
The hook engine does not know anything about DLL injection. The native bootstrap
|
|
16
|
+
does not implement trampoline relocation. They meet only when the bootstrap
|
|
17
|
+
executes the packaged Python payload and that code imports `hookie`.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 2. Hook engine
|
|
22
|
+
|
|
23
|
+
`HookEngine.create_hook()` receives:
|
|
24
|
+
|
|
25
|
+
- a native target address,
|
|
26
|
+
- a Python detour callable,
|
|
27
|
+
- the target argument types,
|
|
28
|
+
- the target return type,
|
|
29
|
+
- optionally a ctypes prototype factory.
|
|
30
|
+
|
|
31
|
+
Hookie wraps the Python detour with `ctypes`, disassembles whole instructions
|
|
32
|
+
from the target prologue, builds a trampoline, and prepares a patch.
|
|
33
|
+
|
|
34
|
+
The hook starts disabled. `enable()` writes the jump into the target prologue.
|
|
35
|
+
`disable()` restores the original bytes.
|
|
36
|
+
|
|
37
|
+
`hook.original(...)` calls the trampoline, not the patched target entrypoint.
|
|
38
|
+
|
|
39
|
+
### x64 patch choices
|
|
40
|
+
|
|
41
|
+
Hookie chooses among:
|
|
42
|
+
|
|
43
|
+
- a 14-byte absolute indirect jump,
|
|
44
|
+
- a 5-byte `rel32` jump when the destination is in range,
|
|
45
|
+
- a 5-byte jump to a nearby relay which then performs the absolute jump.
|
|
46
|
+
|
|
47
|
+
### Relocation
|
|
48
|
+
|
|
49
|
+
Copied prologue instructions cannot always be copied byte-for-byte. Hookie
|
|
50
|
+
relocates:
|
|
51
|
+
|
|
52
|
+
- relative `call`, `jmp`, and conditional branches,
|
|
53
|
+
- branches whose target lies inside the stolen prologue,
|
|
54
|
+
- x64 RIP-relative memory operands,
|
|
55
|
+
- loop/jcxz-family control flow.
|
|
56
|
+
|
|
57
|
+
The trampoline ends with a jump back to the first original instruction after the
|
|
58
|
+
stolen region.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 3. Memory model
|
|
63
|
+
|
|
64
|
+
Executable buffers are allocated writable, populated, then changed to executable
|
|
65
|
+
permissions. Hookie also supports near/range-constrained allocation where x64
|
|
66
|
+
RIP-relative relocation or a `rel32` relay requires it.
|
|
67
|
+
|
|
68
|
+
Hook installation/removal is not thread-safe: another thread executing the
|
|
69
|
+
prologue while its bytes are being replaced can observe a partial patch.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 4. Build identity and directory layout
|
|
74
|
+
|
|
75
|
+
Every `create-dll` / `create-so` operation generates one random **6-byte id**.
|
|
76
|
+
It is rendered as **12 lowercase hex characters**:
|
|
77
|
+
|
|
78
|
+
```text
|
|
79
|
+
6 bytes -> a1b2c3d4e5f6
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
That exact id is the single identity for the build.
|
|
83
|
+
|
|
84
|
+
Normal/path mode:
|
|
85
|
+
|
|
86
|
+
```text
|
|
87
|
+
deps/
|
|
88
|
+
└── a1b2c3d4e5f6/
|
|
89
|
+
├── Python runtime
|
|
90
|
+
├── hookie/
|
|
91
|
+
├── dependencies
|
|
92
|
+
└── hook.py
|
|
93
|
+
|
|
94
|
+
builds/
|
|
95
|
+
└── a1b2c3d4e5f6/
|
|
96
|
+
└── hookie_stub.dll # or .so
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Single-file mode extracts to the same relative name:
|
|
100
|
+
|
|
101
|
+
```text
|
|
102
|
+
%TEMP%\a1b2c3d4e5f6\ # Windows
|
|
103
|
+
$TMPDIR/a1b2c3d4e5f6/ # Linux
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The entrypoint filename remains simply:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
hook.py
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
There is no separate content id and no `hookie_` prefix on the dependency/temp
|
|
113
|
+
directory. `deps/<id>`, `builds/<id>`, and temp extraction all expose the same
|
|
114
|
+
build id. The file itself stays `hook.py`; only its in-memory Python module name
|
|
115
|
+
uses the id (`hookie_<id>`).
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## 5. Payload footer
|
|
120
|
+
|
|
121
|
+
The final DLL/SO carries a 40-byte footer:
|
|
122
|
+
|
|
123
|
+
```c
|
|
124
|
+
struct HookieFooter {
|
|
125
|
+
char magic[17];
|
|
126
|
+
uint8_t mode;
|
|
127
|
+
uint8_t ident_len;
|
|
128
|
+
uint8_t reserved[5];
|
|
129
|
+
uint64_t raw_size;
|
|
130
|
+
uint64_t data_size;
|
|
131
|
+
};
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
`magic` is:
|
|
135
|
+
|
|
136
|
+
```text
|
|
137
|
+
HOOKIE_PAYLOAD_V1
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
There are two modes.
|
|
141
|
+
|
|
142
|
+
### Path mode
|
|
143
|
+
|
|
144
|
+
The data before the footer is the absolute path to `deps/<id>`.
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
[UTF-8 absolute deps path][footer]
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`ident_len` is zero because the id can be obtained from the basename of the
|
|
151
|
+
resolved payload directory.
|
|
152
|
+
|
|
153
|
+
### Single-file mode
|
|
154
|
+
|
|
155
|
+
The data is:
|
|
156
|
+
|
|
157
|
+
```text
|
|
158
|
+
[12-byte ASCII id][LZSS-compressed archive][footer]
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
The native bootstrap extracts the archive to the system temporary directory
|
|
162
|
+
using exactly that id as the directory name.
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## 6. Native bootstrap: high-level flow
|
|
167
|
+
|
|
168
|
+
Windows:
|
|
169
|
+
|
|
170
|
+
```text
|
|
171
|
+
DllMain
|
|
172
|
+
-> create worker thread
|
|
173
|
+
-> resolve payload directory
|
|
174
|
+
-> locate versioned python3XY.dll
|
|
175
|
+
-> reuse matching initialized CPython if present
|
|
176
|
+
OR load bundled python3XY.dll
|
|
177
|
+
-> configure Python paths
|
|
178
|
+
-> execute hook.py as module hookie_<id>
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Linux:
|
|
182
|
+
|
|
183
|
+
```text
|
|
184
|
+
constructor
|
|
185
|
+
-> create worker thread
|
|
186
|
+
-> resolve payload directory
|
|
187
|
+
-> read bundled libpython/runtime version
|
|
188
|
+
-> reuse matching process CPython when safe
|
|
189
|
+
OR load bundled libpython when no Python is present
|
|
190
|
+
-> configure Python paths
|
|
191
|
+
-> execute hook.py as module hookie_<id>
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
The heavy bootstrap work is deliberately not performed directly under Windows
|
|
195
|
+
`DllMain` loader lock.
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## 7. Single-file extraction
|
|
200
|
+
|
|
201
|
+
Packaging serializes each payload file as:
|
|
202
|
+
|
|
203
|
+
```text
|
|
204
|
+
uint32 path_length
|
|
205
|
+
uint64 file_size
|
|
206
|
+
path bytes
|
|
207
|
+
file bytes
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
A zero `path_length` terminates the archive.
|
|
211
|
+
|
|
212
|
+
The archive is compressed with a small LZSS codec and appended to the native
|
|
213
|
+
stub. At runtime the stub:
|
|
214
|
+
|
|
215
|
+
1. reads its own final 40 bytes,
|
|
216
|
+
2. validates the footer,
|
|
217
|
+
3. reads the id + compressed blob,
|
|
218
|
+
4. decompresses it,
|
|
219
|
+
5. creates `<temp>/<id>/`,
|
|
220
|
+
6. recreates the payload tree there.
|
|
221
|
+
|
|
222
|
+
The archive extractor rejects absolute/traversal paths.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 8. Python entrypoint execution
|
|
227
|
+
|
|
228
|
+
The user writes:
|
|
229
|
+
|
|
230
|
+
```text
|
|
231
|
+
hook.py
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Packaging keeps that filename unchanged. The bootstrap derives the id directly
|
|
235
|
+
from the payload directory basename and uses it only for the module name:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
_hookie_id = os.path.basename(os.path.normpath(_hookie_dir))
|
|
239
|
+
_hookie_name = "hookie_" + _hookie_id
|
|
240
|
+
_hookie_file = os.path.join(_hookie_dir, "hook.py")
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
It loads `hook.py` using `importlib.util.spec_from_file_location`, but registers
|
|
244
|
+
it in Python under the deterministic module name:
|
|
245
|
+
|
|
246
|
+
```text
|
|
247
|
+
hookie_a1b2c3d4e5f6
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
No path hash or second random identifier is involved.
|
|
251
|
+
|
|
252
|
+
The module is put in `sys.modules` before execution. This has two useful
|
|
253
|
+
properties:
|
|
254
|
+
|
|
255
|
+
- a second load of the same payload does not blindly execute the same hook setup
|
|
256
|
+
again;
|
|
257
|
+
- module globals keep `HookEngine`, `Hook`, and ctypes callback references alive.
|
|
258
|
+
|
|
259
|
+
The payload top-level is the entrypoint. There is no `install()` callback.
|
|
260
|
+
|
|
261
|
+
---
|
|
262
|
+
|
|
263
|
+
## 9. `sys.path` vs `os.add_dll_directory` on Windows
|
|
264
|
+
|
|
265
|
+
These solve different problems.
|
|
266
|
+
|
|
267
|
+
### `sys.path`
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
sys.path.insert(0, payload_dir)
|
|
271
|
+
sys.path.insert(0, payload_dir / "Lib" / "site-packages")
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
This tells **Python's importer** where to find Python modules and packages such
|
|
275
|
+
as `hookie` and `capstone`.
|
|
276
|
+
|
|
277
|
+
### `os.add_dll_directory`
|
|
278
|
+
|
|
279
|
+
```python
|
|
280
|
+
handle = os.add_dll_directory(payload_dir)
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
This tells the **Windows native loader** to also search the payload directory
|
|
284
|
+
when a `.pyd` or another native component needs dependent DLLs.
|
|
285
|
+
|
|
286
|
+
Adding a directory to `sys.path` does not change Windows DLL dependency search.
|
|
287
|
+
Conversely, `add_dll_directory` does not make Python modules importable.
|
|
288
|
+
|
|
289
|
+
The returned handle owns that registration. If it is closed or garbage
|
|
290
|
+
collected, Windows removes the directory from the DLL search list. Hookie keeps
|
|
291
|
+
those handles alive in:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
sys._hookie_dll_dirs
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
for the lifetime of the embedded/reused interpreter.
|
|
298
|
+
|
|
299
|
+
---
|
|
300
|
+
|
|
301
|
+
## 10. Existing Python runtime policy
|
|
302
|
+
|
|
303
|
+
Hookie currently avoids subinterpreters.
|
|
304
|
+
|
|
305
|
+
### Windows
|
|
306
|
+
|
|
307
|
+
If the payload was built for `python3XY.dll` and that same versioned DLL already
|
|
308
|
+
exists and is initialized in the target, Hookie reuses the target main
|
|
309
|
+
interpreter and acquires the GIL with `PyGILState_Ensure()`.
|
|
310
|
+
|
|
311
|
+
If a different Python minor is present but the requested one is not, Hookie may
|
|
312
|
+
load its own versioned bundled `python3XY.dll` because Windows symbol lookup is
|
|
313
|
+
performed through that explicit module handle.
|
|
314
|
+
|
|
315
|
+
### Linux
|
|
316
|
+
|
|
317
|
+
If a matching CPython major/minor is already present, Hookie reuses it.
|
|
318
|
+
|
|
319
|
+
If a different `libpython` is already mapped, Hookie refuses to load a second
|
|
320
|
+
version. ELF symbol interposition makes two independent process-global CPython
|
|
321
|
+
runtimes substantially less predictable than the Windows versioned-DLL case.
|
|
322
|
+
|
|
323
|
+
---
|
|
324
|
+
|
|
325
|
+
## 11. Bundled runtime lifetime
|
|
326
|
+
|
|
327
|
+
When Hookie owns a newly initialized runtime, it does not finalize Python after
|
|
328
|
+
executing the payload. Native target threads may later enter ctypes callbacks
|
|
329
|
+
created by the hooks. Finalizing the interpreter would leave those native jump
|
|
330
|
+
targets pointing into a dead Python runtime.
|
|
331
|
+
|
|
332
|
+
The bootstrap releases the GIL and parks its worker for process lifetime.
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## 12. Debug mode
|
|
337
|
+
|
|
338
|
+
Windows `--debug` compiles `HK_DEBUG` traces using `OutputDebugStringW`, visible
|
|
339
|
+
in a debugger or DebugView.
|
|
340
|
+
|
|
341
|
+
Linux `--debug` compiles `HK_DEBUG` traces to stderr.
|
|
342
|
+
|
|
343
|
+
Debug builds are compiled from source instead of silently reusing release cache
|
|
344
|
+
artifacts.
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
## 13. Tests
|
|
349
|
+
|
|
350
|
+
`examples/` contains user-facing hook payloads.
|
|
351
|
+
|
|
352
|
+
`tests/` contains regression/native tests. In particular,
|
|
353
|
+
`test_stub_runtime_policy` verifies the CPython bootstrap policy rather than
|
|
354
|
+
instruction relocation:
|
|
355
|
+
|
|
356
|
+
- same-version existing Python is reused;
|
|
357
|
+
- Linux refuses a mismatched already-loaded Python runtime;
|
|
358
|
+
- `hook.py` executes under module name `hookie_<id>`.
|
|
359
|
+
|
|
360
|
+
x86 native tests require a real 32-bit Python/process and 32-bit toolchain.
|
hookie-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sems
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
hookie-0.1.0/MANIFEST.in
ADDED
hookie-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hookie
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Native x86/x64 inline hooking from pure Python on Windows and Linux
|
|
5
|
+
Author: Sems
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Source, https://github.com/SemsYapar/hookie
|
|
8
|
+
Project-URL: Issues, https://github.com/SemsYapar/hookie/issues
|
|
9
|
+
Keywords: hooking,detour,trampoline,minhook,reverse-engineering,ctypes,x86,x64
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Topic :: Software Development :: Disassemblers
|
|
20
|
+
Classifier: Topic :: Security
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: capstone<6,>=5.0.8
|
|
25
|
+
Dynamic: license-file
|
|
26
|
+
|
|
27
|
+
# Hookie
|
|
28
|
+
|
|
29
|
+
Hookie has two useful parts that can be used together **or separately**:
|
|
30
|
+
|
|
31
|
+
1. **Run Python code inside another process.** `create-dll` / `create-so` package a `hook.py` together with Python and the required dependencies so the native stub can execute that Python code inside the target process. You do not have to hook anything to use Hookie this way.
|
|
32
|
+
2. **Hook native functions from Python.** `HookEngine` lets Python detours hook x86/x64 native functions while keeping the original function callable through a trampoline.
|
|
33
|
+
|
|
34
|
+
Put the two together and you can inject Python into a target process and install native hooks there using Python.
|
|
35
|
+
|
|
36
|
+
Windows and Linux are supported; x86 and x64 are supported.
|
|
37
|
+
|
|
38
|
+
## Install
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install hookie
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Hook a function in the current process
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
from hookie import HookEngine
|
|
48
|
+
|
|
49
|
+
engine = HookEngine()
|
|
50
|
+
hook = engine.create_hook(target_addr, detour, argtypes, restype)
|
|
51
|
+
hook.enable()
|
|
52
|
+
|
|
53
|
+
# From the detour:
|
|
54
|
+
# hook.original(...)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Run Python inside another process
|
|
58
|
+
|
|
59
|
+
Write normal top-level Python code in `hook.py`. It may install hooks, inspect the process, log information, or do any other authorized Python-side work.
|
|
60
|
+
|
|
61
|
+
Windows:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
hookie create-dll hook.py --arch x64
|
|
65
|
+
hookie inject-dll target.exe builds/<id>/hookie_stub.dll
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Linux:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
hookie create-so hook.py --arch x64
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Optional build modes:
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
--single-file
|
|
78
|
+
--debug
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
See `examples/` for examples. See `ARCHITECTURE.md` for the trampoline, payload, stub, CPython bootstrap, and runtime-reuse internals.
|
|
82
|
+
|
|
83
|
+
## Current limitations
|
|
84
|
+
|
|
85
|
+
- Installing/removing inline hooks is not thread-safe; patch at a quiet point or stop the relevant threads yourself.
|
|
86
|
+
- In an existing Python process, Windows reuses the same CPython minor version and can use Hookie's bundled runtime when a different minor is present.
|
|
87
|
+
- On Linux, Hookie reuses the same CPython major/minor. If a different `libpython` is already mapped, Hookie refuses to load a second Python runtime.
|
|
88
|
+
- x86 native testing/building requires a real 32-bit Python/process and a 32-bit toolchain.
|
|
89
|
+
|
|
90
|
+
MIT licensed.
|
hookie-0.1.0/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Hookie
|
|
2
|
+
|
|
3
|
+
Hookie has two useful parts that can be used together **or separately**:
|
|
4
|
+
|
|
5
|
+
1. **Run Python code inside another process.** `create-dll` / `create-so` package a `hook.py` together with Python and the required dependencies so the native stub can execute that Python code inside the target process. You do not have to hook anything to use Hookie this way.
|
|
6
|
+
2. **Hook native functions from Python.** `HookEngine` lets Python detours hook x86/x64 native functions while keeping the original function callable through a trampoline.
|
|
7
|
+
|
|
8
|
+
Put the two together and you can inject Python into a target process and install native hooks there using Python.
|
|
9
|
+
|
|
10
|
+
Windows and Linux are supported; x86 and x64 are supported.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install hookie
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Hook a function in the current process
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
from hookie import HookEngine
|
|
22
|
+
|
|
23
|
+
engine = HookEngine()
|
|
24
|
+
hook = engine.create_hook(target_addr, detour, argtypes, restype)
|
|
25
|
+
hook.enable()
|
|
26
|
+
|
|
27
|
+
# From the detour:
|
|
28
|
+
# hook.original(...)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Run Python inside another process
|
|
32
|
+
|
|
33
|
+
Write normal top-level Python code in `hook.py`. It may install hooks, inspect the process, log information, or do any other authorized Python-side work.
|
|
34
|
+
|
|
35
|
+
Windows:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
hookie create-dll hook.py --arch x64
|
|
39
|
+
hookie inject-dll target.exe builds/<id>/hookie_stub.dll
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Linux:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
hookie create-so hook.py --arch x64
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Optional build modes:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
--single-file
|
|
52
|
+
--debug
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
See `examples/` for examples. See `ARCHITECTURE.md` for the trampoline, payload, stub, CPython bootstrap, and runtime-reuse internals.
|
|
56
|
+
|
|
57
|
+
## Current limitations
|
|
58
|
+
|
|
59
|
+
- Installing/removing inline hooks is not thread-safe; patch at a quiet point or stop the relevant threads yourself.
|
|
60
|
+
- In an existing Python process, Windows reuses the same CPython minor version and can use Hookie's bundled runtime when a different minor is present.
|
|
61
|
+
- On Linux, Hookie reuses the same CPython major/minor. If a different `libpython` is already mapped, Hookie refuses to load a second Python runtime.
|
|
62
|
+
- x86 native testing/building requires a real 32-bit Python/process and a 32-bit toolchain.
|
|
63
|
+
|
|
64
|
+
MIT licensed.
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Examples
|
|
2
|
+
|
|
3
|
+
`examples/` contains user-facing payloads. The entrypoint is always `hook.py`,
|
|
4
|
+
and it stays named `hook.py` inside the built payload. The native stub loads it
|
|
5
|
+
under the in-memory module name `hookie_<build-id>`. Its module top-level runs
|
|
6
|
+
once and installs the hooks; there is no separate `install()` callback.
|
|
7
|
+
|
|
8
|
+
Keep the `HookEngine`, `Hook`, and Python detour objects alive at module scope for
|
|
9
|
+
as long as native code can enter the hook.
|
|
10
|
+
|
|
11
|
+
## Windows
|
|
12
|
+
|
|
13
|
+
- `windows/messagebox_payload/hook.py` — rewrites `MessageBoxW` text/caption.
|
|
14
|
+
- `windows/beep_payload/hook.py` — rewrites `kernel32!Beep` arguments and shows
|
|
15
|
+
the x86 `WINFUNCTYPE` / x64 calling-convention pattern.
|
|
16
|
+
|
|
17
|
+
Build one with:
|
|
18
|
+
|
|
19
|
+
```powershell
|
|
20
|
+
hookie create-dll examples\windows\messagebox_payload --arch x64
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Linux
|
|
24
|
+
|
|
25
|
+
- `linux/puts_payload/hook.py` — prefixes strings sent to `libc!puts`.
|
|
26
|
+
- `linux/getpid_payload/hook.py` — demonstrates return-value rewriting by
|
|
27
|
+
changing the observed result of `getpid()`.
|
|
28
|
+
|
|
29
|
+
Build one with:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
hookie create-so examples/linux/puts_payload --arch x64
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
These examples deliberately modify process behavior. Use disposable test
|
|
36
|
+
programs while experimenting.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Demonstrate return-value rewriting by intercepting libc getpid()."""
|
|
2
|
+
import ctypes
|
|
3
|
+
import hookie
|
|
4
|
+
|
|
5
|
+
_engine = hookie.HookEngine()
|
|
6
|
+
_hooks = []
|
|
7
|
+
|
|
8
|
+
libc = ctypes.CDLL(None)
|
|
9
|
+
libc.getpid.argtypes = []
|
|
10
|
+
libc.getpid.restype = ctypes.c_int
|
|
11
|
+
_target = ctypes.cast(libc.getpid, ctypes.c_void_p).value
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _detour():
|
|
15
|
+
# Deliberately obvious demo value; do not use this payload in software that
|
|
16
|
+
# relies on its real PID for synchronization or file naming.
|
|
17
|
+
return _hook.original() + 100000
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_hook = _engine.create_hook(
|
|
21
|
+
_target,
|
|
22
|
+
_detour,
|
|
23
|
+
[],
|
|
24
|
+
ctypes.c_int,
|
|
25
|
+
)
|
|
26
|
+
_hook.enable()
|
|
27
|
+
_hooks.append(_hook)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Prefix strings passed to libc puts()."""
|
|
2
|
+
import ctypes
|
|
3
|
+
import hookie
|
|
4
|
+
|
|
5
|
+
_engine = hookie.HookEngine()
|
|
6
|
+
_hooks = []
|
|
7
|
+
|
|
8
|
+
libc = ctypes.CDLL(None)
|
|
9
|
+
libc.puts.argtypes = [ctypes.c_char_p]
|
|
10
|
+
libc.puts.restype = ctypes.c_int
|
|
11
|
+
_target = ctypes.cast(libc.puts, ctypes.c_void_p).value
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _detour(text):
|
|
15
|
+
original = text or b"(null)"
|
|
16
|
+
rewritten = b"[hookie] " + original
|
|
17
|
+
return _hook.original(rewritten)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
_hook = _engine.create_hook(
|
|
21
|
+
_target,
|
|
22
|
+
_detour,
|
|
23
|
+
libc.puts.argtypes,
|
|
24
|
+
libc.puts.restype,
|
|
25
|
+
)
|
|
26
|
+
_hook.enable()
|
|
27
|
+
_hooks.append(_hook)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Intercept kernel32!Beep and rewrite its frequency/duration."""
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes
|
|
4
|
+
import hookie
|
|
5
|
+
|
|
6
|
+
_engine = hookie.HookEngine()
|
|
7
|
+
_hooks = []
|
|
8
|
+
|
|
9
|
+
kernel32 = ctypes.windll.kernel32
|
|
10
|
+
kernel32.Beep.argtypes = [wintypes.DWORD, wintypes.DWORD]
|
|
11
|
+
kernel32.Beep.restype = wintypes.BOOL
|
|
12
|
+
_target = ctypes.cast(kernel32.Beep, ctypes.c_void_p).value
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _detour(frequency, duration):
|
|
16
|
+
# Keep values in Beep's documented frequency range and shorten long beeps.
|
|
17
|
+
frequency = max(37, min(int(frequency) + 200, 32767))
|
|
18
|
+
duration = min(int(duration), 250)
|
|
19
|
+
return _hook.original(frequency, duration)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_hook = _engine.create_hook(
|
|
23
|
+
_target,
|
|
24
|
+
_detour,
|
|
25
|
+
kernel32.Beep.argtypes,
|
|
26
|
+
kernel32.Beep.restype,
|
|
27
|
+
prototype_factory=(ctypes.WINFUNCTYPE if ctypes.sizeof(ctypes.c_void_p) == 4 else ctypes.CFUNCTYPE),
|
|
28
|
+
)
|
|
29
|
+
_hook.enable()
|
|
30
|
+
_hooks.append(_hook)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Rewrite MessageBoxW text/caption inside the target process."""
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes
|
|
4
|
+
import hookie
|
|
5
|
+
|
|
6
|
+
_engine = hookie.HookEngine()
|
|
7
|
+
_hooks = []
|
|
8
|
+
|
|
9
|
+
user32 = ctypes.windll.user32
|
|
10
|
+
user32.MessageBoxW.argtypes = [
|
|
11
|
+
wintypes.HWND,
|
|
12
|
+
wintypes.LPCWSTR,
|
|
13
|
+
wintypes.LPCWSTR,
|
|
14
|
+
wintypes.UINT,
|
|
15
|
+
]
|
|
16
|
+
user32.MessageBoxW.restype = ctypes.c_int
|
|
17
|
+
_target = ctypes.cast(user32.MessageBoxW, ctypes.c_void_p).value
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _detour(hwnd, text, caption, utype):
|
|
21
|
+
return _hook.original(hwnd, "hooked by hookie", "hookie", utype)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
_hook = _engine.create_hook(
|
|
25
|
+
_target,
|
|
26
|
+
_detour,
|
|
27
|
+
user32.MessageBoxW.argtypes,
|
|
28
|
+
user32.MessageBoxW.restype,
|
|
29
|
+
prototype_factory=(ctypes.WINFUNCTYPE if ctypes.sizeof(ctypes.c_void_p) == 4 else ctypes.CFUNCTYPE),
|
|
30
|
+
)
|
|
31
|
+
_hook.enable()
|
|
32
|
+
_hooks.append(_hook)
|