| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- AWS 인프라 아키텍처
- AWS 침해사고 사례 분석
- AWS 사고 사례 분석
- 침입 차단 시스템(IPS)
- 운영체제
- AWS
- dreamhack
- TryHackMe
- AWS 인프라 분석
- operating system
- C
- AWS IAM Role
- AWS Active Directory
- 리버싱
- 네트워크
- network
- AWS 보안 사고 사례 모음
- programmers
- IAM Federation
- Amazon S3
- 드림핵
- python
- AWS 침해 사고 사례 분석
- terraform
- reversing
- AWS 3 Tier Architecture
- AWS 아키텍처 분석
- AWS 보안 아키텍처 분석
- reversing.kr
- 프로그래머스
- Today
- Total
lhywk 님의 블로그
[Dreamhack] legacyopt 본문
문제 풀이

해당 바이너리를 실행하면 입력을 받고 뭔가의 숫자들을 출력합니다.
220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e
문제에서는 이 output을 함께 제공해 줬습니다.
우리의 입력값을 넣었을 때 저 output이 나오면 올바른 값이 되는 것 같습니다.
먼저 IDA로 소스코드를 확인해 보겠습니다.
1. Main 분석

1. ptr에 0x64만큼 메모리를 할당합니다.
2. fgets 함수로 최대 100글자를 입력받습니다.
3. strcspn 함수로 입력값에서 개행 문자를 찾아서 널 문자로 바꿔버립니다. fgets에서 엔터키를 없애버리는 용도로 쓰이겠네요.
4. v3에 입력값의 길이를 저장합니다.
5. sub_1209 함수를 ptr, s, v3을 인수로 호출합니다.
6. for 반복문으로 1바이트 단위로 16진수로 출력합니다. 위의 sub_1209 함수를 거치고 난 뒤 ouput을 출력합니다.
그러면 여기서 중요하게 분석해야 할 건 sub_1209 함수입니다.
2. sub_1209() 분석
unsigned __int64 __fastcall sub_1209(_BYTE *a1, char *a2, int a3)
{
int v3; // eax
int v4; // edx
char *v5; // rax
char v6; // cl
_BYTE *v7; // rax
char *v8; // rax
char v9; // cl
_BYTE *v10; // rax
char *v11; // rax
char v12; // cl
_BYTE *v13; // rax
char *v14; // rax
char v15; // cl
_BYTE *v16; // rax
char *v17; // rax
char v18; // cl
_BYTE *v19; // rax
char *v20; // rax
char v21; // cl
_BYTE *v22; // rax
char *v23; // rax
char v24; // cl
_BYTE *v25; // rax
char *v26; // rax
char v27; // cl
unsigned __int64 result; // rax
int v32; // [rsp+20h] [rbp-4h]
v3 = a3 + 7;
v4 = a3 + 14;
if ( v3 < 0 )
v3 = v4;
v32 = v3 >> 3;
switch ( a3 % 8 )
{
case 0:
goto LABEL_4;
case 1:
goto LABEL_11;
case 2:
goto LABEL_10;
case 3:
goto LABEL_9;
case 4:
goto LABEL_8;
case 5:
goto LABEL_7;
case 6:
goto LABEL_6;
case 7:
while ( 1 )
{
v8 = a2++;
v9 = *v8;
v10 = a1++;
*v10 = v9 ^ 0x66;
LABEL_6:
v11 = a2++;
v12 = *v11;
v13 = a1++;
*v13 = v12 ^ 0x44;
LABEL_7:
v14 = a2++;
v15 = *v14;
v16 = a1++;
*v16 = v15 ^ 0x11;
LABEL_8:
v17 = a2++;
v18 = *v17;
v19 = a1++;
*v19 = v18 ^ 0x77;
LABEL_9:
v20 = a2++;
v21 = *v20;
v22 = a1++;
*v22 = v21 ^ 0x55;
LABEL_10:
v23 = a2++;
v24 = *v23;
v25 = a1++;
*v25 = v24 ^ 0x22;
LABEL_11:
v26 = a2++;
v27 = *v26;
result = (unsigned __int64)a1++;
*(_BYTE *)result = v27 ^ 0x33;
if ( --v32 <= 0 )
break;
LABEL_4:
v5 = a2++;
v6 = *v5;
v7 = a1++;
*v7 = v6 ^ 0x88;
}
break;
default:
result = (unsigned int)(a3 % 8);
break;
}
return result;
}
코드가 굉장히 복잡해 보이지만 천천히 따라가 보겠습니다.

1. v3 = a3 + 7; -> 입력값의 길이에 7을 더합니다.
2. v4 = a3 + 14; -> 입력값의 길이에 14를 더합니다.
3. v3가 0보다 작다면 v4을 v3에 저장합니다.
4. v32 = v3 >> 3; -> >>3은 2^3으로 나눈 것과 같은 말입니다. 입력값에 7을 더한 후 8로 나눈 값을 v32에 저장합니다.
switch ( a3 % 8 )
{
case 0:
goto LABEL_4;
case 1:
goto LABEL_11;
case 2:
goto LABEL_10;
case 3:
goto LABEL_9;
case 4:
goto LABEL_8;
case 5:
goto LABEL_7;
case 6:
goto LABEL_6;
case 7:
while ( 1 )
{
v8 = a2++;
v9 = *v8;
v10 = a1++;
*v10 = v9 ^ 0x66;
LABEL_6:
v11 = a2++;
v12 = *v11;
v13 = a1++;
*v13 = v12 ^ 0x44;
LABEL_7:
v14 = a2++;
v15 = *v14;
v16 = a1++;
*v16 = v15 ^ 0x11;
LABEL_8:
v17 = a2++;
v18 = *v17;
v19 = a1++;
*v19 = v18 ^ 0x77;
LABEL_9:
v20 = a2++;
v21 = *v20;
v22 = a1++;
*v22 = v21 ^ 0x55;
LABEL_10:
v23 = a2++;
v24 = *v23;
v25 = a1++;
*v25 = v24 ^ 0x22;
LABEL_11:
v26 = a2++;
v27 = *v26;
result = (unsigned __int64)a1++;
*(_BYTE *)result = v27 ^ 0x33;
if ( --v32 <= 0 )
break;
LABEL_4:
v5 = a2++;
v6 = *v5;
v7 = a1++;
*v7 = v6 ^ 0x88;
}
밑의 코드를 보면 switch 문법과 goto 문법이 같이 쓰여 복잡해 보이지만 특정한 패턴을 찾을 수 있습니다.
switch ( a3 % 8 )을 보면 a3는 문자열의 길이였습니다. 8로 나눈 나머지 값을 case로 0~7로 나누어 goto의 레이블로 각각 보내고 있습니다. 8바이트만큼 처리하고 있다고 생각할 수 있습니다.
그리고 각 레이블에는 특정 연산이 있습니다.

1. v20 = a2++; -> a2는 문자열의 주소입니다. 주소를 v20에 넣습니다. 그 후 다음 주소로 증감합니다.
2. v21 = *v20; -> 주소에 있는 글자를 v21에 넣습니다.
3. v22 = a1++; -> a1은 ptr이었습니다. ptr의 주소를 v22에 넣습니다. 그 후 다음 주소로 증감합니다.
4. *v22 = v21 ^ 0x55; -> 주소 안에 글자와 0x55를 XOR 한 값을 저장합니다.
그래서 특정 패턴은 결론적으로 switch문에서 연산할 레이블을 정해주고 v32만큼 반복합니다.
데이터의 길이가 27이라고 가정해 보겠습니다.
1. 총 반복 횟수(v32) 계산: (27 + 7) >> 3 = 4. 즉, 총 4번의 사이클을 돌아야 끝납니다.
2. 진입점 결정: switch(27 % 8) = 3. 따라서 LABEL_9로 점프합니다.
3. 첫 번째 사이클 (3글자 처리): LABEL_9부터 LABEL_11까지 내려오며 3글자를 암호화합니다.

4. 반복 확인: LABEL_11 끝에서 v32를 1 감소시킵니다 (4 → 3). 아직 0이 아니므로 계속 진행합니다.
5. 나머지 사이클 (8 글자씩): 이후에는 LABEL_4(0x88)부터 시작해 while문 맨 위(0x66)를 거쳐 다시 LABEL_11까지, 총 8 글자씩 처리하는 과정을 3번 더 반복합니다.
6. 결과: 3글자 + (8글자 × 3번) = 27글자가 모두 정확히 연산되고 리턴됩니다.
XOR 연산으로 암호화된 값은 다시 XOR 연산을 하면 복호화가 됩니다.
역연산 코드를 작성해 보겠습니다.
output = "220c6a33204455fb390074013c4156d704316528205156d70b217c14255b6ce10837651234464e"
data = bytes.fromhex(output)
length = len(data)
key = [0x88, 0x66, 0x44, 0x11, 0x77, 0x55, 0x22, 0x33]
start_index = (8 - (length % 8)) % 8
for i in range(length):
key_index = (start_index + i) % 8
print(chr(data[i] ^ key[key_index]), end='')
output은 16진수 문자열이므로 bytes.fromhex를 이용해 바이트 배열로 변환하고 그 길이를 length에 저장합니다.
key 리스트는 LABEL_4(0x88)부터 LABEL_11(0x33)까지의 순서대로 작성합니다.
시작 위치(start_index)를 구하기 위해 8 - (length % 8)) % 8을 사용합니다.
예를 들어 길이가 27이라면, 27 % 8 = 3이므로 (8 - 3) % 8 = 5가 됩니다. 즉, 인덱스 5번인 0x55부터 시작하여 0x22, 0x33 순으로 첫 3글자가 처리됩니다.
이후 for문에서 (start_index + i) % 8 연산을 통해 앞서 구한 시작 위치부터 나머지 모든 글자를 순서대로 복호화하게 됩니다.

'Reversing > Dreamhack' 카테고리의 다른 글
| [Dreamhack] please, please, please (0) | 2026.02.04 |
|---|---|
| [Dreamhack] Stop before stops! (0) | 2026.02.03 |
| [Dreamhack] rev-basic-5 (0) | 2026.01.27 |
| [Dreamhack] rev-basic-7 (0) | 2026.01.27 |
| [Dreamhack] flag printer (0) | 2026.01.26 |