Un ami vous demande de l’aide pour déterminer si l’email qu’il vient d’ouvrir au sujet du Covid-19 était malveillant et si c’était le cas, ce qu’il risque. Il prétend avoir essayé d’ouvrir le fichier joint à cet mail sans y parvenir. Peu de temps après, une fenêtre liée à l’anti-virus a indiqué, entre autre, le mot KPOT v2.0 mais rien d’apparent n’est arrivé en dehors de cela. Après une analyse préliminaire, votre ami vous informe qu’il est probable que ce malware ait été légèrement modifié, étant donné que le contenu potentiellement exfiltré (des parties du format de texte et de fichier avant chiffrement) ne semble plus prédictible. Il vous recommande donc de chercher d’autres éléments pour parvenir à l’aider. Vous disposez d’une capture réseau de son trafic pour l’aider à déterminer si des données ont bien été volées et lui dire s’il doit rapidement changer ses mots de passe !

So a friend of us opened an attached file in an email and the antivirus detected a threat (KPOT v2.0). He wants to know if some content was exfiltrated and gave us a network capture.

TL;DR

  • Find the requests to gate.php
  • Extract the GET and POST data
  • Crack the key with a known-plaintext attack

What’s KPOT

If we google KPOT v2.0. we found the following article. Credits to ProofPoint for the following part:

KPOT is a stealer malware that focuses on exfiltrating account information and other data from web browsers, instant messengers, email, VPN, RDP, FTP, cryptocurrency, and gaming software.

It was observed in a variety of email campaigns, there’s an attachment file in the email that contains a macro. This macro fetch a powershell scripts to :

  • hxxps://paste[.]ee/r/BZVbl
  • hxxps://paste[.]ee/r/mbQ6R
  • hxxps://paste[.]ee/r/OsQra
  • hxxp://5.188.60[.]131/a6Y5Qy3cF1sOmOKQ/gate.php

It performs a GET request to the C&C server and the response is a base64 encoded string XOR’d with a hardcoded key (16 chars) that is stored as an encrypted string. Here’s an example of plaintext response:

1111111111111100__DELIMM__A.B.C.D__DELIMM__appdata__GRABBER__*.log,*.txt,__GRABBER__%appdata%__GRABBER__0__GRABBER__1024__DELIMM__desktop_txt__GRABBER__*.txt,__GRABBER__%userprofile%\Desktop__GRABBER__0__GRABBER__150__DELIMM____DELIMM____DELIMM__

After the commands are run, a POST request is sent to the C&C, the data is Xor encrypted with the hardcoded Xor key used in the GET response above and once decrypted contains various data organized into sections. Each section has a start delimiter like “FFFILEE:” or “SYSINFORMATION:” and an end delimiter like “FFFILEE” or “SYSINFORMATION”.

Find the IOC

Ok, so we know how KPOT works, let’s check our pcap and see if we found some IOC:

String search

We find some requests to gate.php:

Gate Request

There’s some 403 or 404 response, but we have 200 responses for /Ym9ubmVfcGlzdGVf/gate.php.

If we decode the url (Ym9ubmVfcGlzdGVf), we find: bonne_piste. So let’s check the data in the GET and POST request:

GET response data:  RHVdQ1V8BFVHAgRSAGNZRisbKDYoBXgpKW0HUgl8WUZMal1HXWIGU0Vtaid0HiE7ORszEhQ8UQUCU2o8dgApNDYBPiw7ZhsIGVUZSR8mEAJYGzM0Ng13JjNgajwUMxgGECUYEkETaiMkc3chdAA3KUQbMzQ2DXcmM2BqPABiWkIrGyg2KAV4KSltUQZCORwZBBsYCxATaiMkc3chdAA3KV5qGAsQYGo7MWB0IXMXOikrYRkAAT5FFhlUXA9UdzQyETcHBws8ajsxYHQhcxc6KSt0MywjHnQmNHdnPG5iNykwASA6KQFqOyltcSZ9GyU7KxszLCAJeS07f2o8

POST response data: llEÇë@@´ñÀ¨Ëq*…öPR¨ÕêèíÂ)<²+>24	t1)bIB`6=~xPCcf6_	IZW"\cE3iU/2FGU \%,"f'|IU7PGB|[B\uRQPPkZ ZzW]1F%_S)DZSg[B%]{TS8j'c8353

It’s Crypto Time!

So we know the data in the GET response are a string XOR’ed with a key and encoded in base64.

We also know the key is 16 chars long and the first 16 chars in the response are either 1 or 0.

So if we xor the first 16 decoded chars with the example above we might find a part of the key:

import base64

key = "1111111111111100"
get_data = base64.b64decode("RHVdQ1V8BFVHAgRSAGNZRisbKDYoBXgpKW0HUgl8WUZMal1HXWIGU0Vtaid0HiE7ORszEhQ8UQUCU2o8dgApNDYBPiw7ZhsIGVUZSR8mEAJYGzM0Ng13JjNgajwUMxgGECUYEkETaiMkc3chdAA3KUQbMzQ2DXcmM2BqPABiWkIrGyg2KAV4KSltUQZCORwZBBsYCxATaiMkc3chdAA3KV5qGAsQYGo7MWB0IXMXOikrYRkAAT5FFhlUXA9UdzQyETcHBws8ajsxYHQhcxc6KSt0MywjHnQmNHdnPG5iNykwASA6KQFqOyltcSZ9GyU7KxszLCAJeS07f2o8")
text = ""

for i in range(16):
    text += chr(get_data[i] ^ ord(key[i % len(key)]))

print(text)

So we find the following key: uDlrdM5dv35c1Riv.

Let’s try to decode all the data to see if this is the good key:

import base64

key = "uDlrdM5dv35c1Riv"
get_data = base64.b64decode("RHVdQ1V8BFVHAgRSAGNZRisbKDYoBXgpKW0HUgl8WUZMal1HXWIGU0Vtaid0HiE7ORszEhQ8UQUCU2o8dgApNDYBPiw7ZhsIGVUZSR8mEAJYGzM0Ng13JjNgajwUMxgGECUYEkETaiMkc3chdAA3KUQbMzQ2DXcmM2BqPABiWkIrGyg2KAV4KSltUQZCORwZBBsYCxATaiMkc3chdAA3KV5qGAsQYGo7MWB0IXMXOikrYRkAAT5FFhlUXA9UdzQyETcHBws8ajsxYHQhcxc6KSt0MywjHnQmNHdnPG5iNykwASA6KQFqOyltcSZ9GyU7KxszLCAJeS07f2o8")
text = ""

for i in range(len(get_data)):
    text += chr(get_data[i] ^ ord(key[i % len(key)]))

print(text)
magnussen@funcMyLife:~/chapardeur$ python3 exploit.py
1111111111111100^_DDLHMM_^218.009.159/373^_DELHML__`pqdat`__GR@BCER^_+.lof,*.tyt-__FR@BBES__%aqpeat`%^_GR@BBER^_1__FR@BBES__1034^_DDLHMM_^deskuoq_tyt^_GR@BBER^_+.tyt-__GSABBES_^%uresprogile%]Ddskuoq__GSABBES_^0_^GSABBDR__0^_EELHML___^DELILM^__^DDLIML__

Ok, so it’s similar to the output we expect, but not quite the same, for example ^_DDLHMM_^ should be __DELIMM__.

Let’s modify the binary string we find earlier in order to find the correct string (we decompose the message in block of 16 chars and modify the binary char where the character is wrong):

import base64

key = "0110101110111110"
get_data = base64.b64decode("RHVdQ1V8BFVHAgRSAGNZRisbKDYoBXgpKW0HUgl8WUZMal1HXWIGU0Vtaid0HiE7ORszEhQ8UQUCU2o8dgApNDYBPiw7ZhsIGVUZSR8mEAJYGzM0Ng13JjNgajwUMxgGECUYEkETaiMkc3chdAA3KUQbMzQ2DXcmM2BqPABiWkIrGyg2KAV4KSltUQZCORwZBBsYCxATaiMkc3chdAA3KV5qGAsQYGo7MWB0IXMXOikrYRkAAT5FFhlUXA9UdzQyETcHBws8ajsxYHQhcxc6KSt0MywjHnQmNHdnPG5iNykwASA6KQFqOyltcSZ9GyU7KxszLCAJeS07f2o8")
text = ""

for i in range(16):
    text += chr(get_data[i] ^ ord(key[i % len(key)]))

print(text)

So the correct key is: tDlsdL5dv25c1Rhv.

Let’s decrypt the GET and POST response:

import base64

get_data = base64.b64decode("RHVdQ1V8BFVHAgRSAGNZRisbKDYoBXgpKW0HUgl8WUZMal1HXWIGU0Vtaid0HiE7ORszEhQ8UQUCU2o8dgApNDYBPiw7ZhsIGVUZSR8mEAJYGzM0Ng13JjNgajwUMxgGECUYEkETaiMkc3chdAA3KUQbMzQ2DXcmM2BqPABiWkIrGyg2KAV4KSltUQZCORwZBBsYCxATaiMkc3chdAA3KV5qGAsQYGo7MWB0IXMXOikrYRkAAT5FFhlUXA9UdzQyETcHBws8ajsxYHQhcxc6KSt0MywjHnQmNHdnPG5iNykwASA6KQFqOyltcSZ9GyU7KxszLCAJeS07f2o8")

post_data = [0x2b, 0x00, 0x3e, 0x32, 0x34, 0x09, 0x74, 0x31, 0x29, 0x62, 0x49, 0x16, 0x42, 0x60, 0x18, 0x13, 0x01, 0x36, 0x3d, 0x06, 0x01, 0x7e, 0x78, 0x50, 0x1a, 0x13, 0x15, 0x43, 0x63, 0x66, 0x1b, 0x05, 0x01, 0x36, 0x5f, 0x09, 0x49, 0x1a, 0x5a, 0x10, 0x04, 0x57, 0x18, 0x22, 0x5c, 0x63, 0x45, 0x33, 0x00, 0x69, 0x1a, 0x1c, 0x55, 0x2f, 0x04, 0x32, 0x19, 0x46, 0x47, 0x06, 0x55, 0x20, 0x5c, 0x06, 0x11, 0x25, 0x19, 0x2c, 0x22, 0x0f, 0x66, 0x27, 0x7c, 0x49, 0x01, 0x55, 0x08, 0x37, 0x50, 0x47, 0x42, 0x7c, 0x5b, 0x42, 0x5c, 0x75, 0x0c, 0x52, 0x13, 0x51, 0x0d, 0x50, 0x50, 0x6b, 0x5a, 0x17, 0x17, 0x20, 0x5a, 0x15, 0x01, 0x7a, 0x57, 0x5d, 0x15, 0x02, 0x06, 0x00, 0x07, 0x31, 0x0d, 0x12, 0x46, 0x25, 0x5f, 0x12, 0x53, 0x29, 0x02, 0x05, 0x44, 0x02, 0x0d, 0x5a, 0x53, 0x67, 0x5b, 0x42, 0x16, 0x25, 0x0d, 0x16, 0x5d, 0x7b, 0x54, 0x53, 0x0b, 0x38, 0x6a, 0x27, 0x63, 0x13, 0x38, 0x33, 0x35, 0x11, 0x33]

key = "tDlsdL5dv25c1Rhv"
text_get = ""
text_post = ""

for i in range(len(get_data)):
    text_get += chr(get_data[i] ^ ord(key[i % len(key)]))

for i in range(len(post_data)):
    text_post += chr(post_data[i] ^ ord(key[i % len(key)]))

print("GET data: \n\n{}\n".format(text_get))
print("POST data: \n\n{}\n".format(text_post))
magnussen@funcMyLife:~/chapardeur$ python3 exploit.py
GET data:

0110101110111110__DELIMM__218.108.149.373__DELIMM__appdata__GRABBER__*.log,*.txt,__GRABBER__%appdata%__GRABBER__0__GRABBER__1024__DELIMM__desktop_txt__GRABBER__*.txt,__GRABBER__%userprofile%\Desktop__GRABBER__0__GRABBER__0__DELIMM____DELIMM____DELIMM__

POST data:

_DRAPEAU_P|us2peurQue2M4l!  R4ssur3z-Votre-Am1-Et-vo1c1Votredr4peau_FCSC
{469e8168718996ec83a92acd6fe6b9c03c6ced2a3a7e7a2089b534baae97a7}
_DRAPEAU_

Nice! We have the flag!

This was a great challenge, one of my favorites, the scenery was great!

Thanks to the FCSC team, this was an awesome CTF, I’ve learned a lot and had a lot of fun. Congratulation for the organization, see you next year!