158 lines
4.4 KiB
Markdown
158 lines
4.4 KiB
Markdown
---
|
|
title: "CTF20K 2025: MPC"
|
|
excerpt: "Reverse-engineering a Multi-Password Checker to extract sensitive information."
|
|
tags: [ctf, rev]
|
|
---
|
|
|
|
|
|
## Recon
|
|
|
|
The challenge starts with a binary file, that has the following behavior:
|
|
|
|
```
|
|
$ ./password_checker
|
|
Enter a password: test
|
|
Password is invalid.
|
|
```
|
|
|
|
We will have to guess the password here. There is no source code available for that program, therefore we have to disassemble it using a tool like Ghidra. The decompiled main function looks like this:
|
|
|
|
```c
|
|
undefined8 main(void)
|
|
|
|
{
|
|
int iVar1;
|
|
long in_FS_OFFSET;
|
|
undefined1 local_78 [104];
|
|
long local_10;
|
|
|
|
local_10 = *(long *)(in_FS_OFFSET + 0x28);
|
|
printf("Enter a password: ");
|
|
__isoc99_scanf(&DAT_004b603b,local_78);
|
|
iVar1 = is_valid_password(local_78);
|
|
if (iVar1 == 0) {
|
|
puts("Password is invalid.");
|
|
}
|
|
else {
|
|
puts("Password is valid.");
|
|
}
|
|
if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) {
|
|
/* WARNING: Subroutine does not return */
|
|
__stack_chk_fail();
|
|
}
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
The program asks for user input, passes it through the `is_valid_password` function and outputs text accordingly. Let's analyze this function:
|
|
|
|
```c
|
|
undefined8 is_valid_password(char *param_1)
|
|
|
|
{
|
|
char cVar1;
|
|
size_t sVar2;
|
|
undefined8 uVar3;
|
|
int local_10;
|
|
|
|
sVar2 = strlen(param_1);
|
|
if ((int)sVar2 == 0x17) {
|
|
for (local_10 = 0; local_10 < 0x17; local_10 = local_10 + 1) {
|
|
cVar1 = transform_char((int)param_1[local_10],local_10);
|
|
if (cVar1 != valid_password_encrypted[local_10]) {
|
|
return 0;
|
|
}
|
|
}
|
|
cVar1 = transform_char((int)param_1[2],2);
|
|
if (cVar1 == '\x1f') {
|
|
cVar1 = transform_char((int)param_1[5],5);
|
|
if (cVar1 == '\x1f') {
|
|
cVar1 = transform_char((int)param_1[8],8);
|
|
if (cVar1 == '\v') {
|
|
uVar3 = 1;
|
|
}
|
|
else {
|
|
uVar3 = 0;
|
|
}
|
|
}
|
|
else {
|
|
uVar3 = 0;
|
|
}
|
|
}
|
|
else {
|
|
uVar3 = 0;
|
|
}
|
|
}
|
|
else {
|
|
uVar3 = 0;
|
|
}
|
|
return uVar3;
|
|
}
|
|
```
|
|
|
|
The function loops through all characters in the given string (user input), and passes each of them into the `transform_char` function. It then checks each char against the `valid_password_encrypted` string at the same index. That means, if we get the encrypted version of the password, and we pass it through an inverse version of `transform_char`, we could get the right password..
|
|
|
|
Decompiling `transform_char` gives us this:
|
|
|
|
```c
|
|
uint transform_char(byte param_1,int param_2)
|
|
|
|
{
|
|
return param_2 + (param_1 ^ 0x33) ^ 0x55;
|
|
}
|
|
```
|
|
|
|
## Exploitation
|
|
|
|
The function is pretty straightforward, as it consists of simple bitwise XOR operations, and one addition. Keeping in mind that both addition and bitwise-XOR have the same priority in computation, and considering the classical left-to-right calculation order, and knowing that XORing two times against the same number gives back the original value, inversing it gives:
|
|
|
|
```c
|
|
param_1 ^ 0x55 - param_2 ^ 0x33
|
|
```
|
|
|
|
Now we have to find the encrypted password. Using the `nm` tool, we can find the memory address of a label in a program:
|
|
|
|
```
|
|
$ nm -C ./password_checker | grep valid_password_encrypted
|
|
00000000004b6010 R valid_password_encrypted
|
|
```
|
|
|
|
Having that specific address in mind, we can boot up our favorite debugger and explore that area:
|
|
|
|
```
|
|
(gdb) x/32bx 0x00000000004b6010
|
|
0x4b6010 <valid_password_encrypted>: 0x34 0x2a 0x1f 0x31 0x0f 0x1f 0x27 0x30
|
|
0x4b6018 <valid_password_encrypted+8>: 0x0b 0x20 0x33 0x19 0x2d 0x34 0x31 0x03
|
|
0x4b6020 <valid_password_encrypted+16>: 0x29 0x27 0x3d 0x0d 0x39 0x09 0x31 0x00
|
|
0x4b6028: 0x45 0x6e 0x74 0x65 0x72 0x20 0x61 0x20
|
|
```
|
|
|
|
The password is a null-terminated string, and by looking at this output we know that it is 23 bytes long. Our final ciphertext is this:
|
|
|
|
```
|
|
34 2a 1f 31 0f 1f 27 30 0b 20 33 19 2d 34 31 03 29 27 3d 0d 39 09 31
|
|
```
|
|
|
|
Now, let's apply the inverse XOR-based transformation to every byte, concatenate the output, and print it as a string, using a simple Python script:
|
|
|
|
```python
|
|
encrypted = [
|
|
0x34, 0x2a, 0x1f, 0x31, 0x0f, 0x1f, 0x27, 0x30,
|
|
0x0b, 0x20, 0x33, 0x19, 0x2d, 0x34, 0x31, 0x03,
|
|
0x29, 0x27, 0x3d, 0x0d, 0x39, 0x09, 0x31
|
|
]
|
|
|
|
def inverse_transform_char(t, i):
|
|
return chr(((t ^ 0x55) - i) ^ 0x33)
|
|
|
|
password = ''.join(inverse_transform_char(t, i) for i, t in enumerate(encrypted))
|
|
print(password)
|
|
```
|
|
|
|
There we go!
|
|
|
|
```
|
|
$ python exploit.py
|
|
RM{Rev_me_or_get_Revkt}
|
|
```
|