From 05b6b79e7cb914acc958e8ec7ef9a6abe5fe6ff1 Mon Sep 17 00:00:00 2001 From: Aleksandr Nogikh Date: Fri, 23 Jul 2021 16:09:45 +0000 Subject: pkg/report: add opcode decompilation functionality Create a module responsible for converting a sequence of machine code bytes into human-readable instructions. --- pkg/report/decompile.go | 122 +++++++++++++++++++++++++++++++++++++++++++ pkg/report/decompile_test.go | 52 ++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 pkg/report/decompile.go create mode 100644 pkg/report/decompile_test.go (limited to 'pkg') diff --git a/pkg/report/decompile.go b/pkg/report/decompile.go new file mode 100644 index 000000000..ea352fc29 --- /dev/null +++ b/pkg/report/decompile.go @@ -0,0 +1,122 @@ +// Copyright 2021 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package report + +import ( + "bufio" + "bytes" + "fmt" + "os" + "regexp" + "strconv" + "time" + + "github.com/google/syzkaller/pkg/osutil" + "github.com/google/syzkaller/sys/targets" +) + +type DecompilerFlagMask uint64 + +// Extra flags that control the flow of decompilation. +const ( + FlagForceArmThumbMode DecompilerFlagMask = 1 << iota +) + +const objdumpCallTimeout = 10 * time.Second + +type DecompiledOpcode struct { + Offset int + IsBad bool + FullDescription string +} + +// Decompiles a byte array with opcodes into human-readable descriptions. +// Target must specify the environment from which the opcodes were taken. +func DecompileOpcodes(rawOpcodes []byte, flags DecompilerFlagMask, target *targets.Target) ([]DecompiledOpcode, error) { + args, err := objdumpBuildArgs(flags, target) + if err != nil { + return nil, err + } + + outBytes, err := objdumpExecutor(rawOpcodes, args, target) + if err != nil { + return nil, err + } + + list := objdumpParseOutput(outBytes) + if len(list) == 0 && len(rawOpcodes) > 0 { + return nil, fmt.Errorf("no instructions found while the total size is %v bytes", len(rawOpcodes)) + } + return list, nil +} + +func objdumpExecutor(rawOpcodes []byte, args []string, target *targets.Target) ([]byte, error) { + fileName, err := osutil.TempFile("syz-opcode-decompiler") + if err != nil { + return nil, fmt.Errorf("failed to create temp file: %v", err) + } + defer os.Remove(fileName) + + err = osutil.WriteFile(fileName, rawOpcodes) + if err != nil { + return nil, fmt.Errorf("failed to write to temp file: %v", err) + } + + return osutil.RunCmd(objdumpCallTimeout, "", target.Objdump, append(args, fileName)...) +} + +// nolint: lll +var objdumpAsmLineRegexp = regexp.MustCompile(`\s+([a-fA-F0-9]+)\:\s+((?:[a-fA-F0-9]{2,8}\s*)*[a-fA-F0-9]{2,8})\s+(.*?)\s*$`) + +func objdumpParseOutput(rawOutput []byte) []DecompiledOpcode { + ret := []DecompiledOpcode{} + for s := bufio.NewScanner(bytes.NewReader(rawOutput)); s.Scan(); { + result := objdumpAsmLineRegexp.FindStringSubmatch(string(s.Bytes())) + if result == nil { + continue + } + offset, err := strconv.ParseUint(result[1], 16, 64) + if err != nil { + continue + } + const objdumpBadCommand = "(bad)" + ret = append(ret, DecompiledOpcode{ + Offset: int(offset), + IsBad: result[3] == objdumpBadCommand, + FullDescription: result[0], + }) + } + return ret +} + +func objdumpBuildArgs(flags DecompilerFlagMask, target *targets.Target) ([]string, error) { + // objdump won't be able to decompile a raw binary file unless we specify the exact + // architecture through the -m parameter. + ret := []string{"-b", "binary", "-D"} + switch target.Arch { + case targets.ARM64: + ret = append(ret, "-maarch64") + case targets.ARM: + ret = append(ret, "-marm") + if flags&FlagForceArmThumbMode != 0 { + ret = append(ret, "-M", "force-thumb") + } + case targets.I386: + ret = append(ret, "-mi386") + case targets.AMD64: + ret = append(ret, "-mi386", "-Mx86-64") + case targets.MIPS64LE: + ret = append(ret, "-mmips") + case targets.PPC64LE: + ret = append(ret, "-mppc") + case targets.S390x: + ret = append(ret, "-m", "s390:64-bit") + case targets.RiscV64: + ret = append(ret, "-mriscv") + default: + return nil, fmt.Errorf("cannot build objdump args for %#v", target.Arch) + } + + return ret, nil +} diff --git a/pkg/report/decompile_test.go b/pkg/report/decompile_test.go new file mode 100644 index 000000000..15843de83 --- /dev/null +++ b/pkg/report/decompile_test.go @@ -0,0 +1,52 @@ +// Copyright 2021 syzkaller project authors. All rights reserved. +// Use of this source code is governed by Apache 2 LICENSE that can be found in the LICENSE file. + +package report + +import ( + "reflect" + "testing" +) + +func TestParseObjdumpOutput(t *testing.T) { + rawResponse := ` +/tmp/binary: file format binary + + +Disassembly of section .data: + +00000000 <.data>: + 0: 55 push %ebp + 1: 53 push %ebx + 2: 31 c0 xor %eax,%eax + 4: e8 f5 bf f7 ff call 0xfff7bffe + 9: ff (bad) +` + opcodes := objdumpParseOutput([]byte(rawResponse)) + expected := []DecompiledOpcode{ + { + Offset: 0, + FullDescription: " 0: 55 push %ebp", + }, + { + Offset: 1, + FullDescription: " 1: 53 push %ebx", + }, + { + Offset: 2, + FullDescription: " 2: 31 c0 xor %eax,%eax", + }, + { + Offset: 4, + FullDescription: " 4: e8 f5 bf f7 ff call 0xfff7bffe", + }, + { + Offset: 9, + IsBad: true, + FullDescription: " 9: ff (bad)", + }, + } + if !reflect.DeepEqual(opcodes, expected) { + t.Errorf("Expected: %#v, got: %#v", expected, opcodes) + } +} -- cgit mrf-deployment