diff --git a/README.md b/README.md
@@ -1,10 +1,6 @@
# draugr
-ELF mapping and analysis tool. Parses ELF32/ELF64, little and big endian, and reports
-sections, segments, symbols, entropy and memory layout.
-
-Written in Odin with no external dependencies. Odin ships no ELF package, so the parser
-is hand-rolled; byte reads go through `core:encoding/endian`.
+ELF mapping and analysis tool. Written in Odin with no third-party dependencies.
## Build
@@ -28,14 +24,22 @@ draugr <elf-path> [flags]
| `--map` | Visual memory layout |
| `--entropy` | Shannon entropy, 256-byte blocks, bits per byte |
| `--hexdump` | Hex dump the whole file |
-| `--disasm` | List executable sections with a byte preview |
-| `--remap <file>` | Remap against a prototype file (stub) |
-| `--json` | JSON output (applies to `--info`) |
+| `--checksec` | Hardening report: RELRO, NX, canary, PIE, RPATH, FORTIFY |
+| `--relocs` | Relocation table and PLT/GOT map |
+| `--code` | Code regions: executable sections with entropy, entry point, function starts |
+| `--call-function <sym>` | Load the file from memory and call one symbol |
+| `--call-args <list>` | Arguments for `--call-function` |
+| `--bin-diff <file>` | Diff this binary's layout against a reference ELF |
+| `--json` | Full structured dump: header, segments, sections, dynamic, symbols, relocations, GOT, checksec |
| `--verbose` | Verbose output |
-`--hexdump`, `--entropy` and `--disasm` each have an `--<name>-out <file>` variant that
+`--hexdump`, `--entropy` and `--code` each have an `--<name>-out <file>` variant that
writes to a file instead of stdout. Flags combine, so `--sections --symbols` prints both.
+`--json` covers the whole structured dump on its own and ignores the selector flags. It is
+rejected alongside `--hexdump`, `--entropy`, `--code`, `--map`, `--bin-diff` and
+`--call-function`, which are visualizations or actions with no JSON form.
+
Both `--flag value` and `--flag=value` work, and a single dash is accepted.
### Output streams
@@ -44,12 +48,58 @@ Data goes to **stdout**. Headers, separators and the `arch=` summary go to **std
So the tool stays readable on a terminal and pipeable everywhere else.
```sh
-draugr --info --json /bin/ls | jq .entry
+draugr --json /bin/ls | jq .needed
+draugr --json /bin/ls | jq .checksec
+draugr --json /bin/ls | jq .got.fclose # GOT slot address, pwntools style
+draugr --json /bin/ls | jq '.dynamic_entries[] | select(.tag == "RUNPATH")'
draugr --sections /bin/ls | awk '$3 == "PROGBITS"'
draugr --symbols /bin/ls | grep ' U ' # undefined imports
+draugr --bin-diff v1.4.2/httpd v1.4.3/httpd # what changed between firmware builds
```
-Exit code is 0 on success, 1 on a read, parse or output-write failure. A failing
-`--<name>-out` does not prevent the other requested analyses from running.
+### Hardening
+
+`--checksec` reports RELRO, stack canary, NX, PIE, RPATH, RUNPATH and FORTIFY. It also
+flags `DT_TEXTREL` and any RWX `PT_LOAD` segment.
+
+Three places where it deliberately disagrees with `checksec(1)`:
+
+- **NX with no `PT_GNU_STACK`** reports `Unknown`, not `Enabled`.
+- **PIE** separates `PIE`, `Static PIE`, `Shared library` and `ET_EXEC`.
+- **FORTIFY** counts distinct functions. `checksec(1)` counts a function twice when it is
+ imported both fortified and unfortified.
+
+### Relocations
+
+`--relocs` parses `RELA`, `REL` and `RELR`, from section headers when present and from the
+dynamic tags otherwise.
+
+### Dynamic linking
+
+`--info` reports `SONAME`, `RPATH`, `RUNPATH` and the `DT_NEEDED` list. `--info --verbose`
+adds the full `.dynamic` table. Unrecognised `DT_` tags print as hex rather than `UNKNOWN`.
+
+### Calling a function
+
+`--call-function` loads the file through a `memfd` and calls a single symbol, without the
+linked object ever reaching disk. A `.o` is linked first with `ld -shared` writing straight
+into the memfd; a `.so` is written to the memfd as-is.
+
+```sh
+draugr --call-function alpha --call-args 21 ./thing.o
+draugr --call-function parse --call-args s:AAAA,64 ./libvendor.so
+```
+
+Arguments are comma-separated: integers (decimal or `0x`), or `s:text` to pass a pointer to
+a NULL-terminated string. Up to six, matching the integer argument registers. Floats and
+structs are not supported, and the return value is read as a 64-bit integer.
+
+`.o` support needs `ld` on `$PATH`, and a cross-linker for a foreign architecture.
+
+### Function starts
+
+`--code` recovers function entry addresses from the `.eh_frame_hdr` binary search table.
+
+draugr does not disassemble. Pipe to `objdump -d` for that.
diff --git a/analysis/analysis_test.odin b/analysis/analysis_test.odin
@@ -433,33 +433,25 @@ test_entropy_output_reports_open_failure :: proc(t: ^testing.T) {
testing.expect(t, err != "", "unwritable out_path must surface an error")
}
-DISASM_HEAD :: "Disassembly Analysis\n" +
- "====================\n\n" +
- "Executable Sections:\n" +
- "Name" + SP17 + "Offset" + SP7 + "Size" + SP9 + "VAddr\n" +
- DASH49 + "\n"
-
-DISASM_FOOT :: "\nNote: Full disassembly requires external disassembler\n" +
- "Consider integrating zydis or capstone for x86/x64\n"
-
@(test)
-test_disasm_output_no_sections :: proc(t: ^testing.T) {
- path := tmp_path("disasm_empty")
+test_code_output_no_sections :: proc(t: ^testing.T) {
+ path := tmp_path("code_none")
defer drop(path)
info := new_info()
defer elf.destroy(&info)
- testing.expect_value(t, disasm_output([]u8{}, &info, path), "")
+ testing.expect_value(t, code_output([]u8{}, &info, path), "")
got := read_all(t, path)
defer delete(got)
- testing.expect_value(t, got, DISASM_HEAD + DISASM_FOOT)
+ testing.expect(t, strings.contains(got, "no section headers"), "must fall back to segments")
+ testing.expect(t, strings.contains(got, "no PT_GNU_EH_FRAME"), "must report missing eh_frame_hdr")
}
@(test)
-test_disasm_output_lists_only_executable_sections :: proc(t: ^testing.T) {
- path := tmp_path("disasm_exec")
+test_code_output_lists_only_executable_sections :: proc(t: ^testing.T) {
+ path := tmp_path("code_exec")
defer drop(path)
info := new_info()
@@ -468,38 +460,26 @@ test_disasm_output_lists_only_executable_sections :: proc(t: ^testing.T) {
append(&info.section_hdrs, elf.Section_Header{type = elf.SHT_NULL})
append(&info.section_hdrs, elf.Section_Header{
- name = 1,
- type = elf.SHT_PROGBITS,
- flags = elf.SHF_EXECINSTR,
- addr = 0x401000,
- offset = 0,
- size = 4,
+ name = 1, type = elf.SHT_PROGBITS, flags = elf.SHF_EXECINSTR,
+ addr = 0x401000, offset = 0, size = 4,
})
append(&info.section_hdrs, elf.Section_Header{
- name = 7,
- type = elf.SHT_PROGBITS,
- flags = 0,
- addr = 0x402000,
- offset = 4,
- size = 4,
+ name = 7, type = elf.SHT_PROGBITS, flags = 0,
+ addr = 0x402000, offset = 4, size = 4,
})
data := []u8{0x90, 0x90, 0xc3, 0x00, 0xde, 0xad, 0xbe, 0xef}
- testing.expect_value(t, disasm_output(data, &info, path), "")
+ testing.expect_value(t, code_output(data, &info, path), "")
got := read_all(t, path)
defer delete(got)
- testing.expect_value(t, got,
- DISASM_HEAD +
- ".text" + SP16 + "0x00000000 0x00000004 0x0000000000401000\n" +
- " First 64 bytes (hex): 90 90 c3 00 \n" +
- DISASM_FOOT)
+ testing.expect(t, strings.contains(got, ".text"), ".text must be listed")
testing.expect(t, !strings.contains(got, ".data"), "non-executable sections must be skipped")
}
@(test)
-test_disasm_output_caps_hex_preview_at_64_bytes :: proc(t: ^testing.T) {
- path := tmp_path("disasm_cap")
+test_code_output_skips_nobits_and_out_of_file_sections :: proc(t: ^testing.T) {
+ path := tmp_path("code_nobits")
defer drop(path)
info := new_info()
@@ -507,90 +487,59 @@ test_disasm_output_caps_hex_preview_at_64_bytes :: proc(t: ^testing.T) {
info.shstrtab = SHSTRTAB[:]
append(&info.section_hdrs, elf.Section_Header{
- name = 1,
- type = elf.SHT_PROGBITS,
- flags = elf.SHF_EXECINSTR,
- size = 200,
+ name = 1, type = elf.SHT_NOBITS, flags = elf.SHF_EXECINSTR, size = 100,
+ })
+ append(&info.section_hdrs, elf.Section_Header{
+ name = 7, type = elf.SHT_PROGBITS, flags = elf.SHF_EXECINSTR,
+ offset = 1000, size = 4,
})
- data := make([]u8, 200)
- defer delete(data)
-
- testing.expect_value(t, disasm_output(data, &info, path), "")
+ data := []u8{1, 2, 3, 4}
+ testing.expect_value(t, code_output(data, &info, path), "")
got := read_all(t, path)
defer delete(got)
-
- PREFIX :: " First 64 bytes (hex): "
- start := strings.index(got, PREFIX)
- testing.expect(t, start >= 0, "hex preview line must be present")
- if start < 0 {
- return
- }
-
- rest := got[start + len(PREFIX):]
- end := strings.index_byte(rest, '\n')
- testing.expect(t, end >= 0, "hex preview line must be terminated")
- if end < 0 {
- return
- }
-
- fields := strings.fields(rest[:end], context.allocator)
- defer delete(fields)
- testing.expect_value(t, len(fields), 64)
+ testing.expect(t, !strings.contains(got, ".text"), "SHT_NOBITS must be skipped")
+ testing.expect(t, strings.contains(got, ".data"), "out-of-file section is still listed")
+ testing.expect(t, strings.contains(got, "0.00"), "unreadable section entropy is zero")
}
@(test)
-test_disasm_output_clamps_section_running_past_eof :: proc(t: ^testing.T) {
- path := tmp_path("disasm_clamp")
+test_code_output_flags_entry_outside_pt_load :: proc(t: ^testing.T) {
+ path := tmp_path("code_entry_out")
defer drop(path)
info := new_info()
defer elf.destroy(&info)
- info.shstrtab = SHSTRTAB[:]
-
- append(&info.section_hdrs, elf.Section_Header{
- name = 1,
- type = elf.SHT_PROGBITS,
- flags = elf.SHF_EXECINSTR,
- offset = 4,
- size = 100,
+ info.header.entry = 0xdeadbeef
+ append(&info.program_hdrs, elf.Program_Header{
+ type = elf.PT_LOAD, vaddr = 0x400000, memsz = 0x1000, flags = elf.PF_R | elf.PF_X,
})
- data := []u8{0, 0, 0, 0, 0xaa, 0xbb}
- testing.expect_value(t, disasm_output(data, &info, path), "")
+ testing.expect_value(t, code_output([]u8{}, &info, path), "")
got := read_all(t, path)
defer delete(got)
- testing.expect(t, strings.contains(got, " First 64 bytes (hex): aa bb \n"),
- "preview must stop at end of file")
+ testing.expect(t, strings.contains(got, "outside every PT_LOAD"), "must flag unmapped entry")
}
@(test)
-test_disasm_output_skips_preview_for_section_beyond_eof :: proc(t: ^testing.T) {
- path := tmp_path("disasm_beyond")
+test_code_output_flags_entry_in_non_executable_segment :: proc(t: ^testing.T) {
+ path := tmp_path("code_entry_nx")
defer drop(path)
info := new_info()
defer elf.destroy(&info)
- info.shstrtab = SHSTRTAB[:]
-
- append(&info.section_hdrs, elf.Section_Header{
- name = 1,
- type = elf.SHT_PROGBITS,
- flags = elf.SHF_EXECINSTR,
- offset = 1000,
- size = 4,
+ info.header.entry = 0x400100
+ append(&info.program_hdrs, elf.Program_Header{
+ type = elf.PT_LOAD, vaddr = 0x400000, memsz = 0x1000, flags = elf.PF_R | elf.PF_W,
})
- data := []u8{1, 2, 3, 4}
- testing.expect_value(t, disasm_output(data, &info, path), "")
+ testing.expect_value(t, code_output([]u8{}, &info, path), "")
got := read_all(t, path)
defer delete(got)
- testing.expect(t, strings.contains(got, ".text"), "section row must still be listed")
- testing.expect(t, !strings.contains(got, "First 64 bytes"),
- "out-of-file section must not print a preview")
+ testing.expect(t, strings.contains(got, "non-executable segment"), "must flag NX entry")
}
MAP_HEAD :: "ELF Memory Map\n" +
@@ -754,16 +703,20 @@ test_map_output_skips_bar_for_pt_null :: proc(t: ^testing.T) {
testing.expect(t, !strings.contains(got, " [|"), "PT_NULL must not draw a bar")
}
-// BUG: print_bar computes offset*50/u64(total) with total = len(data).
-// Empty data divides by zero and kills the process with SIGILL, so this test
-// cannot clean up after itself. It writes to /dev/null to leave nothing behind.
@(test)
-test_map_output_empty_data_with_segment_divides_by_zero :: proc(t: ^testing.T) {
+test_map_output_empty_data_draws_no_bar :: proc(t: ^testing.T) {
+ path := tmp_path("map_empty_bar")
+ defer drop(path)
+
info := new_info()
defer elf.destroy(&info)
append(&info.program_hdrs, elf.Program_Header{type = elf.PT_LOAD, filesz = 1, memsz = 1})
- testing.expect_value(t, map_output([]u8{}, &info, "/dev/null"), "")
+ testing.expect_value(t, map_output([]u8{}, &info, path), "")
+
+ got := read_all(t, path)
+ defer delete(got)
+ testing.expect(t, !strings.contains(got, " [|"), "empty data must not draw a bar")
}
@(test)
@@ -805,26 +758,76 @@ test_map_output_reports_open_failure :: proc(t: ^testing.T) {
}
@(test)
-test_remap_output_stub_reaches_out_file :: proc(t: ^testing.T) {
- path := tmp_path("remap_stub")
+test_bin_diff_missing_reference :: proc(t: ^testing.T) {
+ info := new_info()
+ defer elf.destroy(&info)
+
+ err := bin_diff_output([]u8{}, &info, "/draugr_no_such_file_4c1f", "")
+ testing.expect(t, strings.contains(err, "failed to read"), "missing reference must surface an error")
+}
+
+@(test)
+test_bin_diff_non_elf_reference :: proc(t: ^testing.T) {
+ path := tmp_path("bindiff_notelf")
defer drop(path)
+ _ = os.write_entire_file(path, []u8{'n', 'o', 'p', 'e'})
info := new_info()
defer elf.destroy(&info)
- testing.expect_value(t, remap_output([]u8{}, &info, "/proto/path", path), "")
+ err := bin_diff_output([]u8{}, &info, path, "")
+ testing.expect(t, strings.contains(err, "not an ELF"), "non-ELF reference must surface an error")
+}
- got := read_all(t, path)
+@(test)
+test_bin_diff_reports_section_and_dependency_changes :: proc(t: ^testing.T) {
+ ref_path := tmp_path("bindiff_ref")
+ defer drop(ref_path)
+ out_path := tmp_path("bindiff_out")
+ defer drop(out_path)
+
+ ref := elf.build_dynamic_elf(true, true, elf.DEFAULT_DYN_ENTRIES[:])
+ defer delete(ref)
+ _ = os.write_entire_file(ref_path, ref)
+
+ entries := [][2]u64{
+ {elf.DT_NEEDED, elf.DYN_OFF_NEEDED_1},
+ {elf.DT_STRTAB, elf.DYN_BASE + 0x200},
+ {elf.DT_STRSZ, u64(len(elf.DYN_STR))},
+ {elf.DT_NULL, 0},
+ }
+ mine := elf.build_dynamic_elf(true, true, entries[:])
+ defer delete(mine)
+
+ info, _ := elf.load(mine)
+ defer elf.destroy(&info)
+
+ testing.expect_value(t, bin_diff_output(mine, &info, ref_path, out_path), "")
+
+ got := read_all(t, out_path)
defer delete(got)
- testing.expect_value(t, got, "ELF Remapping (stub - prototype: /proto/path)\n")
+ testing.expect(t, strings.contains(got, "+ libc.so.6"), "reference-only dependency must show as added")
+ testing.expect(t, strings.contains(got, "/opt/runpath"), "runpath change must be reported")
}
@(test)
-test_remap_output_reports_open_failure :: proc(t: ^testing.T) {
- info := new_info()
+test_bin_diff_identical_files_report_no_changes :: proc(t: ^testing.T) {
+ ref_path := tmp_path("bindiff_same_ref")
+ defer drop(ref_path)
+ out_path := tmp_path("bindiff_same_out")
+ defer drop(out_path)
+
+ img := elf.build_dynamic_elf(true, true, elf.DEFAULT_DYN_ENTRIES[:])
+ defer delete(img)
+ _ = os.write_entire_file(ref_path, img)
+
+ info, _ := elf.load(img)
defer elf.destroy(&info)
- err := remap_output([]u8{}, &info, "/proto/path", "/draugr_no_such_dir_4c1f/remap.txt")
+ testing.expect_value(t, bin_diff_output(img, &info, ref_path, out_path), "")
- testing.expect(t, err != "", "unwritable out_path must surface an error")
+ got := read_all(t, out_path)
+ defer delete(got)
+ testing.expect(t, strings.contains(got, "unchanged"), "identical dependencies must report unchanged")
+ testing.expect(t, !strings.contains(got, "->"), "identical files must show no deltas")
}
diff --git a/analysis/bin_diff.odin b/analysis/bin_diff.odin
@@ -0,0 +1,200 @@
+package analysis
+
+import "core:fmt"
+import "core:os"
+import "core:slice"
+import "core:strings"
+
+import "../elf"
+
+Section_Delta :: struct {
+ name : string,
+ a : u64,
+ b : u64,
+ delta : i64,
+}
+
+bin_diff_output :: proc(data: []u8, info: ^elf.ELF_Info, ref_path: string, out_path: string) -> string {
+ ref_data, read_err := os.read_entire_file_from_path(ref_path, context.allocator)
+ if read_err != nil {
+ return fmt.tprintf("failed to read reference file: %s", ref_path)
+ }
+ defer delete(ref_data)
+
+ if !elf.probe(ref_data) {
+ return fmt.tprintf("reference is not an ELF file: %s", ref_path)
+ }
+
+ ref, parse_err := elf.load(ref_data)
+ if parse_err != "" {
+ return fmt.tprintf("reference parse error: %s", parse_err)
+ }
+ defer elf.destroy(&ref)
+
+ f, err := open_out(out_path)
+ if err != "" {
+ return err
+ }
+ defer close_out(f, out_path)
+
+ fmt.fprintf(f, "Binary Diff\n")
+ fmt.fprintf(f, "===========\n\n")
+ fmt.fprintf(f, "reference: %s\n\n", ref_path)
+
+ diff_header(f, info, &ref, len(data), len(ref_data))
+ diff_segments(f, info, &ref)
+ diff_sections(f, info, &ref)
+ diff_needed(f, info, &ref)
+
+ return ""
+}
+
+diff_header :: proc(f: ^os.File, a: ^elf.ELF_Info, b: ^elf.ELF_Info, a_size: int, b_size: int) {
+ fmt.fprintf(f, "Header:\n")
+ row_str(f, "class", "ELF64" if a.is_64bit else "ELF32", "ELF64" if b.is_64bit else "ELF32")
+ row_str(f, "machine", elf.machine_to_str(a.header.machine), elf.machine_to_str(b.header.machine))
+ row_str(f, "type", elf.type_to_str(a.header.type), elf.type_to_str(b.header.type))
+ row_hex(f, "entry", a.header.entry, b.header.entry)
+ row_num(f, "file size", i64(a_size), i64(b_size))
+ fmt.fprintf(f, "\n")
+}
+
+diff_segments :: proc(f: ^os.File, a: ^elf.ELF_Info, b: ^elf.ELF_Info) {
+ fmt.fprintf(f, "Segments:\n")
+ row_num(f, "count", i64(len(a.program_hdrs)), i64(len(b.program_hdrs)))
+
+ n := min(len(a.program_hdrs), len(b.program_hdrs))
+ for i := 0; i < n; i += 1 {
+ pa, pb := a.program_hdrs[i], b.program_hdrs[i]
+ if pa.type == pb.type && pa.vaddr == pb.vaddr && pa.filesz == pb.filesz &&
+ pa.memsz == pb.memsz && pa.flags == pb.flags {
+ continue
+ }
+ label := elf.segment_type_str(pa.type)
+ if pa.type != pb.type {
+ label = fmt.tprintf("%s -> %s", label, elf.segment_type_str(pb.type))
+ }
+ fmt.fprintf(f, " ~ [%d] %s%s%s%s\n", i, label,
+ delta_hex("vaddr", pa.vaddr, pb.vaddr),
+ delta_hex("filesz", pa.filesz, pb.filesz),
+ delta_hex("memsz", pa.memsz, pb.memsz))
+ }
+ fmt.fprintf(f, "\n")
+}
+
+diff_sections :: proc(f: ^os.File, a: ^elf.ELF_Info, b: ^elf.ELF_Info) {
+ a_sizes := section_sizes(a)
+ defer delete(a_sizes)
+ b_sizes := section_sizes(b)
+ defer delete(b_sizes)
+
+ fmt.fprintf(f, "Sections:\n")
+
+ changed := make([dynamic]Section_Delta)
+ defer delete(changed)
+
+ for name, a_size in a_sizes {
+ b_size, in_b := b_sizes[name]
+ if !in_b {
+ fmt.fprintf(f, " - %-24s 0x%x (removed)\n", name, a_size)
+ continue
+ }
+ if a_size != b_size {
+ append(&changed, Section_Delta{name, a_size, b_size, i64(b_size) - i64(a_size)})
+ }
+ }
+ for name, b_size in b_sizes {
+ if _, in_a := a_sizes[name]; !in_a {
+ fmt.fprintf(f, " + %-24s 0x%x (added)\n", name, b_size)
+ }
+ }
+
+ slice.sort_by(changed[:], proc(x, y: Section_Delta) -> bool {
+ return abs_i64(x.delta) > abs_i64(y.delta)
+ })
+ for c in changed {
+ fmt.fprintf(f, " ~ %-24s 0x%x -> 0x%x (%+d)\n", c.name, c.a, c.b, c.delta)
+ }
+
+ if len(changed) == 0 && len(a_sizes) == len(b_sizes) {
+ fmt.fprintf(f, " no size changes\n")
+ }
+ fmt.fprintf(f, "\n")
+}
+
+section_sizes :: proc(info: ^elf.ELF_Info) -> map[string]u64 {
+ out := make(map[string]u64)
+ for shdr in info.section_hdrs {
+ name := elf.get_section_name(info, shdr)
+ if name == "" || shdr.type == elf.SHT_NULL {
+ continue
+ }
+ out[name] = shdr.size
+ }
+ return out
+}
+
+diff_needed :: proc(f: ^os.File, a: ^elf.ELF_Info, b: ^elf.ELF_Info) {
+ fmt.fprintf(f, "Dependencies:\n")
+ changed := false
+ for lib in a.needed {
+ if !slice.contains(b.needed[:], lib) {
+ fmt.fprintf(f, " - %s\n", lib)
+ changed = true
+ }
+ }
+ for lib in b.needed {
+ if !slice.contains(a.needed[:], lib) {
+ fmt.fprintf(f, " + %s\n", lib)
+ changed = true
+ }
+ }
+ if a.runpath != b.runpath {
+ row_str(f, "runpath", a.runpath, b.runpath)
+ changed = true
+ }
+ if a.rpath != b.rpath {
+ row_str(f, "rpath", a.rpath, b.rpath)
+ changed = true
+ }
+ if !changed {
+ fmt.fprintf(f, " unchanged\n")
+ }
+ fmt.fprintf(f, "\n")
+}
+
+delta_hex :: proc(label: string, a: u64, b: u64) -> string {
+ if a == b {
+ return ""
+ }
+ return fmt.tprintf(" %s 0x%x -> 0x%x", label, a, b)
+}
+
+abs_i64 :: proc(v: i64) -> i64 {
+ return -v if v < 0 else v
+}
+
+row_str :: proc(f: ^os.File, label: string, a: string, b: string) {
+ mark := " " if a == b else "~"
+ if a == b {
+ fmt.fprintf(f, " %s %-12s %s\n", mark, label, a)
+ } else {
+ fmt.fprintf(f, " %s %-12s %s -> %s\n", mark, label, a, b)
+ }
+}
+
+row_hex :: proc(f: ^os.File, label: string, a: u64, b: u64) {
+ if a == b {
+ fmt.fprintf(f, " %-12s 0x%x\n", label, a)
+ } else {
+ fmt.fprintf(f, " ~ %-12s 0x%x -> 0x%x\n", label, a, b)
+ }
+}
+
+row_num :: proc(f: ^os.File, label: string, a: i64, b: i64) {
+ if a == b {
+ fmt.fprintf(f, " %-12s %d\n", label, a)
+ } else {
+ fmt.fprintf(f, " ~ %-12s %d -> %d (%+d)\n", label, a, b, b - a)
+ }
+}
diff --git a/analysis/call.odin b/analysis/call.odin
@@ -0,0 +1,125 @@
+package analysis
+
+import "core:c"
+import "core:fmt"
+import "core:os"
+import "core:strconv"
+import "core:strings"
+import "core:sys/linux"
+import "core:sys/posix"
+
+import "../elf"
+
+foreign import libc "system:c"
+
+@(default_calling_convention="c")
+foreign libc {
+ system :: proc(command: cstring) -> c.int ---
+}
+
+Callee :: #type proc "c" (u64, u64, u64, u64, u64, u64) -> u64
+
+// The callee runs in a forked child so a crash is a report, not a dead draugr.
+call_function :: proc(data: []u8, info: ^elf.ELF_Info, path: string, symbol: string, raw_args: string) -> string {
+ fd, err := linux.memfd_create("draugr-call", {})
+ if err != .NONE {
+ return fmt.tprintf("memfd_create failed: %v", err)
+ }
+ defer linux.close(fd)
+
+ fd_path := fmt.tprintf("/proc/self/fd/%d", int(fd))
+
+ if info.header.type == elf.ET_REL {
+ cmd := fmt.ctprintf("ld -shared -o %s %s", fd_path, path)
+ if system(cmd) != 0 {
+ return fmt.tprintf("failed to link %s into a shared object", path)
+ }
+ } else {
+ if _, werr := linux.write(fd, data); werr != .NONE {
+ return fmt.tprintf("writing to memfd failed: %v", werr)
+ }
+ }
+
+ args, parse_err := parse_call_args(raw_args)
+ defer delete(args)
+ if parse_err != "" {
+ return parse_err
+ }
+
+ fmt.fprintf(os.stderr, "calling %s in a forked child, loaded from memory\n", symbol)
+
+ pid, ferr := linux.fork()
+ if ferr != .NONE {
+ return fmt.tprintf("fork failed: %v", ferr)
+ }
+
+ if pid == 0 {
+ run_callee(fd_path, symbol, args)
+ }
+
+ status: u32
+ if _, werr := linux.waitpid(pid, &status, {}, nil); werr != .NONE {
+ return fmt.tprintf("waitpid failed: %v", werr)
+ }
+ return report_status(status)
+}
+
+run_callee :: proc(fd_path: string, symbol: string, args: []u64) -> ! {
+ handle := posix.dlopen(strings.clone_to_cstring(fd_path, context.temp_allocator), {.NOW})
+ if handle == nil {
+ fmt.fprintf(os.stderr, "dlopen: %s\n", posix.dlerror())
+ linux.exit(101)
+ }
+
+ sym := posix.dlsym(handle, strings.clone_to_cstring(symbol, context.temp_allocator))
+ if sym == nil {
+ fmt.fprintf(os.stderr, "dlsym: %s\n", posix.dlerror())
+ linux.exit(102)
+ }
+
+ fn := cast(Callee)sym
+ result := fn(args[0], args[1], args[2], args[3], args[4], args[5])
+
+ fmt.printf("%s returned %d (0x%x)\n", symbol, i64(result), result)
+ os.flush(os.stdout)
+ linux.exit(0)
+}
+
+report_status :: proc(status: u32) -> string {
+ if status & 0x7f == 0 {
+ code := (status >> 8) & 0xff
+ switch code {
+ case 0: return ""
+ case 101: return "dlopen failed, see above"
+ case 102: return "symbol not found, see above"
+ }
+ return fmt.tprintf("callee exited with status %d", code)
+ }
+ return fmt.tprintf("callee died on signal %d", status & 0x7f)
+}
+
+parse_call_args :: proc(raw: string) -> ([]u64, string) {
+ args := make([]u64, 6)
+ if raw == "" {
+ return args, ""
+ }
+
+ fields := strings.split(raw, ",", context.temp_allocator)
+ if len(fields) > 6 {
+ return args, "at most 6 arguments are supported"
+ }
+
+ for field, i in fields {
+ f := strings.trim_space(field)
+ if strings.has_prefix(f, "s:") {
+ args[i] = u64(uintptr(rawptr(strings.clone_to_cstring(f[2:]))))
+ continue
+ }
+ v, ok := strconv.parse_i64_maybe_prefixed(f)
+ if !ok {
+ return args, fmt.tprintf("argument %d is not an integer or s:string: %s", i, f)
+ }
+ args[i] = u64(v)
+ }
+ return args, ""
+}
diff --git a/analysis/call_test.odin b/analysis/call_test.odin
@@ -0,0 +1,59 @@
+package analysis
+
+import "core:testing"
+
+@(test)
+test_call_args_default_to_zero :: proc(t: ^testing.T) {
+ args, err := parse_call_args("")
+ defer delete(args)
+
+ testing.expect_value(t, err, "")
+ testing.expect_value(t, len(args), 6)
+ for a in args {
+ testing.expect_value(t, a, 0)
+ }
+}
+
+@(test)
+test_call_args_parse_decimal_and_hex :: proc(t: ^testing.T) {
+ args, err := parse_call_args("42, 0x2a, -1")
+ defer delete(args)
+
+ testing.expect_value(t, err, "")
+ testing.expect_value(t, args[0], 42)
+ testing.expect_value(t, args[1], 42)
+ testing.expect_value(t, i64(args[2]), -1)
+ testing.expect_value(t, args[3], 0)
+}
+
+@(test)
+test_call_args_string_becomes_a_pointer :: proc(t: ^testing.T) {
+ args, err := parse_call_args("s:hello")
+ defer delete(args)
+
+ testing.expect_value(t, err, "")
+ testing.expect(t, args[0] != 0, "s: argument must produce a non-null pointer")
+
+ str := cstring(rawptr(uintptr(args[0])))
+ testing.expect_value(t, string(str), "hello")
+}
+
+@(test)
+test_call_args_reject_junk_and_overflow :: proc(t: ^testing.T) {
+ junk, err := parse_call_args("notanumber")
+ defer delete(junk)
+ testing.expect(t, err != "", "non-numeric argument must be rejected")
+
+ too_many, err2 := parse_call_args("1,2,3,4,5,6,7")
+ defer delete(too_many)
+ testing.expect(t, err2 != "", "more than 6 arguments must be rejected")
+}
+
+@(test)
+test_report_status_maps_exit_and_signal :: proc(t: ^testing.T) {
+ testing.expect_value(t, report_status(0), "")
+ testing.expect_value(t, report_status(101 << 8), "dlopen failed, see above")
+ testing.expect_value(t, report_status(102 << 8), "symbol not found, see above")
+ testing.expect(t, report_status(3 << 8) != "", "a non-zero exit must be an error")
+ testing.expect_value(t, report_status(11), "callee died on signal 11")
+}
diff --git a/analysis/code.odin b/analysis/code.odin
@@ -0,0 +1,219 @@
+package analysis
+
+import "core:fmt"
+import "core:os"
+
+import "../elf"
+
+code_output :: proc(data: []u8, info: ^elf.ELF_Info, out_path: string) -> string {
+ f, err := open_out(out_path)
+ if err != "" {
+ return err
+ }
+ defer close_out(f, out_path)
+
+ fmt.fprintf(f, "Code Regions\n")
+ fmt.fprintf(f, "============\n\n")
+
+ print_exec_regions(f, data, info)
+ print_entry_analysis(f, info)
+ print_function_starts(f, data, info)
+
+ return ""
+}
+
+print_exec_regions :: proc(f: ^os.File, data: []u8, info: ^elf.ELF_Info) {
+ fmt.fprintf(f, "Executable Sections:\n")
+ fmt.fprintf(f, "%-20s %-18s %-12s %-12s %s\n", "Name", "VAddr", "Offset", "Size", "Entropy")
+
+ found := false
+ for shdr in info.section_hdrs {
+ if shdr.flags & elf.SHF_EXECINSTR == 0 || shdr.type == elf.SHT_NOBITS {
+ continue
+ }
+ found = true
+ fmt.fprintf(f, "%-20s 0x%016x 0x%-10x 0x%-10x %.2f\n",
+ elf.get_section_name(info, shdr), shdr.addr, shdr.offset, shdr.size,
+ region_entropy(data, shdr.offset, shdr.size))
+ }
+
+ if !found {
+ fmt.fprintf(f, " (no section headers, falling back to executable segments)\n")
+ for ph in info.program_hdrs {
+ if ph.type != elf.PT_LOAD || ph.flags & elf.PF_X == 0 {
+ continue
+ }
+ fmt.fprintf(f, "%-20s 0x%016x 0x%-10x 0x%-10x %.2f\n",
+ "PT_LOAD", ph.vaddr, ph.offset, ph.filesz,
+ region_entropy(data, ph.offset, ph.filesz))
+ }
+ }
+ fmt.fprintf(f, "\n")
+}
+
+region_entropy :: proc(data: []u8, offset: u64, size: u64) -> f64 {
+ if !elf.in_bounds(data, offset, size) || size == 0 {
+ return 0.0
+ }
+ return calculate_entropy(data[offset : offset + size])
+}
+
+print_entry_analysis :: proc(f: ^os.File, info: ^elf.ELF_Info) {
+ entry := info.header.entry
+ fmt.fprintf(f, "Entry Point: 0x%016x\n", entry)
+
+ for shdr in info.section_hdrs {
+ if shdr.addr != 0 && entry >= shdr.addr && entry - shdr.addr < shdr.size {
+ fmt.fprintf(f, " in section: %s\n", elf.get_section_name(info, shdr))
+ break
+ }
+ }
+
+ executable := false
+ mapped := false
+ for ph in info.program_hdrs {
+ if ph.type != elf.PT_LOAD || ph.memsz == 0 {
+ continue
+ }
+ if entry >= ph.vaddr && entry - ph.vaddr < ph.memsz {
+ mapped = true
+ if ph.flags & elf.PF_X != 0 {
+ executable = true
+ }
+ }
+ }
+
+ switch {
+ case !mapped:
+ fmt.fprintf(f, " WARNING: entry point is outside every PT_LOAD\n")
+ case !executable:
+ fmt.fprintf(f, " WARNING: entry point is in a non-executable segment\n")
+ case:
+ fmt.fprintf(f, " in an executable segment\n")
+ }
+ fmt.fprintf(f, "\n")
+}
+
+DW_EH_PE_OMIT :: 0xff
+
+DW_EH_PE_ABSPTR :: 0x00
+DW_EH_PE_UDATA2 :: 0x02
+DW_EH_PE_UDATA4 :: 0x03
+DW_EH_PE_UDATA8 :: 0x04
+DW_EH_PE_SDATA2 :: 0x0a
+DW_EH_PE_SDATA4 :: 0x0b
+DW_EH_PE_SDATA8 :: 0x0c
+
+DW_EH_PE_PCREL :: 0x10
+DW_EH_PE_DATAREL :: 0x30
+
+// .eh_frame_hdr holds a sorted table of function start addresses, and it
+// survives stripping.
+print_function_starts :: proc(f: ^os.File, data: []u8, info: ^elf.ELF_Info) {
+ hdr_vaddr, hdr_off, hdr_size, ok := find_eh_frame_hdr(info)
+ if !ok || !elf.in_bounds(data, hdr_off, hdr_size) {
+ fmt.fprintf(f, "Function Starts: no PT_GNU_EH_FRAME\n\n")
+ return
+ }
+
+ starts, err := eh_frame_starts(data, info, hdr_vaddr, hdr_off, hdr_size)
+ defer delete(starts)
+
+ if err != "" {
+ fmt.fprintf(f, "Function Starts: %s\n\n", err)
+ return
+ }
+
+ fmt.fprintf(f, "Function Starts: %d (from .eh_frame_hdr)\n", len(starts))
+ shown := min(len(starts), 10)
+ for i := 0; i < shown; i += 1 {
+ fmt.fprintf(f, " 0x%016x\n", starts[i])
+ }
+ if len(starts) > shown {
+ fmt.fprintf(f, " ... %d more\n", len(starts) - shown)
+ }
+ fmt.fprintf(f, "\n")
+}
+
+find_eh_frame_hdr :: proc(info: ^elf.ELF_Info) -> (vaddr: u64, offset: u64, size: u64, ok: bool) {
+ for ph in info.program_hdrs {
+ if ph.type == elf.PT_GNU_EH_FRAME && ph.filesz > 0 {
+ return ph.vaddr, ph.offset, ph.filesz, true
+ }
+ }
+ return 0, 0, 0, false
+}
+
+eh_frame_starts :: proc(data: []u8, info: ^elf.ELF_Info, hdr_vaddr, hdr_off, hdr_size: u64) -> ([dynamic]u64, string) {
+ starts := make([dynamic]u64)
+ order := elf.byte_order(info.header)
+
+ buf := data[hdr_off : hdr_off + hdr_size]
+ if len(buf) < 4 {
+ return starts, "header too small"
+ }
+ if buf[0] != 1 {
+ return starts, fmt.tprintf("unsupported version %d", buf[0])
+ }
+
+ frame_enc := buf[1]
+ count_enc := buf[2]
+ table_enc := buf[3]
+ pos := 4
+
+ if count_enc == DW_EH_PE_OMIT || table_enc == DW_EH_PE_OMIT {
+ return starts, "no binary search table"
+ }
+ if _, e := read_encoded(buf, &pos, frame_enc, hdr_vaddr, hdr_vaddr, order, info.is_64bit); e != "" {
+ return starts, e
+ }
+ count, cerr := read_encoded(buf, &pos, count_enc, hdr_vaddr, hdr_vaddr, order, info.is_64bit)
+ if cerr != "" {
+ return starts, cerr
+ }
+
+ for i := u64(0); i < count; i += 1 {
+ loc, lerr := read_encoded(buf, &pos, table_enc, hdr_vaddr, hdr_vaddr, order, info.is_64bit)
+ if lerr != "" {
+ return starts, lerr
+ }
+ if _, aerr := read_encoded(buf, &pos, table_enc, hdr_vaddr, hdr_vaddr, order, info.is_64bit); aerr != "" {
+ return starts, aerr
+ }
+ append(&starts, loc)
+ }
+ return starts, ""
+}
+
+read_encoded :: proc(buf: []u8, pos: ^int, enc: u8, pc_vaddr, data_vaddr: u64, order: elf.Byte_Order, is_64bit: bool) -> (u64, string) {
+ base := u64(0)
+ switch enc & 0x70 {
+ case 0: base = 0
+ case DW_EH_PE_PCREL: base = pc_vaddr + u64(pos^)
+ case DW_EH_PE_DATAREL: base = data_vaddr
+ case: return 0, fmt.tprintf("unsupported pointer application 0x%x", enc & 0x70)
+ }
+
+ format := enc & 0x0f
+ if format == DW_EH_PE_ABSPTR {
+ format = DW_EH_PE_UDATA8 if is_64bit else DW_EH_PE_UDATA4
+ }
+
+ value := u64(0)
+ size := 0
+ switch format {
+ case DW_EH_PE_UDATA2: value = u64(elf.read_u16(buf, pos^, order)); size = 2
+ case DW_EH_PE_UDATA4: value = u64(elf.read_u32(buf, pos^, order)); size = 4
+ case DW_EH_PE_UDATA8: value = elf.read_u64(buf, pos^, order); size = 8
+ case DW_EH_PE_SDATA2: value = u64(i64(i16(elf.read_u16(buf, pos^, order)))); size = 2
+ case DW_EH_PE_SDATA4: value = u64(i64(i32(elf.read_u32(buf, pos^, order)))); size = 4
+ case DW_EH_PE_SDATA8: value = elf.read_u64(buf, pos^, order); size = 8
+ case: return 0, fmt.tprintf("unsupported pointer format 0x%x", format)
+ }
+
+ if pos^ + size > len(buf) {
+ return 0, "truncated .eh_frame_hdr"
+ }
+ pos^ += size
+ return base + value, ""
+}
diff --git a/analysis/disasm.odin b/analysis/disasm.odin
@@ -1,57 +0,0 @@
-package analysis
-
-import "core:fmt"
-import "core:os"
-
-import "../elf"
-
-disasm_output :: proc(data: []u8, info: ^elf.ELF_Info, out_path: string) -> string {
- f, err := open_out(out_path)
- if err != "" {
- return err
- }
- defer close_out(f, out_path)
-
- fmt.fprintf(f, "Disassembly Analysis\n")
- fmt.fprintf(f, "====================\n\n")
-
- fmt.fprintf(f, "Executable Sections:\n")
- fmt.fprintf(f, "%-20s %-12s %-12s %s\n", "Name", "Offset", "Size", "VAddr")
- fmt.fprintf(f, "%s\n", "-------------------------------------------------")
-
- for i := 0; i < len(info.section_hdrs); i += 1 {
- shdr := info.section_hdrs[i]
- if shdr.type == elf.SHT_NULL {
- continue
- }
-
- name := elf.get_section_name(info, shdr)
-
- if shdr.flags & elf.SHF_EXECINSTR != 0 {
- fmt.fprintf(f, "%-20s 0x%08x 0x%08x 0x%016x\n",
- name, shdr.offset, shdr.size, shdr.addr)
-
- if int(shdr.offset) < len(data) {
- end := int(shdr.offset + shdr.size)
- if end > len(data) {
- end = len(data)
- }
- section_data := data[int(shdr.offset):end]
-
- fmt.fprintf(f, " First 64 bytes (hex): ")
- for j := 0; j < len(section_data); j += 1 {
- if j >= 64 {
- break
- }
- fmt.fprintf(f, "%02x ", section_data[j])
- }
- fmt.fprintf(f, "\n")
- }
- }
- }
-
- fmt.fprintf(f, "\nNote: Full disassembly requires external disassembler\n")
- fmt.fprintf(f, "Consider integrating zydis or capstone for x86/x64\n")
-
- return ""
-}
diff --git a/analysis/hexdump.odin b/analysis/hexdump.odin
@@ -17,7 +17,6 @@ default_config :: proc() -> HexdumpConfig {
}
}
-// Returns empty string on success, error message on failure
hexdump :: proc(data: []u8, offset: u64, out_path: string) -> string {
cfg := default_config()
diff --git a/analysis/mapper.odin b/analysis/mapper.odin
@@ -74,17 +74,6 @@ map_output :: proc(data: []u8, info: ^elf.ELF_Info, out_path: string) -> string
return ""
}
-remap_output :: proc(data: []u8, info: ^elf.ELF_Info, proto_path: string, out_path: string) -> string {
- f, err := open_out(out_path)
- if err != "" {
- return err
- }
- defer close_out(f, out_path)
-
- fmt.fprintf(f, "ELF Remapping (stub - prototype: %s)\n", proto_path)
- return ""
-}
-
print_bar :: proc(f: ^os.File, offset: u64, size: u64, total: int) {
if total <= 0 {
return
diff --git a/elf/checksec.odin b/elf/checksec.odin
@@ -0,0 +1,216 @@
+package elf
+
+import "core:strings"
+
+Relro :: enum { None, Partial, Full }
+Nx :: enum { Unknown, Enabled, Disabled }
+Pie :: enum { None, Pie, Static_Pie, Shared_Library, Rel }
+
+Checksec :: struct {
+ relro : Relro,
+ nx : Nx,
+ pie : Pie,
+ canary : bool,
+ textrel : bool,
+ rpath : string,
+ runpath : string,
+ rwx_segment : bool,
+ fortified : int,
+ fortifiable : int,
+ partial : int,
+ stripped : bool,
+}
+
+// Derived from glibc's exported __*_chk symbols.
+FORTIFIABLE := [?]string{
+ "asprintf", "confstr", "dprintf", "explicit_bzero", "fdelt", "fgets", "fgets_unlocked",
+ "fgetws", "fgetws_unlocked", "fprintf", "fread", "fread_unlocked", "fwprintf", "getcwd",
+ "getdomainname", "getgroups", "gethostname", "getlogin_r", "gets", "getwd", "inet_ntop",
+ "inet_pton", "longjmp", "mbsnrtowcs", "mbsrtowcs", "mbstowcs", "memcpy", "memmove",
+ "mempcpy", "memset", "memset_explicit", "obstack_printf", "obstack_vprintf", "poll",
+ "ppoll", "pread", "pread64", "printf", "ptsname_r", "read", "readlink", "readlinkat",
+ "realpath", "recv", "recvfrom", "snprintf", "sprintf", "stpcpy", "stpncpy", "strcat",
+ "strcpy", "strlcat", "strlcpy", "strncat", "strncpy", "swprintf", "syslog", "ttyname_r",
+ "vasprintf", "vdprintf", "vfprintf", "vfwprintf", "vprintf", "vsnprintf", "vsprintf",
+ "vswprintf", "vsyslog", "vwprintf", "wcpcpy", "wcpncpy", "wcrtomb", "wcscat", "wcscpy",
+ "wcslcat", "wcslcpy", "wcsncat", "wcsncpy", "wcsnrtombs", "wcsrtombs", "wcstombs", "wctomb",
+ "wmemcpy", "wmemmove", "wmempcpy", "wmemset", "wprintf",
+}
+
+checksec :: proc(info: ^ELF_Info) -> Checksec {
+ c: Checksec
+
+ c.relro = relro_state(info)
+ c.nx = nx_state(info)
+ c.pie = pie_state(info)
+ c.canary = has_symbol(info, "__stack_chk_fail") || has_symbol(info, "__stack_chk_guard")
+ c.textrel = has_textrel(info)
+ c.rpath = info.rpath
+ c.runpath = info.runpath
+ c.rwx_segment = has_rwx_segment(info)
+ c.stripped = len(info.symbols) == 0
+ c.fortified, c.fortifiable, c.partial = fortify_counts(info)
+
+ return c
+}
+
+relro_state :: proc(info: ^ELF_Info) -> Relro {
+ relro := false
+ for ph in info.program_hdrs {
+ if ph.type == PT_GNU_RELRO {
+ relro = true
+ break
+ }
+ }
+ if !relro {
+ return .None
+ }
+
+ if _, ok := dyn_val(info, DT_BIND_NOW); ok {
+ return .Full
+ }
+ if flags, ok := dyn_val(info, DT_FLAGS); ok && flags & DF_BIND_NOW != 0 {
+ return .Full
+ }
+ if flags1, ok := dyn_val(info, DT_FLAGS_1); ok && flags1 & DF_1_NOW != 0 {
+ return .Full
+ }
+ return .Partial
+}
+
+// An absent PT_GNU_STACK is not the same as NX on: the kernel falls back to a
+// build-time default, so the honest answer is unknown.
+nx_state :: proc(info: ^ELF_Info) -> Nx {
+ for ph in info.program_hdrs {
+ if ph.type == PT_GNU_STACK {
+ return .Disabled if ph.flags & PF_X != 0 else .Enabled
+ }
+ }
+ return .Unknown
+}
+
+// ET_DYN alone does not mean PIE: shared libraries are ET_DYN too.
+pie_state :: proc(info: ^ELF_Info) -> Pie {
+ if info.header.type == ET_REL {
+ return .Rel
+ }
+ if info.header.type != ET_DYN {
+ return .None
+ }
+
+ has_interp := false
+ for ph in info.program_hdrs {
+ if ph.type == PT_INTERP {
+ has_interp = true
+ break
+ }
+ }
+ if has_interp {
+ return .Pie
+ }
+
+ flags1, _ := dyn_val(info, DT_FLAGS_1)
+ if flags1 & DF_1_PIE != 0 {
+ return .Static_Pie
+ }
+ if _, ok := dyn_val(info, DT_SONAME); ok {
+ return .Shared_Library
+ }
+ return .Shared_Library
+}
+
+has_textrel :: proc(info: ^ELF_Info) -> bool {
+ if _, ok := dyn_val(info, DT_TEXTREL); ok {
+ return true
+ }
+ flags, _ := dyn_val(info, DT_FLAGS)
+ return flags & DF_TEXTREL != 0
+}
+
+has_rwx_segment :: proc(info: ^ELF_Info) -> bool {
+ for ph in info.program_hdrs {
+ if ph.type != PT_LOAD {
+ continue
+ }
+ if ph.flags & PF_W != 0 && ph.flags & PF_X != 0 {
+ return true
+ }
+ }
+ return false
+}
+
+has_symbol :: proc(info: ^ELF_Info, name: string) -> bool {
+ for sym in info.dyn_symbols {
+ if sym.name == name {
+ return true
+ }
+ }
+ for sym in info.symbols {
+ if sym.name == name {
+ return true
+ }
+ }
+ return false
+}
+
+// __stack_chk_fail is the canary, not a fortified call, so it is excluded.
+fortify_counts :: proc(info: ^ELF_Info) -> (fortified: int, fortifiable: int, partial: int) {
+ seen_chk := make(map[string]bool)
+ seen_raw := make(map[string]bool)
+ defer delete(seen_chk)
+ defer delete(seen_raw)
+
+ for sym in info.dyn_symbols {
+ if sym.name == "" || sym.name == "__stack_chk_fail" || sym.name == "__stack_chk_guard" {
+ continue
+ }
+ if strings.has_prefix(sym.name, "__") && strings.has_suffix(sym.name, "_chk") {
+ seen_chk[sym.name[2 : len(sym.name) - 4]] = true
+ } else {
+ seen_raw[sym.name] = true
+ }
+ }
+
+ for name in FORTIFIABLE {
+ switch {
+ case seen_chk[name]:
+ fortified += 1
+ fortifiable += 1
+ if seen_raw[name] {
+ partial += 1
+ }
+ case seen_raw[name]:
+ fortifiable += 1
+ }
+ }
+ return
+}
+
+relro_str :: proc(r: Relro) -> string {
+ switch r {
+ case .Full: return "Full"
+ case .Partial: return "Partial"
+ case .None: return "None"
+ }
+ return "None"
+}
+
+nx_str :: proc(n: Nx) -> string {
+ switch n {
+ case .Enabled: return "Enabled"
+ case .Disabled: return "Disabled"
+ case .Unknown: return "Unknown (no PT_GNU_STACK)"
+ }
+ return "Unknown"
+}
+
+pie_str :: proc(p: Pie) -> string {
+ switch p {
+ case .Pie: return "PIE"
+ case .Static_Pie: return "Static PIE"
+ case .Shared_Library: return "Shared library (not an executable)"
+ case .Rel: return "Relocatable object"
+ case .None: return "None (ET_EXEC)"
+ }
+ return "None"
+}
diff --git a/elf/checksec_test.odin b/elf/checksec_test.odin
@@ -0,0 +1,113 @@
+package elf
+
+import "core:testing"
+
+checksec_info :: proc(phdrs: []Program_Header, dyn: [][2]u64) -> ELF_Info {
+ info: ELF_Info
+ info.program_hdrs = make([dynamic]Program_Header)
+ info.section_hdrs = make([dynamic]Section_Header)
+ info.symbols = make([dynamic]Symbol)
+ info.dyn_symbols = make([dynamic]Symbol)
+ info.dyn_entries = make([dynamic]Dyn_Entry)
+ info.needed = make([dynamic]string)
+ info.relocs = make([dynamic]Reloc)
+ info.header.type = ET_DYN
+
+ for p in phdrs {
+ append(&info.program_hdrs, p)
+ }
+ for e in dyn {
+ append(&info.dyn_entries, Dyn_Entry{tag = e[0], val = e[1]})
+ }
+ return info
+}
+
+@(test)
+test_relro_needs_bind_now_for_full :: proc(t: ^testing.T) {
+ none := checksec_info({}, {})
+ defer destroy(&none)
+ testing.expect_value(t, relro_state(&none), Relro.None)
+
+ partial := checksec_info({{type = PT_GNU_RELRO}}, {})
+ defer destroy(&partial)
+ testing.expect_value(t, relro_state(&partial), Relro.Partial)
+
+ full := checksec_info({{type = PT_GNU_RELRO}}, {{DT_FLAGS, DF_BIND_NOW}})
+ defer destroy(&full)
+ testing.expect_value(t, relro_state(&full), Relro.Full)
+
+ full1 := checksec_info({{type = PT_GNU_RELRO}}, {{DT_FLAGS_1, DF_1_NOW}})
+ defer destroy(&full1)
+ testing.expect_value(t, relro_state(&full1), Relro.Full)
+}
+
+@(test)
+test_nx_absent_segment_is_unknown :: proc(t: ^testing.T) {
+ unknown := checksec_info({}, {})
+ defer destroy(&unknown)
+ testing.expect_value(t, nx_state(&unknown), Nx.Unknown)
+
+ on := checksec_info({{type = PT_GNU_STACK, flags = PF_R | PF_W}}, {})
+ defer destroy(&on)
+ testing.expect_value(t, nx_state(&on), Nx.Enabled)
+
+ off := checksec_info({{type = PT_GNU_STACK, flags = PF_R | PF_W | PF_X}}, {})
+ defer destroy(&off)
+ testing.expect_value(t, nx_state(&off), Nx.Disabled)
+}
+
+@(test)
+test_pie_distinguishes_shared_library_from_executable :: proc(t: ^testing.T) {
+ pie := checksec_info({{type = PT_INTERP}}, {})
+ defer destroy(&pie)
+ testing.expect_value(t, pie_state(&pie), Pie.Pie)
+
+ lib := checksec_info({}, {{DT_SONAME, 1}})
+ defer destroy(&lib)
+ testing.expect_value(t, pie_state(&lib), Pie.Shared_Library)
+
+ spie := checksec_info({}, {{DT_FLAGS_1, DF_1_PIE}})
+ defer destroy(&spie)
+ testing.expect_value(t, pie_state(&spie), Pie.Static_Pie)
+
+ exe := checksec_info({}, {})
+ defer destroy(&exe)
+ exe.header.type = ET_EXEC
+ testing.expect_value(t, pie_state(&exe), Pie.None)
+}
+
+@(test)
+test_rwx_and_textrel_are_flagged :: proc(t: ^testing.T) {
+ clean := checksec_info({{type = PT_LOAD, flags = PF_R | PF_X}}, {})
+ defer destroy(&clean)
+ testing.expect(t, !has_rwx_segment(&clean), "R X segment is not RWX")
+ testing.expect(t, !has_textrel(&clean), "no DT_TEXTREL")
+
+ rwx := checksec_info({{type = PT_LOAD, flags = PF_R | PF_W | PF_X}}, {{DT_TEXTREL, 0}})
+ defer destroy(&rwx)
+ testing.expect(t, has_rwx_segment(&rwx), "RWX PT_LOAD must be flagged")
+ testing.expect(t, has_textrel(&rwx), "DT_TEXTREL must be flagged")
+
+ flagged := checksec_info({}, {{DT_FLAGS, DF_TEXTREL}})
+ defer destroy(&flagged)
+ testing.expect(t, has_textrel(&flagged), "DF_TEXTREL must be flagged")
+}
+
+@(test)
+test_fortify_excludes_stack_canary :: proc(t: ^testing.T) {
+ info := checksec_info({}, {})
+ defer destroy(&info)
+
+ append(&info.dyn_symbols, Symbol{name = "__stack_chk_fail"})
+ append(&info.dyn_symbols, Symbol{name = "__memcpy_chk"})
+ append(&info.dyn_symbols, Symbol{name = "memcpy"})
+ append(&info.dyn_symbols, Symbol{name = "strcpy"})
+
+ fortified, fortifiable, partial := fortify_counts(&info)
+ testing.expect_value(t, fortified, 1)
+ testing.expect_value(t, fortifiable, 2)
+ testing.expect_value(t, partial, 1)
+
+ c := checksec(&info)
+ testing.expect(t, c.canary, "__stack_chk_fail must still set the canary flag")
+}
diff --git a/elf/dynamic.odin b/elf/dynamic.odin
@@ -0,0 +1,241 @@
+package elf
+
+import "core:fmt"
+
+DT_NULL :: 0
+DT_NEEDED :: 1
+DT_PLTRELSZ :: 2
+DT_PLTGOT :: 3
+DT_HASH :: 4
+DT_STRTAB :: 5
+DT_SYMTAB :: 6
+DT_RELA :: 7
+DT_RELASZ :: 8
+DT_RELAENT :: 9
+DT_STRSZ :: 10
+DT_SYMENT :: 11
+DT_INIT :: 12
+DT_FINI :: 13
+DT_SONAME :: 14
+DT_RPATH :: 15
+DT_SYMBOLIC :: 16
+DT_REL :: 17
+DT_RELSZ :: 18
+DT_RELENT :: 19
+DT_PLTREL :: 20
+DT_DEBUG :: 21
+DT_TEXTREL :: 22
+DT_JMPREL :: 23
+DT_BIND_NOW :: 24
+DT_INIT_ARRAY :: 25
+DT_FINI_ARRAY :: 26
+DT_INIT_ARRAYSZ :: 27
+DT_FINI_ARRAYSZ :: 28
+DT_RUNPATH :: 29
+DT_FLAGS :: 30
+
+DT_PREINIT_ARRAY :: 32
+DT_PREINIT_ARRAYSZ :: 33
+DT_SYMTAB_SHNDX :: 34
+DT_RELRSZ :: 35
+DT_RELR :: 36
+DT_RELRENT :: 37
+
+DT_GNU_HASH :: 0x6ffffef5
+DT_TLSDESC_PLT :: 0x6ffffef6
+DT_TLSDESC_GOT :: 0x6ffffef7
+DT_VERSYM :: 0x6ffffff0
+DT_RELACOUNT :: 0x6ffffff9
+DT_RELCOUNT :: 0x6ffffffa
+DT_FLAGS_1 :: 0x6ffffffb
+DT_VERDEF :: 0x6ffffffc
+DT_VERDEFNUM :: 0x6ffffffd
+DT_VERNEED :: 0x6ffffffe
+DT_VERNEEDNUM :: 0x6fffffff
+DT_AUXILIARY :: 0x7ffffffd
+DT_FILTER :: 0x7fffffff
+
+DF_TEXTREL :: 0x4
+DF_BIND_NOW :: 0x8
+
+DF_1_NOW :: 0x1
+DF_1_PIE :: 0x08000000
+
+DYN_SIZE_64 :: 16
+DYN_SIZE_32 :: 8
+
+Dyn_Entry :: struct {
+ tag : u64,
+ val : u64,
+}
+
+dyn_tag_str :: proc(tag: u64) -> string {
+ switch tag {
+ case DT_NULL: return "NULL"
+ case DT_NEEDED: return "NEEDED"
+ case DT_PLTRELSZ: return "PLTRELSZ"
+ case DT_PLTGOT: return "PLTGOT"
+ case DT_HASH: return "HASH"
+ case DT_STRTAB: return "STRTAB"
+ case DT_SYMTAB: return "SYMTAB"
+ case DT_RELA: return "RELA"
+ case DT_RELASZ: return "RELASZ"
+ case DT_RELAENT: return "RELAENT"
+ case DT_STRSZ: return "STRSZ"
+ case DT_SYMENT: return "SYMENT"
+ case DT_INIT: return "INIT"
+ case DT_FINI: return "FINI"
+ case DT_SONAME: return "SONAME"
+ case DT_RPATH: return "RPATH"
+ case DT_SYMBOLIC: return "SYMBOLIC"
+ case DT_REL: return "REL"
+ case DT_RELSZ: return "RELSZ"
+ case DT_RELENT: return "RELENT"
+ case DT_PLTREL: return "PLTREL"
+ case DT_DEBUG: return "DEBUG"
+ case DT_TEXTREL: return "TEXTREL"
+ case DT_JMPREL: return "JMPREL"
+ case DT_BIND_NOW: return "BIND_NOW"
+ case DT_INIT_ARRAY: return "INIT_ARRAY"
+ case DT_FINI_ARRAY: return "FINI_ARRAY"
+ case DT_INIT_ARRAYSZ: return "INIT_ARRAYSZ"
+ case DT_FINI_ARRAYSZ: return "FINI_ARRAYSZ"
+ case DT_RUNPATH: return "RUNPATH"
+ case DT_FLAGS: return "FLAGS"
+ case DT_PREINIT_ARRAY: return "PREINIT_ARRAY"
+ case DT_PREINIT_ARRAYSZ: return "PREINIT_ARRAYSZ"
+ case DT_SYMTAB_SHNDX: return "SYMTAB_SHNDX"
+ case DT_RELRSZ: return "RELRSZ"
+ case DT_RELR: return "RELR"
+ case DT_RELRENT: return "RELRENT"
+ case DT_GNU_HASH: return "GNU_HASH"
+ case DT_TLSDESC_PLT: return "TLSDESC_PLT"
+ case DT_TLSDESC_GOT: return "TLSDESC_GOT"
+ case DT_VERSYM: return "VERSYM"
+ case DT_RELACOUNT: return "RELACOUNT"
+ case DT_RELCOUNT: return "RELCOUNT"
+ case DT_FLAGS_1: return "FLAGS_1"
+ case DT_VERDEF: return "VERDEF"
+ case DT_VERDEFNUM: return "VERDEFNUM"
+ case DT_VERNEED: return "VERNEED"
+ case DT_VERNEEDNUM: return "VERNEEDNUM"
+ case DT_AUXILIARY: return "AUXILIARY"
+ case DT_FILTER: return "FILTER"
+ }
+ return fmt.tprintf("0x%x", tag)
+}
+
+dyn_is_string_tag :: proc(tag: u64) -> bool {
+ switch tag {
+ case DT_NEEDED, DT_SONAME, DT_RPATH, DT_RUNPATH, DT_AUXILIARY, DT_FILTER:
+ return true
+ }
+ return false
+}
+
+dyn_string :: proc(info: ^ELF_Info, e: Dyn_Entry) -> string {
+ if !dyn_is_string_tag(e.tag) || info.dynstr == nil || e.val > u64(max(u32)) {
+ return ""
+ }
+ return get_symbol_name(info.dynstr, u32(e.val))
+}
+
+vaddr_to_offset :: proc(info: ^ELF_Info, vaddr: u64) -> (u64, bool) {
+ for ph in info.program_hdrs {
+ if ph.type != PT_LOAD || ph.filesz == 0 {
+ continue
+ }
+ if vaddr >= ph.vaddr && vaddr - ph.vaddr < ph.filesz {
+ return ph.offset + (vaddr - ph.vaddr), true
+ }
+ }
+ return 0, false
+}
+
+dyn_val :: proc(info: ^ELF_Info, tag: u64) -> (u64, bool) {
+ for e in info.dyn_entries {
+ if e.tag == tag {
+ return e.val, true
+ }
+ }
+ return 0, false
+}
+
+parse_dynamic :: proc(data: []u8, info: ^ELF_Info) {
+ offset, size, ok := find_dynamic(info)
+ if !ok || !in_bounds(data, offset, size) {
+ return
+ }
+
+ order := byte_order(info.header)
+ entsize := u64(DYN_SIZE_64) if info.is_64bit else u64(DYN_SIZE_32)
+
+ for pos := offset; pos + entsize <= offset + size; pos += entsize {
+ e: Dyn_Entry
+ if info.is_64bit {
+ e.tag = read_u64(data, int(pos), order)
+ e.val = read_u64(data, int(pos + 8), order)
+ } else {
+ e.tag = u64(read_u32(data, int(pos), order))
+ e.val = u64(read_u32(data, int(pos + 4), order))
+ }
+ if e.tag == DT_NULL {
+ break
+ }
+ append(&info.dyn_entries, e)
+ }
+
+ resolve_dynamic_strings(data, info)
+}
+
+find_dynamic :: proc(info: ^ELF_Info) -> (offset: u64, size: u64, ok: bool) {
+ for ph in info.program_hdrs {
+ if ph.type == PT_DYNAMIC && ph.filesz > 0 {
+ return ph.offset, ph.filesz, true
+ }
+ }
+ for shdr in info.section_hdrs {
+ if shdr.type == SHT_DYNAMIC && shdr.size > 0 {
+ return shdr.offset, shdr.size, true
+ }
+ }
+ return 0, 0, false
+}
+
+// DT_STRTAB is a vaddr, so this also works when section headers are stripped.
+dynstr_from_dynamic :: proc(data: []u8, info: ^ELF_Info) -> []u8 {
+ strtab_va, has_va := dyn_val(info, DT_STRTAB)
+ strsz, has_sz := dyn_val(info, DT_STRSZ)
+ if !has_va || !has_sz {
+ return nil
+ }
+ off, ok := vaddr_to_offset(info, strtab_va)
+ if !ok || !in_bounds(data, off, strsz) {
+ return nil
+ }
+ return data[off : off + strsz]
+}
+
+resolve_dynamic_strings :: proc(data: []u8, info: ^ELF_Info) {
+ strtab := dynstr_from_dynamic(data, info)
+ if strtab == nil {
+ strtab = info.dynstr
+ }
+ if strtab == nil {
+ return
+ }
+ info.dynstr = strtab
+
+ for e in info.dyn_entries {
+ if e.val > u64(max(u32)) {
+ continue
+ }
+ name := get_symbol_name(strtab, u32(e.val))
+ switch e.tag {
+ case DT_NEEDED: append(&info.needed, name)
+ case DT_SONAME: info.soname = name
+ case DT_RPATH: info.rpath = name
+ case DT_RUNPATH: info.runpath = name
+ }
+ }
+}
diff --git a/elf/dynamic_test.odin b/elf/dynamic_test.odin
@@ -0,0 +1,181 @@
+package elf
+
+import "core:testing"
+
+DYN_BASE :: 0x400000
+DYN_STR :: "\x00libtest.so.1\x00libc.so.6\x00/opt/rpath\x00/opt/runpath\x00"
+
+DYN_OFF_NEEDED_1 :: 1
+DYN_OFF_NEEDED_2 :: 14
+DYN_OFF_RPATH :: 24
+DYN_OFF_RUNPATH :: 35
+
+build_dynamic_elf :: proc(is64: bool, little: bool, entries: [][2]u64) -> []u8 {
+ dyn_off :: 0x100
+ str_off :: 0x200
+ total :: 0x300
+
+ ph_off := 64 if is64 else 52
+ ph_size := PHDR_SIZE_64 if is64 else PHDR_SIZE_32
+ ent_size := 16 if is64 else 8
+
+ data := make([]u8, total)
+ data[0] = 0x7f
+ data[1] = 'E'
+ data[2] = 'L'
+ data[3] = 'F'
+ data[4] = ELF_CLASS_64 if is64 else ELF_CLASS_32
+ data[5] = ELF_DATA_LSB if little else ELF_DATA_MSB
+ data[6] = 1
+
+ put_u16(data, 16, ET_DYN, little)
+ put_u16(data, 18, EM_X86_64 if is64 else EM_ARM, little)
+
+ if is64 {
+ put_u64(data, 32, u64(ph_off), little)
+ put_u64(data, 40, 0, little)
+ put_u16(data, 54, u16(ph_size), little)
+ put_u16(data, 56, 2, little)
+ } else {
+ put_u32(data, 28, u32(ph_off), little)
+ put_u32(data, 32, 0, little)
+ put_u16(data, 42, u16(ph_size), little)
+ put_u16(data, 44, 2, little)
+ }
+
+ write_phdr(data, ph_off, is64, little, Program_Header{
+ type = PT_LOAD, offset = 0, vaddr = DYN_BASE,
+ filesz = total, memsz = total, flags = PF_R | PF_X, align = 0x1000,
+ })
+ write_phdr(data, ph_off + ph_size, is64, little, Program_Header{
+ type = PT_DYNAMIC, offset = dyn_off, vaddr = DYN_BASE + dyn_off,
+ filesz = u64(len(entries) * ent_size), memsz = u64(len(entries) * ent_size),
+ flags = PF_R | PF_W, align = 8,
+ })
+
+ pos := dyn_off
+ for e in entries {
+ if is64 {
+ put_u64(data, pos, e[0], little)
+ put_u64(data, pos + 8, e[1], little)
+ } else {
+ put_u32(data, pos, u32(e[0]), little)
+ put_u32(data, pos + 4, u32(e[1]), little)
+ }
+ pos += ent_size
+ }
+
+ copy(data[str_off:], DYN_STR)
+ return data
+}
+
+DEFAULT_DYN_ENTRIES := [?][2]u64{
+ {DT_NEEDED, DYN_OFF_NEEDED_1},
+ {DT_NEEDED, DYN_OFF_NEEDED_2},
+ {DT_SONAME, DYN_OFF_NEEDED_1},
+ {DT_RPATH, DYN_OFF_RPATH},
+ {DT_RUNPATH, DYN_OFF_RUNPATH},
+ {DT_STRTAB, DYN_BASE + 0x200},
+ {DT_STRSZ, u64(len(DYN_STR))},
+ {DT_NULL, 0},
+}
+
+check_dynamic :: proc(t: ^testing.T, is64: bool, little: bool) {
+ data := build_dynamic_elf(is64, little, DEFAULT_DYN_ENTRIES[:])
+ defer delete(data)
+
+ info, err := load(data)
+ defer destroy(&info)
+
+ testing.expect_value(t, err, "")
+ testing.expect_value(t, len(info.dyn_entries), 7)
+ testing.expect_value(t, len(info.needed), 2)
+ testing.expect_value(t, info.needed[0], "libtest.so.1")
+ testing.expect_value(t, info.needed[1], "libc.so.6")
+ testing.expect_value(t, info.soname, "libtest.so.1")
+ testing.expect_value(t, info.rpath, "/opt/rpath")
+ testing.expect_value(t, info.runpath, "/opt/runpath")
+}
+
+@(test)
+test_dynamic_elf64_le :: proc(t: ^testing.T) { check_dynamic(t, true, true) }
+
+@(test)
+test_dynamic_elf64_be :: proc(t: ^testing.T) { check_dynamic(t, true, false) }
+
+@(test)
+test_dynamic_elf32_le :: proc(t: ^testing.T) { check_dynamic(t, false, true) }
+
+@(test)
+test_dynamic_elf32_be :: proc(t: ^testing.T) { check_dynamic(t, false, false) }
+
+@(test)
+test_dynamic_stops_at_dt_null :: proc(t: ^testing.T) {
+ entries := [][2]u64{
+ {DT_NEEDED, DYN_OFF_NEEDED_1},
+ {DT_STRTAB, DYN_BASE + 0x200},
+ {DT_STRSZ, u64(len(DYN_STR))},
+ {DT_NULL, 0},
+ {DT_NEEDED, DYN_OFF_NEEDED_2},
+ }
+ data := build_dynamic_elf(true, true, entries[:])
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ testing.expect_value(t, len(info.needed), 1)
+ testing.expect_value(t, info.needed[0], "libtest.so.1")
+}
+
+@(test)
+test_dynamic_strtab_outside_file_is_ignored :: proc(t: ^testing.T) {
+ entries := [][2]u64{
+ {DT_NEEDED, DYN_OFF_NEEDED_1},
+ {DT_STRTAB, DYN_BASE + 0xdeadbeef},
+ {DT_STRSZ, u64(len(DYN_STR))},
+ {DT_NULL, 0},
+ }
+ data := build_dynamic_elf(true, true, entries[:])
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ testing.expect_value(t, len(info.dyn_entries), 3)
+ testing.expect_value(t, len(info.needed), 0)
+}
+
+@(test)
+test_vaddr_to_offset_maps_through_pt_load :: proc(t: ^testing.T) {
+ data := build_dynamic_elf(true, true, DEFAULT_DYN_ENTRIES[:])
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ off, ok := vaddr_to_offset(&info, DYN_BASE + 0x200)
+ testing.expect(t, ok, "vaddr inside PT_LOAD must resolve")
+ testing.expect_value(t, off, 0x200)
+
+ _, miss := vaddr_to_offset(&info, DYN_BASE + 0x99999)
+ testing.expect(t, !miss, "vaddr outside every PT_LOAD must fail")
+}
+
+@(test)
+test_no_phantom_sections_when_shoff_zero :: proc(t: ^testing.T) {
+ data := build_dynamic_elf(true, true, DEFAULT_DYN_ENTRIES[:])
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ testing.expect_value(t, len(info.section_hdrs), 0)
+}
+
+@(test)
+test_dyn_tag_str_unknown_falls_back_to_hex :: proc(t: ^testing.T) {
+ testing.expect_value(t, dyn_tag_str(DT_RUNPATH), "RUNPATH")
+ testing.expect_value(t, dyn_tag_str(DT_VERNEEDNUM), "VERNEEDNUM")
+ testing.expect_value(t, dyn_tag_str(0x70000001), "0x70000001")
+}
diff --git a/elf/elf.odin b/elf/elf.odin
@@ -64,11 +64,14 @@ PT_TLS :: 7
PT_GNU_EH_FRAME :: 0x6474e550
PT_GNU_STACK :: 0x6474e551
PT_GNU_RELRO :: 0x6474e552
+PT_GNU_PROPERTY :: 0x6474e553
PF_X :: 1
PF_W :: 2
PF_R :: 4
+SHF_WRITE :: 1
+SHF_ALLOC :: 2
SHF_EXECINSTR :: 4
SHT_NULL :: 0
@@ -88,6 +91,7 @@ SHT_FINI_ARRAY :: 15
SHT_PREINIT_ARRAY :: 16
SHT_GROUP :: 17
SHT_SYMTAB_SHNDX :: 18
+SHT_RELR :: 19
SHT_GNU_ATTRIBUTES :: 0x6ffffff5
SHT_GNU_HASH :: 0x6ffffff6
SHT_GNU_VERDEF :: 0x6ffffffd
@@ -164,6 +168,12 @@ ELF_Info :: struct {
section_hdrs : [dynamic]Section_Header,
symbols : [dynamic]Symbol,
dyn_symbols : [dynamic]Symbol,
+ dyn_entries : [dynamic]Dyn_Entry,
+ needed : [dynamic]string,
+ relocs : [dynamic]Reloc,
+ soname : string,
+ rpath : string,
+ runpath : string,
is_64bit : bool,
is_pie : bool,
is_dynamic : bool,
diff --git a/elf/json.odin b/elf/json.odin
@@ -0,0 +1,203 @@
+package elf
+
+import "core:encoding/json"
+import "core:fmt"
+
+Json_Symbol :: struct {
+ name : string,
+ value : u64,
+ size : u64,
+ bind : string,
+ type : string,
+ shndx : u16,
+}
+
+Json_Section :: struct {
+ index : int,
+ name : string,
+ type : string,
+ addr : u64,
+ offset : u64,
+ size : u64,
+ flags : u64,
+}
+
+Json_Segment :: struct {
+ type : string,
+ flags : string,
+ offset : u64,
+ vaddr : u64,
+ paddr : u64,
+ filesz : u64,
+ memsz : u64,
+ align : u64,
+}
+
+Json_Dyn :: struct {
+ tag : string,
+ value : u64,
+ name : string,
+}
+
+Json_Reloc :: struct {
+ offset : u64,
+ type : string,
+ addend : i64,
+ symbol : string,
+ source : string,
+}
+
+Json_Checksec :: struct {
+ relro : string,
+ nx : string,
+ pie : string,
+ canary : bool,
+ textrel : bool,
+ rwx_segment : bool,
+ rpath : string,
+ runpath : string,
+ stripped : bool,
+ fortified : int,
+ fortifiable : int,
+ partial : int,
+}
+
+Json_Dump :: struct {
+ class : int,
+ endian : string,
+ type : string,
+ machine : string,
+ entry : u64,
+ flags : u32,
+ pie : bool,
+ is_dynamic : bool,
+ interp_offset : u64,
+ soname : string,
+ rpath : string,
+ runpath : string,
+ needed : []string,
+ segments : []Json_Segment,
+ sections : []Json_Section,
+ dynamic_entries : []Json_Dyn,
+ symbols : []Json_Symbol,
+ dyn_symbols : []Json_Symbol,
+ relocations : []Json_Reloc,
+ got : map[string]u64,
+ checksec : Json_Checksec,
+}
+
+build_json_dump :: proc(info: ^ELF_Info, allocator := context.allocator) -> Json_Dump {
+ segments := make([]Json_Segment, len(info.program_hdrs), allocator)
+ for ph, i in info.program_hdrs {
+ segments[i] = Json_Segment{
+ type = segment_type_str(ph.type),
+ flags = flags_to_str(ph.flags),
+ offset = ph.offset,
+ vaddr = ph.vaddr,
+ paddr = ph.paddr,
+ filesz = ph.filesz,
+ memsz = ph.memsz,
+ align = ph.align,
+ }
+ }
+
+ sections := make([]Json_Section, len(info.section_hdrs), allocator)
+ for shdr, i in info.section_hdrs {
+ sections[i] = Json_Section{
+ index = i,
+ name = get_section_name(info, shdr),
+ type = section_type_str(shdr.type),
+ addr = shdr.addr,
+ offset = shdr.offset,
+ size = shdr.size,
+ flags = shdr.flags,
+ }
+ }
+
+ dyn := make([]Json_Dyn, len(info.dyn_entries), allocator)
+ for e, i in info.dyn_entries {
+ dyn[i] = Json_Dyn{tag = dyn_tag_str(e.tag), value = e.val, name = dyn_string(info, e)}
+ }
+
+ relocs := make([]Json_Reloc, len(info.relocs), allocator)
+ got := make(map[string]u64, allocator)
+ for r, i in info.relocs {
+ relocs[i] = Json_Reloc{
+ offset = r.offset,
+ type = reloc_type_str(info.header.machine, r.type),
+ addend = r.addend,
+ symbol = r.sym_name,
+ source = r.source,
+ }
+ if is_jump_slot(info.header.machine, r.type) && r.sym_name != "" {
+ got[r.sym_name] = r.offset
+ }
+ }
+
+ c := checksec(info)
+
+ return Json_Dump{
+ class = 64 if info.is_64bit else 32,
+ endian = "little" if info.header.data == ELF_DATA_LSB else "big",
+ type = type_to_str(info.header.type),
+ machine = machine_to_str(info.header.machine),
+ entry = info.header.entry,
+ flags = info.header.flags,
+ pie = info.is_pie,
+ is_dynamic = info.is_dynamic,
+ interp_offset = info.interp_offset,
+ soname = info.soname,
+ rpath = info.rpath,
+ runpath = info.runpath,
+ needed = info.needed[:],
+ segments = segments,
+ sections = sections,
+ dynamic_entries = dyn,
+ symbols = to_json_symbols(info.symbols[:], allocator),
+ dyn_symbols = to_json_symbols(info.dyn_symbols[:], allocator),
+ relocations = relocs,
+ got = got,
+ checksec = Json_Checksec{
+ relro = relro_str(c.relro),
+ nx = nx_str(c.nx),
+ pie = pie_str(c.pie),
+ canary = c.canary,
+ textrel = c.textrel,
+ rwx_segment = c.rwx_segment,
+ rpath = c.rpath,
+ runpath = c.runpath,
+ stripped = c.stripped,
+ fortified = c.fortified,
+ fortifiable = c.fortifiable,
+ partial = c.partial,
+ },
+ }
+}
+
+to_json_symbols :: proc(syms: []Symbol, allocator := context.allocator) -> []Json_Symbol {
+ out := make([]Json_Symbol, len(syms), allocator)
+ for sym, i in syms {
+ out[i] = Json_Symbol{
+ name = sym.name,
+ value = sym.value,
+ size = sym.size,
+ bind = symbol_bind_str(sym.bind),
+ type = symbol_type_str(sym.type),
+ shndx = sym.shndx,
+ }
+ }
+ return out
+}
+
+print_json :: proc(info: ^ELF_Info) -> string {
+ defer free_all(context.temp_allocator)
+
+ dump := build_json_dump(info, context.temp_allocator)
+ data, err := json.marshal(dump, {pretty = true, use_spaces = true}, context.temp_allocator)
+ if err != nil {
+ return fmt.tprintf("json marshal failed: %v", err)
+ }
+
+ fmt.println(string(data))
+ return ""
+}
diff --git a/elf/json_test.odin b/elf/json_test.odin
@@ -0,0 +1,50 @@
+package elf
+
+import "core:encoding/json"
+import "core:testing"
+
+// build_json_dump and checksec both use the temp allocator; a free_all inside
+// either one frees the other's slices, which crashed the whole --json path.
+@(test)
+test_json_dump_survives_checksec_allocations :: proc(t: ^testing.T) {
+ data := build_dynamic_elf(true, true, DEFAULT_DYN_ENTRIES[:])
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ append(&info.dyn_symbols, Symbol{name = "__memcpy_chk"})
+ append(&info.dyn_symbols, Symbol{name = "memcpy"})
+
+ dump := build_json_dump(&info, context.temp_allocator)
+ defer free_all(context.temp_allocator)
+
+ testing.expect_value(t, len(dump.needed), 2)
+ testing.expect_value(t, dump.needed[0], "libtest.so.1")
+ testing.expect_value(t, dump.runpath, "/opt/runpath")
+ testing.expect_value(t, dump.checksec.fortified, 1)
+
+ out, err := json.marshal(dump, {pretty = true}, context.temp_allocator)
+ testing.expect(t, err == nil, "marshal must succeed")
+ testing.expect(t, len(out) > 0, "marshal must produce output")
+}
+
+@(test)
+test_json_dump_round_trips_through_parser :: proc(t: ^testing.T) {
+ data := build_dynamic_elf(true, true, DEFAULT_DYN_ENTRIES[:])
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ dump := build_json_dump(&info, context.temp_allocator)
+ out, _ := json.marshal(dump, {}, context.temp_allocator)
+ defer free_all(context.temp_allocator)
+
+ parsed, perr := json.parse(out, allocator = context.temp_allocator)
+ testing.expect(t, perr == nil, "emitted JSON must parse back")
+
+ obj := parsed.(json.Object)
+ testing.expect_value(t, obj["soname"].(json.String), "libtest.so.1")
+ testing.expect_value(t, obj["class"].(json.Float), 64)
+}
diff --git a/elf/loader.odin b/elf/loader.odin
@@ -16,11 +16,16 @@ load :: proc(data: []u8) -> (ELF_Info, string) {
info.section_hdrs = make([dynamic]Section_Header)
info.symbols = make([dynamic]Symbol)
info.dyn_symbols = make([dynamic]Symbol)
+ info.dyn_entries = make([dynamic]Dyn_Entry)
+ info.needed = make([dynamic]string)
+ info.relocs = make([dynamic]Reloc)
// Sections first: PN_XNUM resolves e_phnum through section[0].
parse_section_headers(data, &info)
parse_program_headers(data, &info)
parse_symbols(data, &info)
+ parse_dynamic(data, &info)
+ parse_relocs(data, &info)
return info, ""
}
@@ -30,6 +35,9 @@ destroy :: proc(info: ^ELF_Info) {
delete(info.section_hdrs)
delete(info.symbols)
delete(info.dyn_symbols)
+ delete(info.dyn_entries)
+ delete(info.needed)
+ delete(info.relocs)
}
get_section_name :: proc(info: ^ELF_Info, shdr: Section_Header) -> string {
@@ -166,6 +174,9 @@ parse_section_headers :: proc(data: []u8, info: ^ELF_Info) {
is_64bit := info.is_64bit
order := byte_order(info.header)
+ // e_shoff == 0 means no section header table, so shnum == 0 is not the
+ // SHN_XINDEX escape hatch and section[0] would be the ELF header itself.
+ if header.shoff == 0 { return }
if !in_bounds(data, header.shoff, 0) { return }
min_size := SHDR_SIZE_64 if is_64bit else SHDR_SIZE_32
diff --git a/elf/printer.odin b/elf/printer.odin
@@ -3,14 +3,9 @@ package elf
import "core:fmt"
import "core:os"
-print_info :: proc(info: ^ELF_Info, verbose: bool, json: bool) {
+print_info :: proc(info: ^ELF_Info, verbose: bool) {
f := os.stdout
- if json {
- print_info_json(info)
- return
- }
-
fmt.fprintf(f, "ELF File Information\n")
fmt.fprintf(f, "====================\n\n")
@@ -38,6 +33,22 @@ print_info :: proc(info: ^ELF_Info, verbose: bool, json: bool) {
fmt.fprintf(f, "Interpreter offset: 0x%x\n", info.interp_offset)
}
+ if info.soname != "" {
+ fmt.fprintf(f, "SONAME: %s\n", info.soname)
+ }
+ if info.rpath != "" {
+ fmt.fprintf(f, "RPATH: %s\n", info.rpath)
+ }
+ if info.runpath != "" {
+ fmt.fprintf(f, "RUNPATH: %s\n", info.runpath)
+ }
+ if len(info.needed) > 0 {
+ fmt.fprintf(f, "\nNeeded Libraries: %d\n", len(info.needed))
+ for lib in info.needed {
+ fmt.fprintf(f, " %s\n", lib)
+ }
+ }
+
fmt.fprintf(f, "\n")
fmt.fprintf(f, "Program Headers: %d\n", len(info.program_hdrs))
fmt.fprintf(f, "Section Headers: %d\n", len(info.section_hdrs))
@@ -45,6 +56,17 @@ print_info :: proc(info: ^ELF_Info, verbose: bool, json: bool) {
fmt.fprintf(f, "Symbols (dynamic): %d\n", len(info.dyn_symbols))
if verbose {
+ if len(info.dyn_entries) > 0 {
+ fmt.fprintf(f, "\nDynamic Section: %d entries\n", len(info.dyn_entries))
+ for e in info.dyn_entries {
+ if name := dyn_string(info, e); name != "" {
+ fmt.fprintf(f, " %-14s %s\n", dyn_tag_str(e.tag), name)
+ } else {
+ fmt.fprintf(f, " %-14s 0x%x\n", dyn_tag_str(e.tag), e.val)
+ }
+ }
+ }
+
fmt.fprintf(f, "\n")
fmt.fprintf(f, "Detailed Sections:\n")
for i := 0; i < len(info.section_hdrs); i += 1 {
@@ -89,33 +111,6 @@ print_info :: proc(info: ^ELF_Info, verbose: bool, json: bool) {
}
}
-print_info_json :: proc(info: ^ELF_Info) {
- class_bits := 32
- if info.is_64bit {
- class_bits = 64
- }
- fmt.println("{")
- fmt.printf(" \"class\": %d,\n", class_bits)
- fmt.printf(" \"type\": \"%s\",\n", type_to_str(info.header.type))
- fmt.printf(" \"machine\": \"%s\",\n", machine_to_str(info.header.machine))
- fmt.printf(" \"entry\": \"0x%016x\",\n", info.header.entry)
- pie_str := "false"
- if info.is_pie {
- pie_str = "true"
- }
- fmt.printf(" \"pie\": %s,\n", pie_str)
- dyn_str := "false"
- if info.is_dynamic {
- dyn_str = "true"
- }
- fmt.printf(" \"dynamic\": %s,\n", dyn_str)
- fmt.printf(" \"program_headers\": %d,\n", len(info.program_hdrs))
- fmt.printf(" \"section_headers\": %d,\n", len(info.section_hdrs))
- fmt.printf(" \"symbols_static\": %d,\n", len(info.symbols))
- fmt.printf(" \"symbols_dynamic\": %d\n", len(info.dyn_symbols))
- fmt.printf("}\n")
-}
-
type_to_str :: proc(t: u16) -> string {
switch t {
case ET_REL: return "REL (Relocatable)"
@@ -229,6 +224,7 @@ segment_type_str :: proc(t: u32) -> string {
case PT_GNU_EH_FRAME: return "GNU_EH_FRAME"
case PT_GNU_STACK: return "GNU_STACK"
case PT_GNU_RELRO: return "GNU_RELRO"
+ case PT_GNU_PROPERTY: return "GNU_PROPERTY"
}
return fmt.tprintf("0x%x", t)
}
@@ -238,3 +234,57 @@ flags_to_str :: proc(flags: u64) -> string {
@static table := [8]string{" ", " X", " W ", " WX", "R ", "R X", "RW ", "RWX"}
return table[flags & 7]
}
+
+print_checksec :: proc(info: ^ELF_Info) {
+ f := os.stdout
+ c := checksec(info)
+
+ fmt.fprintf(f, "%-12s %s\n", "RELRO:", relro_str(c.relro))
+ fmt.fprintf(f, "%-12s %s\n", "Stack:", "Canary found" if c.canary else "No canary found")
+ fmt.fprintf(f, "%-12s %s\n", "NX:", nx_str(c.nx))
+ fmt.fprintf(f, "%-12s %s\n", "PIE:", pie_str(c.pie))
+ fmt.fprintf(f, "%-12s %s\n", "RPATH:", c.rpath if c.rpath != "" else "No RPATH")
+ fmt.fprintf(f, "%-12s %s\n", "RUNPATH:", c.runpath if c.runpath != "" else "No RUNPATH")
+ fmt.fprintf(f, "%-12s %s\n", "Symbols:", "No symbols" if c.stripped else "Symbols")
+ fmt.fprintf(f, "%-12s %d of %d fortifiable functions fortified", "FORTIFY:", c.fortified, c.fortifiable)
+ if c.partial > 0 {
+ fmt.fprintf(f, " (%d also imported unfortified)", c.partial)
+ }
+ fmt.fprintf(f, "\n")
+
+ if c.textrel {
+ fmt.fprintf(f, "%-12s DT_TEXTREL present, code segment is writable at load time\n", "TEXTREL:")
+ }
+ if c.rwx_segment {
+ fmt.fprintf(f, "%-12s a PT_LOAD segment is both writable and executable\n", "RWX:")
+ }
+}
+
+print_relocs :: proc(info: ^ELF_Info) {
+ f := os.stdout
+
+ if len(info.relocs) == 0 {
+ fmt.fprintf(f, "No relocations\n")
+ return
+ }
+
+ fmt.fprintf(f, "%-18s %-22s %-16s %s\n", "Offset", "Type", "Addend", "Symbol")
+ for r in info.relocs {
+ addend := fmt.tprintf("0x%x", r.addend) if r.kind == .Rela else "-"
+ fmt.fprintf(f, "0x%016x %-22s %-16s %s\n",
+ r.offset, reloc_type_str(info.header.machine, r.type), addend, r.sym_name)
+ }
+
+ fmt.fprintf(f, "\nPLT/GOT Map:\n")
+ slots := 0
+ for r in info.relocs {
+ if !is_jump_slot(info.header.machine, r.type) || r.sym_name == "" {
+ continue
+ }
+ slots += 1
+ fmt.fprintf(f, " %-32s 0x%016x\n", r.sym_name, r.offset)
+ }
+ if slots == 0 {
+ fmt.fprintf(f, " (no jump slots)\n")
+ }
+}
diff --git a/elf/reloc.odin b/elf/reloc.odin
@@ -0,0 +1,270 @@
+package elf
+
+import "core:fmt"
+
+Reloc_Source :: enum {
+ Rel,
+ Rela,
+ Relr,
+}
+
+Reloc :: struct {
+ offset : u64,
+ addend : i64,
+ sym : u32,
+ type : u32,
+ sym_name : string,
+ source : string,
+ kind : Reloc_Source,
+}
+
+REL_SIZE_64 :: 16
+RELA_SIZE_64 :: 24
+REL_SIZE_32 :: 8
+RELA_SIZE_32 :: 12
+
+parse_relocs :: proc(data: []u8, info: ^ELF_Info) {
+ if parse_relocs_from_sections(data, info) {
+ return
+ }
+ parse_relocs_from_dynamic(data, info)
+}
+
+parse_relocs_from_sections :: proc(data: []u8, info: ^ELF_Info) -> bool {
+ found := false
+ for shdr in info.section_hdrs {
+ kind: Reloc_Source
+ switch shdr.type {
+ case SHT_REL: kind = .Rel
+ case SHT_RELA: kind = .Rela
+ case SHT_RELR: kind = .Relr
+ case: continue
+ }
+ if !in_bounds(data, shdr.offset, shdr.size) || shdr.size == 0 {
+ continue
+ }
+ found = true
+ name := get_section_name(info, shdr)
+ syms := symbols_for_link(info, shdr.link)
+ read_reloc_table(data, info, shdr.offset, shdr.size, kind, name, syms)
+ }
+ return found
+}
+
+symbols_for_link :: proc(info: ^ELF_Info, link: u32) -> []Symbol {
+ if int(link) < len(info.section_hdrs) && info.section_hdrs[link].type == SHT_SYMTAB {
+ return info.symbols[:]
+ }
+ return info.dyn_symbols[:]
+}
+
+parse_relocs_from_dynamic :: proc(data: []u8, info: ^ELF_Info) {
+ syms := info.dyn_symbols[:]
+
+ if addr, ok := dyn_val(info, DT_RELA); ok {
+ if size, ok2 := dyn_val(info, DT_RELASZ); ok2 {
+ read_dynamic_table(data, info, addr, size, .Rela, "DT_RELA", syms)
+ }
+ }
+ if addr, ok := dyn_val(info, DT_REL); ok {
+ if size, ok2 := dyn_val(info, DT_RELSZ); ok2 {
+ read_dynamic_table(data, info, addr, size, .Rel, "DT_REL", syms)
+ }
+ }
+ if addr, ok := dyn_val(info, DT_RELR); ok {
+ if size, ok2 := dyn_val(info, DT_RELRSZ); ok2 {
+ read_dynamic_table(data, info, addr, size, .Relr, "DT_RELR", syms)
+ }
+ }
+ if addr, ok := dyn_val(info, DT_JMPREL); ok {
+ if size, ok2 := dyn_val(info, DT_PLTRELSZ); ok2 {
+ kind: Reloc_Source = .Rela
+ if pltrel, ok3 := dyn_val(info, DT_PLTREL); ok3 && pltrel == DT_REL {
+ kind = .Rel
+ }
+ read_dynamic_table(data, info, addr, size, kind, "DT_JMPREL", syms)
+ }
+ }
+}
+
+read_dynamic_table :: proc(data: []u8, info: ^ELF_Info, vaddr, size: u64, kind: Reloc_Source, source: string, syms: []Symbol) {
+ off, ok := vaddr_to_offset(info, vaddr)
+ if !ok || !in_bounds(data, off, size) {
+ return
+ }
+ read_reloc_table(data, info, off, size, kind, source, syms)
+}
+
+read_reloc_table :: proc(data: []u8, info: ^ELF_Info, offset, size: u64, kind: Reloc_Source, source: string, syms: []Symbol) {
+ if kind == .Relr {
+ read_relr_table(data, info, offset, size, source)
+ return
+ }
+
+ order := byte_order(info.header)
+ is64 := info.is_64bit
+
+ entsize := u64(0)
+ switch {
+ case kind == .Rela && is64: entsize = RELA_SIZE_64
+ case kind == .Rela && !is64: entsize = RELA_SIZE_32
+ case kind == .Rel && is64: entsize = REL_SIZE_64
+ case: entsize = REL_SIZE_32
+ }
+
+ for pos := offset; pos + entsize <= offset + size; pos += entsize {
+ r: Reloc
+ r.kind = kind
+ r.source = source
+
+ raw_info: u64
+ if is64 {
+ r.offset = read_u64(data, int(pos), order)
+ raw_info = read_u64(data, int(pos + 8), order)
+ r.sym = u32(raw_info >> 32)
+ r.type = u32(raw_info & 0xffffffff)
+ if kind == .Rela {
+ r.addend = i64(read_u64(data, int(pos + 16), order))
+ }
+ } else {
+ r.offset = u64(read_u32(data, int(pos), order))
+ raw_info = u64(read_u32(data, int(pos + 4), order))
+ r.sym = u32(raw_info >> 8)
+ r.type = u32(raw_info & 0xff)
+ if kind == .Rela {
+ r.addend = i64(i32(read_u32(data, int(pos + 8), order)))
+ }
+ }
+
+ if int(r.sym) < len(syms) {
+ r.sym_name = syms[r.sym].name
+ }
+ append(&info.relocs, r)
+ }
+}
+
+// RELR packs relative relocations: an even word is an address, an odd word is a
+// bitmap of the words that follow it.
+read_relr_table :: proc(data: []u8, info: ^ELF_Info, offset, size: u64, source: string) {
+ order := byte_order(info.header)
+ is64 := info.is_64bit
+ word := u64(8) if is64 else u64(4)
+ rel_type := relative_reloc_type(info.header.machine)
+
+ base := u64(0)
+ for pos := offset; pos + word <= offset + size; pos += word {
+ entry := read_u64(data, int(pos), order) if is64 else u64(read_u32(data, int(pos), order))
+
+ if entry & 1 == 0 {
+ append(&info.relocs, Reloc{
+ offset = entry, type = rel_type, source = source, kind = .Relr,
+ })
+ base = entry + word
+ continue
+ }
+
+ bits := entry >> 1
+ for i := u64(0); bits != 0; i += 1 {
+ if bits & 1 != 0 {
+ append(&info.relocs, Reloc{
+ offset = base + i * word, type = rel_type, source = source, kind = .Relr,
+ })
+ }
+ bits >>= 1
+ }
+ base += (word * 8 - 1) * word
+ }
+}
+
+R_X86_64_RELATIVE :: 8
+R_386_RELATIVE :: 8
+R_AARCH64_RELATIVE :: 1027
+R_ARM_RELATIVE :: 23
+R_RISCV_RELATIVE :: 3
+
+relative_reloc_type :: proc(machine: u16) -> u32 {
+ switch machine {
+ case EM_X86_64: return R_X86_64_RELATIVE
+ case EM_386: return R_386_RELATIVE
+ case EM_AARCH64: return R_AARCH64_RELATIVE
+ case EM_ARM: return R_ARM_RELATIVE
+ case EM_RISCV: return R_RISCV_RELATIVE
+ }
+ return 0
+}
+
+is_jump_slot :: proc(machine: u16, type: u32) -> bool {
+ switch machine {
+ case EM_X86_64: return type == 7
+ case EM_386: return type == 7
+ case EM_AARCH64: return type == 1026
+ case EM_ARM: return type == 22
+ case EM_RISCV: return type == 5
+ }
+ return false
+}
+
+reloc_type_str :: proc(machine: u16, type: u32) -> string {
+ switch machine {
+ case EM_X86_64:
+ switch type {
+ case 0: return "R_X86_64_NONE"
+ case 1: return "R_X86_64_64"
+ case 2: return "R_X86_64_PC32"
+ case 3: return "R_X86_64_GOT32"
+ case 4: return "R_X86_64_PLT32"
+ case 5: return "R_X86_64_COPY"
+ case 6: return "R_X86_64_GLOB_DAT"
+ case 7: return "R_X86_64_JUMP_SLOT"
+ case 8: return "R_X86_64_RELATIVE"
+ case 9: return "R_X86_64_GOTPCREL"
+ case 16: return "R_X86_64_DTPMOD64"
+ case 17: return "R_X86_64_DTPOFF64"
+ case 18: return "R_X86_64_TPOFF64"
+ case 37: return "R_X86_64_IRELATIVE"
+ case 41: return "R_X86_64_TLSDESC"
+ }
+ case EM_386:
+ switch type {
+ case 0: return "R_386_NONE"
+ case 1: return "R_386_32"
+ case 2: return "R_386_PC32"
+ case 5: return "R_386_COPY"
+ case 6: return "R_386_GLOB_DAT"
+ case 7: return "R_386_JMP_SLOT"
+ case 8: return "R_386_RELATIVE"
+ case 42: return "R_386_IRELATIVE"
+ }
+ case EM_AARCH64:
+ switch type {
+ case 0: return "R_AARCH64_NONE"
+ case 257: return "R_AARCH64_ABS64"
+ case 1024: return "R_AARCH64_COPY"
+ case 1025: return "R_AARCH64_GLOB_DAT"
+ case 1026: return "R_AARCH64_JUMP_SLOT"
+ case 1027: return "R_AARCH64_RELATIVE"
+ case 1032: return "R_AARCH64_IRELATIVE"
+ }
+ case EM_ARM:
+ switch type {
+ case 0: return "R_ARM_NONE"
+ case 2: return "R_ARM_ABS32"
+ case 20: return "R_ARM_COPY"
+ case 21: return "R_ARM_GLOB_DAT"
+ case 22: return "R_ARM_JUMP_SLOT"
+ case 23: return "R_ARM_RELATIVE"
+ case 160: return "R_ARM_IRELATIVE"
+ }
+ case EM_RISCV:
+ switch type {
+ case 0: return "R_RISCV_NONE"
+ case 1: return "R_RISCV_32"
+ case 2: return "R_RISCV_64"
+ case 3: return "R_RISCV_RELATIVE"
+ case 4: return "R_RISCV_COPY"
+ case 5: return "R_RISCV_JUMP_SLOT"
+ case 58: return "R_RISCV_IRELATIVE"
+ }
+ }
+ return fmt.tprintf("%d", type)
+}
diff --git a/elf/reloc_test.odin b/elf/reloc_test.odin
@@ -0,0 +1,134 @@
+package elf
+
+import "core:testing"
+
+build_relr_elf :: proc(is64: bool, little: bool, words: []u64) -> []u8 {
+ dyn_off :: 0x100
+ relr_off :: 0x200
+ total :: 0x300
+
+ ph_off := 64 if is64 else 52
+ ph_size := PHDR_SIZE_64 if is64 else PHDR_SIZE_32
+ word := 8 if is64 else 4
+
+ data := make([]u8, total)
+ data[0] = 0x7f
+ data[1] = 'E'
+ data[2] = 'L'
+ data[3] = 'F'
+ data[4] = ELF_CLASS_64 if is64 else ELF_CLASS_32
+ data[5] = ELF_DATA_LSB if little else ELF_DATA_MSB
+ data[6] = 1
+
+ put_u16(data, 16, ET_DYN, little)
+ put_u16(data, 18, EM_X86_64 if is64 else EM_ARM, little)
+
+ if is64 {
+ put_u64(data, 32, u64(ph_off), little)
+ put_u16(data, 54, u16(ph_size), little)
+ put_u16(data, 56, 2, little)
+ } else {
+ put_u32(data, 28, u32(ph_off), little)
+ put_u16(data, 42, u16(ph_size), little)
+ put_u16(data, 44, 2, little)
+ }
+
+ write_phdr(data, ph_off, is64, little, Program_Header{
+ type = PT_LOAD, offset = 0, vaddr = DYN_BASE,
+ filesz = total, memsz = total, flags = PF_R | PF_X, align = 0x1000,
+ })
+
+ write_phdr(data, ph_off + ph_size, is64, little, Program_Header{
+ type = PT_DYNAMIC, offset = dyn_off, vaddr = DYN_BASE + dyn_off,
+ filesz = 64, memsz = 64, flags = PF_R, align = 8,
+ })
+
+ ent := 16 if is64 else 8
+ dyn := [][2]u64{
+ {DT_RELR, DYN_BASE + relr_off},
+ {DT_RELRSZ, u64(len(words) * word)},
+ {DT_RELRENT, u64(word)},
+ {DT_NULL, 0},
+ }
+ pos := dyn_off
+ for e in dyn {
+ if is64 {
+ put_u64(data, pos, e[0], little)
+ put_u64(data, pos + 8, e[1], little)
+ } else {
+ put_u32(data, pos, u32(e[0]), little)
+ put_u32(data, pos + 4, u32(e[1]), little)
+ }
+ pos += ent
+ }
+
+ pos = relr_off
+ for w in words {
+ if is64 {
+ put_u64(data, pos, w, little)
+ } else {
+ put_u32(data, pos, u32(w), little)
+ }
+ pos += word
+ }
+ return data
+}
+
+@(test)
+test_relr_decodes_address_then_bitmap :: proc(t: ^testing.T) {
+ words := []u64{0x3da0, 0x03, 0x8001}
+ data := build_relr_elf(true, true, words)
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ testing.expect_value(t, len(info.relocs), 3)
+ testing.expect_value(t, info.relocs[0].offset, 0x3da0)
+ testing.expect_value(t, info.relocs[1].offset, 0x3da8)
+ testing.expect_value(t, info.relocs[2].offset, 0x4010)
+
+ for r in info.relocs {
+ testing.expect_value(t, r.type, u32(R_X86_64_RELATIVE))
+ testing.expect_value(t, r.kind, Reloc_Source.Relr)
+ }
+}
+
+@(test)
+test_relr_empty_bitmap_emits_nothing :: proc(t: ^testing.T) {
+ words := []u64{0x1000, 0x01}
+ data := build_relr_elf(true, true, words)
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ testing.expect_value(t, len(info.relocs), 1)
+ testing.expect_value(t, info.relocs[0].offset, 0x1000)
+}
+
+@(test)
+test_relr_big_endian :: proc(t: ^testing.T) {
+ words := []u64{0x2000, 0x07}
+ data := build_relr_elf(true, false, words)
+ defer delete(data)
+
+ info, _ := load(data)
+ defer destroy(&info)
+
+ testing.expect_value(t, len(info.relocs), 3)
+ testing.expect_value(t, info.relocs[0].offset, 0x2000)
+ testing.expect_value(t, info.relocs[1].offset, 0x2008)
+ testing.expect_value(t, info.relocs[2].offset, 0x2010)
+}
+
+@(test)
+test_reloc_info_split_differs_by_class :: proc(t: ^testing.T) {
+ testing.expect_value(t, reloc_type_str(EM_X86_64, 7), "R_X86_64_JUMP_SLOT")
+ testing.expect_value(t, reloc_type_str(EM_AARCH64, 1026), "R_AARCH64_JUMP_SLOT")
+ testing.expect_value(t, reloc_type_str(EM_X86_64, 999), "999")
+
+ testing.expect(t, is_jump_slot(EM_X86_64, 7), "x86-64 JUMP_SLOT")
+ testing.expect(t, is_jump_slot(EM_AARCH64, 1026), "aarch64 JUMP_SLOT")
+ testing.expect(t, !is_jump_slot(EM_X86_64, 8), "RELATIVE is not a jump slot")
+}
diff --git a/main.odin b/main.odin
@@ -13,17 +13,21 @@ Options :: struct {
elf_path : string `args:"pos=0,required" usage:"ELF binary to analyse."`,
hexdump : bool `usage:"Hex dump entire file to stdout."`,
hexdump_out : string `usage:"Hex dump to file."`,
- disasm : bool `usage:"Disassemble code sections."`,
- disasm_out : string `usage:"Disassemble to file."`,
+ code : bool `usage:"Code region report: executable sections, entry point, function starts."`,
+ code_out : string `usage:"Code region report to file."`,
entropy : bool `usage:"Shannon entropy report."`,
entropy_out : string `usage:"Entropy report to file."`,
symbols : bool `usage:"Print symbol table (nm-like)."`,
sections : bool `usage:"Print section headers."`,
segments : bool `usage:"Print program headers (objdump -p style)."`,
show_map : bool `args:"name=map" usage:"Visual memory layout map."`,
- remap : string `usage:"Remap using this prototype file."`,
+ bin_diff : string `args:"name=bin-diff" usage:"Diff this binary's layout against a reference ELF."`,
+ call_func : string `args:"name=call-function" usage:"Load from memory and call this symbol."`,
+ call_args : string `args:"name=call-args" usage:"Comma-separated args for --call-function: integers, or s:string."`,
+ checksec : bool `usage:"Hardening report: RELRO, NX, canary, PIE, RPATH, FORTIFY."`,
+ relocs : bool `usage:"Relocation table and PLT/GOT map."`,
info : bool `usage:"Print ELF summary only."`,
- json : bool `usage:"Output in JSON format."`,
+ json : bool `usage:"Full structured dump as JSON (header, segments, sections, dynamic, symbols, relocations, GOT, checksec)."`,
verbose : bool `usage:"Verbose output."`,
}
@@ -58,8 +62,23 @@ main :: proc() {
elf.machine_to_str(info.header.machine), bits, elf_type, dyn_type)
fmt.fprintln(os.stderr, "")
+ if opts.json {
+ if opts.hexdump || opts.hexdump_out != "" ||
+ opts.entropy || opts.entropy_out != "" ||
+ opts.code || opts.code_out != "" ||
+ opts.show_map || opts.bin_diff != "" || opts.call_func != "" {
+ fmt.fprintln(os.stderr, "--json is not supported with --hexdump, --entropy, --code, --map, --bin-diff or --call-function")
+ os.exit(1)
+ }
+ if err := elf.print_json(&info); err != "" {
+ fmt.fprintln(os.stderr, err)
+ os.exit(1)
+ }
+ return
+ }
+
if opts.info {
- elf.print_info(&info, opts.verbose, opts.json)
+ elf.print_info(&info, opts.verbose)
return
}
@@ -77,9 +96,21 @@ main :: proc() {
fmt.println("")
}
- if opts.disasm || opts.disasm_out != "" {
- section_header("Disassembly", opts.disasm_out)
- report("Disasm", analysis.disasm_output(elf_data, &info, opts.disasm_out), &failed)
+ if opts.code || opts.code_out != "" {
+ section_header("Code Regions", opts.code_out)
+ report("Code", analysis.code_output(elf_data, &info, opts.code_out), &failed)
+ fmt.println("")
+ }
+
+ if opts.checksec {
+ section_header("Checksec", "")
+ elf.print_checksec(&info)
+ fmt.println("")
+ }
+
+ if opts.relocs {
+ section_header("Relocations", "")
+ elf.print_relocs(&info)
fmt.println("")
}
@@ -107,9 +138,15 @@ main :: proc() {
fmt.println("")
}
- if opts.remap != "" {
- section_header("Remap", "")
- report("Remap", analysis.remap_output(elf_data, &info, opts.remap, ""), &failed)
+ if opts.call_func != "" {
+ section_header("Call Function", "")
+ report("Call", analysis.call_function(elf_data, &info, opts.elf_path, opts.call_func, opts.call_args), &failed)
+ fmt.println("")
+ }
+
+ if opts.bin_diff != "" {
+ section_header("Binary Diff", "")
+ report("BinDiff", analysis.bin_diff_output(elf_data, &info, opts.bin_diff, ""), &failed)
fmt.println("")
}
| | | | | | | | | | | | | | | | | | | | |