Keystone is a lightweight multi-platform, multi-architecture assembler framework.
Highlight features:
Multi-architecture, with support for Arm, Arm64 (AArch64/Armv8), Ethereum Virtual Machine, Hexagon, Mips, PowerPC, Sparc, SystemZ, & X86 (include 16/32/64bit).
A package with go bindings is provided here but unfortunately it requires cgo to compile. I love both go and c but cgo is appalling in my opinion. Thankfully the keystone engine provides a DLL we can use. The caveat when using the dll is that it will only run on windows.
If you need to run keystone on linux it's probably easier to use python instead (or cgo).
Documentation
There is no real documentation for the framework. A few examples can be found here and some on their github page.
What I found useful is to download the Windows-Core Engine from here (it includes the precompiled dll).
Make sure to download the dll for the right architecture.
In the 'includes' folder the keystone.h file can be found with descriptions of the exported functions and how the framework should be used. A sample from the header is shown below:
/* Assemble a string given its the buffer, size, start address and number of instructions to be decoded. This API dynamically allocate memory to contain assembled instruction. Resulted array of bytes containing the machine code is put into @*encoding NOTE 1: this API will automatically determine memory needed to contain output bytes in *encoding. NOTE 2: caller must free the allocated memory itself to avoid memory leaking. @ks: handle returned by ks_open() @str: NULL-terminated assembly string. Use ; or \n to separate statements. @address: address of the first assembly instruction, or 0 to ignore. @encoding: array of bytes containing encoding of input assembly string. NOTE: *encoding will be allocated by this function, and should be freed with ks_free() function. @encoding_size: size of *encoding @stat_count: number of statements successfully processed @return: 0 on success, or -1 on failure. On failure, call ks_errno() for error code.*/KEYSTONE_EXPORTintks_asm(ks_engine*ks,constchar*string,uint64_t address,unsignedchar**encoding,size_t*encoding_size,size_t*stat_count);
Golang Implementation
Up until now I only needed to develop 32 and 64bit shellcode for x86 architecture, so I will not bother adding extra constants for arm etc.
Also I am not planning to implement this as part of a large project so I will not implement functions such as ks_free() that frees memory.
Keystone functions
The following functions will be implemented:
ks_open (creates a new instance of keystone)
ks_asm (it receives the assembly string and returns the assembly equivalent bytes)
Let's dive into it.
Code
Firstly we need to download the dll from here and include it in our current working path. We then import the dll using LoadLibrary from the windows package.
If the dll is in a different directory make sure to include the absolute path.
As mentioned previously from the exported functions we will only use ks_open and ks_asm. Using GetProcAddress from the windows package we can get the functions' addresses.
fmt.Println("[+] Getting function addresses") ks_open_proc, err := windows.GetProcAddress(hModule, "ks_open")if err !=nil {return []byte{}, fmt.Errorf("Failed to get address for ks_open\n") } ks_asm_proc, err := windows.GetProcAddress(hModule, "ks_asm")if err !=nil {return []byte{}, fmt.Errorf("Failed to get address for ks_asm\n") }
ks_open() function
/* Create new instance of Keystone engine. @arch: architecture type (KS_ARCH_*) @mode: hardware mode. This is combined of KS_MODE_* @ks: pointer to ks_engine, which will be updated at return time @return KS_ERR_OK on success, or other value on failure (refer to ks_err enum for detailed error).*/KEYSTONE_EXPORTks_errks_open(ks_arch arch,int mode,ks_engine**ks);
As we can see from the above definition in the keystone header we need to define the architecture, mode and provide a pointer of the location where our session handle will be stored.
/* Assemble a string given its the buffer, size, start address and number of instructions to be decoded. This API dynamically allocate memory to contain assembled instruction. Resulted array of bytes containing the machine code is put into @*encoding NOTE 1: this API will automatically determine memory needed to contain output bytes in *encoding. NOTE 2: caller must free the allocated memory itself to avoid memory leaking. @ks: handle returned by ks_open() @str: NULL-terminated assembly string. Use ; or \n to separate statements. @address: address of the first assembly instruction, or 0 to ignore. @encoding: array of bytes containing encoding of input assembly string. NOTE: *encoding will be allocated by this function, and should be freed with ks_free() function. @encoding_size: size of *encoding @stat_count: number of statements successfully processed @return: 0 on success, or -1 on failure. On failure, call ks_errno() for error code.*/KEYSTONE_EXPORTintks_asm(ks_engine*ks,constchar*string,uint64_t address,unsignedchar**encoding,size_t*encoding_size,size_t*stat_count);
From the above definition we will require the following:
the handle returned by ks_open stored in variable ksSession .
We then require a pointer to our null terminated string.
address can be ignored so it will be set to 0
A pointer for the buffer to be written
A pointer for the size of the buffer to be written
A pointer for the number of statements successfully processed
In order to get a pointer to null terminated string the following code can be used.
ptr, err := syscall.BytePtrFromString(asm)if err !=nil {return []byte{}, fmt.Errorf("Failed to get byte ptr from string\n")}
We now have everything we need to call the the ks_asm function