Problem link: https://ctf.0xfun.org/challenges#Chip8%20Emulator-70 Category: Reverse Engineering Points: 250 Level: Medium
Problem Description
Ever wondered how emulators tick under the hood? I built one — the simplest of all, a CHIP-8 emulator. Alongside it, I’ve dropped 100+ games and programs for you to play… or are they really only for playing?
Somewhere deep in this virtual silicon, a flaw hides. Uncover it, and in just quad cycles, the flag is yours. Miss it, and you’ll be stuck endlessly.
Overall
Challenge này cung cấp một CHIP-8 emulator được viết bằng C++ và player cần tìm flag ẩn trong emulator.
Kiểm tra chương trình này bằng command file, ta có kết quả như sau:
$ file chip8Emulator
chip8Emulator: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=fb6d85ef675a37aec6b2457e8a8f18d303362bb2, for GNU/Linux 3.2.0, not stripped
Ta nhận thấy đây là một file PIE, tức address sẽ bị thay đổi trong mỗi lần chạy, và file này chưa bị strip.
Tiếp tục kiểm tra các thư viện được sử dụng, ta có phát hiện sau:
$ ldd chip8Emulator
linux-vdso.so.1 (0x00007ffec57f7000)
libSDL2-2.0.so.0 => /lib/x86_64-linux-gnu/libSDL2-2.0.so.0 (0x00007ac008a2b000)
libcrypto.so.3 => /lib/x86_64-linux-gnu/libcrypto.so.3 (0x00007ac008400000)
libstdc++.so.6 => /lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007ac008000000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007ac008942000)
......
Từ kết quả trả về, ta nhận thấy binary có sử dụng libcrypto.so.3 (OpenSSL). Điều này cho thấy quá trình encode/decode có diễn tra trong binary này. Tiếp tục sử dụng command strings để kiểm tra, ta có được kết quả như sau:
strings chip8Emulator | grep -i "flag\|key\|encrypt\|decrypt\|aes"
EVP_DecryptFinal_ex
EVP_DecryptInit_ex
BIO_set_flags
EVP_DecryptUpdate
EVP_aes_256_cbc
11KeyboardSDL
8Keyboard
keyboardsdl.cpp
key.cpp
_ZN11KeyboardSDL6updateEPhPb
_ZN11KeyboardSDLD1Ev
_ZTS8Keyboard
_ZN8KeyboardC2Ev
BIO_set_flags@OPENSSL_3.0.0
emu_key
_ZN3Cpu29skip_next_inst_if_key_pressedEv
_ZN8Emulator11setKeyboardEP8Keyboard
_ZTV11KeyboardSDL
_ZTI11KeyboardSDL
_ZN11KeyboardSDLC1Ev
EVP_DecryptInit_ex@OPENSSL_3.0.0
_ZN3Cpu14wait_key_pressEv
_ZN8KeyboardC1Ev
_ZN11KeyboardSDLD2Ev
EVP_aes_256_cbc@OPENSSL_3.0.0
EVP_DecryptFinal_ex@OPENSSL_3.0.0
_ZTI8Keyboard
_ZN11KeyboardSDL6deinitEv
_ZN11KeyboardSDLC2Ev
_ZTV8Keyboard
_ZTS11KeyboardSDL
_ZN11KeyboardSDL4initEv
EVP_DecryptUpdate@OPENSSL_3.0.0
keyaSEr
baeS6
Ta có thể tìm thấy nhiều hàm OpenSSL như: EVP_aes_256_cbc, EVP_DecryptInit_ex,….
Analyzing
Thực hiện decompile binary gốc với IDA và sub lại, ta có flow của binary này như sau:
#include <iostream>
#include <string>
#include <memory>
#include "Emulator.h"
#include "DisplaySDL.h"
#include "KeyboardSDL.h"
#include "SoundSDL.h"
#include "CmdLineParser.h"
#include "Logger.h"
int main(int argc, char** argv) {
// Quản lý Logger qua shared_ptr (v10 trong decompile)
std::shared_ptr<Logger> logger = Logger::getInstance();
// Khởi tạo bộ phân tích tham số dòng lệnh
CmdLineParser parser;
parser.parseCmdLine(argc, argv);
// Thiết lập mức độ Log (LogLevel) nếu được người dùng chỉ định
if (parser.isLogLevelSet()) {
int level = parser.getLogLevel();
switch (level) {
case 0: logger->setLogLevel(LogLevel::TRACE); break;
case 1: logger->setLogLevel(LogLevel::DEBUG); break;
case 2: logger->setLogLevel(LogLevel::INFO); break;
case 3: logger->setLogLevel(LogLevel::WARN); break;
case 4: logger->setLogLevel(LogLevel::ERROR); break;
default: break;
}
}
// Kiểm tra xem đường dẫn ROM có được cung cấp hay không
if (!parser.isRomFileNameSet()) {
logger->log("No rom path provided", LogLevel::DEBUG);
exit(1);
}
// Khởi tạo các thành phần phần cứng qua giao diện SDL
DisplaySDL display; // Quản lý hiển thị màn hình
KeyboardSDL keyboard; // Quản lý nhập liệu bàn phím
SoundSDL sound; // Quản lý âm thanh
// Khởi tạo lõi giả lập và kết nối các thiết bị ngoại vi
Emulator emulator;
emulator.setDisplay(&display);
emulator.setKeyboard(&keyboard);
emulator.setSound(&sound);
// Lấy tên file ROM và bắt đầu quá trình giả lập
std::string romPath = parser.getRomFileName();
if (emulator.init(romPath)) {
emulator.run(); // Vòng lặp chính của giả lập
emulator.deinit(); // Giải phóng tài nguyên
}
// Các đối tượng tự động gọi Destructor khi ra khỏi phạm vi hàm main
return 0;
}
Từ mã nguồn assembly, ta xác định được các class chính của binary này. Cụ thể:
- Class
emulatortại0x5CA2:.text:0000000000005CA2 ; try { .text:0000000000005CA2 call _ZN8EmulatorC2Ev ; Emulator::Emulator(void) .text:0000000000005CA2 ; } // starts at 5CA2 - Class
cpu(các hàm thuộc classcpucó dạng_ZN3Cpu....()):LOAD:0000000000000000 ; Source File : 'cpu.cpp' - Class
RomLoader(các hàm thuộc classRomLoadercó dạng_ZN9RomLoader...()):LOAD:0000000000000000 ; Source File : 'romLoader.cpp' - Còn lại là các class I/O Interfaces:
.text:0000000000005C75 ; try { .text:0000000000005C75 call _ZN10DisplaySDLC2Ev ; DisplaySDL::DisplaySDL(void) .text:0000000000005C75 ; } // starts at 5C75 .text:0000000000005C7A lea rax, [rbp+var_1980] .text:0000000000005C81 mov rdi, rax ; this .text:0000000000005C84 ; try { .text:0000000000005C84 call _ZN11KeyboardSDLC1Ev ; KeyboardSDL::KeyboardSDL(void) .text:0000000000005C84 ; } // starts at 5C84 .text:0000000000005C89 lea rax, [rbp+var_1960.resource_mask] .text:0000000000005C90 mov rdi, rax ; this .text:0000000000005C93 ; try { .text:0000000000005C93 call _ZN8SoundSDLC1Ev ; SoundSDL::SoundSDL(void) .text:0000000000005C93 ; } // starts at 5C93
Process of creating flag.txt
Thực tế trong CHIP-8 chuẩn, các instruction bắt đầu bằng Fxxx xử lý timer, keyboard, BCD, memory trong CPU. Tuy nhiên, khi phân tích hàm decode_F_instruction tại địa chỉ 0xE716:
.text:000000000000E744 cmp eax, 0FFh
.text:000000000000E749 jz loc_E84C
...
.text:000000000000E84C loc_E84C: ; CODE XREF: Cpu::decode_F_instruction(void)+33↑j
.text:000000000000E84C mov rax, [rbp+var_68]
.text:000000000000E850 mov rdi, rax ; this
.text:000000000000E853 call _ZN3Cpu16superChipRendrerEv ; Cpu::superChipRendrer(void)
.text:000000000000E858 jmp short loc_E8D1
Ở đây, binary sẽ thực hiện kiểm tra nếu lower_byte == 0xFF, máy tính sẽ thực hiện nhảy tới loc_E84C để gọi hàm đặc biệt _ZN3Cpu16superChipRendrerEv(). Thực hiện phân tích hàm _ZN3Cpu16superChipRendrerEv():
- Hàm này sẽ nhận data và thực hiện Decode Base64:
.text:000000000000E9D7 ; try { .text:000000000000E9D7 call _ZN3Cpu12base64DecodeERKNSt7__cxx1112basic_stringIcSt11char_traitsIcESaIcEEE ; Cpu::base64Decode(std::string const&) .text:000000000000E9D7 ; } // starts at E9D7 - Sau khi decode, hàm sẽ lấy IV (Initialization Vector) bằng cách lấy 16 bytes đầu của dữ liệu Decode Base64:
.text:000000000000EA01 call _ZNK9__gnu_cxx17__normal_iteratorIPhSt6vectorIhSaIhEEEplEl ; __gnu_cxx::__normal_iterator<uchar *,std::vector<uchar>>::operator+(long)- Tại step này, toán tử cộng con trỏ sẽ được sử dụng
(operator + 0x10/16)để tách phần IV và ciphertext - Lúc này, pointer sẽ trỏ tới vị trí sau 16 byte đầu (tức là bắt đầu của ciphertext).
- IV sẽ là 16 byte đầu tiên của mảng đã decode.
- Tại step này, toán tử cộng con trỏ sẽ được sử dụng
- Sau khi lấy được IV, binary sẽ thực hiện giải mã AES-256-CBC với key
emu_key:.text:000000000000EA99 call _EVP_CIPHER_CTX_new // call function to get context of AES ... .text:000000000000EAB7 call _EVP_aes_256_cbc // call init function ... .text:000000000000EAD8 call _EVP_DecryptInit_ex // call init function ... .text:000000000000EB91 call _EVP_DecryptUpdate // decoding ... .text:000000000000EC2D call _EVP_DecryptFinal_ex // decoding ...- Sau khi đã có IV (16 byte đầu) và ciphertext (phần còn lại), chương trình sử dụng hàm của thư viện OpenSSL để giải mã
AES-256-CBC. - Key dùng để giải mã là
emu_key(được lấy hoặc tính toán từ trước). - IV truyền vào là 16 byte đầu vừa tách ra.
- Ciphertext là dữ liệu sau IV.
- Sau khi đã có IV (16 byte đầu) và ciphertext (phần còn lại), chương trình sử dụng hàm của thư viện OpenSSL để giải mã
- Cuối cùng, kết quả sẽ được ghi ra file
.txt(khả năng làflag.txt):.text:000000000000ED3E call __ZStlsIcSt11char_traitsIcESaIcEERSt13basic_ostreamIT_T0_ES7_RKNSt7__cxx1112basic_stringIS4_S5_T1_EE ; std::operator<<<char>(std::ostream &,std::string const&)
Thực hiện kiểm tra hàm encode/decode, ta tìm được chuỗi Base64 trong binary tại địa chỉ 0x13100:
.rodata:0000000000013100 aSmr85ltQh8wbgb db 'SMr85LT/QH8WBgB7FAHDJ+RDYEOzmc+8Hq+2HKyaEbwR0DN9BaUFpMgRyi3p9HBHr'
.rodata:0000000000013100 ; DATA XREF: __static_initialization_and_destruction_0(void)+2C↑o
.rodata:0000000000013141 db 'a+5Hz13INUh5jEc/TSPvAHnbxbmKYQSukvmjEG8Jpb76Qfnv28GvW5Puov9jab0SF'
.rodata:0000000000013182 db 'JVoZMDrHYlfzz7xcxpXRkYiQMElRMEm3MXLyqok/KpRB65upKUMtC20YMG02TnJAe'
.rodata:00000000000131C3 db '63deizlhJWmwYn2UbMR4tU6WCHSF8Il7ShvC9hOOTXFRjOY1bHlutv4dYydyqTB3i'
.rodata:0000000000013204 db '7XP5rZiaK20tUfp5LGF/f+pQkqx4gVfXl2O2Vs1jDcjesb3ezbJT0VJfEreJZbtJJ'
.rodata:0000000000013245 db 'XyWTwybo/3BoBKfD11bf17/6LZg6Z4PEH8FHXUDZV52uLbpMvt3ZrWU5t7p',0
Sau khi quá trình giải mã kết thúc, file output flag.txt sẽ được tạo để ghi dữ liệu. Cụ thể:
- Phần filename sẽ được XOR với
0x2A:.text:000000000000EAF6 mov rax, 5E525E044D4B464Ch .text:000000000000EB00 mov [rbp+var_39], rax // XOR each bytes with 0x2A loops .text:000000000000EBC7 loc_EBC7: ; CODE XREF: Cpu::superChipRendrer(void)+2C7↓j .text:000000000000EBC7 mov eax, [rbp+var_2F0] .text:000000000000EBCD cdqe .text:000000000000EBCF movzx eax, byte ptr [rbp+rax+var_39] .text:000000000000EBD4 xor eax, 2Ah .text:000000000000EBD7 mov edx, eax .text:000000000000EBD9 mov eax, [rbp+var_2F0] .text:000000000000EBDF cdqe .text:000000000000EBE1 mov byte ptr [rbp+rax+var_39], dl .text:000000000000EBE5 add [rbp+var_2F0], 1 - Sau khi XOR, chuỗi
"5E525E044D4B464C"sẽ thànhflag.txt.
Về quá trình tạo emu_key, đây là một process khá phức tạp. Cụ thể key sẽ được tính trong Emulator::init(std::string) với thuật toán rất phức tạp sử dụng các magic constant nên rất khó để implement chính xác. Flow tạo key:
uint32_t constants[4] = {
0xDEADBEEF, // Dead Beef
0xCAFEBABE, // Cafe Babe
0x8BADF00D, // Bad Food
0xFEEDFACE // Feed Face
};
// Các constants khác
0xF0F0F0F0
0x9E3779B1 // Golden ratio constant
0xA5A5A5A5
// Charset cho key generation
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
Anti-debugging in RomLoader
Tuy nhiên, trong RomLoader của binary này có cơ chế check anti-debugging. Cụ thể:
call _ptrace ; ptrace(PTRACE_TRACEME, 0, 0, 0)
cmp rax, 0FFFFFFFFh ; Nếu return -1 = đang bị debug
jnz short continue
; Exit nếu bị debug
call _exit
Do đó, ta cần tạo shared library để bypass ptrace. Cụ thể:
# Tạo file bypass
gcc -shared -fPIC -o bypass_ptrace.so -xc - << 'EOF'
long ptrace() { return 0; }
EOF
# Usage
LD_PRELOAD=./bypass_ptrace.so ./chip8Emulator ...
Khi có shared lib này:
LD_PRELOADinject thư viện trước khi load binary- Hàm
ptrace()của chúng ta luôn return 0 (thành công) - Binary nghĩ không bị debug và tiếp tục chạy bình thường
Create ROM Trigger and get flag
Đối với CHIP-8, để trigger được instruction F0FF, ta cần các step sau:
- 60FF =
LD V0, 0xFF- Load giá trị 0xFF vào register V0 - F0FF = Hidden instruction - Trigger decryption với X=0 (V0)
Từ đây ta tạo ROM file:
# Create ROM file
echo "60FFF0FF12021200" | xxd -r -p > trigger.ch8
# Check
hexdump -C trigger.ch8
# 00000000 60 ff f0 ff 12 02 12 00
Giải thích các bytes trong ROM file:
| Offset | Bytes | Instruction | Ý nghĩa |
|——–|——-|————-|———|
| 0x200 | 60 FF | LD V0, 0xFF | Set V0 = 255 |
| 0x202 | F0 FF | FXFF (X=0) | Trigger superChipRendrer |
| 0x204 | 12 02 | JP 0x202 | Jump back (optional) |
| 0x206 | 12 00 | JP 0x200 | Padding |
Có đầy đủ các file cần thiết, chạy emulator với ROM Trigger để lấy flag
# Chạy với bypass ptrace
LD_PRELOAD=./bypass_ptrace.so ./chip8Emulator -r trigger.ch8
# Get flag
cat flag.txt
Flag cần tìm là 0xfunCTF2025{N0w_y0u_h4v3_clear_1dea_H0w_3mulators_WoRK}
Conclusion
Tóm tắt các bước giải
- Reconnaissance:
- Phân tích binary với
file,ldd,strings - Phát hiện sử dụng OpenSSL (libcrypto)
- Phân tích binary với
- Static Analysis:
- Load vào IDA Pro để disassemble
- Tìm thấy hidden instruction
FXFF - Phát hiện hàm
superChipRendrerlàm AES decryption
- Bypass Anti-Debug:
- Tạo
bypass_ptrace.sovới LD_PRELOAD
- Tạo
- Trigger Exploitation:
- Tạo ROM file với opcodes:
60FF F0FF - Chạy emulator để decrypt và ghi flag
- Tạo ROM file với opcodes:
Kiến thức áp dụng
- CHIP-8 architecture: Hiểu instruction set để phát hiện instruction không chuẩn
- Reverse Engineering: Đọc x86-64 assembly, trace function calls
- Cryptography: Nhận biết AES-256-CBC, Base64, IV extraction
- Anti-debugging bypass: LD_PRELOAD technique