Writing my first forensic tool in Go
Last updated: 2022-01-13
Overview
While reading Hands On System Programming with Go, I discovered the debug/pe
package. To learn more about using Go I decided to spend a few days building a static forensic tool centered around use of this package. The debug/pe package provides functions that make analyzing a PE file easier. A glimpse of promising functions include: Open, ImportedLibraries, FileHeader, OpentionHeader32/64, and StringTable. Limiting myself to a span of a week, I’m reporting on functions I’ve applied to penut
, a simple PE file static analysis tool, and what the next steps are to build upon the application.
The debug/pe
Package
Load PE file for analysis
pe.Open()
will load your executable and prep it for use as type *File. A File has the following struct:
type File struct {
FileHeader
OptionalHeader interface{} // of type *OptionalHeader32 or *OptionalHeader64; added in Go 1.3
Sections []*Section
Symbols []*Symbol // COFF symbols with auxiliary symbol records removed; added in Go 1.1
COFFSymbols []COFFSymbol // all COFF symbols (including auxiliary symbol records); added in Go 1.8
StringTable StringTable // Go 1.8
// contains filtered or unexported fields
}
This is significant because it suggests that bare details of the executable are already available to the operator, ready to print to screen. Here is an example of pe.Open
within the content of the application.
pfile, err := pe.Open(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "pe.Open:%s\n", err)
os.Exit(1)
}
defer pfile.Close()
Sections
As shown in the File
struct above, we have access to Sections []*Section
. This suggests we should be able to output some information related to PE sections. Sections are a basic unit of code or data within a Portable Executable (PE) or Common Object File Format (COFF) file. Section contents vary by section type; each section could occupied by a function or an object, and images make up multiple sections. Some sections serve special purposes, such as .idata, which stores all imported symbols.
func printSections(f *pe.File) {
fmt.Println("Sections:\n")
for _, s := range f.Sections {
fmt.Printf("%s\n", s.Name)
fmt.Printf("%0#8x %s\n", s.VirtualSize, "Virtual Size")
fmt.Printf("%0#8x %s\n", s.VirtualAddress, "Virtual Address")
fmt.Printf("%0#8x %s\n", s.Size, "Size")
fmt.Printf("%0#8x %s\n", s.Offset, "Offset")
fmt.Printf("%0#8x %s\n", s.PointerToRelocations, "Pointer To Relocations")
fmt.Printf("%0#8x %s\n", s.PointerToLineNumbers, "Pointer to Line Numbers")
fmt.Printf("%0#8x %s\n", s.NumberOfRelocations, "Number of Relocations")
fmt.Printf("%0#8x %s\n", s.NumberOfLineNumbers, "Number of Line Numbers")
fmt.Printf("%0#8x %s\n", s.Characteristics, "Characteristics")
fmt.Println()
}
}
The beautiful code in the snippet above was made avaliable by joesephspurrier. Here is a portion of the resulting output:
$go run main.go main.exe
Sections:
.idata
0x000003fc Virtual Size
0x001fb000 Virtual Address
0x00000400 Size
0x001dd000 Offset
0x00000000 Pointer To Relocations
0x00000000 Pointer to Line Numbers
0x00000000 Number of Relocations
0x00000000 Number of Line Numbers
0xc0000040 Characteristics
.reloc
0x0000d6a8 Virtual Size
0x001fc000 Virtual Address
0x0000d800 Size
0x001dd400 Offset
0x00000000 Pointer To Relocations
0x00000000 Pointer to Line Numbers
0x00000000 Number of Relocations
0x00000000 Number of Line Numbers
0x42000040 Characteristics
Next Steps
Right now, the main.go
file must be modified to obtain any information, not much of it actionable. If I were to continue to work on penut
I would like to output the results to a report and instead print a summary out after running penut
. As I continued to experiment with the available types, structs, and functions provided by debug/pe
I found that I would need to better famliarize myself with the Go language in order to digest and parse the information provided by the pe
package.
File Header
We can, for instance, pull File Header information from the new File instance pfile
. The code below is an example of a function that would read in pfile
and print out the File Header information.
func printFileHeader(f *pe.File) {
a := f.FileHeader
fmt.Print("File Header: ")
fmt.Println(a)
}
And the resulting output:
$go run main.go main.exe
File Header: {332 15 0 2010112 3392 224 770}
Looking at the above output of printFileHeader()
we see the value of the pfile
OptionalHeader interface{}
. Microsoft Docs writes, “the PE file header consists of a Microsoft MS-DOS stub, the PE signature, the COFF file header, and an optional header. A COFF object file header consists of a COFF file header and an optional header.” The output of f.FileHeader
consists of 7 values, inconsistent with the 4 values aforementioned (MS-DOS stub, PE signature, COEFF file header, optional header). So what are we seeing here? I’m not yet sure.
String Table
As another example, the StringTable attribute prints out what seems to be a LONG string of bytes.
func printStringTable(f *pe.File) {
a := f.StringTable
fmt.Println(a)
}
This long string of bytes can be cast into a string for human reading.
func printStringTable(f *pe.File) {
a := string(f.StringTable)
fmt.Println(a)
}
The long string appears to be deliniated by a period .
. By itself, the data is not yet actionable, but you can look over the strings output a little easier by replacing the periods with newlines, as shown in the code and output below.
func printStringTable(f *pe.File) {
a := string(f.StringTable)
b := strings.Replace(a, ".", "\n", -1)
fmt.Println(b)
}
[ … ]
Conclusion
While penut
is far from being a functional static analysis tool, it was fun to experiment with the Go debug/pe
package for a few days. If you would like to see a working solution check out soluwalana’s go-pefile github repo.
Thanks for reading! I hope you found the information educational. For even more information, see the following references: