raw-data memdumps

Leveraging Qiling for Kpot strings decryption

August 24, 2021

I drafted this article long time ago, shortly after releasing an overview of Kpot, but I never followed-up on this, anyhow, recently I had some time and thought to polish it a bit and just publish.

I will jump straight to the point, but, for those of you that are not familiar with Qiling just have a look at the repository and documentation portal, you won’t be disappointed!

Last but not least, if you want to follow along you will need ghidra_bridge, so to have Qiling and Ghidra on the same page, since the latter one “speaks” only Python2.7, whereas the former requires Python3 and as you guessed, the bridge will let you run - kind of - Python3 within the NSA’s tool.

Let it be Ghidra

I take as granted that you had a look at the previous analysis and have an idea how the decryption function was discovered and reversed, the whole point of working with Qiling in this scenario is - as lazy reverser - to let the tool do the heavy lifting and let us have the rest of the fun.

For this version of Kpot, two emulation approaches are possible, but the techniques introduced here, will serves you well for any malware that relies on the same code implementation.

The choice

Opt - 1. Since strings gets decrypted in bulk (within one huge single function), soon after the main entry in the code, emulating the wrapper function that hosts all calls to the mw_func_string_decrypt, could be a quick win. Intercepting all MOV operations, when the content of EAX is moved into the global array, would grant access to the decrypted string.

In this case, it becomes trivial to check for the pattern {a3 ?? 5? 41 00 } and build emulation around that, but we will go down another road instead…

Opt - 2. Emulate only the function in charge of the string decryption and craft all the expected parameters

We will investigate this 2nd method, since more precise and, as far as I could see, no one showed how to do this with Qiling, which is not the case for the first strategy.

First thing first, let’s do a quick recap about the decryption function, which uses __cdecl calling convention and it takes three parameters,

- Decryption key
- Encrypted string
- Length of the encrypted string

1
2
3
4
 .text:00405680 PUSH     0x22                               ; length of the encrypted strings
 .text:00405682 PUSH     s_I8VN5ESIRT1VVWX4EQMEPW9XK60WSDIR ; encrypted strings
 .text:00405687 PUSH     DAT_004117bc                       ; key
 .text:0040568c CALL     mw_func_string_decrypt    ; int mw_func_string_decrypt(int key, char *

We do now a fast forward and assume the following (yet again, check the previous blog, which explains all steps how we reached this point):

buffer:list  = [
        key, (enc_str, enc_str_start_address)
]

after harvesting all the required details, the buffer structure, would look like this

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
for key, (enc_str, enc_str_start_address) in buffer:
    key = bytes(key)
    enc_str = bytes(enc_str.bytes)
    print(key, enc_str, len(enc_str))

    # b'kP":E\x7f|f<?A9""vL<+b\x0e 8MjdQQ#6j9:9\x1a\x00' 
    # b'I8VN5ESIRT1VVWX4EQMEPW9XK60WSDIR\x00' 33
    # b'e+4\x12\x03\x07\x16\x11\x05>:V' b'GYYUIRGT0RO3QOE1SSBRTSPV9\x00' 26
    # b"\x1a\x18t'VB!?%6Cd\x14>9/]9D\x15\x04=DC\\*5\x15UJ* ,8
    # i" b'8D9N50NLJP78CWWK2N7IGH619DAC08YICVK\x00' 36

Maximum effort

At this point, we are just missing one single step, the emulation part, since all the details needed to accomplish Opt - 2 were collected and formatted in a way to be consumed by Qiling.

Main points, aka, what we need to make Qiling work for us:

1 Identify the decryption function, start and ending addresses, which in this case are, 0x405623 and 0x40567d

2 define, where in the malware code, a hook will be placed, e.g. 0x40567c

This is needed, since if you recall before exiting, the decrypt function will store the deobfuscated string in the EAX register, and our goal is to extract this value before it gets overwritten by some other operation.

3 Since only one function is emulated and not the whole binary, and the same (function) uses __cdecl calling convention, the stack needs also to be prepared, which requires:

  1. PUSH length of the encrypted string
  2. PUSH encrypted string
  3. PUSH decryption key
  4. PUSH return address, to return once the decryption function has completed

let’s head to the code part

from qiling import *

# stores all printable and not printable decrypted strings
dec_string = list()

start_decryption_function = 0x405623
end_decryption_function = 0x40567d

def extract_eax(ql : core.Qiling):
    """
    Hook code, which gets called every time the decryption
    function is terminating, but before the EAX register
    is modified by some other instructions
    """

    decrypted_string = ql.mem.read(ql.reg.eax, 0x50).split(b"\x00")[0].decode()
    print("Content of EAX: %s @ %s" % (decrypted_string, ql.enc_str_add))
    if decrypted_string.isprintable():
        try:
            # wanna be pprint for crafted label, so that it can be
            # displayed in Ghidra
            label_decrypted_string = \
                '_'.join(decrypted_string[:40].strip("\\").strip("\"").split()) 
            start() # required to start a transaction (modification) - h/t @justfoxing
            # update labels, so to leave untouched the encrypted raw bytes strings
            createLabel(toAddr(ql.enc_str_add), label_decrypted_string, False)
            end(True)
        except Exceptions as err:
            print(f"Error: {err}")
    
    # keep all printable and not printable strings
    # map the decrypted string and the start address of each.

    # e.g. ( hxxp[:]//nkpotu.xyz/Kpot2/gate[.]ph, 0x411798 )
    c = (decrypted_string, ql.enc_str_add)
    dec_string.append(c)

def emulator():
    ql = Qiling([
        "kpotv1/soa_pdf_unpacked.bin"],
        rootfs="qiling/examples/scripts/rootfs/x86_windows",
        libcache=True)

    # hook decrypt function before it exits
    ql.hook_address(extract_eax, 0x40567c)
    
    for key, (enc_str, enc_str_addr) in buffer:
        key = bytes(key)
        enc_str = bytes(enc_str.bytes)
        
        # return address        
        ret = ql.stack_pop()
        
        ### push functions args ####
        # 3rd arg: len of enc_str, int
        ql.stack_push(len(enc_str))
        
        # 2nd arg: enc_string, bytes
        ptr = ql.os.heap.alloc(len(enc_str))
        ql.mem.write(ptr, enc_str)
        ql.stack_push(ptr)
        
        # 1 arg: decryption key, bytes
        ptr = ql.os.heap.alloc(len(key))
        ql.mem.write(ptr, key)
        ql.stack_push(ptr)
        
        # finally, push the returns address, 
        ql.stack_push(ret)
        # expand `ql` object, carring start address
        # (that will be used within Ghidra) of the 
        # decrypted string
        ql.enc_str_add = enc_str_addr

        ql.run(begin=start_decryption_function, end=end_decryption_function)

emulator()

Below, how Ghidra’s listing view looks like after updating the labels with the above script.

One last note, in the example, the start and end addresses of the target function were hardcoded, but everything can be implemented more dynamically, leveraging a YARA rule to track down the function, something like the one below

rule kpotv1_decrypt_strings_function
{
    meta:
        author = "_raw_data_"
        tlp = "WHITE"

        version = "1.0"
        created = "2020-04-13"
        modified = "2020-04-13"

        description = "Kpotv1 string decryption routine"

        reference = "https://raw-data.gitlab.io/post/kpotv1/"

	strings:
        $dec_str_fun = { 5? 8b ?? 5? 5? 5? 8b 75 10 8d 46 01 57 50 e8 ?? ?? ?? ?? 
                        33 db 59 8b f8 85 f6 74 ?? 8b 45 08 2b c7 89 45 fc 8b 4d 
                        0c 8d 34 3b e8 ?? ?? ?? ?? 8b c8 33 d2 8b c3 f7 f1 8b 45 
                        0c 8b 4d fc 8a 04 02 32 04 31 43 88 06 3b 5d 10 72 ?? 8b 
                        75 10 c6 44 37 ff 00 8d 4? }

	condition:
		$dec_str_fun
}

From here, calculating the max and min addresses within the function becomes trivial and could be implemented with a snippet like this

fn = getFunctionContaining("MEMORY-ADDRESS-WHERE-YARA-RETURNED-A-MATCH")

# get location of the first instruction within the function
start_decryption_function = fn.getBody().getMinAddress()

# get location of the last instruction within the function
end_decryption_function = fn.getBody().getMaxAddress()

Tags: