TACIX.AT About
Ransomware in Golang - Part 1
2020/11/09 13:37

In case you missed it, check out Part 0 which will serve as our design doc and outline while we’re building this.

Setup

To get started you’ll need to download Go and follow the install instructions for your operating system. Then you’ll create a directory for this project, I’ll be working out of ~/prog/rw/ where ~ is my home directory on whatever OS I’m working on.

Golang is cross platform, and we’re not doing anything platform specific, so you’re free to work wherever you’d like. However, if you’re looking to learn some basic skills, now would be a great time to start working with virtual machines!

I’ve made some YouTube videos on how to get started with VirtualBox, you can get the links at cybering.cc. It’s super easy, if you can click through two installers, you should have no problem. The videos help through a couple gotchas. So if you’re looking to learn about VMs or Linux, both of which are great skills for jobbos, give it a shot! Your malware will then be contained in a VM, as all malware should be.

Victim Directory

Since we will be repeatedly running and debugging our encryptor, we will need to set up a victim directory so we’re not encrypting our own data, especially when we do not have a decryptor! This directory should be replaceable so we can reinitialize it with ease. We’ll have a master directory and a clone of it to be our directory under test. This way, restoring should be as simple as deleting the directory under test and replacing it with master.

Let’s make _victim/ and seed it with some data. We have a few requirements. We want to have a handful of files that we can check for validity. When you are decrypting there are small things you can mess up, so we want to make sure our files match their originals exactly. Having multiple file types will also help us when we start filtering by extension. A single flat folder will not be realistic, so we will create a directory structure in order to simulate a real file system traversal.

Yours doesn’t have to match mine exactly, but here is a good template structure.

_victim/
	simple.txt
	pics/
		raylan.jpg
		hello.jpg
	docs/
		Resume.pdf
	prog/
		main.go
		main.exe

The file simple.txt is just a text file that has Hello world! in it. In pics/ I have a picture of my dog and another file I found on the internet. The docs/ folder contains my resume which I had sitting around.

For the dummy prog/ folder, we can use this to test our Golang installation. My main.go file has the following contents.

package main

import (
	"fmt"
)

func main() {
	fmt.Println("Greetings from Go!")
}

We’ll be working a lot from the command line. If you’re on Linux the provided terminal is great. On Windows, you could use command prompt, but I generally work from ConEmu. If you want to learn some command line basics, I have some videos here.

Normally, as I’m working I just run my Go files with something like go run main.go on the command line. To produce an executable though, we can use go build main.go. On Windows this will produce main.exe, if you are working on Linux (probably OSX too) you will get an exectuable called main, either is fine.

Restore and Verify

We’re going to create a small utility for restoring our victim directory, and also for verifying that things have been decrypted correctly. Let’s make a new directory ~/prog/rw/util, then create the file util.go in there.

Restoration

Restoration is pretty simple in an OS specific context. On Linux it would simply be rm -r victim and cp -r _victim victim. To reliably copy a directory recursively though, there are a lot of edge cases. Thankfully other people have already done the work on this. We’re going to use the package github.com/otiai10/copy.

Since github.com/otiai10/copy is an external package, in order to import it we will need to run go get github.com/otiai10/copy on the command line. That will download the package and store it so we don’t get an error like the following when we try to run our util program.

util\util.go:5:2: cannot find package "github.com/otiai10/copy" in any of:
        c:\go\src\github.com\otiai10\copy (from $GOROOT)
        C:\Users\tacixat\go\src\github.com\otiai10\copy (from $GOPATH)

We’ll start with this in util.go. Give it a read through and I’ll break it down section by section after. You can also try running it with go run util/util.go. Try adding on the flags -verify and -restore to the end of that and seeing how it handles them.

package main

import (
	"flag"
	"github.com/otiai10/copy"
	"log"
	"os"
)

func main() {
	restore := flag.Bool(
		"restore", false, "restore victim directory from _victim")
	verify := flag.Bool(
		"verify", false, "verify files in victim directory")

	flag.Parse()

	if !*restore && !*verify || *restore && *verify {
		log.Fatal("Requires one of -verify or -restore.")
	}

	if *restore {
		err := os.RemoveAll("victim")
		if err != nil {
			log.Fatal(err)
		}

		err = copy.Copy("_victim", "victim")
		if err != nil {
			log.Fatal(err)
		}
	} else if *verify {
		log.Fatal("Verify not implemented yet!")
	}
}

We start by importing a few packages. In Golang the packages should be listed in alphabetical order. You can do this automatically with go fmt util/util.go, which will tidy up other things about the file as well.

In our main() function we start by declaring two flags. The := operator in Go is a short hand for declaring and defining a variable. This could also be done in two steps, like var restore *Bool and restore = flag.Bool(...) (the * here is saying it is a pointer type). The shorthand is nicer. Remember though, any assignment after declaration would just use =.

	restore := flag.Bool(
		"restore", false, "restore victim directory from _victim")
	verify := flag.Bool(
		"verify", false, "verify files in victim directory")

These are pointers to booleans. Pointers are an address in memory that holds a value (in this case, a boolean value). In order to access the value, we dereference it with * when accessing the variable.

The next block is checking if either both restore and verify are false or both restore and verify are true. This is effectively an exclusive-or (xor). The && (and) operator has a higher precendence than || (or). Think order of operations with multiplication and addition. If we either have both true or neither, we’re going to call log.Fatal(...) which prints the message and exits the program.

The log package has a very similar API to fmt. The primary difference is that it prints to stderr instead of stdout, so it useful for program-related messages, rather than those meant for the end user. It also has nice utility functions like Fatal so we don’t need to call both fmt.Println(...) and sys.Exit(1).

If you need a referesher on boolean logic, check out video 02.04.

	if !*restore && !*verify || *restore && *verify {
		log.Fatal("Requires one of -verify or -restore.")
	}

If all goes well, and we have run the program with either -verify or -restore then we’ll move onto our if statement. Looking at the second part first, we just bail if we call -verify right now. We’ll implement that in a minute.

	} else if *verify {
		log.Fatal("Verify not implemented yet!")
	}

In our restore block we have two actions. The first is calling os.RemoveAll() on our victim/ directory. This will recursively delete (meaning, delete the folder, and all of its contents) the current victim/ dir. This is great when we have half of our ransomware working and we can encrypt the files but not recover them!

The pattern of err = ... and if err != nil { ... } is the standard pattern in Go for handling errors. This is to replace C-style errors where some functions return 0 for OK, and some return NULL (0) in error cases. Instead we just get an error, and the if the error is defined, we can handle it. In this case we handle it by printing it out and exiting the program. This is fine since we’re not expecting to hit these errors very often.

The next block uses the github.com/otiai10/copy package to copy _victim/ to victim/. Easy enough. Only thing to note, is see how we declare and define the first err with := and the second we are just reassigning the existing err variable with =.

		err := os.RemoveAll("victim")
		if err != nil {
			log.Fatal(err)
		}

		err = copy.Copy("_victim", "victim")
		if err != nil {
			log.Fatal(err)
		}

Verification

Our goal with verification is to walk _victim/ (the original) and for each file, verify that its SHA256 hash matches a corresponding file in victim/. There are three components to this.

  1. Reading files to get their bytes.
  2. Taking the SHA256 of some bytes.
  3. Walking a directory.

Thankfully, these are all super easy to do. When I’m working on projects I’ll often have a scratch/ directory where I prototype things and figure out how things work. I’ll walk you through each of these in short scratch programs. Then we can put them together in util.go.

Reading a file

In Go, there are a lot of ways to read a file. We’ll use a wrapper, ioutil, that will open and read the file then just give us back the slice of bytes (bs). We check the error and bail if we have one. Finally we cast our little slice of bytes to a string as we pass it to be printed out.

// scratch/read.go
package main

import (
	"io/ioutil"
	"log"
)

func main() {
	bs, err := ioutil.ReadFile("victim/simple.txt")
	if err != nil {
		log.Fatal(err)
	}
	log.Println(string(bs))
}

Hashing some bytes

Taking a SHA256 is equally as easy. We import crypto/SHA256 and pass in a (string cast to) a slice of bytes to sha256.Sum256(). That gives us back an array of 32 bytes which contains the hash. We print that as hex, giving us a 64 character string.

// scratch/sha256.go
package main

import (
	"crypto/sha256"
	"log"
)

func main() {
	input := "Let's take the sha256 of these bytes!"
	hash := sha256.Sum256([]byte(input))
	log.Printf("%x\n", hash)
}

If you’re on Linux you can run the following to verify the output. It echos out the same string, -n for no trailing newline to match our input in the program. We pipe that to sha256sum and see that we’re getting the same hash.

echo -n "Let's take the sha256 of these bytes!" | sha256sum

If you haven’t seen a hashing function before, SHA256 is a cryptographic hash. Cryptographic hashes are meant to take an arbitrary amount of data and map it to a fixed space (e.g. 32 bytes). They are meant to be infeasible to reverse, meaning you should only be able to discover the input that results in a certain hash through brute force. Collisions, while possible, should be rare enough to not be considered. Other hashing algorithms, such as MD5 and SHA1 can be manipulated to create collisions. This breaks the hash.

For our purposes, since SHA256 is consistent, the same input should hash to the same output, so we’ll use the hashes of two files to see if they are equivalent. This will help check that we are doing things correctly when decrypting.

Walking a directory

Finally we get to take a walk down directory lane. This is also super easy because someone has already done the hard work. We don’t have to screw around with handling file system edge cases or writing a tree traversal. The filepath package has a Walk() function. You provide it with a starting directory and a visitor function that you define. The visitor function gets called on each file or directory.

If you check the docs you’ll see that the function that Walk() takes as its second argument is of the type WalkFunc. That definition is just below. What it is saying is that you need to pass in a function that matches the WalkFunc prototype.

type WalkFunc func(path string, info os.FileInfo, err error) error

In our case, this is onVisit(). In the docs you can check out the definition of the os.FileInfo interface, but the important bit is that it has an IsDir() function that returns true when the visitee (path) is a directory. When it is a directory we can ignore it and return early. On files, we’ll print them out.

// scratch/walk.go
package main

import (
	"flag"
	"log"
	"path/filepath"
	"os"
)

func onVisit(path string, fi os.FileInfo, err error) error {
	if fi.IsDir() {
		return nil
	}
	
	log.Println(path)
	return nil
}

func main() {
	dir := flag.String("dir", "", "Directory to walk.")
	flag.Parse()

	if len(*dir) == 0 {
		log.Fatal("Please provide a -dir...")
	}

	err := filepath.Walk(*dir, onVisit)
	if err != nil {
		log.Fatal(err)
	}
}

Give it a run and see how it works. You can also remove the IsDir() check and see it print out the directories too. Fun.

Putting it all together

Back in util/util.go we’ll update our else if *verify block in main() to contain the following. You’ll also need to update your imports of import.

		err := filepath.Walk("_victim", onVisit)
		if err != nil {
			log.Fatal(err)
		}

Then the party heads over to the onVisit() function. We read and take the hash of the original. Then we read and take the hash of the copy. Finally we check that they match.

It’s all stuff we saw in the last section. We’re being tricky with our paths, since we’re walking _victim/ and checking in on victim/ we just need to drop the underscore off the front to get the corresponding file that we are verfiying. That’s done with the subslice notation [1:], going from the first character (the one after the zeroth) to the end.

func onVisit(path string, fi os.FileInfo, err error) error {
	if fi.IsDir() {
		return nil
	}

	// Read original.
	bsOrig, err := ioutil.ReadFile(path)
	if err != nil {
		log.Println("Error reading", path)
		log.Println(err)
		return nil
	}

	hashOrig := sha256.Sum256(bsOrig)

	// Read copy.
	bsCopy, err := ioutil.ReadFile(path[1:])
	if err != nil {
		log.Println("Error reading", path[1:])
		log.Println(err)
		return nil
	}

	hashCopy := sha256.Sum256(bsCopy)

	// Report mismatches.
	if hashOrig != hashCopy {
		log.Println("Mismatch for", path)
	}

	return nil
}

Give ol’ util/util.go a run with -verify now and see how it does. It probably won’t be too interesting unless you go and edit a file in the clone directory. You can also try deleting a file and see how that gets handled. Afterall, we don’t want our ransomware losing people’s data all willy nilly.

Conclusion

We have a decent setup for restoring our victim/ directory now, we can also check that the files line up between the original and the copy.

Up next in the series we’ll get to generating our server keypair, and we’ll write a couple scratch programs to understand how to use the encryption packages. That will set us up nicely to write the encryptor in Part 3.

Keep on hacking on!