Even if Kpot v1.0 is definitely old, it might be still interesting to some; the sample in question was found on Any.Run.
Objectives
- Defeat the packer
- Decrypt payload strings - Ghidra Jython
- Decrypt network traffic - Stand alone python script
The packer
- 1st layer - Delphi
- Few notes
- Checks user interaction calling repeatedly
User32.dll!GetCursorPos
(getting mouse X and Y coordinates) and alternating calls toNtdll.dll!NtDelayExecution
, sleeping 125 ms between checks - VM env detection with
cpuid
call - Performs injection (
Process Hollowing
) creating a suspended copy of itself under%Temp%
withKernel32.dll!CreateProcessInternalW
- it spoons out the child with
Ntdll.dll!NtUnmapViewOfSection
- writes the carried code thanks to
Ntdll.dll!NtCreateSection
andNtdll.dll!NtMapViewOfSection
- it spoons out the child with
- Finally, calls
Ntdll.dll!NtResumeThread
on the child process, releasing the injected payload
- Checks user interaction calling repeatedly
- Few notes
- 2nd layer - Kpot fresh sample
To extract the payload, there are at least two possible paths one can follow. The tedious version, requires to trace every call to NtMapViewOfSection
and see where the payload will be written in memory or go for the I'm feeling lucky - Google
;~) approach, and check for RWX
/ strings patterns
in the hollowed process memory sections.
Tracing NtMapViewOfSection
Set breakpoints on
bp NtResumeThread
bp NtMapViewOfSection
To keep track of the different maps created between the main process and the (injected) child, a table like the one below can come in handy - thx herrcore
Section handler | Remote address | Local address |
---|---|---|
F4 | 75 C0 00 00 | |
E4 | 00 40 00 00 | 01 6A 00 00 |
E0 | 00 16 00 00 | 01 6C 00 00 |
Once the last NtMapViewOfSection
is hit, NtResumeThread
is the only missing call preventing payload execution. From the table, only the 2nd entry is of interest and it can be seen how code located in the parent process @0x016A0000
is mapped into the child @0x00400000
… this looks a good candidate for a PE base address.
Indeed, checking the location in the Memory dump
reveals a shiny DOS header
I’m feeling lucky
This approach requires only to set one breakpoint, on NtResumeThread
and
1. Attach to the new child process
2. Inspect Memory Map
⇒ Find pattern
⇒ search for “This program cannot”
3. Follow
memory address in Dump
⇒ Follow in Memory Map
Do you see something familiar?
The memory protection at this address is set to ExecuteReadWrite, another hint about injection.
Payload location is now identified, from here the injected Kpot is ready to be execute in memory, from this stage
- it can be dumped to disk
- unmapped
And it’s ready for being analyzed
Go, go, go!
Unpacked sample details:
- md5: ed829243eb730bf09b115f9a4380fdb5
- sha1: b695b29e041cc856b829f4e695e0e9cf74070c0e
- sha256: b2ac6098eee75aff2d72378d5ac487f8f4bf7fc05c20ffe175a95f0c975ed691
Big picture, at its core the analyzed sample has the following structure
Let’s begin … part of the Jython code developed for Kpot is similar to the one used for the Artra downloader
sample, nevertheless, there will be few tweaks here and there to cope with this specimen.
Once loaded in Ghidra, following Exports
⇒ entry
will lead to two calls, the first one, renamed here in mw_main
, jumps straight to the malware core.
Strings decryption instructions are located inside FUN_0040c083
- this is one of the first function to be called after jumping from the entry point - but it’s not called immediately, instead, Kpot will perform some privilege escalation tricks first, restarting itself with the new granted permissions - if necessary.
Privilege escalation functions overview
-
mw_func_CheckProcessToken()
wraps calls checking the process integrity level withOpenProcessToken
⇒GetTokenInformation
⇒GetSidSubAuthorityCount
⇒GetSidSubAuthority
-
If mw_func_CheckProcessToken == 4
(GetSidSubAuthority < 0x2000)[SECURITY_MANDATORY_MEDIUM_RID], performs escalation ⇢CreateProcessAsUserW
if mw_func_CheckProcessToken == 1
(GetSidSubAuthority == 0x1000)[SECURITY_MANDATORY_LOW_RID], performs escalation ⇢ShellExecuteW
(RunAs)
Once checks are completed, the mw_wrap_func_string_decrypt()
function is triggered, and strings are decrypted in bulk.
The decryption function
The wrap function is just initializing, in sequence, the decryption of every single string
Taking for instance the first entry as a reference, it can be clearly seen how mw_func_string_decrypt
takes three parameters
|
|
Every string requires a different key to decrypt itself, once cleaned-up, mw_func_string_decrypt
, looks like the following
Pseudo code can be replicated in python, like this
|
|
To proof the code, some encrypted strings can be taken from the payload as a reference.
|
|
But enough talk … Have at you!
Building the Ghidra script
1. Code reference to the decrypt function
For this sample, code is located @0x00405623
|
|
2. Get function arguments
Given the xrefs address, get_function_args
will scan backwards every code blocks and look for - in reverse order - the following pattern.
PUSH
xor keyPUSH
encrypted string- (
PUSH
length of encrypted string)
The last entry in the list is not really necessary, since it is confirmed it will be the length of the encrypted string, thus it can be skipped and computed at script runtime.
|
|
The last elif
statement, skips operation of type, mov [0x...... ], EAX
and recalls get_function_args
, going back of one instruction. The skipped code is just storing into a memory location the decrypted string of a previous call to mw_func_string_decrypt
- more on this later.
3. Execute decryption
After the previous step, the buffer
variable contains a mapping between the xor keys
and the encrypted strings
. It’s now enough to iterate through it and apply the decryption, calling the decrypt_string
snippet - introduced at the end of the previous section.
|
|
4. Processing results
Finally, decrypted strings are added to Ghidra as comments and labels
|
|
Below a flat list of the revealed strings
http://nkpotu.xyz/Kpot2/gate.ph
rmGJUQE5lueQotqhTfG4DDY
\Microsoft\Windows\CurrentVersion
accounts
\Martin Prikryl\WinSCP 2\Sessions
FileZilla
recentservers
sitemanager
Ipswitch\WS_FTP\Sites\ws_ftp
Web Data
logins
Software
Install Directory
webappsstore
Path
credentials
CABINET
Valve\Steam
config
loginusers
wallet.dat
Crypto
\drivers\
VBox
Guest
Mouse
Video
Ethereum
keystore
Electrum
Bytecoin
Armory
Namecoin
monero-project
wallet_path
key3.db
Browsers
\Microsoft\Windows NT\CurrentVersion
\Microsoft\Cryptography
global-salt
moz_formhistory
moz_cookies
masked_credit_cards
fieldname
baseDomain
host_key
autofill
encrypted_value
is_secure
is_httponly
cape.dll
isSecure
isHttpOnly
%s TRUE %s %s %ld %s %s
expiry
%s x%d
IP: %S
MachineGuid: %s
CPU: %S (%d cores)
RAM: %S MB
Screen: %dx%d
PC: %s
User: %s
LT: %S (UTC+%d:%d)
GPU:
pstorec.dll
vaultcli.dll
The wrap function that was decrypting all strings in bulk looks indeed more talkative than before
Two down, one to go
- Defeat the packer ☑
- Decrypt payload strings ☑
- Decrypt network traffic
Decrypting network traffic
Unveiled strings scattered around the code helps quite a bit, and building the big picture about payload’s functionalities is getting easier. For this part of the analysis, the 2nd string in order of appearance from the top list is fundamental for for the next steps.
DAT_00415C50 = mw_func_string_decrypt((int)&DAT_004117fc,rmGJUQE5lueQotqhTfG4DDY,0x19);
The result of mw_func_string_decrypt
gets stored into DAT_00415C50
, cross-referencing on this memory area (renamed into cnc_enc_key
), leads the analysis to another interesting portion of the code; it turns out, this is the hard-coded xor
key shared with the C&C server.
This snippet block (taken from a bigger chunk of code) is in charge for reading a stream object (use to store collected information from the system), xor
encrypt and send it to the command-and-control server.
Pseudo code can be translated to python as follows
|
|
To test the code, data sent (below an excerpt) to the gate.php
endpoint, gets translated essentially from this
to
(output formatted for better visualization)
USD914BC32101291311131 # bot_id
SYSINFORMATNON: Windows 7 Professional x32
MachineGuid: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
IP: xxxx.xxxx.xxxx.xxxx
CPU: Intel(R) Core(TM) xx-xxxx CPU @ x.xxGHz (x cores)
RAM: xxxx.x MB
Screen: xxxxxxxxx
PC: xxxxxx
Use: xxxxxxxxx
LT: xxxx-xx-x ..... # local time -> %Y-%m-%d %H:%M:%S' (UTC+X)
GPU: xxxxxxxxxxxxxxxxx
Layouts: x/xx/
_SYSINFORMATION_
these are a subset (by default EarthVPN, NordVPN and Firefox credentials are always collected - if present) of the information Kpot will send back to the C&C if none of the stealer functionalities are enabled.
Compared to other Any.Run samples reports, the specimen here analyzed did not retrieve the initial configuration from the server’s config.php
endpoint, but just received “OK
” instead.
Investigating further Kpot core components, yields the bot true capabilities. The memory address, here renamed in features_array_0041676c
is used as a reference to check if specific functions of the bot should be enabled (value set to 1
) or not, as specified by the configuration retrieved from the C&C.
Targeted cryptocurrency
Targeted FTP clients - interesting to see TotalCommander FTP in the list …
Targeted VPN clients
For instance, executing Kpot in another controller env and enabling functions
mw_func_steal_psi_pidgin_cred
mw_func_steal_FileZilla_credentials
mw_func_wrap_steal_VPNs
it transmits the below additional blob
_SYSINFORMATNON_
delimits the end of the previous OS info message (as it was observed in the initial decrypted message), whereas XMPP:
, FTP:
and VPN:
denotes the start of new sections, the end of the same are marked with _XMPP
, _FTP
and _VPN
; last but not least, every section separates exfiltrated data with the corresponding delimiter between underscores (e.g. _FTP_
).
To note, that the VPN delimiter is present but no data was collected, this because one of the VPN client was indeed detected on the system but credentials were not extracted.
ATT&CK Techniques
Tactic | ID | Name |
---|---|---|
Privilege escalation | T1134 | Access Token Manipulation |
Defense evasion | T1107 | File Deletion |
Credential Access | T1552.001 | Credentials from Files |
Credential Access | T1555.003 | Credentials from Web Browser |
Credential Access | T1539 | Steal Web Session Cookie |
Discovery | T1087 | Account Discovery |
Discovery | T1083 | File and Directory Discovery |
Discovery | T1518 | Software Discovery |
Discovery | T1082 | System Information Discovery |
Discovery | T1033 | System Owner/User Discovery |
Collection | T1119 | Automated Collection |
Collection | T1005 | Data from Local System |
Command and Control | T1043 | Commonly Used Port |
Command and Control | T1024 | Custom Cryptography Protocol |
Command and Control | T1132 | Data Encoding |
Command and Control | T1102 | Web Service |
Exfiltration | T1020 | Automated Exfiltration |
Exfiltration | T1002 | Data Compressed |
Exfiltration | T1022 | Data Encrypted |
Exfiltration | T1041 | Exfiltration Over Command and Control Channel |