draugr~ single ELF file analysis tool |
| git clone https://github.com/djnnvx/draugr.git |
| log | files | refs | archive | README |
call.odin (8293B)
1 package analysis 2 3 import "core:c" 4 import "core:encoding/endian" 5 import "core:fmt" 6 import "core:mem" 7 import "core:os" 8 import "core:strconv" 9 import "core:strings" 10 import "core:sys/linux" 11 import "core:sys/posix" 12 13 import "../elf" 14 15 foreign import libc "system:c" 16 17 @(default_calling_convention="c") 18 foreign libc { 19 system :: proc(command: cstring) -> c.int --- 20 dlinfo :: proc(handle: rawptr, request: c.int, info: rawptr) -> c.int --- 21 } 22 23 Link_Map :: struct { 24 l_addr : uintptr, 25 } 26 27 RTLD_DI_LINKMAP :: 2 28 29 Callee :: #type proc "c" (u64, u64, u64, u64, u64, u64) -> u64 30 31 Patch_Kind :: enum { Ret, Jmp, Got } 32 33 Patch :: struct { 34 kind : Patch_Kind, 35 target : string, 36 value : u64, 37 repl : string, 38 } 39 40 // The callee runs in a forked child so a crash is a report, not a dead draugr. 41 call_function :: proc(data: []u8, info: ^elf.ELF_Info, path: string, symbol: string, raw_args: string, raw_patch: string) -> string { 42 args, parse_err := parse_call_args(raw_args) 43 defer delete(args) 44 if parse_err != "" { 45 return parse_err 46 } 47 48 patches, patch_err := parse_patches(raw_patch) 49 defer delete(patches) 50 if patch_err != "" { 51 return patch_err 52 } 53 if len(patches) > 0 && info.header.machine != elf.EM_X86_64 { 54 return "patching only supported on x86-64" 55 } 56 57 fd, err := linux.memfd_create("draugr-call", {}) 58 if err != .NONE { 59 return fmt.tprintf("memfd_create failed: %v", err) 60 } 61 defer linux.close(fd) 62 63 fd_path := fmt.tprintf("/proc/self/fd/%d", int(fd)) 64 65 if info.header.type == elf.ET_REL { 66 cmd := fmt.ctprintf("ld -shared -o %s %s", fd_path, path) 67 if system(cmd) != 0 { 68 return fmt.tprintf("failed to link %s into a shared object", path) 69 } 70 } else { 71 if _, werr := linux.write(fd, data); werr != .NONE { 72 return fmt.tprintf("writing to memfd failed: %v", werr) 73 } 74 } 75 76 fmt.fprintf(os.stderr, "calling %s in a forked child, loaded from memory\n", symbol) 77 78 pid, ferr := linux.fork() 79 if ferr != .NONE { 80 return fmt.tprintf("fork failed: %v", ferr) 81 } 82 83 if pid == 0 { 84 run_callee(fd_path, symbol, args, info, patches) 85 } 86 87 status: u32 88 if _, werr := linux.waitpid(pid, &status, {}, nil); werr != .NONE { 89 return fmt.tprintf("waitpid failed: %v", werr) 90 } 91 return report_status(status) 92 } 93 94 run_callee :: proc(fd_path: string, symbol: string, args: []u64, info: ^elf.ELF_Info, patches: []Patch) -> ! { 95 handle := posix.dlopen(strings.clone_to_cstring(fd_path, context.temp_allocator), {.NOW}) 96 if handle == nil { 97 fmt.fprintf(os.stderr, "dlopen: %s\n", posix.dlerror()) 98 linux.exit(101) 99 } 100 101 for p in patches { 102 if perr := apply_patch(handle, info, p); perr != "" { 103 fmt.fprintf(os.stderr, "patch: %s\n", perr) 104 linux.exit(103) 105 } 106 } 107 108 sym := posix.dlsym(handle, strings.clone_to_cstring(symbol, context.temp_allocator)) 109 if sym == nil { 110 fmt.fprintf(os.stderr, "dlsym: %s\n", posix.dlerror()) 111 linux.exit(102) 112 } 113 114 fn := cast(Callee)sym 115 result := fn(args[0], args[1], args[2], args[3], args[4], args[5]) 116 117 fmt.printf("%s returned %d (0x%x)\n", symbol, i64(result), result) 118 os.flush(os.stdout) 119 linux.exit(0) 120 } 121 122 report_status :: proc(status: u32) -> string { 123 if status & 0x7f == 0 { 124 code := (status >> 8) & 0xff 125 switch code { 126 case 0: return "" 127 case 101: return "dlopen failed, see above" 128 case 102: return "symbol not found, see above" 129 case 103: return "patch failed, see above" 130 } 131 return fmt.tprintf("callee exited with status %d", code) 132 } 133 return fmt.tprintf("callee died on signal %d", status & 0x7f) 134 } 135 136 apply_patch :: proc(handle: posix.Symbol_Table, info: ^elf.ELF_Info, p: Patch) -> string { 137 switch p.kind { 138 case .Ret: 139 fn := sym_addr(handle, p.target) 140 if fn == nil { 141 return fmt.tprintf("symbol not found: %s", p.target) 142 } 143 code := [11]u8{0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0xC3} 144 if werr := fits(info, p.target, len(code)); werr != "" { 145 return werr 146 } 147 endian.unchecked_put_u64le(code[2:10], p.value) 148 return write_code(fn, code[:]) 149 150 case .Jmp: 151 fn := sym_addr(handle, p.target) 152 if fn == nil { 153 return fmt.tprintf("symbol not found: %s", p.target) 154 } 155 to := sym_addr(handle, p.repl) 156 if to == nil { 157 return fmt.tprintf("symbol not found: %s", p.repl) 158 } 159 code := [12]u8{0x48, 0xB8, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xE0} 160 if werr := fits(info, p.target, len(code)); werr != "" { 161 return werr 162 } 163 endian.unchecked_put_u64le(code[2:10], u64(uintptr(to))) 164 return write_code(fn, code[:]) 165 166 case .Got: 167 to := sym_addr(handle, p.repl) 168 if to == nil { 169 return fmt.tprintf("symbol not found: %s", p.repl) 170 } 171 slot := got_slot(handle, info, p.target) 172 if slot == nil { 173 return fmt.tprintf("no GOT slot for import: %s", p.target) 174 } 175 if werr := make_writable(slot, 8); werr != "" { 176 return werr 177 } 178 (^rawptr)(slot)^ = to 179 return "" 180 } 181 return "" 182 } 183 184 sym_addr :: proc(handle: posix.Symbol_Table, name: string) -> rawptr { 185 return posix.dlsym(handle, strings.clone_to_cstring(name, context.temp_allocator)) 186 } 187 188 got_slot :: proc(handle: posix.Symbol_Table, info: ^elf.ELF_Info, import_name: string) -> rawptr { 189 lm: ^Link_Map 190 if dlinfo(rawptr(handle), RTLD_DI_LINKMAP, &lm) != 0 || lm == nil { 191 return nil 192 } 193 for r in info.relocs { 194 if r.sym_name == import_name && (r.type == 6 || r.type == 7) { 195 return rawptr(lm.l_addr + uintptr(r.offset)) 196 } 197 } 198 return nil 199 } 200 201 fits :: proc(info: ^elf.ELF_Info, name: string, need: int) -> string { 202 size := u64(0) 203 for s in info.symbols { 204 if s.name == name && s.size != 0 { size = s.size; break } 205 } 206 if size == 0 { 207 for s in info.dyn_symbols { 208 if s.name == name && s.size != 0 { size = s.size; break } 209 } 210 } 211 if size != 0 && size < u64(need) { 212 return fmt.tprintf("%s is %d bytes, too small for a %d-byte patch", name, size, need) 213 } 214 return "" 215 } 216 217 write_code :: proc(dst: rawptr, code: []u8) -> string { 218 if werr := make_writable(dst, len(code)); werr != "" { 219 return werr 220 } 221 mem.copy(dst, raw_data(code), len(code)) 222 return "" 223 } 224 225 make_writable :: proc(ptr: rawptr, length: int) -> string { 226 page := uintptr(4096) 227 start := uintptr(ptr) & ~(page - 1) 228 end := (uintptr(ptr) + uintptr(length) + page - 1) & ~(page - 1) 229 if err := linux.mprotect(rawptr(start), uint(end - start), {.READ, .WRITE, .EXEC}); err != .NONE { 230 return fmt.tprintf("mprotect failed: %v", err) 231 } 232 return "" 233 } 234 235 parse_patches :: proc(raw: string) -> ([]Patch, string) { 236 if raw == "" { 237 return nil, "" 238 } 239 240 specs := strings.split(raw, ";", context.temp_allocator) 241 patches := make([]Patch, len(specs)) 242 n := 0 243 244 for spec in specs { 245 s := strings.trim_space(spec) 246 if s == "" { 247 continue 248 } 249 250 eq := strings.index_byte(s, '=') 251 if eq < 0 { 252 return patches[:n], fmt.tprintf("patch spec missing '=': %s", s) 253 } 254 lhs := strings.trim_space(s[:eq]) 255 rhs := strings.trim_space(s[eq + 1:]) 256 257 p: Patch 258 switch { 259 case strings.has_prefix(lhs, "got:"): 260 p.kind = .Got 261 p.target = strings.trim_space(lhs[4:]) 262 p.repl = rhs 263 if p.target == "" || p.repl == "" { 264 return patches[:n], fmt.tprintf("got: patch needs got:import=target: %s", s) 265 } 266 case strings.has_prefix(rhs, "ret:"): 267 p.kind = .Ret 268 p.target = lhs 269 v, ok := strconv.parse_i64_maybe_prefixed(strings.trim_space(rhs[4:])) 270 if !ok { 271 return patches[:n], fmt.tprintf("ret: patch needs an integer: %s", s) 272 } 273 p.value = u64(v) 274 case strings.has_prefix(rhs, "jmp:"): 275 p.kind = .Jmp 276 p.target = lhs 277 p.repl = strings.trim_space(rhs[4:]) 278 if p.repl == "" { 279 return patches[:n], fmt.tprintf("jmp: patch needs a target symbol: %s", s) 280 } 281 case: 282 return patches[:n], fmt.tprintf("unknown patch spec: %s", s) 283 } 284 285 if p.target == "" { 286 return patches[:n], fmt.tprintf("patch spec missing symbol: %s", s) 287 } 288 289 patches[n] = p 290 n += 1 291 } 292 293 return patches[:n], "" 294 } 295 296 parse_call_args :: proc(raw: string) -> ([]u64, string) { 297 args := make([]u64, 6) 298 if raw == "" { 299 return args, "" 300 } 301 302 fields := strings.split(raw, ",", context.temp_allocator) 303 if len(fields) > 6 { 304 return args, "at most 6 arguments are supported" 305 } 306 307 for field, i in fields { 308 f := strings.trim_space(field) 309 if strings.has_prefix(f, "s:") { 310 args[i] = u64(uintptr(rawptr(strings.clone_to_cstring(f[2:])))) 311 continue 312 } 313 v, ok := strconv.parse_i64_maybe_prefixed(f) 314 if !ok { 315 return args, fmt.tprintf("argument %d is not an integer or s:string: %s", i, f) 316 } 317 args[i] = u64(v) 318 } 319 return args, "" 320 }