TL;DR
두 문제는 리버싱 단골인 AES 문제이다. 쉬운 문제이니 깊은 설명 없이 풀 것이다.
plastic-shield: 비밀번호 전체가 아닌 특정 지점의 단 1바이트를 Blake2b로 해시하여 AES-256-CBC 키/IV를 파생하고 복호화에 사용한다.
= 게싱으로 풀린다.
plastic-shield 2: 마찬가지로 1바이트 게싱 -> 3바이트 게싱으로 올라간다.
plastic-shield
매우 많은 함수들이 있었지만, 결국 Pure한 AES와 BLAKE2b였다. 커스텀 아님
바이너리 실행흐름
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE v4[256]; // [rsp+0h] [rbp-340h] BYREF
_BYTE v5[16]; // [rsp+100h] [rbp-240h] BYREF
_BYTE v6[32]; // [rsp+110h] [rbp-230h] BYREF
char v7[64]; // [rsp+130h] [rbp-210h] BYREF
_BYTE v8[79]; // [rsp+170h] [rbp-1D0h] BYREF
char v9; // [rsp+1BFh] [rbp-181h] BYREF
char v10[64]; // [rsp+1C0h] [rbp-180h] BYREF
char s[263]; // [rsp+200h] [rbp-140h] BYREF
unsigned __int8 v12; // [rsp+307h] [rbp-39h]
void *ptr; // [rsp+308h] [rbp-38h]
char *v14; // [rsp+310h] [rbp-30h]
unsigned __int64 v15; // [rsp+318h] [rbp-28h]
size_t v16; // [rsp+320h] [rbp-20h]
size_t j; // [rsp+328h] [rbp-18h]
size_t size; // [rsp+330h] [rbp-10h]
unsigned __int64 i; // [rsp+338h] [rbp-8h]
printf("Please enter the password: ");
__isoc99_scanf("%255s", s);
v16 = strlen(s);
v15 = 60 * v16 / 0x64;
v9 = s[v15];
crypto_blake2b(v10, 64LL, &v9, 1LL);
for ( i = 0LL; i <= 0x3F; ++i )
sprintf(&v7[2 * i], "%02x", (unsigned __int8)v10[i]);
v8[64] = 0;
v14 = "713d7f2c0f502f485a8af0c284bd3f1e7b03d27204a616a8340beaae23f130edf65401c1f99fe99f63486a385ccea217";
hex_to_bytes(v7, v6, 32LL);
hex_to_bytes(v8, v5, 16LL);
size = strlen(v14) >> 1;
ptr = malloc(size);
hex_to_bytes(v14, ptr, size);
AES_init_ctx_iv(v4, v6, v5);
AES_CBC_decrypt_buffer(v4, ptr, size);
v12 = *((_BYTE *)ptr + size - 1);
if ( v12 <= 0x10u && v12 )
size -= v12;
printf("Decrypted text: ");
for ( j = 0LL; j < size; ++j )
putchar(*((unsigned __int8 *)ptr + j));
putchar(10);
free(ptr);
return 0;
}
최대 255바이트 비밀번호 s 입력 $\rightarrow$ 패딩이 제거된 복호화된 평문 출력인 간단한 입출력 구조를 갖고 있다.
1. 인덱싱 트릭
v15 = 60 * strlen(s) / 0x64; // 0x64 == 100
v9 = s[v15]; // 딱 이 바이트만 해시
길이가 1이면 idx=0; 길이가 10이면 idx=6 등 원하는 위치를 잡으려면 길이를 조절하면 된다.
2. 해시 $\rightarrow$ 키/IV 파생
crypto_blake2b(v10, 64, &v9, 1); // 1바이트 입력
// v10(64바이트)을 128 hex로 변환해 v7에 씀 → v8까지 넘침
hex_to_bytes(v7, v6, 32); // 키: 해시 앞 32바이트
hex_to_bytes(v8, v5, 16); // IV: 해시 다음 16바이트
Intended Stack Overflow로 해시 문자열 두 버퍼(v7, v8)로 분할
결과적으로 BLAKE2b 결과의 [0:31] 바이트가 키, [32:47] 바이트가 IV가 된다.
3. 복호화 및 패딩 제거
AES_init_ctx_iv(ctx, key(32B), iv(16B));
AES_CBC_decrypt_buffer(ctx, ciphertext, size);
pad = last_byte;
if (1 <= pad <= 0x10) size -= pad; // PKCS#7
Exploitation
길이는 임의로 정해 idx 위치를 통제할 수 있다.
후보 바이트 $c \in [0x00:0xff]$에 대해 H = BLAKE2b([c], 64) $\rightarrow$ key = H[0:32], iv = H[32:48] $\rightarrow$ 복호화, 패딩 제거 이후 알 수도 있다.
AES-CBC-256이지만, BLAKE2b에 들어가는 실질적인 데이터는 1바이트밖에 없다. 즉, 1바이트 게싱만 하면 된다.
from hashlib import blake2b
from Crypto.Cipher import AES
from binascii import unhexlify
C_hex = "713d7f2c0f502f485a8af0c284bd3f1e7b03d27204a616a8340beaae23f130edf65401c1f99fe99f63486a385ccea217"
C = unhexlify(C_hex)
def try_byte(c):
H = blake2b(bytes([c]), digest_size=64).digest()
key = H[:32]
iv = H[32:48]
pt = AES.new(key, AES.MODE_CBC, iv).decrypt(C)
pad = pt[-1]
if 1 <= pad <= 16 and all(x == pad for x in pt[-pad:]):
pt = pt[:-pad]
return pt
for c in range(256):
pt = try_byte(c)
if b"scriptCTF{" in pt:
print(c, chr(c), pt.decode(errors="ignore"))
break
백틱문자(`)에서 평문이 정상적으로 출력된다. 또한, 입력 길이를 1로 두면 idx=0이므로 1글자로 충분하다.
pwn@meow:~/ctftemp/rev$ python3 -u "/home/pwn/ctftemp/rev/pl1.py"
96 ` scriptCTF{20_cau541i71e5_d3f3n5es_d0wn}
pwn@meow:~/ctftemp/rev$ ./plastic-shield
Please enter the password: `
Decrypted text: scriptCTF{20_cau541i71e5_d3f3n5es_d0wn}
번외: fake flag인줄 알고 문제를 안 풀고 있었다.
plastic-shield 2
바이너리 실행흐름
plastic-shield와 비슷한 흐름이다.
int __fastcall main(int argc, const char **argv, const char **envp)
{
_BYTE v4[256]; // [rsp+0h] [rbp-350h] BYREF
char v5[26]; // [rsp+100h] [rbp-250h] BYREF
char nptr[2]; // [rsp+11Ah] [rbp-236h] BYREF
char dest[4]; // [rsp+11Ch] [rbp-234h] BYREF
_BYTE v8[157]; // [rsp+120h] [rbp-230h] BYREF
char v9[19]; // [rsp+1BDh] [rbp-193h] BYREF
char v10[64]; // [rsp+1D0h] [rbp-180h] BYREF
char s[263]; // [rsp+210h] [rbp-140h] BYREF
unsigned __int8 v12; // [rsp+317h] [rbp-39h]
void *ptr; // [rsp+318h] [rbp-38h]
char v14; // [rsp+327h] [rbp-29h]
char *v15; // [rsp+328h] [rbp-28h]
size_t v16; // [rsp+330h] [rbp-20h]
size_t j; // [rsp+338h] [rbp-18h]
size_t size; // [rsp+340h] [rbp-10h]
unsigned __int64 i; // [rsp+348h] [rbp-8h]
printf("Please enter the password: ");
__isoc99_scanf("%255s", s);
v16 = strlen(s);
crypto_blake2b(v10, 64LL, s, v16);
for ( i = 0LL; i <= 0x3F; ++i )
sprintf(&v8[2 * i + 32], "%02x", (unsigned __int8)v10[i]);
v9[3] = 0;
v15 = "e2ea0d318af80079fb56db5674ca8c274c5fd0e92019acd01e89171bb889f6b1";
memset(v8, 0, 0x20uLL);
strncpy(dest, v9, 3uLL);
dest[3] = 0;
hex_to_bytes(dest, v8, 1LL);
nptr[0] = v9[2];
nptr[1] = 0;
v14 = strtol(nptr, 0LL, 16);
v8[1] = 16 * v14;
memset(v5, 0, 0x10uLL);
hex_to_bytes(dest, v5, 1LL);
v5[1] = 16 * v14;
size = strlen(v15) >> 1;
ptr = malloc(size);
hex_to_bytes(v15, ptr, size);
AES_init_ctx_iv(v4, v8, v5);
AES_CBC_decrypt_buffer(v4, ptr, size);
v12 = *((_BYTE *)ptr + size - 1);
if ( v12 <= 0x10u && v12 )
size -= v12;
printf("Decrypted text: ");
for ( j = 0LL; j < size; ++j )
putchar(*((unsigned __int8 *)ptr + j));
putchar(10);
free(ptr);
return 0;
}
하지만 아까처럼 1바이트만 넣어서 해싱하진 않는다. 그치만 뭔가 삐리하다.
스택 변수만 빠르게 정리해보겠다.
rbp-0x140 : 입력 버퍼 s (scanf 대상)
rbp-0x180 : BLAKE2b 출력(바이너리 64B)
rbp-0x210 : 해시의 hex 문자열(128글자) 시작 주소
rbp-0x190 : 위 hex 문자열의 널 종료 지점(= rbp-0x210 + 0x80)
rbp-0x230 : IV 버퍼(처음 0x20 바이트를 0으로 초기화 후, IV[0], IV[1]만 채움)
rbp-0x250 : Key 버퍼(16바이트, Key[0], Key[1]만 채움)
rbp-0x234 : dest[4] (마지막 3글자 + ‘\0’)
rbp-0x236 : nptr[2] (한 글자 + ‘\0’, strtol용)
rbp-0x28 : .rdata 포인터
rbp-0x38 : malloc한 암호문 바이트 배열 포인터
rbp-0x10 : 암호문 길이(바이트) 및 패딩 제거 후 길이
rbp-0x350 : AES 컨텍스트
printf("Please enter the password: ");
scanf("%255s", s);
v16 = strlen(s);
crypto_blake2b(v10, 64, s, v16);
for (i=0; i<=0x3F; ++i)
sprintf(&v8[2*i + 32], "%02x", (unsigned __int8)v10[i]);
v10[64]는 BLAKE2b 64바이트 다이제스트
v8은 0x20 바이트를 먼저 0으로 채워 인덱스 32부터 다이제스트를 16진수 문자열로 채운다.
= 결과적으로 &v8[32]부터 128글자짜리 hex string이 존재
v9[3] = 0;
v15 = "e2ea0d318af80079fb56db5674ca8c274c5fd0e92019acd01e89171bb889f6b1";
memset(v8, 0, 0x20uLL);
strncpy(dest, v9, 3uLL);
dest[3] = 0;
hex_to_bytes(dest, v8, 1LL);
nptr[0] = v9[2];
nptr[1] = 0;
v14 = strtol(nptr, 0LL, 16);
v8[1] = 16 * v14;
v9가 핵심이다. 해시 hex 문자열의 앞 3글자를 담는다.
코드 흐름상 dest에 3글자를 담고 hex_to_bytes(dest, v8, 1)로 앞 2글자를 1바이트로 변환한다. (v8[0])
세 번째 글자는 strtol로 한 글자만 << 4 (*16) 하여 상위 니블로 v8[1] 로 기록한다.
v5 또한 동일 작업을 수행한다.
memset(v5, 0, 0x10uLL);
hex_to_bytes(dest, v5, 1LL);
v5[1] = 16 * v14;
-asm-
lea rax, [rbp+var_250]
mov edx, 10h ; n
mov esi, 0 ; c
mov rdi, rax ; s
call _memset
v5[0] = v8[0], v5[1]=v8[1], 나머지는 0
정리
IV = v8[0..15] = [hex0..1, hex2<<4, 0×14]
Key = v5[0..15] = [hex0..1, hex2<<4, 0×14]
size = strlen(v15) >> 1;
ptr = malloc(size);
hex_to_bytes(v15, ptr, size);
AES_init_ctx_iv(v4, v8, v5);
AES_CBC_decrypt_buffer(v4, ptr, size);
암호문 v15는 32바이트이며, v5를 AES의 KEY, v8을 IV로 세팅하고 AES-CBC 복호화를 수행한다.
v12 = *((_BYTE *)ptr + size - 1);
if ( v12 <= 0x10u && v12 ) size -= v12; // PKCS#7
for (j=0; j< size; ++j) putchar(ptr[j]);
패딩 제거 후 평문 출력
unsigned __int64 __fastcall AES_CBC_decrypt_buffer(__int64 a1, __int64 *a2, unsigned __int64 a3)
{
unsigned __int64 result; // rax
__int64 v6; // [rsp+20h] [rbp-20h]
__int64 v7; // [rsp+28h] [rbp-18h]
unsigned __int64 i; // [rsp+38h] [rbp-8h]
for ( i = 0LL; ; i += 16LL )
{
result = i;
if ( i >= a3 )
break;
v6 = *a2;
v7 = a2[1];
InvCipher(a2, a1); // 현재 블록 백업
XorWithIv(a2, a1 + 240); // 복호화 라운드
*(_QWORD *)(a1 + 240) = v6; // IV와 XOR
*(_QWORD *)(a1 + 248) = v7; // 다음 라운드용 IV *현재 암호블록
a2 += 2;
}
return result;
}
a1+240, +248은 내부 context IV 위치 보관
Exploitation
최종 다이제스트는 64바이트를 그대로 사용하지 않고, 배열에 앞 3글자만 키와 IV에 반영한다는 점이다.
k0 = int(hex[0..1], 16)
k1 = int(hex[2], 16) << 4
---
Key = [k0, k1, 0x00 ..]
IV = [k0, k1, 0x00 ..]
즉 12비트만 찾으면 된다.
from Crypto.Cipher import AES
from hashlib import blake2b
from binascii import unhexlify
C_hex = "713d7f2c0f502f485a8af0c284bd3f1e7b03d27204a616a8340beaae23f130edf65401c1f99fe99f63486a385ccea217"
C = unhexlify(C_hex)
def try_byte(c):
H = blake2b(bytes([c]), digest_size=64).digest()
key = H[:32]
iv = H[32:48]
pt = AES.new(key, AES.MODE_CBC, iv).decrypt(C)
pad = pt[-1]
if 1 <= pad <= 16 and all(x == pad for x in pt[-pad:]):
pt = pt[:-pad]
return pt
for c in range(256):
pt = try_byte(c)
if b"scriptCTF{" in pt:
print(c, chr(c), pt.decode(errors="ignore"))
break
pwn@meow:~/ctftemp$ python3 -u "/home/pwn/ctftemp/rev/pl2.py"
fef scriptCTF{00p513_n07_4641n!}
재밌는 문제, 패딩 주의할 것
'CTF' 카테고리의 다른 글
[Crypto] snakeCTF 2025 Qual free-start write-up (1) | 2025.08.31 |
---|---|
[PWN] scriptCTF 2025 Vault 3 write-up (0) | 2025.08.21 |
[Crypto] scriptCTF 2025 - Secure-Server-2 write-up (1) | 2025.08.21 |
[Crypto] scriptCTF 2025 - EaaS write-up (0) | 2025.08.21 |
[Crypto] CCE 2025 Qual - jokes write-up (2) | 2025.08.21 |