FIRST EDITION – 0.
1 release
Kevin Thomas
Copyright © 2023 My Techno Talent
1
Forward
I remember a time before the days of the internet where computers
were simple yet elegant and beautiful in their design, logic and
functionality.
I was a teenager in the 1980’s when I got my first Commodore 64 for
Christmas and the first thing I did was tear it out of the box and
get it wired up to my console TV as the only thing I needed to see
was that blinking console cursor on that blue background with the
light blue border.
It was a blank slate. There were no libraries. There were no
frameworks. If you wanted to develop something outside of the
handful of games that you could get for it, you program it from
scratch.
In addition to the C-64 there was a 300 baud modem with a 5.25”
floppy disk which read DMBBS 4.8. I quickly read the small
documentation that came with it and quickly took over the only phone
line in the household.
I set up my BBS or bulletin board system, and called it THE
ALLNIGHTER. I set it up and no one called obviously as no one knew
it existed. I joined a local CUF group, computer user federation,
where I met another DMBBS 4.8 user which helped me network my message
boards to him.
At a given time of day my computer would call his and send my
messages to his board and I would receive his messages from his
board. It was computer networking before the internet and it was
simply magic.
Over the next few months he taught me 6502 Assembler which was my
first programming language that I ever learned. Every single
instruction was given consideration of the hardware and a mastery
over the computer was developed as we did literally everything from
scratch on the bare metal of the hardware.
Today we live in an environment of large distributed systems where
there are thousands of libraries and dozens of containers within pods
in a large orchestrated Kubernetes cluster which defines an
application.
2
Between the 1980’s and current, the birth of higher-level languages
has made it possible to develop in a timely manner even on the most
sophisticated distributed systems.
As we work within a series of large cloud ecosystems, there exists a
programming language called Golang, or Go for short, which allows for
easy software development to take advantage of multiple cores within
a modern CPU in addition to out-of-the-box currency and ease of scale
for enterprise-level network and product design.
With every great technology there arises threat actors that exploit
such power.
Go can be compiled easily for multiple operating systems producing a
single binary. The speed and power of Go makes it an easy choice for
modern Malware Developers.
There are literally thousands of books and videos on how to reverse
engineer traditional C binaries but little on Go as it is so
relatively new.
The aim of this book is to teach basic Go and step-by-step reverse
engineer each simple binary to understand what is going on under the
hood.
We will develop within the Windows architecture (Intel x64 CISC) as
most malware targets this platform by orders of magnitude.
In later chapters we will within a Raspberry Pi 64-bit ARM OS so that
you can get a perspective of what hacking that architecture looks
like in Golang at the binary level.
Let’s begin...
3
Table Of Contents
Chapter 1: Hello Distributed System World
Chapter 2: Debugging Hello Distributed System World
Chapter 3: Hacking Hello Distributed System World
Chapter 4: Primitive Types
Chapter 5: Debugging Primitive Types
Chapter 6: Hacking Primitive Types
Chapter 7: Control Flow
Chapter 8: Debugging Control Flow
Chapter 9: Hacking Control Flow
Chapter 10: Advanced Control Flow
Chapter 11: Debugging Advanced Flow Control
Chapter 12: Hacking Advanced Flow Control
4
Chapter 1: Hello Distributed System World
We begin our journey with developing a simple hello world program in
Go on a Windows 64-bit OS.
We will then reverse engineer the binary in IDA Free.
Let’s first download Go for Windows.
[Link]
Let’s download IDA Free.
[Link]
Let’s download Visual Studio Code which we will use as our integrated
development environment.
[Link]
Once installed, let’s add the Go extension within VS Code.
[Link]
Let’s create a new project and get started by following the below
steps.
New File
[Link]
Now let’s populate our [Link] file with the following.
package main
import "fmt"
func main() {
[Link]("hello distributed system world")
}
Let’s open up the terminal by click CTRL+SHIFT+` and type the
following.
go mod init main
go mod tidy
go build
5
Let’s run the binary!
.\[Link]
Output…
hello distributed system world
Congratulations! You just created your first hello world code in Go.
Time for cake!
We simply created a hello world style example to get us started.
In our next lesson we will debug this in IDA Free!
6
Chapter 2: Debugging Hello Distributed
System World
Let’s debug our app within IDA Free.
Open IDA Free and we see the load screen. We can keep all the
defaults and simply click OK.
In Go at the assembler level we will need to search for the entry
point of our app. This is the main_main function. You can use
CTRL+F to search.
7
Now we can double-click on the main_main to launch the focus to this
function and graph.
8
We can see in the bottom left box our “hello distributed system
world” text.
If we double-click on off_4CB850 it will take us to a new window
where the string lives within the binary.
Here we see something very interesting. Unlike a C binary where the
string is terminated by a null character, we see that there is the
raw string in a large pool and a 1eh value which represents the
length of the string in hex.
If we double-click on the “hello distributed system world” text we
will see the string pool within the binary.
9
All of the strings are within this string pool which is a very
different architectural design compared to other languages.
With this basic analysis we have a general idea of what is going on
within this simple binary.
These lessons are designed to be short and digestible so that you can
code and hack along.
In our next lesson we will learn how to hack this string and force
the binary to print something else to the terminal of our choosing.
This will give us the first taste on hacking Go!
10
Chapter 3: Hacking Hello Distributed
System World
Let’s hack our app within IDA Free.
In our last lesson we saw our large string pool. Lets load up IDA
and revisit that pool.
Let’s select Windows then Hex View-1.
Here we see our string’s hex values. It is literally as simple as
this as this will be a very short and rewarding chapter.
Select Edit, Patch program then Change byte…
We can use the Ascii Table at [Link] to change
our string from, hello distributed system world to hacky distributed
system world by simply patching the bytes
11
After we change hello to hacky we have the following.
Now we observe the following change.
Let’s select Edit, Patch program, Apply patches to input file…
Now that we have successfully patched our program, let’s re-run it.
Let’s seek out main_main again in the function tree.
12
Here we can see our revised function.
13
Let’s set a breakpoint by pressing F2 on the call to fmt_Fprintln.
Finally let’s debug!
14
We hit our breakpoint.
Let’s step over the call and watch what happens in the console
window.
Success!
In our next lesson we will begin to understand primitive types in Go.
15
Chapter 4: Primitive Types
Golang has three basic types which are bool, numeric and string.
Once a variable is declared it is automatically populated with a null
value.
Let’s create a new project and get started by following the below
steps.
New File
[Link]
Now let’s populate our [Link] file with the following.
package main
import "fmt"
func main() {
b := true
i := 42
f := 3.14
s := "42"
[Link]("bool: ", b)
[Link]("int: ", i)
[Link]("float32: ", f)
[Link]("string: ", s)
}
Let’s open up the terminal by click CTRL+SHIFT+` and type the
following.
go mod init main
go mod tidy
go build
Let’s run the binary!
.\[Link]
Output…
bool: true
int: 42
float32: 3.14
string: 42
We can clearly see the respective values and how Golang handles them.
In our next lesson we will debug this simple program.
16
Chapter 5: Debugging Primitive Types
Let’s debug our app within IDA Free.
Let’s locate main_main and begin our analysis. In Chapter 2 we went
step-by-step to accomplish this so please refer back if needed.
Let’s set a breakpoint on the lea instruction.
Before we get started I would sync the hex view with RIP as follows.
This way with each step we can see what is going on in the bin.
17
We step until the lea instruction highlighed below.
Let’s double-click on the off_71B8E8 and see what it contains.
We can see there is a string reference here which is, “bool: “, which
should seem familiar from our last lesson. We also see the
RTYPE_string which indicates our type for the “bool: “ and RTYPE_bool
for the true which we will see shortly is a 1.
We also know how Golang handles string lengths. We can see the value
of 6 which indicates the length of the string which as we have
mentioned at length differs from other languages completely as there
is not null terminated.
When we double-click on aBool_2 we get taken to the string pool.
We can see the strings are literally up against one another as this
gives us deeper insight into Golang.
18
As mentioned we also drill down into the true or 1.
Then…
As we continue to press F7 and single-step we will see the calls to
the Golang Stdout file descriptor and the [Link] interface which
allows you to write data to a wide variety of output streams and in
our case stdout.
Finally we call Fprintln to print our string into the terminal.
Our result so far…
We see the int and as well.
19
We see here the literal value of 0x2a is moved into EAX which is the
lower half of RAX which of course is 42 decimal. We see a call to
runtime_convT64 which if you step through it
After calling Fprintln…
Regarding the float we see a very large number being put into RAX.
Digging into the call of runtime_convT64.
20
We see call to runtime_morestack_noctxt which allocates a new stack
for a goroutne and a call to the garbage collector which is
runtime_mallocgc.
As we continue we have to take a step back to the beginning of
main_main where we see a number of xmmwords.
21
The xmmword pointer is a directive that is used to specify the size
and type of a memory operand as it indicates the operand is a 128-bit
value that is stored in the SSE register or memory.
The xmmword pointer is used with other instructions that operate on a
floating-point values using the SSE2 SIMD (Single Instruction,
Multiple Data) instructions.
In our case it does not do any math on it as it simply handles the
conversion of our 3.14 into a printable format.
Keep in mind we used other xmmword pointers for our integers as well
as well as other numbers however var_58 and var_68 is used for our
float.
The review of the following within RDX, RAX and RCX create our float.
The debug055 and debug061 refers to the name of code or data at that
address.
22
The rest of the db values simply hold 0.
We see a similar situation with RCX.
We see that the values have been converted to a printable format
with the help of Golang Stdout file descriptor and the [Link]
interface which allows you to write data to a wide variety of output
streams as we mentioned.
Finally we see the same behavior with our string. We have done this
before in our last debug so we do not have to cover this again but as
an exercise please step through the Assembler.
23
This now gives you a good handle of how Golang handles its
implementation under the hood.
In our next chapter we will hack some of these values.
24
Chapter 6: Hacking Primitive Types
Let’s hack our app within IDA Free.
Lets load up IDA and put a breakpoint on our bool string.
Double clicking on the offset we should see the following as we saw
in the last lesson.
Lets double click on the label.
We see our familiar string pool. Let’s select Edit, Patch program,
Apply patches to input file…
0x62 we know is ‘b’ so lets change that to an ‘f’.
25
Let’s create another breakpoint and dig in again.
Here we can double click on the label.
We can see that we were set to true so lets make that a 0 instead.
26
Let’s select Edit, Patch program, Apply patches to input file…
Now that we have successfully patched our program, let’s re-run it.
Ahh yes! You can follow the same technique to hack the rest of the
program but this is all you need to get the job done.
In our next chapter we will explore control flow.
27
Chapter 7: Control Flow
Golang has three basic kinds of basic control flow which is if-else,
for and switch-case. We will focus on the if-else as they will not
be that different in the assembler.
Let’s create a new project and get started by following the below
steps.
New File
[Link]
Now let’s populate our [Link] file with the following.
package main
import "fmt"
func main() {
num := 42
if num == 42 {
[Link](num, "the answer to life")
} else {
[Link](num, "not the answer to life")
}
}
Let’s open up the terminal by click CTRL+SHIFT+` and type the
following.
go mod init main
go mod tidy
go build
Let’s run the binary!
.\[Link]
Output…
42 the answer to life
This trivial example demonstrates basic control flow in Go. In our
next lesson we will debug this simple program.
28
Chapter 8: Debugging Control Flow
Let’s debug our app within IDA Free.
Open IDA Free and we see the load screen. We can keep all the
defaults and simply click OK. Let’s load main_main.
So take a moment and look at this disassembly. What do you NOT see
that was in our original source code?
If you said, “not the answer to life”, you would be correct.
So what happened?
29
Here the compiler optimized away this else statement as there were no
conditions where it would be used therefore we ONLY see. “the answer
to life”.
I deliberately created this example to show that everything is not as
it seems on the surface. We must be aware of compiler optimization
as this will happen over and over in every language.
Let’s close this and mod our original source code to force an option
to not optimize away.
Let’s close IDA Free and go back to VS Code.
Let’s create a new project and get started by following the below
steps.
New File
[Link]
Now let’s populate our [Link] file with the following.
package main
import (
"bufio"
"fmt"
"os"
"strconv"
)
func main() {
scanner := [Link]([Link])
[Link]("Enter your favorite number: ")
[Link]()
input := [Link]()
num, err := [Link](input)
if err != nil {
[Link]("Invalid input")
return
}
if num == 42 {
[Link](num, "the answer to life")
} else {
[Link](num, "not the answer to life")
}
}
30
Let’s open up the terminal by click CTRL+SHIFT+` and type the
following.
go mod init main
go mod tidy
go build
Let’s run the binary!
.\[Link]
Output…
Enter your favorite number: 42
42 the answer to life
Enter your favorite number: 66
66 not the answer to life
Enter your favorite number: ff
Invalid input
Here we see three independent runs of the code to show that if we
enter 42 we get, “the answer to life” and if we enter in another
valid integer we get, “not the answer to life” and finally if we
enter in a non-integer we have proper error correction.
Let’s debug our new app within IDA Free.
Open IDA Free and we see the load screen. We can keep all the
defaults and simply click OK. Let’s load main_main.
We start off with our stdin input to obtain a response from a user.
31
If the user enters in something invalid, non-integer, we hit this
block.
Otherwise we can see our two other choice blocks.
Here we see if they enter in decimal 42. We see a mov into eax, 2ah.
Hmm, is that 42? Well yes it is the hex equivalent to 42 and
therefore we get, “the answer to life” otherwise if another valid
integer we get, “not the answer to life”.
In our next less we will hack this simple application!
32
Chapter 9: Hacking Control Flow
Let’s hack our app within IDA Free.
Lets load up IDA and put a breakpoint on our jump if not zero after
the prompt to enter your favorite number.
Let’s patch the assembler to jump if zero so that we get our positive
condition of, “the answer to life”.
Let’s select Edit, Patch program, Assemble…
We simply changed the instruction to jump if zero.
We can remove the breakpoint and do the same thing to the
jnz condition below otherwise if we left it at this point we would
get, “not the answer to life”, however we would have still hacked the
invalid input check successfully.
Now that both instructions are patched ensure you do the following.
33
Let’s select Edit, Patch Program, Apply patches to input file…
We can clearly see here at the top that both instructions have been
patched and we can see how the outcome has been altered.
Now lets set a breakpoint on the return at the bottom and at this
point it should be our only breakpoint.
Now run the debugger and enter in a Y which would be normally invalid
and look at the result in the terminal.
Here we can clearly see how we hacked this operation to our liking.
In our next lesson we will cover Advanced Control Flow.
34
Chapter 10: Advanced Control Flow
Today we will focus on the switch-case control flow.
Let’s create a new project and get started by following the below
steps.
New File
[Link]
Now let’s populate our [Link] file with the following.
package main
import (
"fmt"
)
func main() {
i := 42
switch i {
case 42:
[Link]("forty-two")
case 1337:
[Link]("thirteen thirty seven")
case 3:
[Link]("three")
}
}
Let’s open up the terminal by click CTRL+SHIFT+` and type the
following.
go mod init main
go mod tidy
go build
Let’s run the binary!
.\[Link]
Output…
forty-two
This example demonstrates switch-case control flow in Go. In our
next lesson we will debug this simple program.
35
Chapter 11: Debugging Advanced Control
Flow
Let’s debug our app within IDA Free.
Open IDA Free and we see the load screen. We can keep all the
defaults and simply click OK. Let’s load main_main.
This will be a very simple lesson. I want you to take a moment and
read the disassembled code. Do you notice anything?
Our original source code utilized a switch statement however the
input was hardcoded. If you remember our original control flow
lessons we had the same issue where the compiler optimized away the
other options as they will never be reached.
I deliberately created this example as you have seen the flow before
however wanted to show you what a switch case looked like at the
machine level.
Put a breakpoint on the jbe within the first block.
36
You will see that we are comparing rsp with what is pointed to at
r14+10h. We know under normal conditions this will flow to the left
block.
So much about reverse engineering is understanding the flow even when
compiler optimizations come into play. This is why these lessons are
good to experiment with so when you face this in the wild you will
have a better understanding of what is going on, bit-by-bit.
In our next lesson we will hack this to go into the right block.
37
Chapter 12: Hacking Advanced Control Flow
Let’s hack our app within IDA Free.
Lets load up IDA and put a breakpoint on our jump if below or equal
prompt to enter loc_47971C.
When we press F7 we see we go into the left block.
38
We step through and we know it will print out forty-two in our
console.
Let’s hack that value to jump if not below or equal, patch and rerun.
Remember you need to patch and apply patches.
It changed to jump if above but that is ok. Let’s step and we can
clearly see us moving into the right block.
39
When we run it through we see it terminate and our console remain
empty.
These are simple hacks but taking the time to practice these will
help you master the binary manipulation under the hood.
I hope these twelve chapters helped you to get a good handle on
hacking with Golang!
40