draugr.git


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 }