Reversing the shad0w framework part 2
2020-07-01
Contuing from reversing the shad0w framework part 1 we will look at the registration of the beacon and how tasks for the beacon are received.
Just like the previous part this is not meant to defeat the framework in any kind of way, but meant as a reverse engineering exercise for the people that want to follow along, or for the people that are just generally interested in reading this kind of stuff.
Reverse engineering the beacon
In the previous part I also presented the x64dbg script that will take you to the entrypoint of the beacon. Before the beacon is able to execute any tasks however, it will need to register at the C2.
We will begin with looking at the first piece of information that the beacon will retrieve (yes we already hovered over this a little bit), before moving on to the part of reading the received tasks.
Retrieving user information
Before the beacon registers at the C2, it will gather some user information. It will gather this information making use of the Windows API’s.
The first routine that is executed by the beacon is a routine to retrieve the username of the machine, as well as some machine information. The following API calls are used to retrieve this information (https://github.com/bats3c/shad0w/blob/2e4232a3ab95130c5c1bb6dbbbc8ee268161c049/beacon/build/core.c#L35):
GetUserNameA
– retrieves the current username;GetComputerNameExA
– retrieves the domain of the target machine;GetComputerName
– retrieves the hostname of the target machine;
When entering the beacon routine, one of the first calls that is made is the routine to retrieve the information using above calls:
NOTE: I changed the name of the call to make it more clear that this is the call to retrieve the basic user information. You will see this more often in the other snippets of disassembly as well.
After retrieving the basic user information, the beacon will make a call to GetBasicCompInfo
(https://github.com/bats3c/shad0w/blob/2e4232a3ab95130c5c1bb6dbbbc8ee268161c049/beacon/src/core.c#L66).
This call can also be recognized by the VerifyVersionInfoW
call that is followed by the RtlGetVersion
call to retrieve operating system information. This API address is retrieved using GetProcAddress
that is called using ntdll.dll
with LoadLibrary
. The reason this is done with ntdll.dll
is because this is where RtlGetVersion
is specified as stated in the MSDN: https://docs.microsoft.com/en-us/windows/win32/devnotes/rtlgetversion#requirements.
RtlGetVersion
is a more reliable way to get information about the operating system than simply using the major and minor operating system versions. This is probably the reason why this API is used to retrieve this information.
First beacon check-in
The first beacon check-in is different than the beacon check-ins that come after registering the beacon at the C2 (as can be seen in the code: https://github.com/bats3c/shad0w/blob/2e4232a3ab95130c5c1bb6dbbbc8ee268161c049/beacon/src/core.c#L155).
To communicate with the C2, the beacon will dynamically load the necessary API calls, it does this by decrypting the DLL name, using this as parameter for the LoadLibrary call and use GetProcAddress
to retrieve the address of the DLL loaded with LoadLibrary
(MDSN: https://docs.microsoft.com/en-us/windows/win32/api/libloaderapi/nf-libloaderapi-GetProcAddress).
When looking at the disassembler it will look a little bit like this (again used the labels to change some of the calls to be more descriptive):
When we look at the address of the encrypted WinHttpCloseHandle
in memory we can see that there are indeed multiple encrypted strings at this point in memory:
56 47 70 74 53 33 64 33 63 30 42 76 62 48 42 6D VGptS3d3c0BvbHBm
53 32 4A 74 5A 32 39 6D 00 4D 43 34 70 4C 7A 4D S2JtZ29m.MC4pLzM
7A 4E 32 6B 6A 4B 79 73 3D 00 41 7A 30 36 48 43 zN2kjKys=.Az06HC
41 67 4A 42 73 6B 4D 54 6F 3D 00 5A 31 6C 65 65 AgJBskMTo=.Z1lee
45 52 45 51 48 4E 66 58 6C 35 56 55 30 51 3D 00 EREQHNfXl5VU0Q=.
52 33 6C 2B 57 47 52 6B 59 46 39 67 64 58 35 43 R3l+WGRkYF9gdX5C
64 57 46 6C 64 57 4E 6B 00 00 2F 00 72 00 65 00 dWFldWNk../.r.e.
NOTE: The above encrypted strings are delimited by the NULL byte \x00
After opening a socket by executing the WinHttpOpen
, it will create a connection using WinHttpConnect
. A POST request is created using the following parameters when calling the WinHttpSendRequest
API:
URI: /register
METHOD: POST
HEADER: Content-Type: application/x-www-form-urlencoded\r\n
HEADER: Content-length
DATA: Buffer containing the user and system info, as well as the security setting of the beacon
This can be observed in the disassembly view:
The above disassembly corresponds to the following line of code: https://github.com/bats3c/shad0w/blob/2e4232a3ab95130c5c1bb6dbbbc8ee268161c049/beacon/src/core.c#L380.
After the above call has been made, the beacon loads the WinHttpReceiveReponse
so it is able to also retrieve a handle on the HTTP connection that was established when sending the data.
Now comes the more important part; after the WinHttpReceiveResponse
call, the beacon will make a call to WinHttpReadData
. This will read the response received of the C2.
The WinHttpReadData
looks like this in the disassembly view:
NOTE: You can see the above structure in the code: https://github.com/bats3c/shad0w/blob/2e4232a3ab95130c5c1bb6dbbbc8ee268161c049/beacon/src/core.c#L429, as well as on this MSDN page: https://docs.microsoft.com/en-us/windows/win32/api/winhttp/nf-winhttp-WinHttpReadData
The above information tells us that there are a couple of things we should watch, following some of MSDN X64 calling convention (https://docs.microsoft.com/en-us/cpp/build/x64-calling-convention?view=vs-2019#callercallee-saved-registers), and reverse engineering:
RDX – pointer to start the buffer;
ECX (could be other nonvolatile register) so we know how much data is received;
R8 (could be other nonvolatile register) so we know how much still needs to be read;
And indeed, if we look at the memory pointed to by RSI we can see the received contents:
7B 22 69 64 22 3A 20 22 31 30 64 65 66 62 36 61 {"id": "10defb6a
61 64 64 34 30 65 63 66 61 61 65 65 64 37 63 36 add40ecfaaeed7c6
39 66 34 36 34 31 39 62 22 2C 20 22 61 6C 69 76 9f46419b", "aliv
65 22 3A 20 74 72 75 65 7D 3F 1B D5 FB 7F 00 00 e": true}?.Õû...
NOTE: The displayed ID is a MD5 sum made of the current timestamp of the C2 (https://github.com/bats3c/shad0w/blob/2e4232a3ab95130c5c1bb6dbbbc8ee268161c049/lib/tools.py#L7).
Reading received tasks
So now we can test some things; the code shows different opcodes and execution id’s in the code, let’s play around a little bit.
Take for example the pwd
command, this command is identified by opcode 0x3000
in the code: https://github.com/bats3c/shad0w/blob/2e4232a3ab95130c5c1bb6dbbbc8ee268161c049/beacon/src/core.c#L514.
When searching for pwd
in the Github repository, you will see that there is a Python file that shows the execution id (0x4000
) and the opcode (0x3000
) used for this command. If we execute this command in the C2 console, we can actually see this coming back in memory as well by looking at the buffer that RDX is pointing to after the WinHttpReadData
call:
7B 22 74 61 73 6B 22 3A 20 31 36 33 38 34 2C 20 {"task": 16384,
22 61 72 67 73 22 3A 20 22 7B 5C 22 6F 70 5C 22 "args": "{\"op\"
3A 20 31 32 32 38 38 2C 20 5C 22 61 72 67 73 5C : 12288, \"args\
22 3A 20 5C 22 6E 75 6C 6C 5C 22 7D 22 2C 20 22 ": \"null\"}", "
61 6C 69 76 65 22 3A 20 74 72 75 65 7D 00 AB AB alive": true}.««
In the above data that is stored in memory we observe that the task
key holds the value of 16384
(or 0x4000
), and the op
key holds the value of 12288
(or 0x3000
). We can also see that this command does not take any arguments as the args
key has a null value. The args holds a null value because this command uses built-in Windows API calls to retrieve the information that is requested , and thus doesn’t need any arguments to do so. It is able to retrieve the current working directory by calling the GetCurrentDirectory
API.
Whenever there is a whole module being loaded, like when executing the upload
command in the C2, the beacon makes excessive use of the heap to store larger payloads.
The first call that is observed after the WinHttpReadData
call, is responsible for writing the buffer that has been received until then onto the heap. To see where the payload is in this process , the vsnprintf
call can be usedto check the buffer so far.
vsnprintf
writes formatted data from variable arguments into a buffer (http://www.cplusplus.com/reference/cstdio/vsnprintf/). By checking the value RCX is pointing to in memory we can find the end of the buffer that has been read by WinHttpReadData
so far.
So from the information that we observed so far it could be possible to store the location of the buffer and its size by storing the value RCX is pointing to after the vsnprintf
call, together with the value in RAX
which points to its size. (value in RCX
– value in RAX
) = starting address of the complete buffer.
The memmove
call will move the received arguments to another place in the heap. This spot in the heap can be found when looking at the RAX register and following that in the memory dump.
Automagically reading received tasks
So now onto a bit of fun, I wrote a little x64dbg script that brings you not only to the entrypoint of the beacon, but will also keep an eye on the received tasks by reading the buffer that is received with WinHttpReadData
.
When running the script, keep in mind that it will pause every time after returning from the API call. To continue the script execution, just press the spacebar.
The first time running this script will present you with the beacon register process, this can be seen by the md5 that is given back as its ID:
When the beacon is just listening for tasks, you will observe the 4096
value, AKA 0x1000
. If we execute a command like whoami
in the C2 console, we’ll see it will receive a different task number, and a blob of base64 encoded data:
Conclusion part 2
So now we know how the beacon registers itself at the C2, what the first check-in looks like, and how the beacon receives its tasks, including where to read out the received buffer.
In the next part we will continue with the different commands it can actually receive and how this routine looks in the disassembly.
Thanks for reading and keep tuned for part 3
Last updated