⚠ Spoiler: Đây là write-up cho các challenge của Flare-on 9 tổ chức vào khoảng tháng 11/2022 tại Website.
[05] T8
FLARE FACT #823: Studies show that C++ Reversers have fewer friends on average than normal people do. That's why you're here, reversing this, instead of with them, because they don't exist.
We’ve found an unknown executable on one of our hosts. The file has been there for a while, but our networking logs only show suspicious traffic on one day. Can you tell us what happened?
7-zip password: flare
Công cụ sử dụng:
- CFF Explorer
- IDA Pro, x64dbg
- Python
- flask: pip install flask
- Wireshark
Challenge cung cấp cho ta 2 file:
- t8.exe - file nghi ngờ
- traffic.pcapng - chứa networking log liên quan tới file nghi ngờ.
Sử dụng Wireshark để quan sát tập tin chứa dữ liệu mạng:
24 packet network chứa trong đó 2 TCP stream với nội dung truy vấn HTTP.
Đối với TCP Stream là loại HTTP thì Wireshark cho phép thao tác để lấy dữ liệu trong trường data của method POST một cách dễ dàng.
Quay trở lại với file t8.exe, để bước vào phân tích thì cần biết các thông tin liên quan trước. Đầu tiên là đọc metadata của t8.exe sử dụng CFF Explorer cho thấy tập tin là PE32 và có nhiều khả năng sử dụng chương trình biên dịch VS C++ . Do đó cần sử dụng công cụ như x64dbg và/hoặc IDA Pro, Ghidra để phân tích tĩnh cũng như phân tích động.
Lưu ý: địa chỉ hàm có thể thay đổi khi thực hiện phân tích động trong IDA do ASLR của hệ điều hành.
Trong hàm main tại 00404680
có kiểm tra điều kiện
while ( sub_404570(xmmword_45088C, DWORD1(xmmword_45088C), DWORD2(xmmword_45088C), HIDWORD(xmmword_45088C)) != 15 )
Sleep(43200000u);
Nội dung 00404680
thực hiện tính toán với số thực. Khi mình tìm Google thì nhận thấy đây là hàm tính toán liên quan tới xác định chu kì mặt trăng (Moon phase). Với kết quả (sub_404570() == 15) thì có thể xác định là thời điểm hiện tại là ngày rằm (trăng tròn).
Còn về giá trị của xmmword_45088C
thì nó đã được khởi tạo trước trong hàm sub_401020
được gọi từ hàm _initterm
(CRT startup được thực hiện trước hàm main)
int __cdecl sub_404570(unsigned int a1, unsigned int a2)
{
unsigned int v2; // ecx
int v3; // esi
unsigned int v4; // eax
float v5; // xmm0_4
float v9; // [esp+2Ch] [ebp+14h]
v2 = HIWORD(a1);
v3 = (unsigned __int16)a1 - 1;
if ( HIWORD(a1) > 2u )
v3 = (unsigned __int16)a1;
v4 = v2 + 12;
if ( v2 > 2 )
v4 = HIWORD(a1);
v9 = (float)((float)((double)(int)(v3 / 100 / 4
+ HIWORD(a2)
+ (int)((double)(v3 + 4716) * 365.25)
- (int)((double)(int)(v4 + 1) * -30.6001)
- v3 / 100
+ 2)
- 1524.5)
- 2451549.5)
/ 29.53;
v5 = floor(v9);
return (int)roundf((float)(v9 - v5) * 29.53);
}
Để vượt qua kiểm tra thì có thể đơn giản chỉnh lại thời gian ngày là ngày rằm (full moon).
Chạy chương trình và bắt dữ liệu mạng bằng Wireshark ta biết t8.exe sẽ phân giải tên miền flare-on.com và kết nối tới bằng giao thức HTTP.
Do không kiểm soát được địa chỉ cũng như địa chỉ IP trả về nên ta cần phải tìm cách chèn dữ liệu giả trỏ tới địa chỉ máy chủ có thể kiểm soát được, ví dụ như localhost
Trong môi trường Windows, chỉnh sửa file hosts
trong thư mục C:\Windows\System32\drivers\etc
sẽ có hiệu quả với challenge này.
# localhost name resolution is handled within DNS itself.
# 127.0.0.1 localhost
# ::1 localhost
127.0.0.1 flare-on.com
Đồng thời cần có một chương trình để xử lý các truy vấn HTTP POST. Thư viện Flask dành cho Python là một giải pháp nhanh gọn để giả lập trả lại response. Bạn đọc có thể sử dụng mã nguồn sau:
from flask import Flask
from flask import request
app = Flask(__name__)
import binascii
c=True
@app.route("/", methods=["GET","POST"])
def hello_world():
global c
ua = request.headers.get('User-Agent')
print(ua)
if c:
print("1")
r = "TdQdBRa1nxGU06dbB27E7SQ7TJ2+cd7zstLXRQcLbmh2nTvDm1p5IfT/Cu0JxShk6tHQBRWwPlo9zA1dISfslkLgGDs41WK12ibWIflqLE4Yq3OYIEnLNjwVHrjL2U4Lu3ms+HQc4nfMWXPgcOHb4fhokk93/AJd5GTuC5z+4YsmgRh1Z90yinLBKB+fmGUyagT6gon/KHmJdvAOQ8nAnl8K/0XG+8zYQbZRwgY6tHvvpfyn9OXCyuct5/cOi8KWgALvVHQWafrp8qB/JtT+t5zmnezQlp3zPL4sj2CJfcUTK5copbZCyHexVD4jJN+LezJEtrDXP1DJNg=="
else:
print("2")
r = "F1KFlZbNGuKQxrTD/ORwudM8S8kKiL5F906YlR8TKd8XrKPeDYZ0HouiBamyQf9/Ns7u3C2UEMLoCA0B8EuZp1FpwnedVjPSdZFjkieYqWzKA7up+LYe9B4dmAUM2lYkmBSqPJYT6nEg27n3X656MMOxNIHt0HsOD0d+"
c=not(c)
return r
app.run(port=80, threaded=True)
Sau khi debug thì ta có vftable (virtual function table) của lớp CClientSock tại 0044B918
.rdata:0044B918 ??_7CClientSock@@6B@ dd offset sub_4035F0
.rdata:0044B918 ; DATA XREF: sub_4034C0+3D↑o
.rdata:0044B918 ; sub_4035F0+9↑o
.rdata:0044B91C dd offset sub_403770 ; 01 - string copy
.rdata:0044B920 dd offset sub_4037A0 ; 02
.rdata:0044B924 dd offset sub_403C20 ; 03
.rdata:0044B928 dd offset sub_403CE0 ; 04
.rdata:0044B92C dd offset sub_4036D0 ; 05
.rdata:0044B930 dd offset sub_403860 ; 06 - decrypt response
.rdata:0044B934 dd offset sub_403D70 ; 07 - POST request
.rdata:0044B938 dd offset sub_404200 ; 08 - request 01
.rdata:0044B93C dd offset sub_4043F0 ; 09
.rdata:0044B940 dd offset sub_403910 ; 10 - calculate MD5 hash
Tại sub_404200
chuỗi trả về được decrypt bằng key tạo random trong User-agent. Sau đó xử lý để decrypt tiếp bằng cách split token ,
và lấy giá trị tính toán sub_404570
(tính âm lịch) và dựa theo chỉ số mảng trong hàm sub_4041E0
để tạo ra chuỗi trong bộ nhớ tại v9. Kết quả trả về cho hàm main, nối chuỗi với @flare-on.com
v2 = a2;
Context = a2;
(*(void (__thiscall **)(void *, wchar_t **))(*(_DWORD *)this + 36))(this, String);
v21 = 0;
v3 = (wchar_t *)String;
if ( v16 >= 8 )
v3 = String[0];
v4 = (unsigned int *)wcstok_s(v3, L",", &Context);
v18 = 0;
v19 = 7;
LOWORD(Block[0]) = 0;
LOBYTE(v21) = 1;
if ( v4 )
{
do
{
v5 = sub_404570(*v4, v4[1]);
Src = (unsigned __int16)sub_4041E0(v5);
v6 = wcslen((const unsigned __int16 *)&Src);
v7 = v18;
if ( v6 > v19 - v18 )
{
LOBYTE(v13) = 0;
sub_405CF0(Block, v6, v13, (int)&Src, v6);
}
else
{
v8 = v18 + v6;
v9 = Block;
v18 += v6;
if ( v19 >= 8 )
v9 = (void **)Block[0];
memmove((char *)v9 + 2 * v7, &Src, 2 * v6);
*((_WORD *)v9 + v8) = 0;
}
v4 = (unsigned int *)wcstok_s(0, L",", &Context);
}
while ( v4 );
v2 = a2;
}
Trong đó *(_DWORD *)this + 36)
là virtual method trỏ tới hàm thứ 10 trong vftable và gọi tới hàm decrypt RC4 sub_403860
(vf thứ 7)
- sub_4011C0 - setup
- sub_401120 - decrypt buffer
Mã Python mô phỏng lại hàm giải mã như sau:
import hashlib, base64
class RC4:
"""
This class implements the RC4 streaming cipher.
Derived from http://cypherpunks.venona.com/archive/1994/09/msg00304.html
"""
def __init__(self, key, streaming=True):
assert(isinstance(key, (bytes, bytearray)))
# key scheduling
S = list(range(0x100))
j = 0
for i in range(0x100):
j = (S[i] + key[i % len(key)] + j) & 0xff
S[i], S[j] = S[j], S[i]
self.S = S
# in streaming mode, we retain the keystream state between crypt()
# invocations
if streaming:
self.keystream = self._keystream_generator()
else:
self.keystream = None
def crypt(self, data):
"""
Encrypts/decrypts data (It's the same thing!)
"""
assert(isinstance(data, (bytes, bytearray)))
keystream = self.keystream or self._keystream_generator()
return bytes([a ^ b for a, b in zip(data, keystream)])
def _keystream_generator(self):
"""
Generator that returns the bytes of keystream
"""
S = self.S.copy()
x = y = 0
while True:
x = (x + 1) & 0xff
y = (S[x] + y) & 0xff
S[x], S[y] = S[y], S[x]
i = (S[x] + S[y]) & 0xff
yield S[i]
def getmd5(s):
return hashlib.md5(s).hexdigest().encode('utf-16le')
key = "FO9" + "11950" # FO9 + <random string from RNG extract from user-agent>
key = getmd5(key)
plaintext = RC4(key).crypt(ciphertext) # or plaintext
Từ các thông tin trên có thể giả lập lại quá trình mã hóa bằng Flask
from flask import Flask
from flask import request
app = Flask(__name__)
class RC4:
"""
...
"""
import binascii
c=True
import hashlib, base64
def getmd5(s):
return hashlib.md5(s).hexdigest().encode('utf-16le')
@app.route("/", methods=["GET","POST"])
def hello_world():
global c
ua = request.headers.get('User-Agent')
print(ua)
if c:
key = "FO9" + ua.split(" ")[-1][:-1]
print(key)
key = key.encode('utf-16le')
print("1")
b = "TdQdBRa1nxGU06dbB27E7SQ7TJ2+cd7zstLXRQcLbmh2nTvDm1p5IfT/Cu0JxShk6tHQBRWwPlo9zA1dISfslkLgGDs41WK12ibWIflqLE4Yq3OYIEnLNjwVHrjL2U4Lu3ms+HQc4nfMWXPgcOHb4fhokk93/AJd5GTuC5z+4YsmgRh1Z90yinLBKB+fmGUyagT6gon/KHmJdvAOQ8nAnl8K/0XG+8zYQbZRwgY6tHvvpfyn9OXCyuct5/cOi8KWgALvVHQWafrp8qB/JtT+t5zmnezQlp3zPL4sj2CJfcUTK5copbZCyHexVD4jJN+LezJEtrDXP1DJNg=="
b = base64.b64decode(b)
ciphertext = RC4(getmd5("FO911950".encode('utf-16le'))).crypt(b)
ciphertext = RC4(getmd5(key)).crypt(ciphertext)
print(getmd5(key))
print(binascii.hexlify(request.data))
r= base64.b64encode(ciphertext)
else:
print("2")
r = "F1KFlZbNGuKQxrTD/ORwudM8S8kKiL5F906YlR8TKd8XrKPeDYZ0HouiBamyQf9/Ns7u3C2UEMLoCA0B8EuZp1FpwnedVjPSdZFjkieYqWzKA7up+LYe9B4dmAUM2lYkmBSqPJYT6nEg27n3X656MMOxNIHt0HsOD0d+"
c=not(c)
return r
app.run(port=80, threaded=True)
Flag chính là chuỗi trong bộ nhớ được sử dụng để tính hash MD5 trong sub_403910
Flag: i_s33_you_m00n@flare-on.com
Tại sub_403AC0
chuỗi trả về trong request HTTP POST thứ 2 sau khi decrypt là shellcode để lấy địa chỉ hàm FatalAppExitA báo lỗi.