Password Manager (re)
Striking back! Your team has re-taken control over the gateway and recovered what seems to be a password manager used by COViD operatives. Unlock the database to retrieve the credentials.
First thing to do is always to check what type of program this is. Running file
tells me that it is a .NET program.
➜ file CODEC.exe
CODEC.exe: PE32 executable (console) Intel 80386 Mono/.Net assembly, for MS Windows
Running the program in PowerShell gives the following output. Since it checks for a password, but does not read from standard input, I’m quite sure it reads the password from the program arguments.
PS S:\RE Challenges\re-challenge-3> .\CODEC.exe
Failed to verify master password
PS S:\RE Challenges\re-challenge-3> .\CODEC.exe aaaaa
Failed to verify master password
Static Analysis
I used dnSpy to analyse the program. Make sure to use the win32 version of dnSpy because this is a PE32 executable.
Clicking on <Module>
in the Assembly Explorer pane on the left will show the decompilation of the program.
In the explorer, we can also see CrtImplementationDetails
and CppImplementationDetails
, indicating that this program (or some parts of it) was compiled from C/C++ code.
The cleaned up version (with renamed symbols) of main
is showed below. I added markers (// [1]
, // [2]
, etc) for easy reference to different sections of the code.
// Token: 0x06000003 RID: 3 RVA: 0x00001310 File Offset: 0x00000710
internal unsafe static int main(int argc, sbyte** argv)
{
if (argc > 1)
{
// [1]
sbyte* password = *(int*)(argv + 4 / sizeof(sbyte*));
sbyte* password_start = password;
sbyte* password_end = password;
if (*password_start != 0)
{
do
{
password_end++;
}
while (*password_end != 0);
}
if (password_end - password_start == 32)
{
// [2]
<Module>.g_INPUT = password;
$ArrayType$$$BY0CB@D $ArrayType$$$BY0CB@D;
<Module>.memmove((void*)(&$ArrayType$$$BY0CB@D), (void*)<Module>.g_INPUT, 32U);
*(ref $ArrayType$$$BY0CB@D + 32) = 0;
// [3]
<Module>.printf("Decrypting database");
byte[] hash = new byte[32];
int count = 0;
do
{
hash[count] = <Module>.g_INPUT[count];
count++;
}
while (count < 32);
// [4]
SHA256 sha = SHA256.Create();
byte[] hash = sha.ComputeHash(array);
uint count2 = 2000000U;
do
{
hash = sha.ComputeHash(hash);
count2 -= 1U;
}
while (count2 > 0U);
// [5]
if (array2[0] == 95 && array2[1] == 218)
{
// [6]
<Module>.func1();
<Module>.func4(<Module>.g_INPUT, 1);
<Module>.func2();
<Module>.func1();
<Module>.func4(<Module>.g_INPUT, 13);
<Module>.func2();
// [7]
bool correct = true;
int num6 = <Module>.__imp_result + 3;
byte* ptr = <Module>.g_INPUT + 1;
byte* ptr2 = <Module>.__imp_result - <Module>.g_INPUT;
uint count3 = 8U;
do
{
int num8 = (*(ptr - 1) == *(num6 - 3)) ? 1 : 0;
flag = (((flag ? 1 : 0) & (byte)num8) != 0);
int num9 = (*ptr == ptr2[ptr]) ? 1 : 0;
flag = (((flag ? 1 : 0) & (byte)num9) != 0);
int num10 = (ptr[1] == *(num6 - 1)) ? 1 : 0;
flag = (((flag ? 1 : 0) & (byte)num10) != 0);
int num11 = (ptr[2] == *num6) ? 1 : 0;
flag = (((flag ? 1 : 0) & (byte)num11) != 0);
num6 += 4;
ptr += 4;
count3 -= 1U;
}
while (count3 > 0U);
// [8]
if (correct)
{
// print success message
}
else
{
// print failed
}
return 0;
}
// print failed
return 0;
}
}
// print failed
return 0;
}
The code is long, but could be summarized as follows:
- [1] Get the input from program arguments, and check if it is 32-bytes long.
- [2] Copy the input into another array data structure. Also saves the pointer to the input in
g_INPUT
. - [3] Again copy the input into another array.
- [4] Compute the SHA256 of the input 2000000 times.
- [5] Compare the first 2 bytes of the hash, to determine whether to continue with the input validation.
- [6] Call
func1()
,func(2)
,func4()
, which probably does something with the input. - [7] Compare
__imp_result
withg_INPUT
.- The code in the do-while loop may seem complicated, but actually it just compares the bytes between the 2 arrays, like a
memcmp
. - Knowing that the code does this, we can speculate that our input (which
g_INPUT
points to) has been modified in [6], then compared with__imp_result
which is the target array of bytes obtained when the input is correct.
- The code in the do-while loop may seem complicated, but actually it just compares the bytes between the 2 arrays, like a
- [8] Prints the success or failure messages depending on the result of [7].
So, the important parts of the program is [6], where the input is most likely transformed, before it is checked in [7].
[4] and [5] might look scary, as we need to make sure the first 2 bytes of the SHA256 hash of the input matches some values. However, it is actually not a big concern. It could be possible that there are many possible inputs that satisfy step [7], and step [5] is just to narrow them down to only one case. We shall only worry about this after reversing step [6].
func1
func1
could be cleaned up as follows. I renamed the long symbol <Module>.??_C@_0EB@IKAMFDMJ@?T?$JMz?d?$BA?aV?f?n?Y?$BOw?$LG?$HN?$IE?$DNmjD?$HL?FC?$BNa?$LJJ?$KI?$BJ90?O@
to key
, and simplified the code in the loop:
// Token: 0x06000001 RID: 1 RVA: 0x0000117C File Offset: 0x0000057C
internal unsafe static void func1()
{
int key_index = 32;
sbyte* key_end = key;
do
{
key_end++;
}
while (*key_end != 0);
uint key_len = key_end - key;
int input_index = 0;
do
{
<Module>.g_INPUT[input_index + 0] += key[(key_index + 20) % key_len];
<Module>.g_INPUT[input_index + 1] += key[(key_index + 30) % key_len];
<Module>.g_INPUT[input_index + 2] += key[(key_index + 40) % key_len];
<Module>.g_INPUT[input_index + 3] += key[(key_index + 50) % key_len];
key_index = (key_index + 17) % key_len;
input_index += 4;
}
while (input_index < 32);
}
In this function, each byte of the input is added with an offset, obtained from a global array key
. We shall revisit this function later when writing the script to find the correct password.
func2
func2
could be rewritten as the following.
// Token: 0x06000002 RID: 2 RVA: 0x00001234 File Offset: 0x00000634
internal unsafe static void func2()
{
uint* ptr = (uint*)<Module>.g_INPUT;
uint tmp;
ptr[0] = ptr[0] >> 20 | ptr[0] << 12;
tmp = ptr[0] ^ ptr[1];
ptr[1] = tmp >> 20 | tmp << 12;
tmp = ptr[1] ^ ptr[2];
ptr[2] = tmp >> 20 | tmp << 12;
tmp = ptr[2] ^ ptr[3];
ptr[3] = tmp >> 20 | tmp << 12;
tmp = ptr[3] ^ ptr[4];
ptr[4] = tmp >> 20 | tmp << 12;
tmp = ptr[4] ^ ptr[5];
ptr[5] = tmp >> 20 | tmp << 12;
tmp = ptr[5] ^ ptr[6];
ptr[6] = tmp >> 20 | tmp << 12;
ptr[7] = ptr[6] ^ ptr[7];
}
This function takes the input as an array of uint
(4-byte) values, i.e. consider 4 bytes as one block. Each block is xored with the one before it, then rotated right by 12 bytes.
We shall revisit this function later when writing the script to find the password.
func4
If you tried to look for func4
in the decompilation, you would realize it does not exist. Huh??? We could only find this:
// Token: 0x06000047 RID: 71 RVA: 0x00001090 File Offset: 0x00000490
[SuppressUnmanagedCodeSecurity]
[MethodImpl(MethodImplOptions.Unmanaged | MethodImplOptions.PreserveSig)]
internal unsafe static extern void func4(byte*, int);
Recall that earlier I mentioned that the program is likely compiled from C/C++
code. So, the definiton of func4
could be in native x86 assembly, so it does not appear in dnSpy. The next logical step is to find it, so I opened up the binary in Ghidra.
The question now is, where in the binary is the function? Notice that above the declaration of func4
, there is the following signature.
Token: 0x06000047 RID: 71 RVA: 0x00001090 File Offset: 0x00000490
So, we do know the offset of the function in the binary. In particular 0x1090
, as given by RVA: 0x00001090
. Navigating to 0x401090
in Ghidra (0x400000
is the starting address assumed by Ghidra), I managed to obtain the decompilation of func4
.
uint func4(char *input, uint start)
{
indices[32] = {0x1d, 0xd, 0x10, 0x14, /* and more */};
counter = 0x10;
do {
index = start + 1 & 0x8000001f;
if ((int)index < 0) {
index = (index - 1 | 0xffffffe0) + 1;
}
c2 = (byte *)(input + indices[index]);
index = start & 0x8000001f;
if ((int)index < 0) {
index = (index - 1 | 0xffffffe0) + 1;
}
c1 = (byte *)(input + indices[index]);
start = start + 2;
*c1 = *c1 ^ *c2;
*c2 = *c2 ^ *c1;
*c1 = *c1 ^ *c2;
bVar1 = *c2 % 0x46 + *c1;
*c1 = bVar1;
*c2 = *c2 + bVar1 % 0x32;
counter = counter + -1;
} while (counter != 0);
return bVar1 / 0x32;
}
Tip: The following code is the XOR swap algorithm.
*c1 = *c1 ^ *c2;
*c2 = *c2 ^ *c1;
*c1 = *c1 ^ *c2;
Here we see that the program
- Takes 2 indices from the
indices
array - Swaps the values at those indices of the
input
array - Performs some operations (addition and modulus) on these values
- Saves them back into the
input
array
__imp_result
Lastly, I need to know the values that the program is comparing the transformed input with, i.e. __imp_result
. Similar to func4
, the values of __imp_result
cannot be found in the decompilation. Only the following declaration exists.
// Token: 0x04000037 RID: 55 RVA: 0x00004504 File Offset: 0x00002D04
internal unsafe static $ArrayType$$$BY0A@E* __imp_result;
However, when navigating to 0x4004504
(because RVA: 0x4504
) in Ghidra, there seems to be some program metadata at this location, instead of a plain array of values to compare with.
IMAGE_LOAD_CONFIG_DIRECTORY32_00404460 XREF[1]: 004001c8(*)
00404460 a4 00 00 IMAGE_LO = BB40E64Eh
00 00 00 = 00401edb
00 00 00 = 1Fh
00404500 00 ?? 00h
00404501 00 ?? 00h
00404502 00 ?? 00h
00404503 00 ?? 00h
00404504 38 ?? 38h 8 ? -> 0040a038
00404505 a0 ?? A0h
00404506 40 ?? 40h @
00404507 00 ?? 00h
CLI_METADATA_HEADER
00404508 42 53 4a CLI_META must be 0x424a5342
42 01 00
01 00 00
CLI_Stream_#~
00404574 00 00 00 #~ Always 0
00 02 00
00 10 57
004059e6 00 ?? 00h
004059e7 00 ?? 00h
This makes sense, as __imp_result
is an $ArrayType$$$BY0A@E*
(whatever this is) object, and it probably contains the pointer to the actual array of values (same as g_INPUT
as seen earlier). Nevermind, I can run the program in a debugger and look for the values in the program memory.
To do so, I set a breakpoint after a line that reads the value of __imp_result
. I also need to set a breakpoint where the SHA256 hash is checked, so that I can modify the values to pass the check.
As shown in the video above, I was able to find the array in memory, by looking into the memory pointed by num6
, which is assigned __imp_result+3
.
Time to find the password
Quick recap of the password checking routine:
// [6]
<Module>.func1();
<Module>.func4(<Module>.g_INPUT, 1);
<Module>.func2();
<Module>.func1();
<Module>.func4(<Module>.g_INPUT, 13);
<Module>.func2();
// [7] (simplified)
memcmp(<Module>.g_INPUT, <Module>.__imp_result, 32);
Now that I briefly know what each of func1
, func2
and func4
does, I can write a z3 script to find the password. I can reimplement the program in Python, then let the z3 solver solve for inputs that satisfy the checks.
First, I made some helper functions, to convert between blocks of 4 bytes and a single byte. This is to support the operations of func2
as seen earlier.
from z3 import *
# Combine 4 single bytes to a block of 4 bytes
def pack_bv(a):
return Concat(a[3], a[2], a[1], a[0])
def pack_arr(arr):
res = []
for i in range(8):
res.append(pack_bv(arr[i*4:i*4+4]))
return res
# Split a block of 4 bytes into 4 single bytes
def unpack_bv(n):
res = [0,0,0,0]
res[3] = Extract(31, 24, n)
res[2] = Extract(23, 16, n)
res[1] = Extract(15, 8, n)
res[0] = Extract(7, 0, n)
return res
def unpack_arr(arr):
res = []
for i in range(8):
res += unpack_bv(arr[i])
return res
Then, I reimplemented func1
, func2
and func4
in Python.
The steps I took to extract the offsets used in func1
of the program are:
- Set a breakpoint at the line after
func1
is called. - Provide an input of 32
A
s. - Read the contents of
g_INPUT
, as each byte of the input is added with an offset value. - Subtract the bytes obtained in Step 3 by 0x41 (
'A'
) to get the offsets.
func_1_offsets = [166, 192, 238, 68, 225, 61, 74, 62, 29, 110, 6, 166, 23, 171, 20, 225, 124, 122, 182, 29, 217, 123, 48, 23, 168, 116, 4, 124, 210, 186, 165, 217]
def func1(arr):
res = []
for i in range(32):
res.append(arr[i])
for i in range(32):
res[i] = res[i] + func_1_offsets[i]
return res
def func2(arr):
arr = pack_arr(arr)
res = []
for i in range(8):
res.append(arr[i])
for i in range(7):
b1_idx = i
b1 = res[b1_idx]
d1 = b1
b2_idx = i + 1
b2 = res[b2_idx]
d2 = b2
res[b2_idx] = RotateRight(d1, 20) ^ d2
res[b1_idx] = RotateRight(d1, 20)
res = unpack_arr(res)
return res
func_4_indices = [0x1d,0xd,0x10,0x14,4,0x16,0x15,0x18,0x1f,0xb,1,0,9,7,8,5,0x19,6,0x13,0xf,0x17,0x1b,3,2,0x1a,0x12,0x1e,0x1c,10,0xc,0x11,0xe]
def func4(arr, start):
res = []
for i in range(32):
res.append(arr[i])
for _ in range(16):
i1 = func_4_indices[(start) % 32]
i2 = func_4_indices[(start + 1) % 32]
res[i1], res[i2] = res[i2], res[i1]
tmp = URem(res[i2], 0x46) + res[i1]
res[i1] = tmp
res[i2] = res[i2] + URem(tmp, 0x32)
start += 2
start %= 32
return res
With the functions defined, I replicate the part where the input is transformed.
password_bvs = []
for i in range(32):
password_bvs.append(BitVec("password_" + str(i), 8))
password = password_bvs
password = func1(password)
password = func4(password, 1)
password = func2(password)
password = func1(password)
password = func4(password, 13)
password = func2(password)
And set up the z3 Solver
with constraints such that the transformed password
matches the values in imp_result
extracted earlier.
solver = Solver()
imp_result = [0x76, 0xcf, 0x96, 0x48, 0x70, 0x61, 0x04, 0x8c, 0x1f, 0xc5, 0x25, 0xf3, 0xdc, 0xa0, 0x3c, 0xc8, 0x92, 0x8a, 0x2d, 0xeb, 0x24, 0x65, 0x8c, 0xf5, 0xd6, 0xb8, 0x11, 0x04, 0x6d, 0x90, 0x08, 0x60]
for i in range(32):
solver.add(imp_result[i] == password[i])
Finally, ask the solver
to solve for the password, and get flag.
print(solver.check())
model = solver.model()
flag = []
for i in range(32):
d = model[password_bvs[i]].as_long()
flag.append(d)
print("".join("{:c}".format(s) for s in flag))
govtech-csg{h0pp1ng_btwn_w0rlds}