Packs
Points 0
Solves 0
This can be VERY easy or VERY hard for you, depends on how you do it, I guess…
After receiving a challenge file named Packs.exe
, out of curiosity, I tried opening it with a ZIP utility, which revealed an interesting section named .themida
. This immediately revealed that the executable contained Themida, a well-known commercial software protection program.
After some research, I discovered a specialized tool for unpacking Themida-protected executables: Dynamic unpacker and import fixer for Themida/WinLicense 2.x and 3.x. Using this tool allowed me to retrieve the original, unpacked executable for analysis.
With the unpacked binary in hand, I proceeded to reverse engineer the main functionality. The primary function responsible for flag validation is shown below:
int sub_401400()
{
int v0; // edi
int v1; // edx
int v2; // eax
unsigned int v3; // esi
int v4; // eax
int v5; // eax
int v6; // eax
int v7; // ecx
unsigned int v8; // ecx
int v9; // edx
__int128 v11; // [esp+Ch] [ebp-34h] BYREF
int v12; // [esp+1Ch] [ebp-24h]
unsigned int v13; // [esp+20h] [ebp-20h]
__int64 v14; // [esp+24h] [ebp-1Ch] BYREF
int v15; // [esp+2Ch] [ebp-14h]
int v16; // [esp+3Ch] [ebp-4h]
v12 = 0;
v11 = 0;
v13 = 15;
LOBYTE(v11) = 0;
v16 = 0;
sub_401750(std::cout, aFlag);
sub_401970(std::cin, &v11);
v15 = 0;
v14 = 0;
sub_4012F0(&v14, &v11);
LOBYTE(v16) = 1;
v0 = v14;
v1 = dword_4054BC;
if ( HIDWORD(v14) - (_DWORD)v14 != dword_4054C0 - dword_4054BC )
{
v2 = sub_401750(std::cout, aWrongLength);
std::istream::operator>>(v2, sub_401B70);
v1 = dword_4054BC;
}
v3 = 0;
if ( dword_4054C0 != v1 )
{
do
{
if ( *(_BYTE *)(v0 + v3) != *(_BYTE *)(v1 + v3) )
{
v4 = sub_401750(std::cout, aNope);
std::istream::operator>>(v4, sub_401B70);
v1 = dword_4054BC;
}
++v3;
}
while ( v3 < dword_4054C0 - v1 );
}
v5 = sub_401750(std::cout, aYay);
std::istream::operator>>(v5, sub_401B70);
if ( v0 )
{
v6 = v0;
v7 = v15 - v0;
if ( (unsigned int)(v15 - v0) >= 0x1000 )
{
v0 = *(_DWORD *)(v0 - 4);
v8 = v7 + 35;
if ( (unsigned int)(v6 - v0 - 4) > 0x1F )
LABEL_14:
invalid_parameter_noinfo_noreturn(v8);
}
sub_4021EE(v0);
}
if ( v13 > 0xF )
{
v9 = v11;
if ( v13 + 1 >= 0x1000 )
{
v9 = *(_DWORD *)(v11 - 4);
v8 = v13 + 36;
if ( (unsigned int)(v11 - v9 - 4) > 0x1F )
goto LABEL_14;
}
sub_4021EE(v9);
}
return 0;
}
This function handles user input and performs the flag validation. It begins by reading input from the user, then processes it through the sub_4012F0
function, and finally compares the transformed input against a hardcoded value. Looking at the code, we can see the program prints “Wrong Length” if the length doesn’t match, “Nope” if any character comparison fails, and “Yay” if the input is correct.
The next critical function to analyze is sub_4012F0
, which transforms the user input:
unsigned int *__fastcall sub_4012F0(unsigned int *a1, _DWORD *a2)
{
unsigned int v4; // esi
unsigned int v5; // eax
unsigned int i; // edx
_DWORD *v7; // eax
int v8; // edx
int v9; // eax
unsigned int v10; // eax
char v11; // cl
unsigned int v13; // [esp+8h] [ebp-4h]
_BYTE *v14; // [esp+8h] [ebp-4h]
*(_QWORD *)a1 = 0;
a1[2] = 0;
v13 = a2[4];
*a1 = 0;
a1[1] = 0;
a1[2] = 0;
if ( v13 )
{
sub_401E10(a1, v13);
v4 = *a1;
j_memset(*a1, 0, v13);
a1[1] = v4 + v13;
}
v5 = a2[4];
for ( i = 0; i < v5; v5 = a2[4] )
{
v7 = a2;
if ( a2[5] > 0xFu )
v7 = (_DWORD *)*a2;
*(_BYTE *)(i + *a1) = *((_BYTE *)v7 + i);
++i;
}
v8 = 0;
if ( v5 )
{
do
{
v14 = (_BYTE *)(v8 + *a1);
if ( v8 <= 10 )
v9 = ((2 * (unsigned __int8)(*v14 - v8)) | ((unsigned __int8)(*v14 - v8) >> 7)) - 5;
else
v9 = ((16 * (unsigned __int8)(*v14 - v8 + 1)) | ((unsigned __int8)(*v14 - v8 + 1) >> 4)) ^ 0x7A;
if ( (v8 & 1) == 0 )
{
v10 = (unsigned __int8)((4 * ~(_BYTE)v9) | ((unsigned __int8)~(_BYTE)v9 >> 6));
v9 = (16 * v10) | (v10 >> 4);
}
if ( v8 == 4 )
v11 = 74 - v9;
else
v11 = v8 + v9 + 45;
++v8;
*v14 = v11;
}
while ( (unsigned int)v8 < a2[4] );
}
return a1;
}
This function is responsible for transforming the user’s input string before it’s compared with the expected value. It performs a series of complex operations on each byte of the input, which vary based on the position of the byte:
- For bytes at positions ≤ 10, it calculates
((2 * (byte - position)) | ((byte - position) >> 7)) - 5
- For bytes at positions > 10, it calculates
((16 * (byte - position + 1)) | ((byte - position + 1) >> 4)) ^ 0x7A
- For bytes at even positions, it applies additional transformations involving bit operations and nibble swapping
- A special case for the byte at position 4, where the final calculation is
74 - result
- For all other positions, the final result is
position + result + 45
The transformed input is then compared against a hardcoded expected value. By identifying this expected value in the disassembly, we can work backwards to determine the original input:
.text:0040101A C7 44 24 0C CA B1 CA A7 mov dword ptr [esp+0Ch], 0A7CAB1CAh
.text:00401022 8D 4C 24 0C lea ecx, [esp+0Ch]
.text:00401026 C7 44 24 10 B1 CB D2 B7 mov dword ptr [esp+10h], 0B7D2CBB1h
.text:0040102E 8B C7 mov eax, edi
.text:00401030 C7 44 24 14 E1 8F BF 26 mov dword ptr [esp+14h], 26BF8FE1h
.text:00401038 C7 44 24 18 32 A6 27 DB mov dword ptr [esp+18h], 0DB27A632h
.text:00401040 C7 44 24 1C 2E CC DC 98 mov dword ptr [esp+1Ch], 98DCCC2Eh
.text:00401048 C7 44 24 20 61 51 5C 03 mov dword ptr [esp+20h], 35C5161h
.text:00401050 C7 44 24 24 85 E4 84 47 mov dword ptr [esp+24h], 4784E485h
.text:00401058 C7 44 24 28 B9 45 B3 75 mov dword ptr [esp+28h], 75B345B9h
.text:00401060 C7 44 24 2C 76 FC AB 2E mov dword ptr [esp+2Ch], 2EABFC76h
.text:00401068 C7 44 24 30 72 1B 6C B2 mov dword ptr [esp+30h], 0B26C1B72h
.text:00401070 C7 44 24 34 AA 94 C3 42 mov dword ptr [esp+34h], 42C394AAh
.text:00401078 C7 44 24 38 C5 23 DC EA mov dword ptr [esp+38h], 0EADC23C5h
.text:00401080 2B C1 sub eax, ecx
.text:00401082 74 2A jz short loc_4010AE
.text:00401084 50 push eax
.text:00401085 B9 BC 54 40 00 mov ecx, offset dword_4054BC
.text:0040108A E8 81 0D 00 00 call sub_401E10
.text:0040108F 8B 35 BC 54 40 00 mov esi, ds:dword_4054BC
From this assembly code, I was able to extract the expected ciphertext: cab1caa7b1cbd2b7e18fbf2632a627db2eccdc9861515c0385e48447b945b37576fcab2e721b6cb2aa94c342c523dcea
To recover the original flag, I needed to reverse the transformation process. After analyzing the operations, I created a Python script to invert each step:
def swap_nibbles(b): # self-inverse
return ((b << 4) | (b >> 4)) & 0xFF
def rol(b, r):
r &= 7
return ((b << r) | (b >> (8 - r))) & 0xFF
def ror(b, r):
r &= 7
return ((b >> r) | (b << (8 - r))) & 0xFF
def invert_byte(i, y):
# undo final add
if i == 4:
t = (74 - y) & 0xFF
else:
t = (y - (i + 45)) & 0xFF
# undo even-index twist
if (i & 1) == 0:
# t = swap_nibbles(u), u = ROL2(~t0)
u = swap_nibbles(t)
a = ror(u, 2) # a = ROR2(u) = ~t0
t0 = (~a) & 0xFF
else:
t0 = t
# undo first stage
if i <= 10:
w = (t0 + 5) & 0xFF
if (w & 1) == 0:
d = (w >> 1) & 0x7F # d in [0..127]
else:
d = ((w - 1) >> 1) + 128 # d in [128..255]
x = (d + i) & 0xFF
else:
d = swap_nibbles(t0 ^ 0x7A)
x = (d - 1 + i) & 0xFF
return x
ct = bytes.fromhex('cab1caa7b1cbd2b7e18fbf2632a627db2eccdc9861515c0385e48447b945b37576fcab2e721b6cb2aa94c342c523dcea')
plain = bytes(invert_byte(i, b) for i, b in enumerate(ct))
print(plain.decode())
GEMASTIK18{S1mpl3_P4ck3r_f0r_4_S1mpl3_Ch4ll3nge}