draugr~ single ELF file analysis tool |
| git clone https://github.com/djnnvx/draugr.git |
| log | files | refs | archive | README |
main.odin (7250B)
1 package main 2 3 import "core:flags" 4 import "core:fmt" 5 import "core:os" 6 7 import "./elf" 8 import "./analysis" 9 10 // Author: djnn 11 12 Options :: struct { 13 elf_path : string `args:"pos=0,required" usage:"ELF binary to analyse."`, 14 hexdump : bool `usage:"Hex dump entire file to stdout."`, 15 hexdump_out : string `usage:"Hex dump to file."`, 16 code : bool `usage:"Code region report: executable sections, entry point, function starts."`, 17 code_out : string `usage:"Code region report to file."`, 18 entropy : bool `usage:"Shannon entropy report."`, 19 entropy_out : string `usage:"Entropy report to file."`, 20 symbols : bool `usage:"Print symbol table (nm-like)."`, 21 sections : bool `usage:"Print section headers."`, 22 segments : bool `usage:"Print program headers (objdump -p style)."`, 23 show_map : bool `args:"name=map" usage:"Visual memory layout map."`, 24 bin_diff : string `args:"name=bin-diff" usage:"Diff this binary's layout against a reference ELF."`, 25 call_func : string `args:"name=call-function" usage:"Load from memory and call this symbol."`, 26 call_args : string `args:"name=call-args" usage:"Comma-separated args for --call-function: integers, or s:string."`, 27 patch : string `args:"name=patch" usage:"Semicolon-separated in-memory patches applied before --call-function. Specs: sym=ret:N, sym=jmp:target, got:sym=target."`, 28 checksec : bool `usage:"Hardening report: RELRO, NX, canary, PIE, RPATH, FORTIFY."`, 29 relocs : bool `usage:"Relocation table and PLT/GOT map."`, 30 info : bool `usage:"Print ELF summary only."`, 31 json : bool `usage:"Full structured dump as JSON (header, segments, sections, dynamic, symbols, relocations, GOT, checksec)."`, 32 verbose : bool `usage:"Verbose output."`, 33 } 34 35 main :: proc() { 36 opts: Options 37 flags.parse_or_exit(&opts, os.args, .Unix) 38 39 elf_data, read_err := os.read_entire_file_from_path(opts.elf_path, context.allocator) 40 if read_err != nil { 41 fmt.fprintf(os.stderr, "Failed to read file: %s\n", opts.elf_path) 42 os.exit(1) 43 } 44 45 if !elf.probe(elf_data) { 46 fmt.fprintf(os.stderr, "Not a valid ELF file: %s\n", opts.elf_path) 47 os.exit(1) 48 } 49 50 info, parse_err := elf.load(elf_data) 51 if parse_err != "" { 52 fmt.fprintf(os.stderr, "ELF parse error: %s\n", parse_err) 53 os.exit(1) 54 } 55 defer elf.destroy(&info) 56 57 bits := 64 if info.is_64bit else 32 58 elf_type := "PIE" if info.is_pie else "ET_EXEC" 59 dyn_type := "dynamic" if info.is_dynamic else "static" 60 61 fmt.fprintln(os.stderr, "----------------------------------------") 62 fmt.fprintf(os.stderr, "arch=%-12s class=ELF%d\t%s\t%s\n", 63 elf.machine_to_str(info.header.machine), bits, elf_type, dyn_type) 64 fmt.fprintln(os.stderr, "") 65 66 if opts.patch != "" && opts.call_func == "" { 67 fmt.fprintln(os.stderr, "--patch requires --call-function") 68 os.exit(1) 69 } 70 71 if opts.json { 72 if opts.hexdump || opts.hexdump_out != "" || 73 opts.entropy || opts.entropy_out != "" || 74 opts.code || opts.code_out != "" || 75 opts.show_map || opts.bin_diff != "" || opts.call_func != "" { 76 fmt.fprintln(os.stderr, "--json is not supported with --hexdump, --entropy, --code, --map, --bin-diff or --call-function") 77 os.exit(1) 78 } 79 if err := elf.print_json(&info); err != "" { 80 fmt.fprintln(os.stderr, err) 81 os.exit(1) 82 } 83 return 84 } 85 86 if opts.info { 87 elf.print_info(&info, opts.verbose) 88 return 89 } 90 91 failed := false 92 93 if opts.hexdump || opts.hexdump_out != "" { 94 section_header("Hexdump", opts.hexdump_out) 95 report("Hexdump", analysis.hexdump(elf_data, 0, opts.hexdump_out), &failed) 96 fmt.println("") 97 } 98 99 if opts.entropy || opts.entropy_out != "" { 100 section_header("Entropy Analysis", opts.entropy_out) 101 report("Entropy", analysis.entropy_output(elf_data, opts.entropy_out), &failed) 102 fmt.println("") 103 } 104 105 if opts.code || opts.code_out != "" { 106 section_header("Code Regions", opts.code_out) 107 report("Code", analysis.code_output(elf_data, &info, opts.code_out), &failed) 108 fmt.println("") 109 } 110 111 if opts.checksec { 112 section_header("Checksec", "") 113 elf.print_checksec(&info) 114 fmt.println("") 115 } 116 117 if opts.relocs { 118 section_header("Relocations", "") 119 elf.print_relocs(&info) 120 fmt.println("") 121 } 122 123 if opts.symbols { 124 section_header("Symbol Table (nm-style)", "") 125 print_symbols(&info) 126 fmt.println("") 127 } 128 129 if opts.sections { 130 section_header("Section Headers", "") 131 print_sections(&info) 132 fmt.println("") 133 } 134 135 if opts.segments { 136 section_header("Program Headers (objdump-style)", "") 137 print_segments(&info) 138 fmt.println("") 139 } 140 141 if opts.show_map { 142 section_header("Memory Map", "") 143 report("Map", analysis.map_output(elf_data, &info, ""), &failed) 144 fmt.println("") 145 } 146 147 if opts.call_func != "" { 148 section_header("Call Function", "") 149 report("Call", analysis.call_function(elf_data, &info, opts.elf_path, opts.call_func, opts.call_args, opts.patch), &failed) 150 fmt.println("") 151 } 152 153 if opts.bin_diff != "" { 154 section_header("Binary Diff", "") 155 report("BinDiff", analysis.bin_diff_output(elf_data, &info, opts.bin_diff, ""), &failed) 156 fmt.println("") 157 } 158 159 if failed { 160 os.exit(1) 161 } 162 } 163 164 report :: proc(label: string, err: string, failed: ^bool) { 165 if err == "" { 166 return 167 } 168 fmt.fprintf(os.stderr, "%s error: %s\n", label, err) 169 failed^ = true 170 } 171 172 section_header :: proc(title: string, out_path: string) { 173 fmt.fprintln(os.stderr, "----------------------------------------") 174 if out_path == "" { 175 fmt.fprintf(os.stderr, "%s:\n", title) 176 } else { 177 fmt.fprintf(os.stderr, "%s -> %s\n", title, out_path) 178 } 179 } 180 181 print_symbols :: proc(info: ^elf.ELF_Info) { 182 print_symbol_table(info.symbols[:]) 183 print_symbol_table(info.dyn_symbols[:]) 184 } 185 186 print_symbol_table :: proc(syms: []elf.Symbol) { 187 for sym in syms { 188 if sym.name == "" { 189 continue 190 } 191 if sym.shndx == elf.SHN_UNDEF { 192 fmt.printf("%18s %c %s\n", "", sym_to_nm_type(sym), sym.name) 193 } else { 194 fmt.printf("0x%016x %c %s\n", sym.value, sym_to_nm_type(sym), sym.name) 195 } 196 } 197 } 198 199 // Odin's fmt zero-fills numeric widths, so decimal columns pad via a string. 200 print_sections :: proc(info: ^elf.ELF_Info) { 201 fmt.printf("%-4s %-20s %-10s %-10s %-10s %s\n", 202 "Idx", "Name", "Type", "Addr", "Offset", "Size") 203 204 for shdr, i in info.section_hdrs { 205 name := elf.get_section_name(info, shdr) 206 if name == "" { 207 name = fmt.tprintf("[%d]", i) 208 } 209 fmt.printf("%-4s %-20s %-10s 0x%08x 0x%08x 0x%x\n", 210 fmt.tprintf("%d", i), name, elf.section_type_str(shdr.type), 211 shdr.addr, shdr.offset, shdr.size) 212 } 213 } 214 215 print_segments :: proc(info: ^elf.ELF_Info) { 216 fmt.printf("%-13s %-5s %-10s %-10s %-10s %s\n", 217 "Type", "Flags", "Offset", "VirtAddr", "FileSize", "MemSize") 218 219 for ph in info.program_hdrs { 220 fmt.printf("%-13s %-5s 0x%08x 0x%08x 0x%08x 0x%08x\n", 221 elf.segment_type_str(ph.type), 222 elf.flags_to_str(ph.flags), 223 ph.offset, ph.vaddr, ph.filesz, ph.memsz) 224 } 225 } 226 227 sym_to_nm_type :: proc(sym: elf.Symbol) -> u8 { 228 if sym.shndx == elf.SHN_UNDEF { 229 return 'U' 230 } 231 232 letter: u8 = '?' 233 switch sym.type { 234 case elf.STT_NOTYPE: letter = ' ' 235 case elf.STT_OBJECT: letter = 'O' 236 case elf.STT_FUNC: letter = 'T' 237 case elf.STT_SECTION: letter = 'S' 238 case elf.STT_FILE: letter = 'F' 239 } 240 241 if sym.bind == elf.STB_LOCAL && letter >= 'A' && letter <= 'Z' { 242 letter += 'a' - 'A' 243 } 244 return letter 245 }