Unlike previous binaries, we won't do any disassembly using gdb
. Rather, we will use this challenge to demonstrate Ghidra instead. This does not replace using gdb
for dynamic analysis but is a good tool for static analysis.
We first use checksec
to see what security features are enabled:
$ checksec ./chall [*] '/ironforge/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled Stripped: No
This binary is compiled with all protections enabled. We can't stack smash, we can't overwrite the GOT table, and we can't use absolute addressing. What can we do? Not much.
The most common solution to dealing with fully protected binaries is to use the binary as expected and craft input that reaches an unexpected state. Ghidra helps us understand the program's control flow.
We can use Ghidra to disassemble the binary. If we open the binary in Ghidra, we see the following:
When looking at Ghidra, we notice a few things:
main()
function at first. The Functions folder on the left side lets us access the rest of the function. We can also double-click a function to jump to it.undefined
plus the number of bytes it thinks the type is.Below is the default disassembly of main()
provided by Ghidra:
undefined8 main(void) { int iVar1; char local_28 [32] = { 0 }; setbuf(stdin,(char *)0x0); setbuf(stdout,(char *)0x0); setbuf(stderr,(char *)0x0); printf("What is the password?\n> "); fgets(local_28,0x20,stdin); iVar1 = atoi(local_28); if (iVar1 == 0x12345) { win(); } else { puts("You lose!"); } return 0; }
Realistically, we don't have much cleaning up to do. This is very similar to how a C program would look. We are offered 40
bytes of input. This input is converted to an integer and compared with 0x12345
. If our input matches, it calls win()
. Let's check out win()
:
void win(void) { system("cat flag.txt"); return; }
This does exactly what we'd expect it to.
Unlike the binex section, our goal isn't to exploit the binary. We aim to craft the right input to pass all the checks and reach the win()
function. In this case, we know we need to pass 0x12345
.
If we try to pass 0x12345
as input, we get the following:
$ ./chall What is the password? > 0x12345 You lose!
Why doesn't this work? atoi()
converts from a string to an integer. This function does not understand hex. Instead, we can pass the integer equivalent of 0x12345
:
$ ./chall What is the password? > 74565 IFC{PL4C3H0LD3R_FL4G_H3R3!}
This gives us the flag! we can also write this script using pwntools:
from pwn import * proc = process("./chall") key = 0x12345 proc.sendline(str(key).encode()) proc.interactive()
If we print our payload (i.e., print(str(key).encode())
), we'd see our payload is b'74565'
.