cheerleader 0.2.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.
- cheerleader-0.2.0/PKG-INFO +643 -0
- cheerleader-0.2.0/README.md +628 -0
- cheerleader-0.2.0/pyproject.toml +25 -0
- cheerleader-0.2.0/pyproject.toml.orig +22 -0
- cheerleader-0.2.0/src/cheerleader/.DS_Store +0 -0
- cheerleader-0.2.0/src/cheerleader/__init__.py +44 -0
- cheerleader-0.2.0/src/cheerleader/agent/__init__.py +9 -0
- cheerleader-0.2.0/src/cheerleader/agent/agent.py +81 -0
- cheerleader-0.2.0/src/cheerleader/agent/agent_settings.py +46 -0
- cheerleader-0.2.0/src/cheerleader/formats/__init__.py +45 -0
- cheerleader-0.2.0/src/cheerleader/formats/base.py +11 -0
- cheerleader-0.2.0/src/cheerleader/formats/elf.py +730 -0
- cheerleader-0.2.0/src/cheerleader/formats/macho.py +682 -0
- cheerleader-0.2.0/src/cheerleader/libs/__init__.py +0 -0
- cheerleader-0.2.0/src/cheerleader/libs/cfg.py +201 -0
- cheerleader-0.2.0/src/cheerleader/libs/disasm.py +119 -0
- cheerleader-0.2.0/src/cheerleader/libs/types.py +165 -0
- cheerleader-0.2.0/src/cheerleader/tui/__init__.py +3 -0
- cheerleader-0.2.0/src/cheerleader/tui/app.py +156 -0
- cheerleader-0.2.0/src/cheerleader/tui/app.tcss +191 -0
- cheerleader-0.2.0/src/cheerleader/tui/highlight.py +208 -0
- cheerleader-0.2.0/src/cheerleader/tui/screens.py +175 -0
- cheerleader-0.2.0/src/cheerleader/tui/tabs.py +861 -0
- cheerleader-0.2.0/src/cheerleader/tui/widgets.py +111 -0
- cheerleader-0.2.0/src/cheerleader/util/__init__.py +14 -0
- cheerleader-0.2.0/src/cheerleader/util/json_parse.py +32 -0
- cheerleader-0.2.0/src/cheerleader/util/utils.py +27 -0
- cheerleader-0.2.0/src/cheerleader/util/uuids.py +23 -0
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: cheerleader
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A simple but effective binary analyzer with optional AI
|
|
5
|
+
Author: Marco Caimi
|
|
6
|
+
Author-email: Marco Caimi <mcaimi@redhat.com>
|
|
7
|
+
Requires-Dist: capstone>=5.0
|
|
8
|
+
Requires-Dist: deepagents>=0.7.4
|
|
9
|
+
Requires-Dist: langchain[openai]>=1.3.14
|
|
10
|
+
Requires-Dist: macholib>=1.16.4
|
|
11
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
12
|
+
Requires-Dist: textual>=8.2.8
|
|
13
|
+
Requires-Python: >=3.11
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# cheerleader
|
|
17
|
+
|
|
18
|
+
A terminal-based binary inspector for macOS and Linux. Opens Mach-O executables, dylibs, and object files as well as ELF executables and shared libraries, and presents their internal structure — segments, sections, dynamic libraries, symbol tables, exports, dynamic relocations / dyld chained fixups, **disassembled code**, **interactive call-flow graphs**, and **control flow graphs (CFG)** — in an interactive text UI.
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- macOS (arm64 or x86_64) or Linux (x86_64, aarch64, …)
|
|
27
|
+
- [uv](https://github.com/astral-sh/uv) ≥ 0.12
|
|
28
|
+
|
|
29
|
+
Python and all dependencies are managed by `uv`; no manual `pip install` is needed.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
git clone <repo>
|
|
37
|
+
cd cheerleader
|
|
38
|
+
uv sync
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
uv run cheerleader <binary>
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Examples:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
# Mach-O
|
|
53
|
+
uv run cheerleader /bin/ls
|
|
54
|
+
uv run cheerleader /opt/homebrew/lib/libuv.1.0.0.dylib
|
|
55
|
+
uv run cheerleader ./MyApp.app/Contents/MacOS/MyApp
|
|
56
|
+
|
|
57
|
+
# ELF
|
|
58
|
+
uv run cheerleader /usr/bin/ls
|
|
59
|
+
uv run cheerleader /lib/x86_64-linux-gnu/libc.so.6
|
|
60
|
+
uv run cheerleader ./myprogram
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## TUI layout
|
|
66
|
+
|
|
67
|
+
```
|
|
68
|
+
┌─ cheerleader — binary inspector ─────────────────────────── 12:34:56 ─┐
|
|
69
|
+
│ libfoo.dylib arm64 64-bit MH_DYLIB │ ← InfoBar
|
|
70
|
+
│ UUID: AABBCC… Min OS: 14.0.0 SDK: 15.0.0 │
|
|
71
|
+
├────────────────────────────────────────────────────────────────────────┤
|
|
72
|
+
│ Segments │ Sections │ Libraries │ Symbols │ Exports │ Fixups │ Disasm │ ← tabs
|
|
73
|
+
├────────────────────────────────────────────────────────────────────────┤
|
|
74
|
+
│ │
|
|
75
|
+
│ (scrollable DataTable for the active tab) │ ← content
|
|
76
|
+
│ │
|
|
77
|
+
├────────────────────────────────────────────────────────────────────────┤
|
|
78
|
+
│ q Quit r Reload s Switch slice 1-7 Tabs │ ← Footer
|
|
79
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
The **Disasm tab** uses a split layout:
|
|
83
|
+
|
|
84
|
+
```
|
|
85
|
+
├─────────────────────────────────────────────────────────────────────────┤
|
|
86
|
+
│ __TEXT,__text │ Address │ Bytes │ Mnemonic │ Ops │
|
|
87
|
+
│ __TEXT,__stubs │ 0x100003f44 │ ff 25 … │ jmp │ … │
|
|
88
|
+
│ LOAD_0,.text │ 0x100003f4a │ 68 00 … │ push │ 0 │
|
|
89
|
+
│ … │ … │ … │ … │ … │
|
|
90
|
+
│ ├────────────────────────────────────────────────── │
|
|
91
|
+
│ │ __TEXT,__text — 3,817 instructions │
|
|
92
|
+
└────────────────────────────────────────────────────────────────────────┘
|
|
93
|
+
← section list (28) ← disassembly table (remaining width) → status bar
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
For Mach-O binaries sections are listed as `SegmentName,SectionName` (e.g. `__TEXT,__text`). For ELF binaries they appear as `LoadSegment,SectionName` (e.g. `LOAD_0,.text`).
|
|
97
|
+
|
|
98
|
+
### Global keybindings
|
|
99
|
+
|
|
100
|
+
| Key | Action |
|
|
101
|
+
|-------------|-------------------------------------------------|
|
|
102
|
+
| `q` | Quit |
|
|
103
|
+
| `r` | Reload file from disk |
|
|
104
|
+
| `s` | Open slice picker (fat/universal binaries only) |
|
|
105
|
+
| `1` | Segments tab |
|
|
106
|
+
| `2` | Sections tab |
|
|
107
|
+
| `3` | Libraries tab |
|
|
108
|
+
| `4` | Symbols tab |
|
|
109
|
+
| `5` | Exports tab |
|
|
110
|
+
| `6` | Fixups tab |
|
|
111
|
+
| `7` | Disasm tab |
|
|
112
|
+
| `↑↓` | Scroll table rows |
|
|
113
|
+
| `PgUp/PgDn` | Page through table |
|
|
114
|
+
|
|
115
|
+
### Tab-local keybindings
|
|
116
|
+
|
|
117
|
+
**Symbols tab** — filter visible rows:
|
|
118
|
+
|
|
119
|
+
| Key | Filter |
|
|
120
|
+
|-----|---------------------|
|
|
121
|
+
| `a` | All symbols |
|
|
122
|
+
| `e` | External (global) |
|
|
123
|
+
| `u` | Undefined (imports) |
|
|
124
|
+
| `n` | No stabs (default) |
|
|
125
|
+
|
|
126
|
+
**Fixups tab** — filter visible rows:
|
|
127
|
+
|
|
128
|
+
| Key | Filter |
|
|
129
|
+
|-----|-----------|
|
|
130
|
+
| `a` | All |
|
|
131
|
+
| `b` | Binds |
|
|
132
|
+
| `r` | Rebases |
|
|
133
|
+
|
|
134
|
+
**Disasm tab** — click or navigate the section list on the left to switch sections; disassembly loads in the background. Press `c` to open the call-flow panel or `f` to open the control flow graph for the enclosing function.
|
|
135
|
+
|
|
136
|
+
**Call flow panel** (`c` from Disasm tab):
|
|
137
|
+
|
|
138
|
+
| Key | Action |
|
|
139
|
+
|--------|--------------------------------------------------|
|
|
140
|
+
| `↑↓` | Move through the tree |
|
|
141
|
+
| `Enter`| Drill into the selected function's call flow |
|
|
142
|
+
| `b` | Go back to the previous function |
|
|
143
|
+
| `Esc` | Close the panel |
|
|
144
|
+
|
|
145
|
+
**Control flow graph** (`f` from Disasm tab):
|
|
146
|
+
|
|
147
|
+
| Key | Action |
|
|
148
|
+
|-------------------|--------------------------------|
|
|
149
|
+
| `↑↓` / `PgUp/PgDn` | Scroll through the graph |
|
|
150
|
+
| `Esc` | Close the panel |
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Tab reference
|
|
155
|
+
|
|
156
|
+
### 1 · Segments
|
|
157
|
+
|
|
158
|
+
One row per memory segment.
|
|
159
|
+
|
|
160
|
+
- **Mach-O**: one row per `LC_SEGMENT_64` / `LC_SEGMENT` load command.
|
|
161
|
+
- **ELF**: one row per `PT_LOAD` program header. Named `LOAD_0`, `LOAD_1`, … in order of appearance. Sections not mapped to any `PT_LOAD` segment (e.g. debug sections) are grouped under `OTHER`.
|
|
162
|
+
|
|
163
|
+
| Column | Description |
|
|
164
|
+
|---------------|------------------------------------------------------|
|
|
165
|
+
| Segment | Segment name (`__TEXT`, `LOAD_0`, …) |
|
|
166
|
+
| VM Addr | Virtual memory base address |
|
|
167
|
+
| VM Size | Size in virtual memory (may be larger than on disk) |
|
|
168
|
+
| File Off | Byte offset of segment data within the file |
|
|
169
|
+
| File Size | Byte size of segment data on disk |
|
|
170
|
+
| Prot init/max | `rwx` permission bits: initial / maximum |
|
|
171
|
+
| Sections | Number of sections inside this segment |
|
|
172
|
+
|
|
173
|
+
### 2 · Sections
|
|
174
|
+
|
|
175
|
+
One row per section, flattened across all segments.
|
|
176
|
+
|
|
177
|
+
- **Mach-O**: section name and parent segment come directly from the section header.
|
|
178
|
+
- **ELF**: section names are the raw ELF section names (`.text`, `.data`, `.rodata`, …). The parent segment is the `PT_LOAD` segment whose virtual address range contains the section, or `OTHER` for unmapped sections.
|
|
179
|
+
|
|
180
|
+
| Column | Description |
|
|
181
|
+
|----------|---------------------------------------------------------|
|
|
182
|
+
| Segment | Parent segment name |
|
|
183
|
+
| Section | Section name |
|
|
184
|
+
| Addr | Virtual address |
|
|
185
|
+
| Size | Byte size |
|
|
186
|
+
| File Off | File offset of section content |
|
|
187
|
+
| Align | Alignment expressed as power of two (`2^n`) |
|
|
188
|
+
| Type | Section type decoded from flags |
|
|
189
|
+
| Relocs | Number of relocation entries (Mach-O only) |
|
|
190
|
+
|
|
191
|
+
### 3 · Libraries
|
|
192
|
+
|
|
193
|
+
One row per dynamic library dependency.
|
|
194
|
+
|
|
195
|
+
- **Mach-O**: one row per `LC_LOAD_DYLIB` / `LC_LOAD_WEAK_DYLIB` / `LC_REEXPORT_DYLIB` / `LC_LAZY_LOAD_DYLIB` load command.
|
|
196
|
+
- **ELF**: one row per `DT_NEEDED` entry in the `.dynamic` section.
|
|
197
|
+
|
|
198
|
+
| Column | Description |
|
|
199
|
+
|-------------|----------------------------------------------------------|
|
|
200
|
+
| # | Library ordinal |
|
|
201
|
+
| Name | Library path or SONAME |
|
|
202
|
+
| Current Ver | Version string (Mach-O only; empty for ELF) |
|
|
203
|
+
| Compat Ver | Minimum compatibility version (Mach-O only) |
|
|
204
|
+
| Load Type | `LOAD_DYLIB`, `DT_NEEDED`, `REEXPORT_DYLIB`, etc. |
|
|
205
|
+
| LC Offset | File offset of the load command or dynamic entry |
|
|
206
|
+
|
|
207
|
+
### 4 · Symbols
|
|
208
|
+
|
|
209
|
+
- **Mach-O**: parsed from `LC_SYMTAB` (`nlist_64` / `nlist` entries).
|
|
210
|
+
- **ELF**: parsed from `.symtab` when present; falls back to `.dynsym` for stripped binaries.
|
|
211
|
+
|
|
212
|
+
| Column | Description |
|
|
213
|
+
|---------|------------------------------------------------|
|
|
214
|
+
| Address | Virtual address (0 for undefined symbols) |
|
|
215
|
+
| Type | `UNDEF`, `ABS`, `SECT` (ELF: derived from `st_shndx`) |
|
|
216
|
+
| Sect | Section index (0 = no section / undefined) |
|
|
217
|
+
| Binding | `global`, `private`, or `local` |
|
|
218
|
+
| Name | Symbol name from the string table |
|
|
219
|
+
|
|
220
|
+
Default filter hides debug stab entries (Mach-O `N_STAB`). ELF symbols never set the stab flag so the filter has no effect on them.
|
|
221
|
+
|
|
222
|
+
### 5 · Exports
|
|
223
|
+
|
|
224
|
+
- **Mach-O**: walked from the compressed exports trie pointed to by `LC_DYLD_EXPORTS_TRIE`.
|
|
225
|
+
- **ELF**: global, defined symbols from `.dynsym` (symbols with non-zero address and global/weak binding).
|
|
226
|
+
|
|
227
|
+
| Column | Description |
|
|
228
|
+
|---------|----------------------------------------------------|
|
|
229
|
+
| Address | VM address of the exported symbol |
|
|
230
|
+
| Flags | Export flags (Mach-O) or sym_type (ELF) |
|
|
231
|
+
| Name | Fully-qualified mangled export name |
|
|
232
|
+
|
|
233
|
+
### 6 · Fixups
|
|
234
|
+
|
|
235
|
+
Dynamic pointer fixups resolved at load time.
|
|
236
|
+
|
|
237
|
+
- **Mach-O**: parsed from `LC_DYLD_CHAINED_FIXUPS`. Encodes both **bind** (import from library) and **rebase** (image-internal pointer) slots in a compact linked-list chain per page.
|
|
238
|
+
- **ELF**: parsed from all `SHT_RELA` and `SHT_REL` sections (`.rela.dyn`, `.rela.plt`, `.rel.dyn`, `.rel.plt`, etc.). Each relocation entry maps to a bind (undefined symbol reference) or rebase (defined symbol reference / image-internal pointer).
|
|
239
|
+
|
|
240
|
+
| Column | Description |
|
|
241
|
+
|-----------------|------------------------------------------------------------------|
|
|
242
|
+
| Segment | Segment containing the fixup slot |
|
|
243
|
+
| Address | VM address of the slot |
|
|
244
|
+
| Kind | Pointer format (Mach-O: `64_OFFSET`, `ARM64E`, …; ELF: `R_N`) |
|
|
245
|
+
| Type | `bind` (import) or `rebase` (internal) |
|
|
246
|
+
| Library | Source library for binds (Mach-O: resolved from ordinal; ELF: n/a) |
|
|
247
|
+
| Symbol / Target | Symbol name (binds) or target VM address (rebases) |
|
|
248
|
+
| Addend | Constant added to the resolved value |
|
|
249
|
+
|
|
250
|
+
### 7 · Disasm
|
|
251
|
+
|
|
252
|
+
Interactive disassembler powered by [capstone](https://www.capstone-engine.org/). The tab shows two panels:
|
|
253
|
+
|
|
254
|
+
- **Left (28 cols)**: list of all sections in executable segments (`initprot & 0x4`). For Mach-O, `__TEXT,__text` is selected automatically on load.
|
|
255
|
+
- **Right**: disassembly table for the selected section. Populated in a background thread so the UI stays responsive for large sections.
|
|
256
|
+
|
|
257
|
+
| Column | Description |
|
|
258
|
+
|----------|-----------------------------------------------------------|
|
|
259
|
+
| Address | Virtual address of the instruction |
|
|
260
|
+
| Bytes | Raw instruction bytes as hex pairs (e.g. `55 48 89 e5`) |
|
|
261
|
+
| Mnemonic | Instruction mnemonic (e.g. `push`, `mov`, `bl`) |
|
|
262
|
+
| Operands | Decoded operands in AT&T / Intel / ARM syntax |
|
|
263
|
+
|
|
264
|
+
The status bar at the bottom shows the section name, instruction count, and the `c → call flow f → cfg` hints once disassembly completes.
|
|
265
|
+
|
|
266
|
+
Capstone architecture mapping:
|
|
267
|
+
|
|
268
|
+
| Binary arch | Capstone arch / mode |
|
|
269
|
+
|------------------------------------------|-------------------------------|
|
|
270
|
+
| `x86_64` | `CS_ARCH_X86 / CS_MODE_64` |
|
|
271
|
+
| `x86` | `CS_ARCH_X86 / CS_MODE_32` |
|
|
272
|
+
| `arm64`, `arm64e`, `arm64_32`, `aarch64` | `CS_ARCH_ARM64 / CS_MODE_ARM` |
|
|
273
|
+
| `arm` | `CS_ARCH_ARM / CS_MODE_ARM` |
|
|
274
|
+
| `riscv` / `riscv64` | `CS_ARCH_RISCV / CS_MODE_RISCV32/64` |
|
|
275
|
+
|
|
276
|
+
#### Call flow panel
|
|
277
|
+
|
|
278
|
+
Press `c` while an instruction row is selected to open the **call flow panel** — a centred modal overlay showing the call graph for the function that contains the selected instruction.
|
|
279
|
+
|
|
280
|
+
```
|
|
281
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
282
|
+
│ _uv_fs_poll_init 0xf40 │
|
|
283
|
+
├──────────────────────────────────────────────────────────────────────────┤
|
|
284
|
+
│ Call Graph │
|
|
285
|
+
│ ▼ ▶ Calls (4) │
|
|
286
|
+
│ │ _uv__handle_init 0x56e4 │
|
|
287
|
+
│ │ _uv__fs_poll_cb 0x1234 │
|
|
288
|
+
│ │ _uv_fs_poll_stop 0x5a10 │
|
|
289
|
+
│ │ <indirect:x8> │
|
|
290
|
+
│ ▼ ▶ Called by (2) │
|
|
291
|
+
│ _uv_fs_poll_start 0x1000 │
|
|
292
|
+
│ sub_3a90 0x3a90 │
|
|
293
|
+
├──────────────────────────────────────────────────────────────────────────┤
|
|
294
|
+
│ ↑↓ navigate Enter drill in b back Esc close │
|
|
295
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
- **▶ Calls** (green) — functions directly called by the current function. Indirect calls (e.g. `blr x8`, `call rax`) appear as `<indirect:operand>`.
|
|
299
|
+
- **▶ Called by** (yellow) — functions that call the current function within the disassembled section.
|
|
300
|
+
- Selecting a leaf and pressing **Enter** navigates to that function's call flow.
|
|
301
|
+
- **b** steps back through the navigation history.
|
|
302
|
+
- Functions not in the symbol table are named `sub_ADDR` using their hex address. Stripped binaries will show fewer named functions.
|
|
303
|
+
|
|
304
|
+
> **Note:** the call graph is built from the currently disassembled section only. For the most complete graph, disassemble the main code section (`.text` / `__TEXT,__text`).
|
|
305
|
+
|
|
306
|
+
#### Control flow graph
|
|
307
|
+
|
|
308
|
+
Press `f` while an instruction row is selected to open the **control flow graph (CFG)** — a scrollable modal showing the function's basic blocks and the branches between them.
|
|
309
|
+
|
|
310
|
+
```
|
|
311
|
+
┌──────────────────────────────────────────────────────────────────────────┐
|
|
312
|
+
│ CFG: _process_args · 0x100003f44 · 4 blocks │
|
|
313
|
+
├──────────────────────────────────────────────────────────────────────────┤
|
|
314
|
+
│ Block #1 0x100003f44 ⬤ entry │
|
|
315
|
+
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
|
316
|
+
│ │ 0x100003f44 55 push rbp │ │
|
|
317
|
+
│ │ 0x100003f45 48 89 e5 mov rbp, rsp │ │
|
|
318
|
+
│ │ 0x100003f48 48 85 ff test rdi, rdi │ │
|
|
319
|
+
│ │ 0x100003f4b 74 10 je 0x100003f5d │ │
|
|
320
|
+
│ └──────────────────────────────────────────────────────────────────────┘ │
|
|
321
|
+
│ ├── cond → Block #3 0x100003f5d │
|
|
322
|
+
│ └── fall → Block #2 0x100003f4d │
|
|
323
|
+
│ │
|
|
324
|
+
│ Block #2 0x100003f4d ← from #1 │
|
|
325
|
+
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
|
326
|
+
│ │ 0x100003f4d 48 8b 07 mov rax, qword ptr [rdi] │ │
|
|
327
|
+
│ │ 0x100003f50 ff d0 call rax │ │
|
|
328
|
+
│ │ 0x100003f52 eb 06 jmp 0x100003f5a │ │
|
|
329
|
+
│ └──────────────────────────────────────────────────────────────────────┘ │
|
|
330
|
+
│ └── jump → Block #4 0x100003f5a │
|
|
331
|
+
│ │
|
|
332
|
+
│ Block #3 0x100003f5d ← from #1 │
|
|
333
|
+
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
|
334
|
+
│ │ 0x100003f5d 5d pop rbp │ │
|
|
335
|
+
│ │ 0x100003f5e c3 ret │ │
|
|
336
|
+
│ └──────────────────────────────────────────────────────────────────────┘ │
|
|
337
|
+
│ └── ret │
|
|
338
|
+
│ │
|
|
339
|
+
│ Block #4 0x100003f5a ← from #2 │
|
|
340
|
+
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
|
341
|
+
│ │ 0x100003f5a 5d pop rbp │ │
|
|
342
|
+
│ │ 0x100003f5b c3 ret │ │
|
|
343
|
+
│ └──────────────────────────────────────────────────────────────────────┘ │
|
|
344
|
+
│ └── ret │
|
|
345
|
+
├──────────────────────────────────────────────────────────────────────────┤
|
|
346
|
+
│ ↑↓ / PgUp / PgDn scroll Esc close │
|
|
347
|
+
└──────────────────────────────────────────────────────────────────────────┘
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
Edge types and their colours:
|
|
351
|
+
|
|
352
|
+
| Edge | Colour | Meaning |
|
|
353
|
+
|------------|--------|-------------------------------------------------------|
|
|
354
|
+
| `fall` | green | Sequential execution after a conditional branch |
|
|
355
|
+
| `cond` | yellow | Taken branch of a conditional (`je`, `cbz`, …) |
|
|
356
|
+
| `jump` | blue | Unconditional jump (`jmp`, `b`) |
|
|
357
|
+
| `ret` | red | Return instruction — no successor |
|
|
358
|
+
| `indirect` | dim | Branch through a register (`br x8`, `jmp rax`) |
|
|
359
|
+
|
|
360
|
+
Back-edges (jumps to a block with a lower address — typical of loops) are annotated with `(back-edge)`.
|
|
361
|
+
|
|
362
|
+
Each block header lists its predecessor block numbers (`← from #N, #M, …`) so you can trace how control arrives without scrolling.
|
|
363
|
+
|
|
364
|
+
> **Note:** the CFG is built for the function that contains the selected instruction. Function boundaries are derived from the symbol table, so stripped binaries will show the entire section as one large function.
|
|
365
|
+
|
|
366
|
+
---
|
|
367
|
+
|
|
368
|
+
## Architecture
|
|
369
|
+
|
|
370
|
+
### Source layout
|
|
371
|
+
|
|
372
|
+
```
|
|
373
|
+
src/cheerleader/
|
|
374
|
+
├── __init__.py # CLI entry point (main())
|
|
375
|
+
├── formats/
|
|
376
|
+
│ ├── __init__.py # Format detection + parser dispatcher
|
|
377
|
+
│ ├── base.py # FormatParser protocol
|
|
378
|
+
│ ├── macho.py # Mach-O parser (fat, thin, 32/64, LE/BE)
|
|
379
|
+
│ └── elf.py # ELF/ELF64 parser (LE/BE, all common arches)
|
|
380
|
+
├── libs/
|
|
381
|
+
│ ├── __init__.py
|
|
382
|
+
│ ├── types.py # Shared dataclasses (BinaryInfo, Section, Symbol, …)
|
|
383
|
+
│ ├── disasm.py # Format-agnostic disassembly + string extraction
|
|
384
|
+
│ └── cfg.py # Call graph + control flow graph builders
|
|
385
|
+
└── tui/
|
|
386
|
+
├── __init__.py
|
|
387
|
+
├── app.py # Textual App + top-level widgets
|
|
388
|
+
├── tabs.py # One TabPane per data view
|
|
389
|
+
├── screens.py # Modal screens (SlicePicker, CallFlow, CFG)
|
|
390
|
+
├── widgets.py # Shared widget helpers
|
|
391
|
+
└── highlight.py # Syntax highlighting for disassembly
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
### Format detection
|
|
395
|
+
|
|
396
|
+
`cheerleader.formats.detect_format(path)` reads the first 4 bytes:
|
|
397
|
+
|
|
398
|
+
| Magic bytes | Format |
|
|
399
|
+
|--------------------|----------|
|
|
400
|
+
| `\x7fELF` | `"elf"` |
|
|
401
|
+
| Mach-O magic (any) | `"macho"` |
|
|
402
|
+
| anything else | `"unknown"` |
|
|
403
|
+
|
|
404
|
+
`cheerleader.formats.parse(path, **kwargs)` dispatches to the appropriate parser and always returns a `BinaryInfo` (or subclass). Callers are format-agnostic.
|
|
405
|
+
|
|
406
|
+
### Class diagram
|
|
407
|
+
|
|
408
|
+
```
|
|
409
|
+
cheerleader.libs.types
|
|
410
|
+
─────────────────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
413
|
+
│ BinaryInfo (base) │
|
|
414
|
+
│─────────────────────────────────────────────────────────────────────│
|
|
415
|
+
│ path: str │
|
|
416
|
+
│ arch: str e.g. "arm64", "x86_64", "aarch64" │
|
|
417
|
+
│ bits: int 32 or 64 │
|
|
418
|
+
│ file_type: str "MH_EXECUTE", "ET_DYN", … │
|
|
419
|
+
│ slice_offset: int absolute file offset of this slice (0 = thin)│
|
|
420
|
+
│ exports: list[dict] {"name", "addr", "flags"} │
|
|
421
|
+
│ error: str | None set if parse fails non-fatally │
|
|
422
|
+
│─────────────────────────────────────────────────────────────────────│
|
|
423
|
+
│ segments: list[Segment] │
|
|
424
|
+
│ libraries: list[Library] │
|
|
425
|
+
│ symbols: list[Symbol] │
|
|
426
|
+
│ chained_fixups: list[ChainedFixup] │
|
|
427
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
428
|
+
▲ ▲
|
|
429
|
+
│ │
|
|
430
|
+
┌────────┴───────────┐ ┌──────────┴──────────────┐
|
|
431
|
+
│ MachOInfo │ │ ELFInfo │
|
|
432
|
+
│────────────────────│ │──────────────────────────│
|
|
433
|
+
│ flags: int │ │ flags: int e_flags │
|
|
434
|
+
│ ncmds: int │ │ entry: int e_entry │
|
|
435
|
+
│ uuid: str | None │ │ os_abi: str │
|
|
436
|
+
│ min_os: str | None │ │ soname: str | None │
|
|
437
|
+
│ sdk: str | None │ │ interp: str | None │
|
|
438
|
+
│ source_version │ │ rpath: str | None │
|
|
439
|
+
│ dylinker │ │ runpath: str | None │
|
|
440
|
+
│ rpaths: list[str] │ └──────────────────────────┘
|
|
441
|
+
└────────────────────┘
|
|
442
|
+
|
|
443
|
+
┌─────┬──────────┐ ┌────┬─────────────────────────────────┐
|
|
444
|
+
│ Segment │ │ Library │
|
|
445
|
+
│────────────────│ │──────────────────────────────────────│
|
|
446
|
+
│ name │ │ name: str install path / SONAME │
|
|
447
|
+
│ vmaddr │ │ current_version: str │
|
|
448
|
+
│ vmsize │ │ compat_version: str │
|
|
449
|
+
│ fileoff │ │ load_type: str "LOAD_DYLIB"/ │
|
|
450
|
+
│ filesize │ │ "DT_NEEDED", … │
|
|
451
|
+
│ maxprot │ │ offset: int file offset │
|
|
452
|
+
│ initprot │ └──────────────────────────────────────┘
|
|
453
|
+
│ prot_str ──────┤ property → "r-x/r-x"
|
|
454
|
+
│────────────────│
|
|
455
|
+
│ sections: list[Section] │
|
|
456
|
+
└──────┬─────────┘
|
|
457
|
+
│ 0..*
|
|
458
|
+
┌──────▼──────────────────────────────────────────────────────────┐
|
|
459
|
+
│ Section │
|
|
460
|
+
│─────────────────────────────────────────────────────────────────│
|
|
461
|
+
│ name: str "__text" / ".text" │
|
|
462
|
+
│ segment: str parent segment name │
|
|
463
|
+
│ addr: int virtual address │
|
|
464
|
+
│ size: int │
|
|
465
|
+
│ offset: int file offset (0 for BSS / SHT_NOBITS) │
|
|
466
|
+
│ align: int alignment (power-of-two exponent for Mach-O; │
|
|
467
|
+
│ actual byte alignment for ELF) │
|
|
468
|
+
│ flags: int type + attribute bitmask │
|
|
469
|
+
│ reloff: int relocation entries offset (Mach-O only) │
|
|
470
|
+
│ nreloc: int relocation count (Mach-O only) │
|
|
471
|
+
│ type_str ──────property → decoded section type string │
|
|
472
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
473
|
+
|
|
474
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
475
|
+
│ Symbol │
|
|
476
|
+
│─────────────────────────────────────────────────────────────────│
|
|
477
|
+
│ name: str from string table │
|
|
478
|
+
│ addr: int virtual address (0 = undefined) │
|
|
479
|
+
│ sym_type: int Mach-O n_type byte, or mapped ELF equivalent: │
|
|
480
|
+
│ 0x00 (N_UNDF) = SHN_UNDEF │
|
|
481
|
+
│ 0x02 (N_ABS) = SHN_ABS │
|
|
482
|
+
│ 0x0E (N_SECT) = defined in a section │
|
|
483
|
+
│ sect: int section index (st_shndx for ELF) │
|
|
484
|
+
│ desc: int n_desc / st_other │
|
|
485
|
+
│ external: bool global or weak binding │
|
|
486
|
+
│ stab: bool debug stab entry (always False for ELF) │
|
|
487
|
+
│ type_str ───────property → "UNDEF" / "ABS" / "SECT" / "STAB" │
|
|
488
|
+
│ binding ────────property → "global" / "private" / "local" │
|
|
489
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
490
|
+
|
|
491
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
492
|
+
│ ChainedFixup │
|
|
493
|
+
│─────────────────────────────────────────────────────────────────│
|
|
494
|
+
│ segment: str segment containing the fixup slot │
|
|
495
|
+
│ offset: int VM address of the slot │
|
|
496
|
+
│ kind: str Mach-O: pointer format; ELF: "R_N" │
|
|
497
|
+
│ lib_ordinal: int | None 1-based library index (Mach-O binds) │
|
|
498
|
+
│ name: str | None symbol name (binds) or lib name │
|
|
499
|
+
│ addend: int value added to the resolved address │
|
|
500
|
+
│ is_rebase: bool True = internal pointer, False = import │
|
|
501
|
+
│ target: int | None rebased target VM address (Mach-O rebases) │
|
|
502
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
503
|
+
|
|
504
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
505
|
+
│ DisasmInstruction │
|
|
506
|
+
│─────────────────────────────────────────────────────────────────│
|
|
507
|
+
│ addr: int virtual address of the instruction │
|
|
508
|
+
│ size: int byte length │
|
|
509
|
+
│ mnemonic: str e.g. "mov", "bl", "push" │
|
|
510
|
+
│ op_str: str operand string in capstone syntax │
|
|
511
|
+
│ raw: bytes raw instruction bytes │
|
|
512
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Parser internals
|
|
518
|
+
|
|
519
|
+
### Mach-O
|
|
520
|
+
|
|
521
|
+
#### Magic and endianness detection
|
|
522
|
+
|
|
523
|
+
The fat binary magic (`0xCAFEBABE`) is always stored big-endian. Each embedded thin Mach-O slice has its own magic in its own native byte order. The parser reads the slice magic as **little-endian** and maps it to a `struct` endian prefix:
|
|
524
|
+
|
|
525
|
+
| Value (read LE) | Constant | File byte order |
|
|
526
|
+
|-----------------|----------------|-----------------|
|
|
527
|
+
| `0xFEEDFACE` | `MH_MAGIC` | little-endian |
|
|
528
|
+
| `0xCEFAEDFE` | `MH_CIGAM` | big-endian |
|
|
529
|
+
| `0xFEEDFACF` | `MH_MAGIC_64` | little-endian |
|
|
530
|
+
| `0xCFFAEDFE` | `MH_CIGAM_64` | big-endian |
|
|
531
|
+
|
|
532
|
+
#### Load command walk
|
|
533
|
+
|
|
534
|
+
After the Mach-O header, load commands are laid out contiguously. Each begins with `(cmd: u32, cmdsize: u32)`. The parser dispatches on `cmd`:
|
|
535
|
+
|
|
536
|
+
| `cmd` | What is extracted |
|
|
537
|
+
|----------------------------|----------------------------------------------------|
|
|
538
|
+
| `LC_SEGMENT_64` | Segment fields + nested section structs |
|
|
539
|
+
| `LC_LOAD_DYLIB` (variants) | Library name, version fields |
|
|
540
|
+
| `LC_SYMTAB` | `symoff`, `nsyms`, `stroff`, `strsize` |
|
|
541
|
+
| `LC_UUID` | 16-byte UUID |
|
|
542
|
+
| `LC_BUILD_VERSION` | Platform, `minos`, `sdk` |
|
|
543
|
+
| `LC_SOURCE_VERSION` | Packed 40-bit A.B.C.D.E version |
|
|
544
|
+
| `LC_RPATH` | Runtime search path string |
|
|
545
|
+
| `LC_LOAD_DYLINKER` | Dynamic linker path |
|
|
546
|
+
| `LC_DYLD_EXPORTS_TRIE` | Blob offset + size for export trie |
|
|
547
|
+
| `LC_DYLD_CHAINED_FIXUPS` | Blob offset for chained fixup header |
|
|
548
|
+
|
|
549
|
+
#### Exports trie
|
|
550
|
+
|
|
551
|
+
The exports trie is a compressed prefix tree. Each node has a ULEB128 `terminal_size`, optional export record (flags + address), a child count, and per-child NUL-terminated label strings with ULEB128 node offsets. The parser walks it recursively accumulating the prefix string and emitting a record at every terminal node.
|
|
552
|
+
|
|
553
|
+
#### Dyld chained fixups
|
|
554
|
+
|
|
555
|
+
`LC_DYLD_CHAINED_FIXUPS` points to a blob with a `dyld_chained_fixups_header`, a `dyld_chained_starts_in_image` (one offset per segment), and per-segment `dyld_chained_starts_in_segment` (page size, pointer format, per-page chain start offsets). For each page the parser follows a singly-linked chain of 8-byte pointer slots; each slot is either a **bind** (high bit set, carries import-table ordinal + symbol) or a **rebase** (image-internal pointer). The next-slot offset is packed into unused bits of the pointer value.
|
|
556
|
+
|
|
557
|
+
---
|
|
558
|
+
|
|
559
|
+
### ELF
|
|
560
|
+
|
|
561
|
+
#### Header and identity
|
|
562
|
+
|
|
563
|
+
The 16-byte ELF ident sets `EI_CLASS` (1 = 32-bit, 2 = 64-bit) and `EI_DATA` (1 = LE, 2 = BE). All subsequent parsing uses the derived `is64` flag and `endian` prefix (`<` / `>`). The remaining header fields supply `e_type` (file type), `e_machine` (architecture), `e_entry` (entry point), `e_phoff` / `e_phnum` / `e_phentsize` (program headers), and `e_shoff` / `e_shnum` / `e_shentsize` / `e_shstrndx` (section headers).
|
|
564
|
+
|
|
565
|
+
#### Program headers → Segments
|
|
566
|
+
|
|
567
|
+
Each `PT_LOAD` program header becomes a `Segment`. For 64-bit: `p_type(I) p_flags(I) p_offset(Q) p_vaddr(Q) p_paddr(Q) p_filesz(Q) p_memsz(Q) p_align(Q)`. For 32-bit: `p_type(I) p_offset(I) p_vaddr(I) p_paddr(I) p_filesz(I) p_memsz(I) p_flags(I) p_align(I)` (note `p_flags` position differs between 32 and 64-bit ELF).
|
|
568
|
+
|
|
569
|
+
ELF permission bits (`PF_R=4`, `PF_W=2`, `PF_X=1`) are remapped to Mach-O-style VM protection bits (`VM_PROT_READ=1`, `VM_PROT_WRITE=2`, `VM_PROT_EXECUTE=4`) so the shared UI code (which tests `initprot & 0x4` for executable sections) works correctly for both formats.
|
|
570
|
+
|
|
571
|
+
`PT_INTERP` and `PT_DYNAMIC` program headers are noted for later parsing.
|
|
572
|
+
|
|
573
|
+
#### Section headers → Sections
|
|
574
|
+
|
|
575
|
+
For 64-bit: `sh_name(I) sh_type(I) sh_flags(Q) sh_addr(Q) sh_offset(Q) sh_size(Q) sh_link(I) sh_info(I) sh_addralign(Q) sh_entsize(Q)` (40 bytes). For 32-bit: all fields are 32-bit (40 bytes total). `sh_name` is an offset into the section-header string table (`.shstrtab`), identified by `e_shstrndx`.
|
|
576
|
+
|
|
577
|
+
Each section is assigned to the `PT_LOAD` segment whose virtual address range contains `sh_addr`. Sections outside any PT_LOAD range (typically debug sections with `sh_addr == 0`) are collected into a virtual `OTHER` segment. `SHT_NOBITS` sections (`.bss`) have their `offset` set to 0 since they occupy no file space.
|
|
578
|
+
|
|
579
|
+
#### Dynamic section
|
|
580
|
+
|
|
581
|
+
The `.dynamic` section (or `PT_DYNAMIC` program header for stripped binaries) contains an array of `(d_tag, d_val)` pairs. The parser makes two passes: first to locate `DT_STRTAB` (virtual address of the dynamic string table) and `DT_STRSZ`, then to extract `DT_NEEDED` entries (each a string-table offset → `Library`), `DT_SONAME`, `DT_RPATH`, and `DT_RUNPATH`. The `DT_STRTAB` virtual address is converted to a file offset via the segment map.
|
|
582
|
+
|
|
583
|
+
#### Symbol table
|
|
584
|
+
|
|
585
|
+
Both `.symtab` (full symbol table) and `.dynsym` (dynamic symbol table) share the same `Elf_Sym` layout.
|
|
586
|
+
|
|
587
|
+
64-bit (`Elf64_Sym`, 24 bytes): `st_name(I) st_info(B) st_other(B) st_shndx(H) st_value(Q) st_size(Q)`
|
|
588
|
+
|
|
589
|
+
32-bit (`Elf32_Sym`, 16 bytes): `st_name(I) st_value(I) st_size(I) st_info(B) st_other(B) st_shndx(H)` (field order differs from 64-bit)
|
|
590
|
+
|
|
591
|
+
`st_info` encodes `st_bind = st_info >> 4` (0=LOCAL, 1=GLOBAL, 2=WEAK) and `st_type = st_info & 0xF`. The `st_shndx` is mapped to the generic `sym_type` field using Mach-O constants for UI filter compatibility: `SHN_UNDEF → N_UNDF (0x00)`, `SHN_ABS → N_ABS (0x02)`, anything else → `N_SECT (0x0E)`. The `external` flag is set for GLOBAL and WEAK bindings. `.symtab` is preferred; `.dynsym` is used as a fallback for stripped binaries.
|
|
592
|
+
|
|
593
|
+
#### Dynamic relocations → ChainedFixup
|
|
594
|
+
|
|
595
|
+
All sections of type `SHT_RELA` and `SHT_REL` are parsed. `sh_link` points to the associated symbol table section (used to resolve symbol names).
|
|
596
|
+
|
|
597
|
+
`Elf64_Rela` (24 bytes): `r_offset(Q) r_info(Q) r_addend(q)`. `Elf64_Rel` (16 bytes): `r_offset(Q) r_info(Q)`. For 64-bit: `sym_idx = r_info >> 32`, `rel_type = r_info & 0xFFFFFFFF`. For 32-bit: `sym_idx = r_info >> 8`, `rel_type = r_info & 0xFF`.
|
|
598
|
+
|
|
599
|
+
Each entry becomes a `ChainedFixup` with `kind = "R_<type>"`, `name` resolved from the symbol table, `is_rebase = False` when the symbol is undefined (library import), `is_rebase = True` for defined symbols and image-internal relocations. The `r_offset` virtual address is converted to a file offset to identify the containing segment.
|
|
600
|
+
|
|
601
|
+
---
|
|
602
|
+
|
|
603
|
+
## Shared utilities (`cheerleader.libs`)
|
|
604
|
+
|
|
605
|
+
### `disasm.disassemble_section(info, seg_name, sect_name)`
|
|
606
|
+
|
|
607
|
+
Locates the named section in `info.segments`, reads raw bytes from disk (`info.slice_offset + section.offset`), and passes them to `capstone.Cs.disasm()` with the section's virtual address so that decoded instruction addresses are correct VM addresses. Returns `[]` if capstone is unavailable or the section has no on-disk content.
|
|
608
|
+
|
|
609
|
+
### `disasm.extract_strings(info, min_len=4)`
|
|
610
|
+
|
|
611
|
+
Scans all data-bearing segments for printable ASCII strings of at least `min_len` characters. For each string, binary-searches the sorted section address range list to annotate the owning `segment,section`. Returns a sorted `list[BinaryString]`.
|
|
612
|
+
|
|
613
|
+
### `cfg.build_call_graph(info, instrs)`
|
|
614
|
+
|
|
615
|
+
Seeds a function address map from `N_SECT`-typed symbols and export entries, then walks every call/bl/blx/blr instruction to build forward (callees) and reverse (callers) adjacency lists. `CallGraph.func_at(addr)` binary-searches the sorted address list to map any instruction address to its enclosing function.
|
|
616
|
+
|
|
617
|
+
### `cfg.build_cfg(instrs, func_name, func_addr)`
|
|
618
|
+
|
|
619
|
+
Four-pass basic-block builder: (1) identify leaders from branch targets and post-branch instructions, (2) group instructions into `CFGBlock`s, (3) compute successor edges (`fall`, `cond`, `jump`, `ret`, `indirect`), (4) back-fill predecessor lists. The CFG screen renders blocks in BFS order from the entry block.
|
|
620
|
+
|
|
621
|
+
---
|
|
622
|
+
|
|
623
|
+
## Dependencies
|
|
624
|
+
|
|
625
|
+
| Package | Role |
|
|
626
|
+
|------------|------------------------------------------------------------|
|
|
627
|
+
| `textual` | TUI framework (widgets, layout, events) |
|
|
628
|
+
| `capstone` | Cross-platform disassembly engine (x86, ARM, ARM64, RISC-V)|
|
|
629
|
+
| `rich` | Rich text rendering (pulled in by textual) |
|
|
630
|
+
|
|
631
|
+
The parser cores (`macho.py`, `elf.py`) use only Python stdlib (`struct`, `os`, `dataclasses`, `enum`). `capstone` is imported lazily inside `disassemble_section()` so all other features work even without it.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
635
|
+
## License
|
|
636
|
+
|
|
637
|
+
This software is released under the GPL v3 license (see LICENSE file)
|
|
638
|
+
|
|
639
|
+
---
|
|
640
|
+
|
|
641
|
+
## Wtf about the name??
|
|
642
|
+
|
|
643
|
+
I am not at all good at choosing names for code repos. For this one I asked my 5-yo daughter... it is good enough for me :)
|