TL;DR
- 실행 파일 cie는 입력 PNG를 읽어 자체 포맷(HSPACE)로 인코딩한다.
- flag.out은 해당 자체 포맷으로 인코딩되어있다.
- 최종 목표는 원본 이미지를 복원해 플래그를 추출하는 것이다.
Analysis
ELF
1. PNG stream load
_BOOL8 __fastcall sub_1CE90(__int64 *a1, __int64 a2){ *a1 = sub_2420(a2, (__int64)(a1 + 1), (__int64)a1 + 12, (__int64)(a1 + 2), 3u); return *a1 != 0;}
__int64 __fastcall sub_2420(__int64 a1, __int64 a2, __int64 a3, __int64 a4, unsigned int a5){ __int64 v6; // [rsp+0h] [rbp-40h] FILE *stream; // [rsp+8h] [rbp-38h]
stream = (FILE *)sub_24C0(a1, &unk_24680); if ( stream ) { v6 = sub_2520(stream, a2, a3, a4, a5); fclose(stream); return v6; } else { sub_24F0("can't fopen"); return 0; }}PNG를 로드하고 픽, 높이, 픽셀 수를 가져옴.
2. 스트림 변환
unsigned __int64 __fastcall sub_1D5B0(_DWORD *a1){ unsigned __int64 result; // rax char v2; // [rsp+5h] [rbp-2Bh] char v3; // [rsp+6h] [rbp-2Ah] char v4; // [rsp+7h] [rbp-29h] char v5; // [rsp+17h] [rbp-19h] unsigned __int64 i; // [rsp+18h] [rbp-18h] unsigned __int64 v7; // [rsp+20h] [rbp-10h]
v7 = 3 * a1[3] * a1[2]; sub_1DD80(a1 + 6, v7); v3 = **(_BYTE **)a1; *(_BYTE *)sub_1DE20(a1 + 6, 0) = v3; v4 = *(_BYTE *)(*(_QWORD *)a1 + 1LL); *(_BYTE *)sub_1DE20(a1 + 6, 1) = v4; v5 = *(_BYTE *)(*(_QWORD *)a1 + 2LL); *(_BYTE *)sub_1DE20(a1 + 6, 2) = v5; for ( i = 3; ; ++i ) { result = i; if ( i >= v7 ) break; v2 = *(_BYTE *)(*(_QWORD *)a1 + i) - *(_BYTE *)(*(_QWORD *)a1 + i - 3); *(_BYTE *)sub_1DE20(a1 + 6, i) = v2; } return result;}RGB 스트림을 채널별 누적 델타로 변환한다. 첫 3바이트는 그대로, 이후 값은 같은 채널 이전 픽셀과의 차이
3. Huffman Codes
__int64 __fastcall sub_1D6B0(__int64 a1){ _DWORD *v1; // rax int i; // [rsp+20h] [rbp-100h] __int64 v4; // [rsp+2Ch] [rbp-F4h] char v5; // [rsp+37h] [rbp-E9h] BYREF __int64 v6; // [rsp+38h] [rbp-E8h] BYREF _QWORD v7[2]; // [rsp+40h] [rbp-E0h] BYREF char v8; // [rsp+56h] [rbp-CAh] char v9; // [rsp+57h] [rbp-C9h] BYREF _BYTE v10[28]; // [rsp+58h] [rbp-C8h] BYREF _BYTE v11[12]; // [rsp+74h] [rbp-ACh] BYREF __int64 v12; // [rsp+80h] [rbp-A0h] __int64 v13; // [rsp+88h] [rbp-98h] BYREF _QWORD v14[2]; // [rsp+90h] [rbp-90h] BYREF __int64 v15; // [rsp+A0h] [rbp-80h] BYREF unsigned __int8 v16; // [rsp+AFh] [rbp-71h] __int64 v17; // [rsp+B0h] [rbp-70h] BYREF _QWORD v18[2]; // [rsp+B8h] [rbp-68h] BYREF char v19; // [rsp+DBh] [rbp-45h] BYREF int v20; // [rsp+DCh] [rbp-44h] BYREF _BYTE v21[24]; // [rsp+E0h] [rbp-40h] BYREF __int64 v22; // [rsp+F8h] [rbp-28h] char *v23; // [rsp+100h] [rbp-20h] char *v24; // [rsp+108h] [rbp-18h] char *v25; // [rsp+118h] [rbp-8h]
v22 = a1; v20 = 0; v23 = &v19; v25 = &v19; sub_1E830(v21, 256, &v20); v24 = &v19; sub_1F720(&v19); v18[1] = a1 + 24; v18[0] = sub_1E8B0(a1 + 24); v17 = sub_1E8E0(a1 + 24); while ( (sub_1E910(v18, &v17) & 1) != 0 ) { v16 = *(_BYTE *)sub_1E960(v18); v1 = (_DWORD *)sub_1E980(v21, v16); ++*v1; sub_1E9A0(v18); } v15 = 0; sub_1E9C0(a1, v21, &v15); sub_1ECD0(a1, v15, 0); v14[1] = a1 + 72; v14[0] = sub_1ED90(a1 + 72); v13 = sub_1EDC0(a1 + 72); while ( (sub_1EDF0(v14, &v13) & 1) != 0 ) { v12 = sub_1EE20(v14); sub_1EE90(v11, v12, v12 + 4); sub_1EE60(a1 + 128, v11); sub_1EEC0(v14); } sub_1D2E0(v10); v9 = 0; v8 = 0; v7[1] = a1 + 24; v7[0] = sub_1E8B0(a1 + 24); v6 = sub_1E8E0(a1 + 24); while ( (sub_1E910(v7, &v6) & 1) != 0 ) { v5 = *(_BYTE *)sub_1E960(v7); v4 = *(_QWORD *)sub_1EEF0(a1 + 72, &v5); for ( i = BYTE4(v4) - 1; i >= 0; --i ) { v9 |= (((unsigned int)v4 >> i) & 1) << (7 - v8++); if ( v8 == 8 ) { sub_1EF20(v10, &v9); v9 = 0; v8 = 0; } } sub_1E9A0(v7); } if ( v8 ) sub_1EF20(v10, &v9); sub_1EFC0(a1 + 48, v10); sub_1F280(v10); return sub_1F2F0(v21);}델타 버퍼로부터 빈도를 계산해 허프만 코드북을 만들고 비트스트림을 생성한다. 출력은 (심볼, 코드길이, 코드) 12바이트 엔트리 묶음과 압축 데이터이다.
4. 상수 키 XOR
unsigned __int64 __fastcall sub_1DB00(__int64 a1, __int64 a2){ unsigned __int64 v2; // rcx unsigned __int64 result; // rax _BYTE *v4; // rax char v5; // [rsp+4h] [rbp-2Ch] unsigned __int64 i; // [rsp+18h] [rbp-18h]
for ( i = 0; ; ++i ) { v2 = sub_1DC20(a2); result = i; if ( i >= v2 ) break; v5 = *(_BYTE *)(a1 + (i & 0x1F) + 152); v4 = (_BYTE *)sub_1DE20(a2, i); *v4 ^= v5; } return result;}
__int64 __fastcall sub_1DC20(_QWORD *a1){ return a1[1] - *a1;}
unsigned __int64 __fastcall sub_1DB90(__int64 a1, __int64 a2){ unsigned __int64 v2; // rcx unsigned __int64 result; // rax unsigned __int8 *v4; // rax char v5; // [rsp+7h] [rbp-29h] unsigned __int64 i; // [rsp+18h] [rbp-18h]
for ( i = 0; ; ++i ) { v2 = sub_1DC20(a2); result = i; if ( i >= v2 ) break; v4 = (unsigned __int8 *)sub_1DE20(a2, i); v5 = sub_23880(a1, *v4); *(_BYTE *)sub_1DE20(a2, i) = v5; } return result;}
__int64 __fastcall sub_23880(__int64 a1, unsigned __int8 a2){ return ((int)a2 >> 1) & 1u | (unsigned __int8)((2 * (((int)a2 >> 6) & 1)) | (4 * (((int)a2 >> 3) & 1)) | (8 * (((int)a2 >> 4) & 1)) | (16 * (((int)a2 >> 5) & 1)) | (32 * (((int)a2 >> 2) & 1)) | ((((int)a2 >> 7) & 1) << 6) | ((a2 & 1) << 7));}32바이트 상수 키랑 XOR하고 sub_1DB90이 고정 비트순열을 적용한다.
상수값은 여기에 있다.
__int64 __fastcall sub_1CDE0(__int64 a1){ __int64 result; // rax
*(_QWORD *)a1 = 0; *(_DWORD *)(a1 + 8) = 0; *(_DWORD *)(a1 + 12) = 0; *(_DWORD *)(a1 + 16) = 0; sub_1D2E0(a1 + 24); sub_1D2E0(a1 + 48); sub_1D300(a1 + 72); sub_1D320(a1 + 128); result = a1; *(_QWORD *)(a1 + 152) = 0x22148FE000A8F028LL; *(_QWORD *)(a1 + 160) = 0xD59FC621AB138A2ELL; *(_QWORD *)(a1 + 168) = 0xF943272CFF4841EFLL; *(_QWORD *)(a1 + 176) = 0xA93EE157F3AEC4C9LL; return result;}정리하면 28f0a800e08f14222e8a13ab21c69fd5ef4148ff2c2743f9c9c4aef357e13ea9
마지막으로 헤더(36바이트) + 코드북(엔트리 수 x 6바이트) + 비트스트림으로 기록
flag.out
![[cie-flag.out.png]]
| Offset | Size | Desc |
|---|---|---|
| 0x00 | 6 | HSPACE Magic |
| 0x06 | 1 | 버전 (관측값 0x01) |
| 0x07 | 1 | 플래그 (관측값 0xA8) |
| 0x08 | 4 | 폭 (little endian) |
| 0x0C | 4 | 높이 |
| 0x10 | 4 | 압축 데이터 길이 (encoded_len) |
| 0x14 | 4 | 원본 RGB 길이 (decoded_len) |
| 0x18 | 4 | 허프만 엔트리 수 (code_count, 관측값 165) |
| 0x1C | 8 | 예약(0) |
| 0x24 | 2 | 실제 코드북 엔트리 수 (헤더 값과 동일) |
| 이후 | 6 x N | 허프만 엔트리: sym(1) + len(1) + code(4) |
| 마지막 | encoded_len | 비트스트림 (XOR/비트순열 적용됨) |
Exploit
- 헤더, 코드북 파싱: 코드북은 (길이, 코드) 심볼로 재구성
- 비트스트림 후처리
- 복원비트순열 역변환:
b7 b0 b5 b2 b3 b4 b1 b6 - 32바이트 키와 반복 XOR.
- 복원비트순열 역변환:
- 허프만 복호화: 비트 단위로 읽어 코드북과 일치하는 순간 심볼 출력
- 델타 역변환: 인덱스 3부터
buf[i] = (buf[i] + buf[i-3]) & 0xFF. - 폭과 높이에 맞춰 RGB Buffer를 PNG로 저장
import structfrom pathlib import Pathfrom PIL import Image
KEY = bytes.fromhex("28f0a800e08f14222e8a13ab21c69fd5ef4148ff2c2743f9c9c4aef357e13ea9")
def inv_perm(byte: int) -> int: return ( ((byte >> 7) & 1) | (((byte >> 0) & 1) << 1) | (((byte >> 5) & 1) << 2) | (((byte >> 2) & 1) << 3) | (((byte >> 3) & 1) << 4) | (((byte >> 4) & 1) << 5) | (((byte >> 1) & 1) << 6) | (((byte >> 6) & 1) << 7) )
def read_header(data: bytes): header_fmt = "<6sBBIIIIIII" header_size = struct.calcsize(header_fmt) magic, version, flags, width, height, enc_len, total_len, code_count, _, _ = struct.unpack( header_fmt, data[:header_size] ) if magic != b"HSPACE": raise ValueError("Invalid magic value") return { "width": width, "height": height, "encoded_len": enc_len, "decoded_len": total_len, "code_count": code_count, }, header_size
def parse_codebook(data: bytes, offset: int, count: int): (entry_count,) = struct.unpack_from("<H", data, offset) offset += 2 table = {} for _ in range(entry_count): symbol = data[offset] length = data[offset + 1] code = struct.unpack_from("<I", data, offset + 2)[0] offset += 6 code &= (1 << length) - 1 table[(length, code)] = symbol return table, offset
def undo_postprocessing(payload: bytes) -> bytes: buf = bytearray(inv_perm(b) for b in payload) key_len = len(KEY) for i in range(len(buf)): buf[i] ^= KEY[i % key_len] return bytes(buf)
def huffman_decode(bitstream: bytes, table: dict, expected_len: int) -> bytearray: output = bytearray() acc = 0 bits = 0 max_len = max(length for length, _ in table.keys()) for byte in bitstream: for shift in range(7, -1, -1): bit = (byte >> shift) & 1 acc = ((acc << 1) | bit) & ((1 << max_len) - 1) bits += 1 key = (bits, acc) symbol = table.get(key) if symbol is not None: output.append(symbol) acc = 0 bits = 0 if len(output) == expected_len: return output return output
def reverse_delta(data: bytearray): for i in range(3, len(data)): data[i] = (data[i] + data[i - 3]) & 0xFF
def decode_hspace(path: Path) -> Image.Image: raw = path.read_bytes() header, offset = read_header(raw) codebook, offset = parse_codebook(raw, offset, header["code_count"]) payload = raw[offset : offset + header["encoded_len"]] processed = undo_postprocessing(payload) decoded = huffman_decode(processed, codebook, header["decoded_len"]) reverse_delta(decoded) return Image.frombytes("RGB", (header["width"], header["height"]), bytes(decoded))
if __name__ == "__main__": img = decode_hspace(Path("flag2.out")) img.save("flag.png")![[cie-flag.png]]
HSPACE{3c1016249b1cb2308262855b5fe4e5e8432076c7f7ff5ae4df4670e9af86faed}