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).
Implemented in C/C++ languages, with bindings for Java, Masm, Visual Basic, C#, PowerShell, Perl, Python, NodeJS, Ruby, Go, Rust, Haskell & OCaml available.
Native support for Windows & *nix (with Mac OSX, Linux, *BSD & Solaris confirmed).
Thread-safe by design.
Open source.
Keystone is based on , but it goes much further with .
Find in this more technical details behind our assembler engine.
Keystone with GoLang
A package with go bindings is provided 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
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_EXPORT
int ks_asm(ks_engine *ks,
const char *string,
uint64_t address,
unsigned char **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
fmt.Println("[+] Loading keystone.dll")
hModule, err := windows.LoadLibrary("keystone.dll")
if err != nil {
return []byte{}, fmt.Errorf("Failed to load Libray\n")
}
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_EXPORT
ks_err ks_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_EXPORT
int ks_asm(ks_engine *ks,
const char *string,
uint64_t address,
unsigned char **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
With this sequence of commands we should expect the following output
The output of our script matches the expected results. Great :)
There is no real documentation for the framework. A few examples can be found and some on their .
What I found useful is to download the Windows-Core Engine from (it includes the precompiled dll).
Firstly we need to download the dll from and include it in our current working path. We then import the dll using LoadLibrary from the windows package.