Position
The serial depends on the position of each character of the name, so the keygen has to invert that mapping.
취미로 푸는 워게임 풀이기 때문에, 전문성이나 깊이 있는 이해도가 떨어질 수 있음. 개인 기록용으로 올리는 거기 때문에, 전문성을 원하는 거면 다른 보안 블로그의 풀이를 찾아보기 바람.
파일은 두 가지가 있다. 하나는 실행 파일이고 하나는 워게임의 instruction 을 담은 노트이다.
IDA / x32dbg 에서 실행 파일을 연 후 좌측 Symbols(또는 Modules/Functions) 창을 확인하고 메인 함수로 들어간다 (DLL 말고 exe). x32dbg 에서도 충분히 문제 풀이를 할 수 있지만, 이번에는 소스코드를 보고 싶기 때문에 아이다를 사용한다 (F5). x32dbg 로 소스 코드를 보고 싶으면 단순히 어셈블리어를 AI 를 통해 정리하면 된다.
문자열 찾기에서 “wrong” 같은 문자열을 찾아 들어간다. 코드를 디버깅해보면, 코드는 네임을 먼저 얻고 특정 calculation 을 통해 시리얼 키값을 만들어낸다. 그 후 유저에게 시리얼 키값을 인풋으로 받아낸 후, calculation 으로 만들어낸 시리얼 키값과 유저 인풋을 비교해 correct / wrong 여부를 결정해낸다.
어떤 식으로 네임으로 시리얼 키값을 만들어 내는지 분석하고, 그에 맞는 특정 시리얼 키에 맞을 수 있는 네임을 알아내는 파이썬 코드를 짜기 위해 F5 으로 소스 코드를 본다. 확인해보면, 대략 구조는 name[0], name[1] 을 사용해서 serial[0]~[4] 까지 생성하고, name[2], name[3] 을 사용해서 serial[6]~[10] 을 생성하는 구조이다.
밑의 코드는 참고로 원본 코드가 너무 길기 때문에 연산 방법을 알기 위해 불필요해 보이는 부분을 정리한 코드다.
int __stdcall sub_401740(int a1) {
int i = 0, j;
wchar_t s[4]; // 원래 v50
wchar_t t[11]; // 원래 v51
GetInput1(s); // 길이 4
// 길이 체크
if (*(DWORD *)(s - 12) != 4)
return 0;
// a~z 체크
for (i = 0; i < 4; i++) {
if (s[i] < 'a' || s[i] > 'z')
return 0;
}
// 중복 문자 체크
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
if (i != j && s[i] == s[j])
return 0;
}
}
GetInput2(t); // 길이 11
if (*(DWORD *)(t - 12) != 11 || t[5] != '-')
return 0;
// === s[0], s[1] 기반 ===
if (t[0] != '0' + (((s[0] & 1) ? 6 : 5) + ((s[1] & 4) ? 2 : 1))) return 0;
if (t[1] != '0' + (((s[0] & 8) ? 6 : 5) + ((s[1] & 8) ? 2 : 1))) return 0;
if (t[2] != '0' + (((s[0] & 2) ? 6 : 5) + ((s[1] & 16) ? 2 : 1))) return 0;
if (t[3] != '0' + (((s[0] & 4) ? 6 : 5) + ((s[1] & 1) ? 2 : 1))) return 0;
if (t[4] != '0' + (((s[0] & 16) ? 6 : 5) + ((s[1] & 2) ? 2 : 1))) return 0;
// === s[2], s[3] 기반 ===
if (t[6] != '0' + (((s[2] & 1) ? 6 : 5) + ((s[3] & 4) ? 2 : 1))) return 0;
if (t[7] != '0' + (((s[2] & 8) ? 6 : 5) + ((s[3] & 8) ? 2 : 1))) return 0;
if (t[8] != '0' + (((s[2] & 2) ? 6 : 5) + ((s[3] & 16) ? 2 : 1))) return 0;
if (t[9] != '0' + (((s[2] & 4) ? 6 : 5) + ((s[3] & 1) ? 2 : 1))) return 0;
if (t[10] != '0' + (((s[2] & 16) ? 6 : 5) + ((s[3] & 2) ? 2 : 1))) return 0;
return 1;
}
디컴파일해서 만들어낸 소스코드를 보면서 디버깅하면 훨씬 이해가 쉽다. 밑에는 그 결과들이다.
Name = c0 c1 c2 c3
- 길이: 4
- 문자: 'a' ~ 'z'
- 중복 불가
숫자 생성 규칙:
4글자 소문자 이름 → 각 문자 비트 분해 → 숫자 10개 생성 → XXXXX-YYYYY 키 생성
이 결과들이 앞에서 나온 값과 더해져 사용됩니다. 참고로 세 번째, 네 번째 문자 생성도 동일 구조다.
AI 에게 위의 분석들과 함께, 특정 키젠에 따라 가능한 p 로 끝나는 네임 리스트를 얻는 파이썬 코드를 요구한다. 아래는 그 결과 값이다.
import string
import itertools
SERIAL = "76876-77776"
def check(name: str, serial: str) -> bool:
# name constraints (원본 v50 검사)
if len(name) != 4:
return False
if any(c < 'a' or c > 'z' for c in name):
return False
if len(set(name)) != 4:
return False
# serial constraints (원본 v51 검사)
if len(serial) != 11 or serial[5] != '-':
return False
s = [ord(c) for c in name]
t = serial
# === s[0], s[1] 기반 ===
if int(t[0]) != ((6 if (s[0] & 1) else 5) + (2 if (s[1] & 4) else 1)): return False
if int(t[1]) != ((6 if (s[0] & 8) else 5) + (2 if (s[1] & 8) else 1)): return False
if int(t[2]) != ((6 if (s[0] & 2) else 5) + (2 if (s[1] & 16) else 1)): return False
if int(t[3]) != ((6 if (s[0] & 4) else 5) + (2 if (s[1] & 1) else 1)): return False
if int(t[4]) != ((6 if (s[0] & 16) else 5) + (2 if (s[1] & 2) else 1)): return False
# === s[2], s[3] 기반 ===
if int(t[6]) != ((6 if (s[2] & 1) else 5) + (2 if (s[3] & 4) else 1)): return False
if int(t[7]) != ((6 if (s[2] & 8) else 5) + (2 if (s[3] & 8) else 1)): return False
if int(t[8]) != ((6 if (s[2] & 2) else 5) + (2 if (s[3] & 16) else 1)): return False
if int(t[9]) != ((6 if (s[2] & 4) else 5) + (2 if (s[3] & 1) else 1)): return False
if int(t[10]) != ((6 if (s[2] & 16) else 5) + (2 if (s[3] & 2) else 1)): return False
return True
results = []
letters = string.ascii_lowercase
for a, b, c in itertools.product(letters, repeat=3):
name = a + b + c + "p" # 끝이 p
if len(set(name)) != 4:
continue
if check(name, SERIAL):
results.append(name)
print("Possible passwords ending with 'p':")
for r in results:
print(r)
print("\nTotal found:", len(results))
이렇게 만든 코드를 사용하면 가능한 name 이 이렇게 나온다.
bump
cqmp
ftmp
여기서 auth 인증되는 FLAG 값은 Bump 다.

