What, Why
If you're here, you've heard of Cobalt Strike. However, many a operator often use these tools without any understanding of what happens underneath the hood. In this short series, we'll be reverse engineering a staged cobalt strike beacon and try to determine what it's actually doing and maybe what we can do to improve the trade craft. In this series, I'll be using the radare2 reverse engineering framework simply because I think it's cool.
Getting Started
We'll be using radare2 without any fancy plugins like r2dec or r2ghidra for this tutorial so it should be pretty easy to get setup. For practice, I'll host the staged payload here. Simply download the latest release from the github and start that bad puppy up with
r2 artifact.exe
Keep in mind that radare2 has a very active community and updates are coming in all the time. Things may look slightly different if you're looking at this post far into the future.
Next, let's analyze the binary. This being on the smaller side, we can auto analyze to the best of r2's ability without having to worry about it taking forever.
[0x004014b0]> aaaaa
INFO: Analyze all flags starting with sym. and entry0 (aa)
INFO: Analyze imports (af@@@i)
INFO: Analyze entrypoint (af@ entry0)
INFO: Analyze symbols (af@@@s)
INFO: Analyze all functions arguments/locals (afva@@@F)
INFO: Analyze function calls (aac)
INFO: Analyze len bytes of instructions for references (aar)
INFO: Finding and parsing C++ vtables (avrr)
INFO: Analyzing methods (af @@ method.*)
INFO: Recovering local variables (afva@@@F)
INFO: Type matching analysis for all functions (aaft)
INFO: Propagate noreturn information (aanr)
INFO: Scanning for strings constructed in code (/azs)
INFO: Finding function preludes (aap)
INFO: Enable anal.types.constraint for experimental type propagation
INFO: Reanalizing graph references to adjust functions count (aarr)
INFO: Autoname all functions (.afna@@c:afla)
For the uninitiated, r2 can look very daunting, but with a little experience it becomes very intuitive for most operations. For example, the a
keyword is used to access the analysis functions. Adding more a's to it makes it analyze more. Use the ?
in any context to get info about a command or a sub command.
[0x004014b0]> a?
Usage: a [abdefFghoprxstc] [...]
| a alias for aai - analysis information
| a:[cmd] run a command implemented by an analysis plugin (like : for io)
| a* same as afl*;ah*;ax*
| aa[?] analyze all (fcns + bbs) (aa0 to avoid sub renaming)
| a8 [hexpairs] analyze bytes
| ab[?] analyze basic block
| ac[?] manage classes
| aC[?] analyze function call
| ad[?] analyze data trampoline (wip) (see 'aod' to describe mnemonics)
| ad [from] [to] analyze data pointers to (from-to)
| ae[?] [expr] analyze opcode eval expression (see ao)
| af[?] analyze functions
| aF same as above, but using anal.depth=1
| ag[?] [options] draw graphs in various formats
| ah[?] analysis hints (force opcode size, ...)
| ai [addr] address information (show perms, stack, heap, ...)
| aj same as a* but in json (aflj)
| aL[jq] list all asm/anal plugins (See `e asm.arch=?` and `La[jq]`)
| an[?] [name] show/rename/create whatever var/flag/function used in current instruction
| ao[?] [len] analyze Opcodes (or emulate it)
| aO[?] [len] analyze N instructions in M bytes
| ap find prelude for current offset
| ar[?] like 'dr' but for the esil vm. (registers)
| as[?] [num] analyze syscall using dbg.reg
| av[?] [.] show vtables
| avg[?] [.] manage global variables
| ax[?] manage refs/xrefs (see also afx?)
Now to get into it. For some easy wins, one of the first places to check is the Import Address Table.
[0x004014b0]> ii
[Imports]
nth vaddr bind type lib name
――――――――――――――――――――――――――――――――――――――――――
1 0x00409244 NONE FUNC KERNEL32.dll CloseHandle
2 0x0040924c NONE FUNC KERNEL32.dll ConnectNamedPipe
3 0x00409254 NONE FUNC KERNEL32.dll CreateFileA
4 0x0040925c NONE FUNC KERNEL32.dll CreateNamedPipeA
5 0x00409264 NONE FUNC KERNEL32.dll CreateThread
6 0x0040926c NONE FUNC KERNEL32.dll DeleteCriticalSection
7 0x00409274 NONE FUNC KERNEL32.dll EnterCriticalSection
8 0x0040927c NONE FUNC KERNEL32.dll GetCurrentProcess
9 0x00409284 NONE FUNC KERNEL32.dll GetCurrentProcessId
10 0x0040928c NONE FUNC KERNEL32.dll GetCurrentThreadId
11 0x00409294 NONE FUNC KERNEL32.dll GetLastError
12 0x0040929c NONE FUNC KERNEL32.dll GetModuleHandleA
13 0x004092a4 NONE FUNC KERNEL32.dll GetProcAddress
14 0x004092ac NONE FUNC KERNEL32.dll GetStartupInfoA
15 0x004092b4 NONE FUNC KERNEL32.dll GetSystemTimeAsFileTime
16 0x004092bc NONE FUNC KERNEL32.dll GetTickCount
<--- SNIP --->
31 0x00409334 NONE FUNC KERNEL32.dll VirtualAlloc
32 0x0040933c NONE FUNC KERNEL32.dll VirtualProtect
33 0x00409344 NONE FUNC KERNEL32.dll VirtualQuery
34 0x0040934c NONE FUNC KERNEL32.dll WriteFile
1 0x0040935c NONE FUNC msvcrt.dll __C_specific_handler
Here in table we can see different functions that are typically used for local process injection. We can cross reference the juicy functions like CreateThread, VirtualProtect, etc to see where they're called.
[0x004014b0]> axt sym.imp.KERNEL32.dll_CreateThread
sub.sub.flProtect_40152e_40152e 0x4015b6 [CALL:--x] call qword [sym.imp.KERNEL32.dll_CreateThread]
sub.sub.size_401795_401795 0x401822 [CALL:--x] call qword [sym.imp.KERNEL32.dll_CreateThread]
(nofunc) 0x402fc8 [CODE:--x] jmp qword [sym.imp.KERNEL32.dll_CreateThread]
We see the CreateThread windows API call used in two different functions. We'll analyze them and rename them based on the function they serve.
First, we'll start with the function sub.sub.flProtect_40152e_40152e
.
[0x004014b0]> pdf @ sub.sub.flProtect_40152e_40152e
; CALL XREF from sub.sub.size_401795_401795 @ 0x401787(x)
┌ 154: sub.sub.flProtect_40152e_40152e (int64_t arg1, int64_t arg2, int64_t arg3);
│ ; arg int64_t arg1 @ rcx
│ ; arg int64_t arg2 @ rdx
│ ; arg int64_t arg3 @ r8
│ ; var DWORD dwCreationFlags @ rsp+0x20
│ ; var LPDWORD lpThreadId @ rsp+0x28
│ ; var PDWORD lpflOldProtect @ rsp+0x3c
<--- SNIP --->
At the very beginning, we see this function takes three arguments. It might be useful to cross reference this function and find out what these arguments are to help up see the picture.
[0x004014b0]> axt sub.sub.flProtect_40152e_40152e
sub.sub.size_401795_401795 0x401787 [CALL:--x] call sub.sub.flProtect_40152e_40152e
It's called exactly once, so let's look into the sub.sub.size_401795_401795
function that also appeared in the last cross reference search.
Something to note here: the address claiming to belong to the sub.sub.size_401795_401795
function that calls the sub.sub.flProtect_40152e_40152e
function has an address less than the starting address of the function. Meaning that a linear disassembly command pdf
wouldn't suffice and we need the to recursively disassemble across the function graph command pdr.
0x004014b0]> pdr @ sub.sub.size_401795_401795
<--- SNIP --->
│ 0x00401773 85c0 test eax, eax
│ 0x00401775 74e7 je 0x40175e
| // true: 0x0040175e false: 0x00401777
│ 0x00401777 8b1587280000 mov edx, dword [0x00404004] ; [0x404004:4]=0x37c ; int64_t arg2
│ 0x0040177d 4c8d058428.. lea r8, [0x00404008] ; int64_t arg3
│ 0x00401784 4889d9 mov rcx, rbx ; int64_t arg1
│ 0x00401787 e8a2fdffff call sub.sub.flProtect_40152e_40152e
<--- SNIP --->
In x64 assembly, there are a few conventions and it's important to be mindful about the target architecture the binary you're reversing is for. For example, the Linux x64 calling convention passes arguments 1-6 into the RDI, RSI, RDX RCX, R8, and R9 registers respectively, and if you need more they are pushed to the stack. In our case, Windows, the first four arguments go into the RCX, RDX, R8, and R9 registers.
Looking at the above snippet, we can see radare did some work for us. Argument 2 is a DWORD with a value of 0x037c, and argument 3 uses the lea
instruction to calculate a pointer to the 0x00404008
address. Next, we need to find out what the first argument is. Looking up higher in the function we can see:
<--- SNIP --->
│ 0x00401748 48630db528.. movsxd rcx, dword [0x00404004] ; [0x404004:4]=0x37c ; size_t size
│ 0x0040174f e88c170000 call sub.sub.sub.msvcrt.dll_malloc_402ee0_402ee0
│ 0x00401754 488b35b97b.. mov rsi, qword [sym.imp.KERNEL32.dll_Sleep] ; [0x409314:8]=0x9662 reloc.KERNEL32.dll_Sleep ; "b\x96"
│ 0x0040175b 4889c3 mov rbx, rax
<--- SNIP --->
The value 0x037c
was passed into the malloc
function call, which returns a pointer to the allocated memory. Using the mov
instruction, it is then copied into the rbx register.
Putting all of this together, We can paint a picture of something like the following C:
sub.sub.flProtect_40152e_40152e(buffer,0x037c,&data);
Which begs the question of what's at data
? Let's check with r2.
[0x004014b0]> px @ 0x00404008
- offset - 8 9 A B C D E F 1011 1213 1415 1617 89ABCDEF01234567
0x00404008 12fc 6313 0000 0000 0000 0000 eeb4 e0f7 ..c.............
0x00404018 e214 ab13 12fc 2242 53ac 3142 44b4 52c1 ......"BS.1BD.R.
0x00404028 77b4 e841 72b4 e841 0ab4 e841 32b4 e861 w..Ar..A...A2..a
0x00404038 42b4 6ca4 58b6 2e22 dbb4 52d3 bec0 026f B.l.X.."..R....o
0x00404048 10d0 4352 d335 6e52 133d 81fe 40bd 325b ..CR.5nR.=..@.2[
0x00404058 99ae 4398 50c0 2b12 c29a e26b 0af7 6166 ..C.P.+....k..af
0x00404068 6077 e39b 12fc 635b 973c 1774 5afd b343 `w....c[.<.tZ..C
0x00404078 99b4 7b57 99bc 435a 132c 8045 5a03 aa52 ..{W..CZ.,.EZ..R
0x00404088 99c8 eb5b 132a 2e22 dbb4 52d3 bebd a2da ...[.*."..R.....
0x00404098 1fbd 62d2 2a1c 16e2 5eff 2f37 1ab9 5ac2 ..b.*...^./7..Z.
0x004040a8 6724 3b57 99bc 475a 132c 0552 99f0 2b57 g$;W..GZ.,.R..+W
0x004040b8 99bc 7f5a 132c 2298 1674 2b12 c2bd 3b52 ...Z.,"..t+...;R
0x004040c8 4aa2 3a49 53a4 224a 53a6 2b90 fedc 2241 J.:IS."JS.+..."A
0x004040d8 ed1c 3b52 4ba6 2b98 0015 2cec ed03 3e79 ..;RK.+...,...>y
0x004040e8 12b5 dd64 7b92 0a7d 7788 6352 44b5 eaf5 ...d{..}w.cRD...
0x004040f8 5e75 9252 a8b0 1435 1503 b65b 2335 2b22 ^u.R...5...[#5+"
While there's definitely something here, it's nothing that we can understand at the moment. However, all encrypted data must become unencrypted at some point in order to execute, so we'll look for where that happens.
Looking further down the function (keeping in mind that this function is non-linear), we see a call to the GetTicketCount function, which returns the uptime of the machine in seconds.
<--- SNIP --->
│ 0x00401799 ff151d7b0000 call qword [sym.imp.KERNEL32.dll_GetTickCount] ; [0x4092bc:8]=0x9572 reloc.KERNEL32.dll_GetTickCount ; "r\x95" ; DWORD GetTickCount(void)
│ 0x0040179f b9aa260000 mov ecx, 0x26aa
│ 0x004017a4 31d2 xor edx, edx
│ 0x004017a6 41b95c000000 mov r9d, 0x5c ; '\\' ; 92
│ 0x004017ac f7f1 div ecx
│ 0x004017ae 488d0dfb71.. lea rcx, [0x004089b0] ; char *s
│ 0x004017b5 41b85c000000 mov r8d, 0x5c ; '\\' ; 92
│ 0x004017bb c74424505c.. mov dword [var_50h], 0x5c ; '\\'
│ ; [0x5c:4]=-1 ; 92
│ 0x004017c3 c744244865.. mov dword [var_48h], 0x65 ; 'e'
│ ; [0x65:4]=-1 ; 101
│ 0x004017cb c744244070.. mov dword [var_40h], 0x70 ; 'p'
│ ; [0x70:4]=-1 ; 112
│ 0x004017d3 c744243869.. mov dword [var_38h], 0x69 ; 'i'
│ ; [0x69:4]=-1 ; 105
│ 0x004017db c744243070.. mov dword [var_30h], 0x70 ; 'p'
│ ; [0x70:4]=-1 ; 112
│ 0x004017e3 c74424285c.. mov dword [lpThreadId], 0x5c ; '\\'
│ ; [0x5c:4]=-1 ; 92
│ 0x004017eb c74424202e.. mov dword [dwCreationFlags], 0x2e ; '.'
│ ; [0x2e:4]=-1 ; 46
│ 0x004017f3 89542458 mov dword [var_58h], edx
│ 0x004017f7 488d152238.. lea rdx, str._c_c_c_c_c_c_c_c_cMSSE__d_server ; 0x405020 ; "%c%c%c%c%c%c%c%c%cMSSE-%d-server" ; const char *format
│ 0x004017fe e80d170000 call sub.sub.sub.msvcrt.dll_sprintf_402f10_402f10
<--- SNIP --->
Then, a segmented string is loaded byte by byte (possibly to avoid detection with something like strings) and formatted with sprintf. This string is then stored in the .data section at 0x004089b0
.
Next, there's a call to the CreateThread function that starts the sub.sub.sub.dwPipeMode_4015d0_401685_401685
function.
[0x0040152e]> pdf @ sub.sub.sub.dwPipeMode_4015d0_401685_401685
; DATA XREF from sub.sub.size_401795_401795 @ 0x401803(r)
┌ 29: sub.sub.sub.dwPipeMode_4015d0_401685_401685 ();
│ 0x00401685 4883ec28 sub rsp, 0x28
│ 0x00401689 8b1575290000 mov edx, dword [0x00404004] ; [0x404004:4]=0x37c ; int64_t arg2
│ 0x0040168f 488d0d7e29.. lea rcx, [0x00404014] ; int64_t arg1
│ 0x00401696 e835ffffff call sub.sub.dwPipeMode_4015d0_4015d0
│ 0x0040169b 31c0 xor eax, eax
│ 0x0040169d 4883c428 add rsp, 0x28
└ 0x004016a1 c3 ret
<--- SNIP --->
This is a short function that seems to setup the sub.sub.dwPipeMode_4015d0_4015d0
function something like:
sub.sub.dwPipeMode_4015d0_4015d0(&data2, 0x037c);
Which is pretty close to the previous data
we found before.
[0x0040152e]> px @ 0x00404014
- offset - 1415 1617 1819 1A1B 1C1D 1E1F 2021 2223 456789ABCDEF0123
0x00404014 eeb4 e0f7 e214 ab13 12fc 2242 53ac 3142 .........."BS.1B
0x00404024 44b4 52c1 77b4 e841 72b4 e841 0ab4 e841 D.R.w..Ar..A...A
0x00404034 32b4 e861 42b4 6ca4 58b6 2e22 dbb4 52d3 2..aB.l.X.."..R.
0x00404044 bec0 026f 10d0 4352 d335 6e52 133d 81fe ...o..CR.5nR.=..
0x00404054 40bd 325b 99ae 4398 50c0 2b12 c29a e26b @.2[..C.P.+....k
0x00404064 0af7 6166 6077 e39b 12fc 635b 973c 1774 ..af`w....c[.<.t
0x00404074 5afd b343 99b4 7b57 99bc 435a 132c 8045 Z..C..{W..CZ.,.E
0x00404084 5a03 aa52 99c8 eb5b 132a 2e22 dbb4 52d3 Z..R...[.*."..R.
0x00404094 bebd a2da 1fbd 62d2 2a1c 16e2 5eff 2f37 ......b.*...^./7
0x004040a4 1ab9 5ac2 6724 3b57 99bc 475a 132c 0552 ..Z.g$;W..GZ.,.R
0x004040b4 99f0 2b57 99bc 7f5a 132c 2298 1674 2b12 ..+W...Z.,"..t+.
0x004040c4 c2bd 3b52 4aa2 3a49 53a4 224a 53a6 2b90 ..;RJ.:IS."JS.+.
0x004040d4 fedc 2241 ed1c 3b52 4ba6 2b98 0015 2cec .."A..;RK.+...,.
0x004040e4 ed03 3e79 12b5 dd64 7b92 0a7d 7788 6352 ..>y...d{..}w.cR
0x004040f4 44b5 eaf5 5e75 9252 a8b0 1435 1503 b65b D...^u.R...5...[
0x00404104 2335 2b22 c0b1 52d3 5fcd aa52 42bd 3352
After creating the thread, the function jumps up to where we saw our aforementioned malloc and loops around a call to sub.sub.lpSecurityAttributes_4016a2_4016a2
and sleep
│ 0x00401748 48630db528.. movsxd rcx, dword [0x00404004] ; [0x404004:4]=0x37c ; size_t size
│ 0x0040174f e88c170000 call sub.sub.msvcrt.dll_malloc_402ee0
│ 0x00401754 488b35b97b.. mov rsi, qword [sym.imp.KERNEL32.dll_Sleep] ; [0x409314:8]=0x9662 reloc.KERNEL32.dll_Sleep ; "b\x96"
│ 0x0040175b 4889c3 mov rbx, rax
| // true: 0x0040175e
│ ; CODE XREF from sub.size_401795 @ 0x401775(x)
│ 0x0040175e b900040000 mov ecx, 0x400 ; 1024
│ 0x00401763 ffd6 call rsi
│ 0x00401765 8b1599280000 mov edx, dword [0x00404004] ; [0x404004:4]=0x37c
│ 0x0040176b 4889d9 mov rcx, rbx
│ 0x0040176e e82fffffff call sub.lpSecurityAttributes_4016a2
│ 0x00401773 85c0 test eax, eax
│ 0x00401775 74e7 je 0x40175e
This would look something like this in C:
void data = malloc(0x037c)
int test = 0;
do {
sleep(1024);
test = sub.sub.lpSecurityAttributes_4016a2_4016a2(data, 0x037c);
}
while (!test)
Disassembling the function we have a call to the CreateFileA function with the location to the string that was generated in the previous function passed as an argument.
│ 0x004016aa 4531c9 xor r9d, r9d ; LPSECURITY_ATTRIBUTES lpSecurityAttributes
│ 0x004016ad 41b803000000 mov r8d, 3 ; DWORD dwShareMode
│ 0x004016b3 4889cf mov rdi, rcx ; arg1
│ 0x004016b6 89d3 mov ebx, edx ; arg2
│ 0x004016b8 c744244c00.. mov dword [var_4ch], 0
│ 0x004016c0 48c7442430.. mov qword [hTemplateFile], 0 ; HANDLE hTemplateFile
│ 0x004016c9 c744242880.. mov dword [dwFlagsAndAttributes], 0x80 ; pe_nt_image_headers64
│ ; [0x80:4]=-1 ; DWORD dwFlagsAndAttributes
│ 0x004016d1 ba00000080 mov edx, 0x80000000 ; DWORD dwDesiredAccess
│ 0x004016d6 c744242003.. mov dword [dwCreationDisposition], 3 ; DWORD dwCreationDisposition
│ 0x004016de 488d0dcb72.. lea rcx, [0x004089b0] ; LPCSTR lpFileName
│ 0x004016e5 ff15697b0000 call qword [sym.imp.KERNEL32.dll_CreateFileA]
<--- SNIP --->
Which should return a handle to the file (in this case, it looks like a named pipe). If a valid handle is returned ReadFile is called. ReadFile then reads the data from the named pipe then stores into it the malloc'd memory that was passed into the function. Let's rename this function read_and_store_data
.
[0x00401795]> afn read_and_store_data sub.sub.lpSecurityAttributes_4016a2_4016a2
[0x00401795]> afl | grep read
0x004016a2 7 160 read_and_store_data
And rename sub.sub.size_401795_401795
to create_pipe_name_and_fork
as it creates the name for the pipe and spawns another thread to sub.sub.dwPipeMode_4015d0_4015d0
.
[0x00401795]> afn create_pipe_name_and_fork sub.sub.size_401795_401795
[0x00401795]> axt sym.imp.KERNEL32.dll_CreateThread
sub.sub.flProtect_40152e_40152e 0x4015b6 [CALL:--x] call qword [sym.imp.KERNEL32.dll_CreateThread]
create_pipe_name_and_fork 0x401822 [CALL:--x] call qword [sym.imp.KERNEL32.dll_CreateThread]
(nofunc) 0x402fc8 [CODE:--x] jmp qword [sym.imp.KERNEL32.dll_CreateThread]
Let's seek to the forked function and rename some variables now that we have a better understanding.
[0x00401795]> s sub.sub.dwPipeMode_4015d0_4015d0
[0x004015d0]> afvn data2 arg1
[0x004015d0]> afvn size arg2
[0x004015d0]> pdf
; CALL XREF from sub.sub.sub.dwPipeMode_4015d0_401685_401685 @ 0x401696(x)
┌ 181: sub.sub.dwPipeMode_4015d0_4015d0 (int64_t data2, int64_t size);
│ ; arg int64_t data2 @ rcx
│ ; arg int64_t size @ rdx
<--- SNIP --->
Within the function, we can see a call to the CreateNamedPipeA that passes in the memory location of our created string from the last function 0x004089b0
.
<--- SNIP --->
│ 0x004015d8 4531c0 xor r8d, r8d ; DWORD dwPipeMode
│ 0x004015db 41b901000000 mov r9d, 1
│ 0x004015e1 4889cf mov rdi, rcx ; arg1
│ 0x004015e4 89d6 mov esi, edx ; arg2
│ 0x004015e6 c744244c00.. mov dword [var_4ch], 0
│ 0x004015ee 48c7442438.. mov qword [var_38h], 0
│ 0x004015f7 c744243000.. mov dword [var_30h], 0
│ 0x004015ff ba02000000 mov edx, 2 ; DWORD dwOpenMode
│ 0x00401604 c744242800.. mov dword [var_28h], 0
│ 0x0040160c c744242000.. mov dword [var_20h], 0
│ 0x00401614 488d0d9573.. lea rcx, [0x004089b0] ; LPCSTR lpName
│ 0x0040161b ff153b7c0000 call qword [sym.imp.KERNEL32.dll_CreateNamedPipeA]
<--- SNIP --->
Next, a call to the ConnectNamedPipe where a handle to the created named pipe is passed in.
<--- SNIP --->
│ 0x00401621 4889c3 mov rbx, rax
│ 0x00401624 488d40ff lea rax, [rax - 1]
│ 0x00401628 4883f8fd cmp rax, 0xfffffffffffffffd
│ 0x0040162c 774e ja 0x40167c
│ 0x0040162e 31d2 xor edx, edx ; LPOVERLAPPED lpOverlapped
│ 0x00401630 4889d9 mov rcx, rbx ; HANDLE hNamedPipe
│ 0x00401633 ff15137c0000 call qword [sym.imp.KERNEL32.dll_ConnectNamedPipe]
<--- SNIP --->
If the connection was successful, the program writes the data to the pipe and closes the handle.
<--- SNIP --->
│ 0x00401639 85c0 test eax, eax
│ 0x0040163b 488b2d0a7d.. mov rbp, qword [sym.imp.KERNEL32.dll_WriteFile] ; [0x40934c:8]=0x96da reloc.KERNEL32.dll_WriteFile
│ 0x00401642 752a jne 0x40166e
│ 0x00401644 eb36 jmp 0x40167c
│ ; CODE XREF from sub.dwPipeMode_4015d0 @ 0x401670(x)
│ 0x00401646 48c7442420.. mov qword [var_20h], 0
│ 0x0040164f 4c8d4c244c lea r9, [var_4ch]
│ 0x00401654 4189f0 mov r8d, esi
│ 0x00401657 4889fa mov rdx, rdi
│ 0x0040165a 4889d9 mov rcx, rbx
│ 0x0040165d ffd5 call rbp
│ 0x0040165f 85c0 test eax, eax
│ 0x00401661 740f je 0x401672
│ 0x00401663 8b44244c mov eax, dword [var_4ch]
│ 0x00401667 89c2 mov edx, eax
│ 0x00401669 29c6 sub esi, eax
│ 0x0040166b 4801d7 add rdi, rdx
│ ; CODE XREF from sub.dwPipeMode_4015d0 @ 0x401642(x)
│ 0x0040166e 85f6 test esi, esi
│ 0x00401670 7fd4 jg 0x401646
│ ; CODE XREF from sub.dwPipeMode_4015d0 @ 0x401661(x)
│ 0x00401672 4889d9 mov rcx, rbx ; HANDLE hObject
│ 0x00401675 ff15c97b0000 call qword [sym.imp.KERNEL32.dll_CloseHandle]
<--- SNIP --->
We're getting close to seeing the full picture of this portion of the malware. Let's rename this function create_pipe_and_write
because it creates a named pipe and writes data to it.
[0x004015d0]> afn create_pipe_and_write sub.sub.dwPipeMode_4015d0
Let's summarize what we know to be true so far.
- A pipe name is generated.
- A named pipe is created with that name.
- Data from location
0x00404014
with a size of 0x037c
is being written to that pipe.
- In another thread, in a loop, the program tries to connect to the pipe trying again after sleeping.
- If successful, memory with a size of
0x037c
is allocated, and the program reads the payload from the pipe.
- This payload is then stored into the allocated memory, and is passed to the
sub.sub.flProtect_40152e_40152e
function with a size of 0x037c
, and some data stored at 0x00404008
.
The path seems clear. We need to see what the sub.sub.flProtect_40152e_40152e
function does with this data. This very first call is to VirtualAlloc that allocates a size of 0x037c
.
<--- SNIP --->
│ 0x00401538 41b904000000 mov r9d, 4 ; DWORD flProtect
│ 0x0040153e 4863f2 movsxd rsi, edx ; arg2
│ 0x00401541 4989cc mov r12, rcx ; arg1
│ 0x00401544 89d7 mov edi, edx ; arg2
│ 0x00401546 4c89c5 mov rbp, r8 ; arg3
│ 0x00401549 4889f2 mov rdx, rsi ; SIZE_T dwSize
│ 0x0040154c 41b800300000 mov r8d, 0x3000 ; DWORD flAllocationType
│ 0x00401552 31c9 xor ecx, ecx ; LPVOID lpAddress
│ 0x00401554 ff15da7d0000 call qword [sym.imp.KERNEL32.dll_VirtualAlloc]
<--- SNIP --->
Next, it seems that a loop is created that reads the malloc'd byte by byte, xor's it against the data written at 0x00404008
, and stores the result in the newly allocated memory created by VirtualAlloc. This looks like an xor decryption function! The data at 0x00404008
must be an xor key.
<--- SNIP --->
│ ┌─< 0x0040155f eb11 jmp 0x401572
│ │ ; CODE XREF from sub.flProtect_40152e @ 0x401576(x)
│ ┌──> 0x00401561 83e203 and edx, 3
│ ╎│ 0x00401564 8a541500 mov dl, byte [rbp + rdx]
│ ╎│ 0x00401568 41321404 xor dl, byte [r12 + rax]
│ ╎│ 0x0040156c 881403 mov byte [rbx + rax], dl
│ ╎│ 0x0040156f 48ffc0 inc rax
│ ╎│ ; CODE XREF from sub.flProtect_40152e @ 0x40155f(x)
│ ╎└─> 0x00401572 39f8 cmp eax, edi
│ ╎ 0x00401574 89c2 mov edx, eax
│ └──< 0x00401576 7ce9 jl 0x401561
<--- SNIP --->
Then, VirtualProtect is called to give the page executable permissions, and CreateThread is called to execute the now decrypted payload.
│ 0x00401578 4889d9 mov rcx, rbx
│ 0x0040157b e873ffffff call sub.KERNEL32.dll_GetModuleHandleA]_4014f3 ; sub.KERNEL32.dll_GetModuleHandleA]_4014f3
│ 0x00401580 4c8d4c243c lea r9, [lpflOldProtect] ; PDWORD lpflOldProtect
│ 0x00401585 4889f2 mov rdx, rsi ; SIZE_T dwSize
│ 0x00401588 4889d9 mov rcx, rbx ; LPVOID lpAddress
│ 0x0040158b 41b820000000 mov r8d, 0x20 ; 32 ; DWORD flNewProtect
│ 0x00401591 ff15a57d0000 call qword [sym.imp.KERNEL32.dll_VirtualProtect] ;
│ 0x00401597 4c8d0552ff.. lea r8, [0x004014f0] ; LPTHREAD_START_ROUTINE lpStartAddress
│ 0x0040159e 4989d9 mov r9, rbx ; LPVOID lpParameter
│ 0x004015a1 31d2 xor edx, edx ; SIZE_T dwStackSize
│ 0x004015a3 31c9 xor ecx, ecx ; LPSECURITY_ATTRIBUTES lpThreadAttributes
│ 0x004015a5 48c7442428.. mov qword [lpThreadId], 0 ; LPDWORD lpThreadId
│ 0x004015ae c744242000.. mov dword [dwCreationFlags], 0 ; DWORD dwCreationFlags
│ 0x004015b6 ff15a87c0000 call qword [sym.imp.KERNEL32.dll_CreateThread]
We now see the full picture. A graph, as a treat.

Next, let's try extracting the payload and writing it to a file in tmp.
[0x004014b0]> s 0x00404014
[0x00404014]> y 0x037c
[0x00404014]> ytf /tmp/payload
Next, I wrote a xor decryptor in Rust (the GOAT) to read this file and with the key, create a new file that's hopefully an unencrypted payload.
use std::fs::File;
use std::io::{Read, Write};
fn xor_decrypt(encrypted_data: &[u8], key: u32) -> Vec<u8> {
let key_bytes = key.to_le_bytes();
let mut decrypted_data = Vec::with_capacity(encrypted_data.len());
for (i, &byte) in encrypted_data.iter().enumerate() {
let decrypted_byte = byte ^ key_bytes[i % 4]; decrypted_data.push(decrypted_byte);
}
decrypted_data
}
fn main() -> std::io::Result<()> {
let mut file = File::open("/tmp/payload")?;
let mut encrypted_data = Vec::new();
file.read_to_end(&mut encrypted_data)?;
let key = 0x1363FC12;
let decrypted_data = xor_decrypt(&encrypted_data, key);
let mut output_file = File::create("/tmp/decrypted_payload")?;
output_file.write_all(&decrypted_data)?;
Ok(())
}
The moment of truth, let's see if we have valid instructions...
❯ r2 /tmp/decrypted_payload
[0x00000000]> aa
[0x00000000]> pdf
┌ 210: fcn.00000000 ();
│ ; var int64_t var_8h @ rsp+0x8
│ 0x00000000 fc cld
│ 0x00000001 4883e4f0 and rsp, 0xfffffffffffffff0
│ 0x00000005 e8c8000000 call 0xd2
│ 0x0000000a 4151 push r9
│ 0x0000000c 4150 push r8
│ 0x0000000e 52 push rdx
│ 0x0000000f 51 push rcx
│ 0x00000010 56 push rsi
│ 0x00000011 4831d2 xor rdx, rdx
│ 0x00000014 65488b5260 mov rdx, qword gs:[rdx + 0x60]
│ 0x00000019 488b5218 mov rdx, qword [rdx + 0x18]
│ 0x0000001d 488b5220 mov rdx, qword [rdx + 0x20]
We have valid instructions!!! If you look at the strings, you can even see the IP address that I configured the beacon to reach out to and a Mozilla User-Agent string.
[0x00000000]> izz
[Strings]
nth paddr vaddr len size section type string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0 0x0000000a 0x0000000a 9 10 ascii AQAPRQVH1
1 0x00000028 0x00000028 4 5 ascii JJM1
2 0x0000002d 0x0000002d 6 8 utf8 H1,<a|
3 0x00000040 0x00000040 4 5 ascii RAQH
4 0x0000007d 0x0000007d 4 6 utf8 H1,A
5 0x0000008e 0x0000008e 5 6 ascii L$\bE9
6 0x000000b1 0x000000b1 14 15 ascii AXAX^YZAXAYAZH
7 0x000000c6 0x000000c6 5 6 ascii XAYZH
8 0x000000d7 0x000000d7 7 8 ascii wininet
9 0x000000e9 0x000000e9 4 5 ascii Lw&\a
10 0x000000fb 0x000000fb 5 6 ascii APAPA
11 0x00000116 0x00000116 5 6 ascii AQAQj
12 0x0000014b 0x0000014b 5 6 ascii Pj\n_H
13 0x00000186 0x00000186 5 6 ascii /oi1T
14 0x0000018c 0x0000018c 5 6 ascii \fE\v}E
15 0x00000196 0x00000196 5 6 ascii pw71n
16 0x000001c4 0x000001c4 4 6 utf8 /3dҼ
17 0x000001d6 0x000001d6 82 83 ascii User-Agent: Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; SV1)\r\n
18 0x0000026e 0x0000026e 6 7 ascii I*|[xk
19 0x00000293 0x00000293 5 6 ascii <)\nF.
20 0x000002a6 0x000002a6 4 5 ascii *0M`
21 0x000002d8 0x000002d8 5 7 utf8 22 0x0000035a 0x0000035a 4 5 ascii XXXH
23 0x0000036a 0x0000036a 13 14 ascii 192.168.0.103
That was a lot. Working on part 2 where I'll analyze this new payload with some dynamic analysis.