Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tetctf 2024 apt writeup #2

Merged
merged 4 commits into from
May 23, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion content/blog/sasctf2024-stuhnet/index.md
Original file line number Diff line number Diff line change
@@ -6,10 +6,12 @@ params:
links:
- name: channel
link: https://t.me/theinkyvoid
title: "SAS CTF 2024 - CK0P0 CTYXHET writeup"
title: "SAS CTF 2024 - CK0P0 CTYXHET"
tldr: "challenge we solved by running angr on a binary generated from pseudocode of the wasm file"
date: "2024-05-21T22:24:53+02:00"
tags: [reverse]
summary: |
We were given a website that loads a WASM module. The website splits a string into 6 parts, then runs 6 different WASM check functions on the parts. If they are successful, it gives us the flag. I used wasm2ida to get an ELF binary with the same code and Ghidra with the WASM plugin to get decompilation. The checks themselves looked SMT-solvable, so I first tried to use Z3 but failed miserably. I then went on to use angr on a binary I wrote myself with the functions copied from disassembly.
---

## Basics
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/MsMpEng.exe
Binary file not shown.
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/dmp1
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/dmp2
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/dmp3
Binary file not shown.
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/important_note.txt
Binary file not shown.
71 changes: 71 additions & 0 deletions content/blog/tetctf2024-apt/index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
---
params:
authors:
- name: "falamous"
social: https://t.me/falamous
links:
- name: channel
link: https://t.me/theinkyvoid
title: "TeTCTF 2024 - APT"
tldr: "interesting malware reverse engineering task"
date: "2024-05-23"
tags: [reverse]
summary: |
Basically, we were given a malware sample (whether it was "real" or not is to be determined), a traffic dump, and a file supposedly encrypted by it `important_note.txt`. The sample consisted of a small binary `MsMpEng.exe`, a binary file `AmMonitoringProvider.mof`, and a DLL `mpsvc.dll`. The binary calls `ServiceCrtMain` from the DLL and there its purpose is concluded. `ServiceCrtMain` opens the binary file, decrypts it (using the first 256 bytes of the DLL as the key), then maps it to executable memory. It then passes that address as a callback to `LineDDA`. With the help of one of my teammates, we managed to dump the aforementioned memory to a file. I could have reverse engineered it, but having binwalked it, I isolated the part of the dump that was a DLL (no doubt being loaded by the dumped code) and started analyzing `malware.dll`, which was the right call.
---

# Advanced Persistent Threat (TeTCTF 2024)

To be honest, we started playing TetCTF for imaginary rating points, but it turned out to be a pretty cool CTF overall. One of the best challenges I've solved was APT (Advanced Persistent Threat):

## Loaders

Basically, we were given a malware sample (whether it was "real" or not is to be determined), a traffic dump, and a file supposedly encrypted by it [important_note.txt](important_note.txt). The sample consisted of a small binary [MsMpEng.exe](MsMpEng.exe), a binary file [AmMonitoringProvider.mof](AmMonitoringProvider.mof), and a DLL [mpsvc.dll](mpsvc.dll). The binary calls `ServiceCrtMain` from the DLL and there its purpose is concluded. `ServiceCrtMain` opens the binary file, decrypts it (using the first 256 bytes of the DLL as the key), then maps it to executable memory. It then passes that address as a callback to `LineDDA`. With the help of one of my teammates, we managed to dump the aforementioned memory to a file. I could have reverse engineered it, but having binwalked it, I isolated the part of the dump that was a DLL (no doubt being loaded by the dumped code) and started analyzing [malware.dll](malware.dll), which was the right call.

## Malware

The DLL turned out to be the real malware, not just yet another loader. `DllMain` uses the same `LineDDA` trick to make debugging harder. So I proceeded with static analysis. What seems to be the main function is busy initializing some structs: the first one `funcs` is a structure that holds pointers to some functions, most of which are built-in, but two are custom. I skipped them for now and only ended up coming back to one of them much later; the second `virus_obj` is the main object operated on, it contains the CNC host `"totally-not-malicious-host.local"` and port `1337` (of course) and a set of other strange fields initially set to zero; the last one `some_tree` was somewhat of an enigma that I only ended up figuring out near the very end. Next, the main loop. First comes a pretty clean connect function. Then the loop. While some field of the `virus_obj` is non-zero (which is some health var, here named `working`), 3 functions are executed.

## Starting Reverse Engineering

The first one looks very complicated, but after looking at it for a while, I figured out it operated on some sort of vector (or `std::string`) structure. 10 minutes later I marked out some common functions: something that looked like an append and a couple of destructors. The first call here is actually a clean function, which `recv`s 4 bytes (the header) which must be `b"\xbe\xba\xfe\xca"`, then 4 bytes - `int` - length and then `length` bytes of the actual packet. After that, some complicated mess happens, but that didn't concern me at that point as I moved on to the third function.

## Main Switch Case

I skipped the second function as it looked compiler-y and complicated, as opposed to the third which looked quite logical. It mainly consists of a simple switch case based on the first character of the parameter (the packet), so a typical set of commands. To add to that, the commands themselves looked quite typical: some common prepare functions, a handler (to which the parameters are the begin and end of the packet vector offset by one - the command itself).

![The switch case in question](switch_case.webp)

## Case 4

The simplest command is command 4, which simply tries to load a DLL and then calls some function on 2 bytes of a result. That function looks a lot like the `recv` function, but in reverse: first some similar nonsense, then a clean `send` function (which sends `b"\xbe\xba\xfe\xca"`, 4 bytes of length, and then the packet).

## Case 1

The next command used - 1 is a bit more complicated. It allocates some executable memory, writes the packet offset by 8 into it. Then it takes the first and second 4 bytes as offsets to 2 parts of the memory - 2 functions, then puts those addresses into the global `virus_obj` struct. So of course, I pulled the code out of the traffic and analyzed it. But first, I back-referenced where those virtual functions were used: `recv` and `send`. As the functions looked cryptographic and the traffic later appeared to be encrypted, and especially because the second one (decrypt) checked that the buffer it was given as an argument started with `b"AETX"` which appeared a lot in traffic, I found out these were the encryption and decryption handlers set by the CNC. They are called before sending and after receiving in a specific way (with `funcs` as the first argument, input pointer, size) which will be a common pattern. So I implemented the decryption in Python and started decrypting the following traffic.

![Setting the decrypt handler](set_decrypt_handler.webp)

![Encryption function](encryption_function.webp)

## Case 0

The next command (0 I think) is exactly the same in principle: the handler is executed after decryption, before encryption, but it seems to use hash importing. The function in `funcs` at offset 0 is probably then some `import_by_hash`, but I didn't reverse engineer it yet. Instead, by some miracle, after googling the hash of the function name, I found some Indian shellcode project which happened to use the exact same hash and figured out the hash was of the function `RtlDecompress`. Some googling constants later and I installed an `lznt1` library to decompress the decrypted packets. The malware somewhat assembles itself from the CNC: first encryption, then compression; which is an interesting concept, but personally, if I were writing malware, I would at least provide some defaults.

![Decompression function](decompression_function.webp)

## Last Packet

It seemed no other handlers were added so I proceeded right to the last packet which contained some juicy constants `"encrypted_by_pepega"` and `"important_note.txt"` - probably the encryption key and file to encrypt. The command 3 in this packet starts with a call to a tree function - lower bound. The tree in question is only referenced a couple of times: in its initialization and in another command - 2. And it just so happens that there is a huge packet with command 2. The command 2 looks quite simple - just an insert. So command 3 operates on the inserted data. First, it decrypts it in a similar but not the same fashion to the decryption handler and then calls the decrypted data like a handler. So I implemented this decryption - `decrypt2` in Python, decrypted the code, and started analyzing it. The code contains 3 functions, the first one is a parser and so does not concern us, it calls the last on our packet data. The second uses a lot of importing by hash and so reverse engineering that functionality was now inevitable.

## The Encryption Function

I was kind of familiar with import by hash, but I thought it was some built-in hash that was used. Having dug a bit deeper, I found out instead the way it works is you iterate over all of the modules, compute the hash of their name, then if the hash matches it returns the module. Same thing for functions in a module. I started frantically searching for a list of all WinAPI functions. Some 40 minutes later, I got desperate. I had a handy-dandy bit of code from a shellcode project that would find the function in a module with the exact same hash function (because I got lucky remember). So I took a list of all of the DLLs currently installed on my Wine and iterated over all of them, trying to find the function inside of it. If I did, I would print its name. With a little hack, I was able to get the names of all the hash imported functions in no time. The third function amounted to opening the file, mapping it in memory, then calling `sub_100(funcs, mapped_file, '_'.join(key, GetComputerNameA()), GetUserNameA))`. The second function `sub_100` hashes the key using SHA256, seeds that to ARC4, and encrypts the mapped file with that. Needless to say, I found both the computer name and the username in some outgoing packets. I implemented the whole thing in C with WinAPI, it worked, and I got the flag.

![sub_100](sub_100.webp)

One last sour note is that for some reason I had to seed the hash with the string AND the null byte, which wasted a bit of time.

## Conclusion

This was a very interesting challenge and a neat intro to malware reverse engineering.
Binary file added content/blog/tetctf2024-apt/malware.dll
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/mpsvc.dll
Binary file not shown.
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/sub_100.webp
Binary file not shown.
Binary file added content/blog/tetctf2024-apt/switch_case.webp
Binary file not shown.
13 changes: 12 additions & 1 deletion tailwind.config.js
Original file line number Diff line number Diff line change
@@ -23,7 +23,18 @@ module.exports = {
"DejaVu Sans Mono",
"monospace",
],
postSans: ["system-ui", "sans-serif"],
postSans: [
"ui-rounded",
"Hiragino Maru Gothic ProN",
"Quicksand",
"Comfortaa",
"Manjari",
"Arial Rounded MT",
"Arial Rounded MT Bold",
"Calibri",
"source-sans-pro",
"sans-serif",
],
landingSans: ["system-ui", "sans-serif"],
},
},