배경
2025년 8월에 진행한 FLARE ON 2016 스터디 진행중 나온 풀이를 저장하게 되었습니다.
chal1



401260함수는 0x3f를 봐서 base64와 비슷해보이지만, 413000데이터를 참조하고 있었다. 해당 데이터를 보니 zyxabcdefg등으로 알파벳이 일부 옮겨진듯하다
x2dtJEOmyjacxDemx2eczT5cVS9fVUGvWTuZWjuexjRqy24rV29q을 위 로직처럼 일부 옮긴 후 base64 디코딩을 하면 된다
import base64
dat = "x2dtJEOmyjacxDemx2eczT5cVS9fVUGvWTuZWjuexjRqy24rV29q"
def decode(c: str) -> str:
if c.isdigit():
return c
UPPER = c.isupper()
c = c.lower()
result = ""
if c == "x":
result = "c"
elif c == "y":
result = "b"
elif c == "z":
result = "a"
else:
result = chr(ord(c) + 3)
if UPPER:
result = result.upper()
return result
dat = "".join([decode(c) for c in dat])
print(dat)
print(base64.b64decode(dat).decode())
# c2gwMHRpbmdfcGhpc2hfaW5fYV9iYXJyZWxAZmxhcmUtb24uY29t
# sh00ting_phish_in_a_barrel@flare-on.com
chal2

윈디펜더에 걸리거나 하는 점들을 보아 랜섬웨어와 암호화된 파일로 보입니다
-
현재 디렉토리의 Briefcase파일이 있는지 확인
-
현재 드라이브 시리얼 번호 기반으로 체크
-
401940에서 프로그램 내장의 시리얼 번호를 이용해 xor시켜 값을 복호화합니다 복호화된 값-
thosefilesreallytiedthefoldertogether - 401080에서 crypt provider를 생성하고 rsa_aes 키를 401148에서 지정합니다
- those…문자열을 sha 해시화 후 CryptDeriveKey를 이용해 키로 지정합니다
- 401300에서 암호화를 시작합니다. Briefcase라는 폴더 내부 파일들을 가져와 암호화를 합니다 401990에서는 파일이 디렉토리인지 검사합니다.

-
401770에서 암호화를 하고 401500에서 저장하는것으로 보입니다
-
401770
- 파일명을 md5합니다
- md5 결과값을 IV로 사용합니다, 패딩값이 있을 경우 AB입니다
iv값은 확인하였으나 CryptDeriveKey 호출 과정에서 부족한 키 데이터가 어떻게 expand되는지 확인하지 못했습니다.
→ 확인해보니 expanded key 계산 필요 없이, crypt메소드만 decrypt메소드로 바꾸면 해결된다고 합니다
chal3


-
인자가 있는지 확인합니다
-
실행프로그램 경로에 R이 있는지 확인합니다
-
r 뒤의 문장 및 인자를 이용해 해시를 생성합니다
-
v22에 flare on등의 문자열을 저장합니다

-
실행프로그램 자신 자체를 읽어 RSDS문자열을 찾은 뒤, 해당 문자열 뒤에 있는 16바이트 데이터를 v23에 저장합니다

-
b6 1e 2d d0은 0x12345678과 xor됩니다

계산한 해시값에 R 이후의 글자와 xor 이후 일부 데이터를 md5하는것으로 보이나, 정확히 어떤 데이터가 md5되는지 확인하지 못하였습니다
chal4

dll 디컴파일한 함수의 반환값들이 특정 숫자를 반환하고 있어서, 매핑 프로그램 작성하였습니다
#include <stdio.h>
#include <stdlib.h> // qsort 사용을 위함
#include <windows.h>
// DLL 함수 포인터 타입 정의
typedef unsigned int(__stdcall* DLL_FUNCTION_PTR)();
// 함수 호출 결과를 저장할 구조체
typedef struct
{
int ordinal;
unsigned int returnValue; // uint4에 해당
int isValid; // 함수 호출이 성공했는지 여부 (0: 실패, 1: 성공)
} FunctionResult;
// qsort를 위한 비교 함수
// 오름차순 정렬 (returnA - returnB)
int
compareFunctionResults(const void* a, const void* b)
{
FunctionResult* resultA = (FunctionResult*)a;
FunctionResult* resultB = (FunctionResult*)b;
// 유효하지 않은 결과는 맨 뒤로 보내거나, 특정 처리를 할 수 있습니다.
// 여기서는 유효한 결과만 정렬하고, 유효하지 않은 결과는 정렬 대상에서 제외한다고 가정하거나
// 유효한 결과와 유효하지 않은 결과를 분리하여 처리하는 것이 좋습니다.
// 간단하게 유효한 결과들만 비교하여 정렬합니다.
if (resultA->isValid && resultB->isValid) {
if (resultA->returnValue < resultB->returnValue)
return -1;
if (resultA->returnValue > resultB->returnValue)
return 1;
return 0;
}
// 유효하지 않은 결과는 비교에서 제외하거나 별도 처리
return 0;
}
// 내림차순 정렬 (returnB - returnA)
int
compareFunctionResultsDesc(const void* a, const void* b)
{
FunctionResult* resultA = (FunctionResult*)a;
FunctionResult* resultB = (FunctionResult*)b;
if (resultA->isValid && resultB->isValid) {
if (resultA->returnValue > resultB->returnValue)
return -1;
if (resultA->returnValue < resultB->returnValue)
return 1;
return 0;
}
return 0;
}
int
main()
{
const char* dllPath = "flareon2016challenge.dll";
const int startOrdinal = 1;
const int endOrdinal = 48;
const int numFunctions = endOrdinal - startOrdinal + 1;
HMODULE hDll = NULL;
FunctionResult* results = (FunctionResult*)malloc(sizeof(FunctionResult) * numFunctions);
if (results == NULL) {
printf("Error: Memory allocation failed.\n");
return 1;
}
hDll = LoadLibraryA(dllPath);
if (hDll == NULL) {
printf("Error: Failed to load DLL '%s'. GetLastError: %lu\n", dllPath, GetLastError());
free(results);
return 1;
}
printf("DLL '%s' loaded successfully.\n", dllPath);
int validCount = 0; // 유효한 결과의 개수
for (int i = 0; i < numFunctions; ++i) {
int ordinal = startOrdinal + i;
results[i].ordinal = ordinal;
results[i].isValid = 0; // 기본적으로 실패로 설정
DLL_FUNCTION_PTR pFunc = (DLL_FUNCTION_PTR)GetProcAddress(hDll, (LPCSTR)ordinal);
if (pFunc != NULL) {
results[i].returnValue = pFunc();
results[i].isValid = 1; // 성공
printf("Ordinal %d: Returned value = %u\n", ordinal, results[i].returnValue);
validCount++;
} else {
printf("Ordinal %d: Function not found or cannot be called. GetLastError: %lu\n",
ordinal,
GetLastError());
}
}
printf("\n--- 오디널 및 반환값 목록 --- (위와 동일)\n");
for (int i = 0; i < numFunctions; ++i) {
if (results[i].isValid) {
printf("Ordinal %d: %u\n", results[i].ordinal, results[i].returnValue);
} else {
printf("Ordinal %d: (호출 실패)\n", results[i].ordinal);
}
}
// 유효한 결과만 담을 새 배열 (선택 사항: 메모리 최적화)
FunctionResult* validResults = (FunctionResult*)malloc(sizeof(FunctionResult) * validCount);
if (validResults == NULL) {
printf("Error: Memory allocation failed for valid results.\n");
FreeLibrary(hDll);
free(results);
return 1;
}
int currentValidIndex = 0;
for (int i = 0; i < numFunctions; ++i) {
if (results[i].isValid) {
validResults[currentValidIndex++] = results[i];
}
}
printf("\n--- 반환값 기준 정렬 (오름차순) ---\n");
// qsort를 사용하여 반환값 기준으로 정렬
// validResults 배열의 유효한 항목들만 정렬합니다.
qsort(validResults, validCount, sizeof(FunctionResult), compareFunctionResults);
for (int i = 0; i < validCount; ++i) {
printf("Ordinal %d: %u\n", validResults[i].ordinal, validResults[i].returnValue);
}
printf("\n--- 반환값 기준 정렬 (내림차순) ---\n");
// 내림차순 정렬을 위한 비교 함수 사용
qsort(validResults, validCount, sizeof(FunctionResult), compareFunctionResultsDesc);
for (int i = 0; i < validCount; ++i) {
printf("Ordinal %d: %u\n", validResults[i].ordinal, validResults[i].returnValue);
}
FreeLibrary(hDll);
free(results); // 원래 배열 해제
free(validResults); // 유효한 결과 배열 해제
return 0;
}
Ordinal 35: 1
Ordinal 22: 2
Ordinal 25: 3
Ordinal 9: 4
Ordinal 45: 5
Ordinal 23: 6
Ordinal 36: 7
Ordinal 2: 8
Ordinal 12: 9
Ordinal 37: 10
Ordinal 14: 11
Ordinal 18: 12
Ordinal 8: 13
Ordinal 46: 14
Ordinal 30: 15
Ordinal 7: 16
Ordinal 4: 17
Ordinal 42: 18
Ordinal 34: 19
Ordinal 47: 20
Ordinal 11: 21
Ordinal 32: 22
Ordinal 27: 23
Ordinal 39: 24
Ordinal 10: 25
Ordinal 3: 26
Ordinal 21: 27
Ordinal 13: 28
Ordinal 16: 29
Ordinal 33: 31
Ordinal 26: 32
Ordinal 17: 33
Ordinal 44: 34
Ordinal 6: 35
Ordinal 20: 36
Ordinal 48: 37
Ordinal 19: 38
Ordinal 29: 39
Ordinal 38: 40
Ordinal 24: 41
Ordinal 15: 42
Ordinal 41: 43
Ordinal 31: 44
Ordinal 28: 45
Ordinal 43: 46
Ordinal 5: 47
Ordinal 40: 48
Ordinal 1: 51
반환값 30 및 49 50이 빕니다
-
반환값 순서대로 정렬 후 실행하였으나 실패하였습니다.
-
반환값을 prestep으로 보고 정렬 후 실행하였으나 실패하였습니다.
51, 1, 35,...,15, 30, 50, 50, 50..., -
반환값을 nextstep으로 보고 정렬 후 실행하였습니다
30, ..., 1, 51, 50, ...
#include <stdio.h> // printf를 사용하기 위함
#include <stdlib.h> // malloc, free 사용을 위함
#include <windows.h> // Windows API 함수들을 사용하기 위함
// DLL 함수 포인터 타입 정의
typedef unsigned int(__stdcall* DLL_FUNCTION_PTR)();
// 함수 호출 결과를 저장할 구조체
typedef struct
{
int ordinal;
unsigned int returnValue; // uint4에 해당
int isValid; // 함수 호출이 성공했는지 여부 (0: 실패, 1: 성공)
} FunctionResult;
int
main()
{
// 호출할 DLL 파일의 경로를 지정합니다.
const char* dllPath = "flareon2016challenge.dll";
// 호출할 오디널 번호들을 원하는 순서대로 배열에 작성합니다.
int ordinal_array[] = { 30, 15, 42, 18, 12, 9, 4, 17, 33, 31, 44, 34, 19, 38, 40, 48, 37,
10, 25, 3, 26, 32, 22, 2, 8, 13, 28, 45, 5, 47, 20, 36, 7, 16,
29, 39, 24, 41, 43, 46, 14, 11, 21, 27, 23, 6, 35, 1, 51, 50, 50,
50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 50, 49 };
const int num_ordinals = sizeof(ordinal_array) / sizeof(ordinal_array[0]);
// 결과 저장을 위한 동적 배열 할당
FunctionResult* results = (FunctionResult*)malloc(sizeof(FunctionResult) * num_ordinals);
if (results == NULL) {
printf("Error: Memory allocation failed.\n");
return 1;
}
HMODULE hDll = NULL;
// DLL 로드
hDll = LoadLibraryA(dllPath);
if (hDll == NULL) {
printf("Error: Failed to load DLL '%s'. GetLastError: %lu\n", dllPath, GetLastError());
free(results);
return 1;
}
printf("DLL '%s' loaded successfully.\n", dllPath);
// 각 오디널별 함수 호출 (배열 순서대로)
for (int i = 0; i < num_ordinals; ++i) {
int ordinal = ordinal_array[i];
results[i].ordinal = ordinal;
results[i].isValid = 0; // 기본적으로 실패로 설정
printf("Ordinal %d 함수 호출 시도...\n", ordinal);
// GetProcAddress 함수는 DLL에서 내보낸 함수의 주소를 가져옵니다.
DLL_FUNCTION_PTR pFunc = (DLL_FUNCTION_PTR)GetProcAddress(hDll, (LPCSTR)ordinal);
if (pFunc != NULL) {
results[i].returnValue = pFunc();
results[i].isValid = 1; // 성공
printf(" Ordinal %d: 반환값 = %u\n", ordinal, results[i].returnValue);
} else {
printf(" Ordinal %d: 함수를 찾을 수 없거나 호출할 수 없습니다. GetLastError: %lu\n",
ordinal,
GetLastError());
}
}
// DLL 언로드 (메모리 해제)
FreeLibrary(hDll);
printf("\nDLL '%s' unloaded.\n", dllPath);
printf("\n--- 호출 순서 및 결과 요약 ---\n");
for (int i = 0; i < num_ordinals; ++i) {
if (results[i].isValid) {
printf("Ordinal %d: %u\n", results[i].ordinal, results[i].returnValue);
} else {
printf("Ordinal %d: (호출 실패)\n", results[i].ordinal);
}
}
free(results); // 할당된 메모리 해제
return 0; // 성공적으로 프로그램 종료
}
답지를 보니 nextstep으로 보는 것은 맞았지만, 51 ordinal 실행 시 복호화 된 데이터는 실행프로그램이었다고 합니다. 메모리를 덤프해 나온 실행 프로그램을 리버싱하여 50 ordinal을 실행하는 것이라고 합니다
chal5

분석 결과, 401610 및 hashNth 및 401880에서 해시를 여러번 반복하며 복호화를 진행하는 루틴이 있습니다
어떤 방법으로 해시화 및 복호화를 하는지는 체크하였지만, 입력 값이 조금이라도 바뀔 경우 해시 값이 많이 바뀌어 어떻게 푸는지는 추측하지 못했습니다
chal6


cpython이 내장된 C++프로그램으로 보인다

py2exe로 생성된 프로그램으로 추정됩니다. 한번에 맞춰도 플래그가 나오지 않으므로 코드를 봐야합니다
unpy2exe를 이용해 해제시 poc.py.pyc가 나옵니다

# Visit https://www.lddgo.net/en/string/pyc-compile-decompile for more information
# Version : Python 2.7
import sys
-1
import random
__version__ = 'Flare-On ultra python obfuscater 2000'
-1
target = random.randint(1, 101)
count = 1
-1
error_input = ''
while None:
if True:
-1
print '(Guesses: %d) Pick a number between 1 and 100:' % count,
input = sys.stdin.readline()
try:
input = int(input, 0)
except:
-1
error_input = input
-1
print 'Invalid input: %s' % error_input
continue
-1
if target == input:
break
-1
-1
count += 1
if target == input:
-1
win_msg = 'Wahoo, you guessed it with %d guesses\n' % count
-1
sys.stdout.write(win_msg)
if count == 1:
print 'Status: super guesser %d' % count
-1
sys.exit(1)
if error_input != '':
-1
tmp = ''.join((lambda .0: pass)(error_input)).encode('hex')
if tmp != '312a232f272e27313162322e372548':
sys.exit(0)
-1
-1
-1
-1
-1
-1
stuffs = [
67,
139,
119,
165,
232,
86,
207,
61,
79,
67,
45,
58,
230,
190,
181,
74,
65,
148,
71,
243,
246,
67,
142,
60,
61,
92,
58,
115,
240,
226,
171]
import hashlib
-1
stuffer = hashlib.md5(win_msg + tmp).digest()
for x in range(len(stuffs)):
-1
-1
-1
-1
-1
-1
print chr(stuffs[x] ^ ord(stuffer[x % len(stuffer)])),
print
gpt로 스크립트를 만들어 실행하면
import hashlib
# 검증 상수 (hex) 및 그에 대응하는 ASCII
expected_tmp_hex = "312a232f272e27313162322e372548"
expected_tmp_ascii = bytes.fromhex(expected_tmp_hex) # b"1*#/'.'11b2.7%H"
# 사용자가 입력/가공한 결과가 expected_tmp_hex 인지 확인하는 단계가 원래 필요
tmp = expected_tmp_ascii # 원 코드 의도 반영: 비교를 통과했다고 가정
stuffs = [
67,
139,
119,
165,
232,
86,
207,
61,
79,
67,
45,
58,
230,
190,
181,
74,
65,
148,
71,
243,
246,
67,
142,
60,
61,
92,
58,
115,
240,
226,
171,
]
for i in range(200):
win_msg = f"Wahoo, you guessed it with {i} guesses\n".encode()
# 원 코드에서는 md5(win_msg + tmp_hex_string) 이었을 가능성 있음(파이썬2 바이트 문자열)
# tmp 를 hex 문자열로 쓸지, ASCII 바이트로 쓸지 모호하여 둘 다 시도해봄
m1 = hashlib.md5(win_msg + expected_tmp_ascii).digest()
m2 = hashlib.md5(win_msg + expected_tmp_hex.encode("ascii")).digest()
def xor_decode(stuffs, key):
return bytes([stuffs[i] ^ key[i % len(key)] for i in range(len(stuffs))])
print("Try ASCII tmp :", xor_decode(stuffs, m1))
print("Try HEX tmp :", xor_decode(stuffs, m2))

chal7
chal8

간단한 윈도우 cli프로그램이다
복호화시 this is the wrong password가 비밀번호이며, 2549 조건과 걸려서 fail이 된다
답지를 보니 dos헤더에 실제 프로그램이 있었다고 한다