-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.go
284 lines (244 loc) · 7.76 KB
/
main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
// compiler attempts to ease the difficulty of writing ROP chains by
// compiling a human readable "source" format into a binary ROP chain.
// compiler writes the resulting "unresolved ROP chain" to stdout.
//
// # How it works
//
// Compilation is accomplished by first parsing two pieces of data:
//
// - A ROP chain source file
// - A binary file containing ROP gadgets. This must be injected
// into the runner program using the injector program (this file
// can be generated using the "nasm" assembler, refer to the
// examples directory)
//
// ROP gadgets referenced in the source file are looked-up in the specified
// ROP chain binary file. The "chain" file should match the data that was
// written to the runner program using the injector program.
//
// The compiler translates the source file into a payload known as the
// "unsresolved ROP chain" which is interpreted by the runner program
// at runtime.
//
// While we assume the runner program is written in Go, there is (theoretically)
// nothing stopping us from writing the the runner in a different programming
// language. There is no contract between the compiler and the runner beyond
// resolving gadget offsets into addresses.
//
// # Source file format
//
// The ROP chain source file consists of newline delimited strings. Each line
// starts with an identifier string followed by arguments in the format of:
//
// <identifier-string> <argument>
//
// Possible identifier strings consists of the following:
//
// - g: ROP gadget in human-readable assembly format
// - d: A blob of arbitrary, hex-encoded data (automatically
// left-padded with zeros)
// - D: Same as "d", but no zero padding is performed
//
// # Output file format
//
// The compiler produces a blob of binary data that we refer to as the
// "unresolved ROP chain". It is "unresolved" because each gadget is
// represented by its offset within the ROP gadget file. We use offsets
// because gadgets' memory addresses cannot be known ahead of time due
// to PIE and ASLR. Thus, the runner program must resolve these offsets
// into memory addresses when it parses the unresolved ROP chain.
//
// The runner program assumes that the first instruction in the chain
// is a "ret" instruction. This begins execution of the ROP chain.
//
// # Future work
//
// We can potentially make this less error-prone by reading the available
// ROP gadgets from the runner program. We did not pursue this because
// we were unsure how Go programs may be structured in the future.
package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/hex"
"flag"
"fmt"
"log"
"os"
"sort"
"strings"
"rop-interpreter/internal/asm"
"golang.org/x/arch/x86/x86asm"
)
func main() {
log.SetFlags(0)
err := mainWithError()
if err != nil {
log.Fatalf("fatal: %s", err)
}
}
func mainWithError() error {
chainSrcPath := flag.String(
"src",
"",
"Path to the ROP chain source file")
availableGadgetsPath := flag.String(
"gadgets",
"",
"Path to a binary file containing available ROP gadgets (the nasm output file)")
writeGadgetsStdout := flag.Bool(
"write-gadgets",
false,
"Write ROP gadgets found in the available gadgets file to stdout and exit")
flag.Parse()
binaryRopGadgets, err := os.ReadFile(*availableGadgetsPath)
if err != nil {
return fmt.Errorf("failed to read available gadgets file %q - %w",
*availableGadgetsPath, err)
}
availableGadgets := make(map[string]ropGadget)
var parentGadget ropGadget
var nextOffset uint64
// Populate the available gadgets map with gadgets found
// in the binary gadgets blob.
err = asm.DecodeX86(binaryRopGadgets, 64, func(inst x86asm.Inst, index int) {
nextOffset += uint64(inst.Len)
parentGadget.instructions = append(parentGadget.instructions, inst)
if inst.Op == x86asm.RET {
var parentOffset uint64 = parentGadget.offset
var previousInstSize uint64
// The following code creates multiple gadgets
// for instruction sequences that contain more
// than one instruction. For example, consider
// the following gadget:
//
// pop rdi
// pop rsi
// pop rdx
// ret
//
// While the above example is only one gadget,
// it can be logically divided into four gadgets:
//
// 1. pop rdi; pop rsi; pop rdx; ret
// 2. pop rsi; pop rdx; ret
// 3. pop rdx; ret
// 4. ret
//
// This allows the compiler to reuse existing ROP
// gadgets if they contain a useful instruction.
for i := 0; i < len(parentGadget.instructions); i++ {
childGadget := ropGadget{
instructions: parentGadget.instructions[i:],
offset: parentOffset + previousInstSize,
}
availableGadgets[childGadget.String()] = childGadget
parentOffset = childGadget.offset
previousInstSize = uint64(parentGadget.instructions[i].Len)
}
parentGadget = ropGadget{
// TODO: nextOffset can get replaced by previousOffset + previousInstSize
offset: nextOffset,
}
return
}
})
if err != nil {
return fmt.Errorf("failed to decode binary rop gadgets - %w", err)
}
if *writeGadgetsStdout {
var gadgetList []ropGadget
for _, ropGadget := range availableGadgets {
gadgetList = append(gadgetList, ropGadget)
}
sort.SliceStable(gadgetList, func(i, j int) bool {
return gadgetList[i].offset < gadgetList[j].offset
})
for _, gadget := range gadgetList {
fmt.Printf("offset: %d, gadget: %s\n",
gadget.offset, gadget.String())
}
return nil
}
chainSrc, err := os.ReadFile(*chainSrcPath)
if err != nil {
return fmt.Errorf("failed to read rop chain source file %q - %w",
*chainSrcPath, err)
}
ropChain, err := compileRopChain(chainSrc, availableGadgets)
if err != nil {
return fmt.Errorf("failed to compile rop chain - %w", err)
}
_, err = os.Stdout.Write(ropChain)
if err != nil {
return err
}
return nil
}
type ropGadget struct {
instructions []x86asm.Inst
offset uint64
}
func (o *ropGadget) String() string {
var ropGadgetInstructions string
for index, inst := range o.instructions {
if index == 0 {
ropGadgetInstructions = strings.ToLower(inst.String())
} else {
ropGadgetInstructions += "; " + strings.ToLower(inst.String())
}
}
return ropGadgetInstructions
}
// compileRopChain takes ROP chain source data, looks up its
// gadgets in the specified map and produces a binary ROP chain
// consisting of gadgets and their offsets.
//
// The ROP runner / interpreter must lookup the final addresses
// of the gadgets at runtime due to PIE and ASLR.
func compileRopChain(chainSrc []byte, availableGadgets map[string]ropGadget) ([]byte, error) {
var ropChain []byte
scanner := bufio.NewScanner(bytes.NewReader(chainSrc))
lineNum := 0
for scanner.Scan() {
lineNum++
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, ";") {
continue
}
ropType, value, found := strings.Cut(line, ": ")
if !found {
return nil, fmt.Errorf("line %d: separator ':' not found", lineNum)
}
value = strings.TrimPrefix(value, "0x")
switch ropType {
case "g":
ropGadget, hasIt := availableGadgets[value]
if !hasIt {
return nil, fmt.Errorf("line %d: failed to find rop gadget in rop gadget binary: %q", lineNum, value)
}
ropOffset := ropGadget.offset | 0xba6865776dbe0000
ropChain = binary.BigEndian.AppendUint64(ropChain, ropOffset)
case "d", "D":
if ropType == "d" && len(value) < 16 {
value = strings.Repeat("0", 16-len(value)) + value
}
data, err := hex.DecodeString(value)
if err != nil {
return nil, fmt.Errorf("line %d: failed to decode data - %w", lineNum, err)
}
decodedLen := len(data)
temp := make([]byte, decodedLen)
for i := range data {
temp[decodedLen-1-i] = data[i]
}
data = temp
ropChain = append(ropChain, data...)
}
}
if scanner.Err() != nil {
return nil, scanner.Err()
}
return ropChain, nil
}