
UPX로 packed 되어 있다.
UPX로 다시 unpack 할 수 있다.

crackme를 실행하면 serial을 입력하는 칸과 입력을 확인하는 GO! 버튼으로 이루어져 있다.
DialogFunc을 보면 입력값을 검증하는 함수를 찾을 수 있다.
int __stdcall WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nShowCmd)
{
...
DialogBoxParamA(hInstance, 0x73, hWndParent, DialogFunc, 0);
...
}
if ( message == WM_COMMAND )
{
switch ( wparam )
{
case 0x3EAu:
input_length = GetDlgItemTextA(hDlg, 1001, input_data, 50);
verify(&loc_406BE1);
return 0;
verify에선 입력을 xor 한 뒤 hash를 만들어 hash와 글자 수를 확인한다.
index = 0;
for ( i = 0; i < 24; i += 6 )
{
if ( !input_data[index] )
index = 0;
v3 = input_data[index] ^ key[i];
v4 = index + 1;
xored_data[i] = v3;
if ( !input_data[v4] )
v4 = 0;
v5 = input_data[v4] ^ key[i + 1];
v6 = v4 + 1;
xored_data[i + 1] = v5;
if ( !input_data[v6] )
v6 = 0;
v7 = input_data[v6] ^ key[i + 2];
v8 = v6 + 1;
xored_data[i + 2] = v7;
if ( !input_data[v8] )
v8 = 0;
v9 = input_data[v8] ^ key[i + 3];
v10 = v8 + 1;
xored_data[i + 3] = v9;
if ( !input_data[v10] )
v10 = 0;
v11 = input_data[v10] ^ key[i + 4];
v12 = v10 + 1;
xored_data[i + 4] = v11;
if ( !input_data[v12] )
v12 = 0;
v13 = input_data[v12] ^ key[i + 5];
index = v12 + 1;
xored_data[i + 5] = v13;
}
if ( hash_xored() == 0x5A6AA47D && input_length == 22 )
{
CurrentProcessId = GetCurrentProcessId();
handle = OpenProcess(0x28u, 1, CurrentProcessId);
WriteProcessMemory(handle, lpBaseAddress, xored_data, 0x18u, 0);
return 1;
}
else
{
MessageBoxA(hWndParent, "Try again!", Caption, 0x40u);
return 0;
}
unsigned int hash_xored()
{
unsigned int i; // esi
unsigned int v1; // edx
unsigned int v2; // ecx
unsigned int v3; // edx
unsigned int v4; // ecx
unsigned int v5; // edx
unsigned int v6; // ecx
unsigned int v7; // edx
unsigned int v8; // eax
unsigned int v9; // edx
unsigned int v10; // eax
unsigned int v11; // eax
unsigned int v12; // eax
unsigned int v13; // eax
unsigned int v14; // eax
unsigned int v15; // eax
char v16; // cl
int magic_table[257]; // [esp+8h] [ebp-410h]
int v19; // [esp+40Ch] [ebp-Ch]
v19 = 0;
for ( i = 0; i < 0x100; ++i )
{
v1 = (i >> 1) ^ 0xEDB88320;
if ( (i & 1) == 0 )
v1 = i >> 1;
v2 = (v1 >> 1) ^ 0xEDB88320;
if ( (v1 & 1) == 0 )
v2 = v1 >> 1;
v3 = (v2 >> 1) ^ 0xEDB88320;
if ( (v2 & 1) == 0 )
v3 = v2 >> 1;
v4 = (v3 >> 1) ^ 0xEDB88320;
if ( (v3 & 1) == 0 )
v4 = v3 >> 1;
v5 = (v4 >> 1) ^ 0xEDB88320;
if ( (v4 & 1) == 0 )
v5 = v4 >> 1;
v6 = (v5 >> 1) ^ 0xEDB88320;
if ( (v5 & 1) == 0 )
v6 = v5 >> 1;
v7 = (v6 >> 1) ^ 0xEDB88320;
if ( (v6 & 1) == 0 )
v7 = v6 >> 1;
v8 = (v7 >> 1) ^ 0xEDB88320;
if ( (v7 & 1) == 0 )
v8 = v7 >> 1;
magic_table[i] = v8;
}
v9 = 0;
v10 = ~v19;
do
{
v11 = magic_table[(v10 ^ xored_data[v9])] ^ (v10 >> 8);
v12 = magic_table[(v11 ^ xored_data[v9 + 1])] ^ (v11 >> 8);
v13 = magic_table[(v12 ^ xored_data[v9 + 2])] ^ (v12 >> 8);
v14 = magic_table[(v13 ^ xored_data[v9 + 3])] ^ (v13 >> 8);
v15 = magic_table[(v14 ^ xored_data[v9 + 4])] ^ (v14 >> 8);
v16 = xored_data[v9 + 5];
v9 += 6;
v10 = magic_table[(v15 ^ v16)] ^ (v15 >> 8);
}
while ( v9 < 0x18 );
return ~v10;
}
hash_xored의 역산은 할 수 없고 brute force 또한 입력의 크기가 커서 불가능하다.
그러나 비밀번호 비교 이후의 동작을 보면 문제를 풀 수 있다.
The goal is to find the valid serial. A window will pop up to let you know you found it :-)
There is only one valid serial.
readme를 보면 성공 message가 뜬다고 하는데 문제를 푸는 데 이 정보를 이용할 수 있다.
if ( hash_xored() == 0x5A6AA47D && input_length == 22 )
{
CurrentProcessId = GetCurrentProcessId();
handle = OpenProcess(0x28u, 1, CurrentProcessId);
WriteProcessMemory(handle, lpBaseAddress, xored_data, 0x18u, 0);
return 1;
}
코드를 보면 비교 후에 WriteProcessMemory로 특정 위치에 xor 한 데이터를 써주는데 확인해보면 verify 이 후 (eax==1) 실행되는 위치임을 알 수 있다.
.text:00406BCA mov input_length, eax
.text:00406BCF push offset loc_406BE1 ; lpBaseAddress
.text:00406BD4 call verify
.text:00406BD9 add esp, 4
.text:00406BDC cmp eax, 0
.text:00406BDF jz short loc_406C10
.text:00406BE1
.text:00406BE1 loc_406BE1: ; DATA XREF: DialogFunc+38F↑o
.text:00406BE1 nop
.text:00406BE2 nop
해당 영역은 nop으로 채워져 있으며 검증 이후 xor 결과로 다시 쓰여진다.
if ( hash_xored() == 0x5A6AA47D && input_length == 22 )
{
CurrentProcessId = GetCurrentProcessId();
handle = OpenProcess(0x28u, 1, CurrentProcessId);
WriteProcessMemory(handle, lpBaseAddress, xored_data, 0x18u, 0);
return 1;
}
else
{
MessageBoxA(hWndParent, "Try again!", Caption, 0x40u);
return 0;
}
실패시 Try again! 메시지 박스를 띄우는 걸로 보아 성공 시에도 메시지 박스를 띄운다는 걸 알 수 있다.

바이너리를 확인하면 성공시 출력되는 문자열을 찾을 수 있다.
6A 40 - push 40 { 64 }
68 28B04000 - push crackme.exe+B028 { ("Information") }
68 F8924000 - push crackme.exe+92F8 { ("Try again!") }
FF 35 F0B44000 - push [crackme.exe+B4F0] { (24710324) }
FF 15 DC904000 - call dword ptr [crackme.exe+90DC] { ->USER32.MessageBoxA }
위 실패 메시지를 바꿔준다.
6A 40 - push 40 { 64 }
68 28B04000 - push crackme.exe+B028 { ("Information") }
68 38B04000 - push crackme.exe+B038 { ("Well done!") }
FF 35 F0B44000 - push [crackme.exe+B4F0] { (24710324) }
FF 15 DC904000 - call dword ptr [crackme.exe+90DC] { ->USER32.MessageBoxA }
그러면 이렇게 되는데 nop으로 채워져있는 공간에 위 코드가 쓰여야 하므로
xored_data는 6A 40 68 ... FF 15 DC 90 40 00이 된다.
xored_data는 input_data와 key를 xor한 것이니 key와 xor 해서 input_data를 구할 수 있다.
input_length는 22여야 하므로 22글자만 출력한다.
key = [
0x09, 0x32, 0x09, 0x4B, 0xDB, 0x2D, 0x65, 0x1B, 0x16, 0xDF,
0x2E, 0x65, 0xD2, 0x5E, 0x99, 0xD7, 0x2B, 0x73, 0xD2, 0x74,
0xAF, 0xE3, 0x23, 0x72
]
xored_data = [
0x6A, 0X40, 0X68, 0X28, 0XB0, 0X40, 0X00, 0X68, 0X38, 0XB0, 0X40, 0X00, 0XFF, 0X35, 0XF0, 0XB4, 0X40, 0X00,
0xFF, 0X15, 0XDC, 0X90, 0X40, 0X00
]
input_data = ''
for i in range(len(xored_data)):
input_data = input_data + chr(xored_data[i] ^ key[i])
print(input_data[:22])
구한 serial을 입력하면 성공 메시지가 출력되는 것을 확인 할 수 있다.
