Skip to content

Commit de1a46f

Browse files
committed
initial commit
Signed-off-by: Massimiliano Giovagnoli <maxgio92@pm.me>
1 parent eb649cf commit de1a46f

11 files changed

Lines changed: 613 additions & 0 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/testdata/demo-app
2+
/prologo

README.md

Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
# prologo
2+
3+
A Go library for detecting function prologues in ELF binaries by disassembling and analyzing instruction patterns.
4+
5+
## Overview
6+
7+
**prologo** is a Go library that disassembles the `.text` section of ELF executables and identifies common function prologue patterns used by compilers.
8+
9+
## Features
10+
11+
- **ELF Binary Support**: Parses and analyzes ELF format executables
12+
- **Multiple Prologue Detection**: Recognizes various function entry patterns
13+
- **Pattern Classification**: Labels detected prologues by type
14+
- **Fast Analysis**: Efficient Go-based disassembly
15+
16+
## Supported Architectures
17+
18+
- **x86_64** (AMD64) - Full support
19+
20+
## Supported Function Prologues
21+
22+
prologo detects the following x86_64 function prologue patterns:
23+
24+
### 1. Classic Frame Pointer Setup
25+
```asm
26+
push rbp ; Save caller's frame pointer
27+
mov rbp, rsp ; Set up new frame pointer
28+
```
29+
30+
**Detection Label**: `classic`
31+
32+
**Description**: Traditional prologue that establishes a complete stack frame with base pointer.
33+
34+
- Used by: Non-optimized builds (`-O0`), code with `-fno-omit-frame-pointer`
35+
36+
---
37+
38+
### 2. No-Frame-Pointer Function
39+
```asm
40+
sub rsp, 0x20 ; Allocate stack space directly
41+
```
42+
43+
**Detection Label**: `no-frame-pointer`
44+
45+
**Description**: Optimized prologue that skips frame pointer setup entirely.
46+
47+
- Used by: Optimized builds (`-O2`, `-O3`), `-fomit-frame-pointer` flag
48+
49+
---
50+
51+
### 3. Push-Only Prologue
52+
```asm
53+
push rbp ; Save frame pointer only
54+
```
55+
56+
**Detection Label**: `push-only`
57+
58+
**Description**: Minimal prologue that saves RBP but doesn't establish a frame chain.
59+
60+
---
61+
62+
### 4. LEA-Based Stack Allocation
63+
```asm
64+
lea rsp, [rsp-0x20] ; Allocate using LEA instead of SUB
65+
```
66+
67+
**Detection Label**: `lea-based`
68+
69+
**Description**: Alternative stack allocation using LEA instruction. LEA doesn't modify CPU flags (unlike SUB).
70+
71+
---
72+
73+
## Usage
74+
75+
Import prologo in your Go project:
76+
77+
```go
78+
package main
79+
80+
import (
81+
"fmt"
82+
"log"
83+
"os"
84+
85+
"github.com/maxgio92/prologo"
86+
)
87+
88+
func main() {
89+
f, err := os.Open("./myapp")
90+
if err != nil {
91+
log.Fatal(err)
92+
}
93+
defer f.Close()
94+
95+
prologues, err := prologo.DetectProloguesFromELF(f)
96+
if err != nil {
97+
log.Fatal(err)
98+
}
99+
100+
counts := make(map[prologo.PrologueType]int)
101+
for _, p := range prologues {
102+
fmt.Printf("[%s] 0x%x: %s\n", p.Type, p.Address, p.Instructions)
103+
counts[p.Type]++
104+
}
105+
106+
fmt.Println()
107+
for typ, n := range counts {
108+
fmt.Printf(" %s: %d\n", typ, n)
109+
}
110+
fmt.Printf("Total: %d\n", len(prologues))
111+
}
112+
```
113+
114+
#### Example output
115+
116+
```
117+
[classic] 0x401000: push rbp; mov rbp, rsp
118+
[classic] 0x401020: push rbp; mov rbp, rsp
119+
[no-frame-pointer] 0x401040: sub rsp, 0x20
120+
[push-only] 0x401060: push rbp
121+
...
122+
123+
classic: 1547
124+
no-frame-pointer: 1
125+
push-only: 2
126+
Total: 1550
127+
```
128+
129+
#### API Reference
130+
131+
**Functions:**
132+
133+
```go
134+
// Core detection — works on raw machine code bytes, no I/O.
135+
func DetectPrologues(code []byte, baseAddr uint64) []Prologue
136+
137+
// Convenience wrapper — parses ELF from the reader, extracts .text, calls DetectPrologues.
138+
func DetectProloguesFromELF(r io.ReaderAt) ([]Prologue, error)
139+
```
140+
141+
**Types:**
142+
143+
```go
144+
type PrologueType string
145+
146+
const (
147+
PrologueClassic PrologueType = "classic"
148+
PrologueNoFramePointer PrologueType = "no-frame-pointer"
149+
ProloguePushOnly PrologueType = "push-only"
150+
PrologueLEABased PrologueType = "lea-based"
151+
)
152+
153+
type Prologue struct {
154+
Address uint64 `json:"address"`
155+
Type PrologueType `json:"type"`
156+
Instructions string `json:"instructions"`
157+
}
158+
```
159+
160+
`DetectPrologues` accepts raw bytes and a base virtual address, making it format-agnostic (works with ELF, PE, Mach-O, raw dumps).
161+
162+
`DetectProloguesFromELF` accepts an `io.ReaderAt` (e.g. `*os.File`) and handles ELF parsing internally.
163+
164+
## Implementation Details
165+
166+
### Architecture
167+
168+
```
169+
┌─────────────────┐
170+
│ ELF Binary │
171+
└────────┬────────┘
172+
173+
174+
┌─────────────────┐
175+
│ ELF Parser │ ← debug/elf package
176+
│ (.text section)│
177+
└────────┬────────┘
178+
179+
180+
┌─────────────────┐
181+
│ Disassembler │ ← golang.org/x/arch/x86/x86asm
182+
│ (x86_64 decode)│
183+
└────────┬────────┘
184+
185+
186+
┌─────────────────┐
187+
│ Pattern Matcher │ ← Prologue detection logic
188+
│ (1-2 inst seq) │
189+
└────────┬────────┘
190+
191+
192+
┌─────────────────┐
193+
│ []Prologue │
194+
│ (addr + type) │
195+
└─────────────────┘
196+
```
197+
198+
### Limitations
199+
200+
- **No Symbol Information**: Works on stripped binaries but reports addresses only
201+
- **Heuristic-Based**: May have false positives in data sections or inline data
202+
- **Linear Disassembly**: Doesn't handle indirect jumps or computed addresses
203+
- **No CET Support**: ENDBR64 detection not yet implemented
204+
205+
## Dependencies
206+
207+
- **Go 1.21+**
208+
- [`golang.org/x/arch/x86/x86asm`](https://pkg.go.dev/golang.org/x/arch/x86/x86asm) - x86 disassembler
209+
- `debug/elf` (standard library) - ELF parser
210+
211+
## References
212+
213+
- [System V AMD64 ABI](https://refspecs.linuxbase.org/elf/x86_64-abi-0.99.pdf)
214+
- [Intel 64 and IA-32 Architectures Software Developer Manuals](https://www.intel.com/content/www/us/en/developer/articles/technical/intel-sdm.html)
215+
- [Go x86 Assembler](https://pkg.go.dev/golang.org/x/arch/x86/x86asm)
216+
- [ELF Format Specification](https://refspecs.linuxfoundation.org/elf/elf.pdf)
217+
218+
---
219+
220+
**Author**: Massimiliano Giovagnoli ([@maxgio92](https://github.com/maxgio92))

detector.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package prologo
2+
3+
import (
4+
"debug/elf"
5+
"fmt"
6+
"io"
7+
8+
"golang.org/x/arch/x86/x86asm"
9+
)
10+
11+
// DetectPrologues analyzes raw machine code bytes and returns detected function
12+
// prologues. baseAddr is the virtual address corresponding to the start of code.
13+
// This function performs no I/O and works with any binary format.
14+
func DetectPrologues(code []byte, baseAddr uint64) []Prologue {
15+
var result []Prologue
16+
17+
offset := 0
18+
addr := baseAddr
19+
var prevInsn *x86asm.Inst
20+
21+
for offset < len(code) {
22+
inst, err := x86asm.Decode(code[offset:], 64)
23+
if err != nil {
24+
offset++
25+
addr++
26+
prevInsn = nil
27+
continue
28+
}
29+
30+
// Pattern 1: Classic frame pointer setup - push rbp; mov rbp, rsp
31+
if prevInsn != nil &&
32+
prevInsn.Op == x86asm.PUSH && prevInsn.Args[0] == x86asm.RBP &&
33+
inst.Op == x86asm.MOV && inst.Args[0] == x86asm.RBP && inst.Args[1] == x86asm.RSP {
34+
result = append(result, Prologue{
35+
Address: addr - uint64(prevInsn.Len),
36+
Type: PrologueClassic,
37+
Instructions: "push rbp; mov rbp, rsp",
38+
})
39+
}
40+
41+
// Pattern 2: No-frame-pointer function - sub rsp, imm
42+
if inst.Op == x86asm.SUB && inst.Args[0] == x86asm.RSP {
43+
if imm, ok := inst.Args[1].(x86asm.Imm); ok && imm > 0 {
44+
if prevInsn == nil || prevInsn.Op == x86asm.RET {
45+
result = append(result, Prologue{
46+
Address: addr,
47+
Type: PrologueNoFramePointer,
48+
Instructions: fmt.Sprintf("sub rsp, 0x%x", imm),
49+
})
50+
}
51+
}
52+
}
53+
54+
// Pattern 3: Push rbp as first instruction
55+
if inst.Op == x86asm.PUSH && inst.Args[0] == x86asm.RBP {
56+
if prevInsn == nil || prevInsn.Op == x86asm.RET {
57+
result = append(result, Prologue{
58+
Address: addr,
59+
Type: ProloguePushOnly,
60+
Instructions: "push rbp",
61+
})
62+
}
63+
}
64+
65+
// Pattern 4: Stack allocation with lea - lea rsp, [rsp-imm]
66+
if inst.Op == x86asm.LEA && inst.Args[0] == x86asm.RSP {
67+
if prevInsn == nil || prevInsn.Op == x86asm.RET {
68+
result = append(result, Prologue{
69+
Address: addr,
70+
Type: PrologueLEABased,
71+
Instructions: "lea rsp, [rsp-offset]",
72+
})
73+
}
74+
}
75+
76+
prevInsn = &inst
77+
offset += inst.Len
78+
addr += uint64(inst.Len)
79+
}
80+
81+
return result
82+
}
83+
84+
// DetectProloguesFromELF parses an ELF binary from the given reader, extracts
85+
// the .text section, and returns detected function prologues.
86+
func DetectProloguesFromELF(r io.ReaderAt) ([]Prologue, error) {
87+
f, err := elf.NewFile(r)
88+
if err != nil {
89+
return nil, fmt.Errorf("failed to parse ELF file: %w", err)
90+
}
91+
defer f.Close()
92+
93+
textSec := f.Section(".text")
94+
if textSec == nil {
95+
return nil, fmt.Errorf("no .text section found")
96+
}
97+
98+
code, err := textSec.Data()
99+
if err != nil && err != io.EOF {
100+
return nil, fmt.Errorf("failed to read .text section: %w", err)
101+
}
102+
103+
return DetectPrologues(code, textSec.Addr), nil
104+
}

0 commit comments

Comments
 (0)