⚠ 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.

Weve 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:

Untitled

24 packet network chứa trong đó 2 TCP stream với nội dung truy vấn HTTP.

Untitled

Đố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.

Untitled

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+3Do
.rdata:0044B918                                         ; sub_4035F0+9o
.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

Untitled

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.

Untitled