BinCrypt Breaker
6 minutes to read
We are given a binary called checker and another file called file.bin:
$ file checker file.bin
checker: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9fefdd7084413189f57c43226551db9ecc3ad994, for GNU/Linux 4.4.0, not stripped
file.bin: data
Decompilation
If we open the checker binary in IDA, we see the following main function:
int __fastcall main(int argc, const char** argv, const char** envp) {
int fd; // [rsp+0h] [rbp-1030h]
int v5; // [rsp+4h] [rbp-102Ch]
char* envpa; // [rsp+8h] [rbp-1028h] BYREF
char* argva[2]; // [rsp+10h] [rbp-1020h] BYREF
char s[4104]; // [rsp+20h] [rbp-1010h] BYREF
unsigned __int64 canary; // [rsp+1028h] [rbp-8h]
canary = __readfsqword(0x28u);
fd = decrypt();
if (snprintf(s, 0x1000u, "/proc/self/fd/%d", fd) >= 0) {
v5 = open(s, 0);
close(fd);
argva[0] = "<anonymous>";
argva[1] = 0;
envpa = 0;
fexecve(v5, argva, &envpa);
return 0;
} else {
perror("formatting");
return 1;
}
}
It simply calls decrypt, which returns a file descriptor, and then executes the file with fexecve. This is decrypt:
int __fastcall decrypt() {
int _c; // eax
int c; // [rsp+8h] [rbp-18h] BYREF
int fd; // [rsp+Ch] [rbp-14h]
FILE* stream; // [rsp+10h] [rbp-10h]
unsigned __int64 canary; // [rsp+18h] [rbp-8h]
canary = __readfsqword(0x28u);
stream = fopen("file.bin", "rb");
if (!stream) {
perror("Library not found.");
exit(1);
}
if ((fd = open(".", 4784641, 493)) < 0) {
perror("File create issue.");
exit(1);
}
while (1) {
if ((c = fgetc(stream)) == -1) {
break;
}
_c = c;
LOBYTE(_c) = c ^ 0xAB;
c = _c;
write(fd, &c, 1u);
}
fclose(stream);
return fd;
}
As can be seen, decrypt opens the file file.bin, applies XOR with 0xab to every byte and saves the output in another file, whose file descriptor is returned and afterwards executed in main. Therefore, we can decrypt this file.bin and get such hidden file:
$ python3 -q
>>> from pwn import xor
>>>
>>> with open('file.bin', 'rb') as f, open('file.bin.dec', 'wb') as g:
... g.write(xor(f.read(), 0xab))
...
14496
>>> exit
$ file file.bin.dec
file.bin.dec: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=b3951d24e24832cde139b7d2f872b35c6a2c004c, for GNU/Linux 4.4.0, stripped
Hidden binary
As can be seen it is another ELF binary, so let’s decompile it with IDA. This is main:
__int64 __fastcall main(int argc, char** argv, char** envp) {
char flag[40]; // [rsp+0h] [rbp-30h] BYREF
unsigned __int64 canary; // [rsp+28h] [rbp-8h]
canary = __readfsqword(0x28u);
printf("Enter the flag (without `HTB{}`): ");
__isoc99_scanf("%28s", flag);
if ((unsigned int) sub_14A1(flag)) {
puts("Wrong flag");
} else {
puts("Correct flag");
}
return 0;
}
This is just a checker. We are requested to input the flag (without HTB{}) and the function sub_14A1 will check if it is correct or not:
_BOOL8 __fastcall sub_14A1(const char* flag) {
size_t len1; // rbx
size_t len2; // rax
_BOOL4 ret; // [rsp+14h] [rbp-2Ch]
char* str1; // [rsp+18h] [rbp-28h]
char* enc_str1; // [rsp+18h] [rbp-28h]
char* str2; // [rsp+20h] [rbp-20h]
char* enc_str2; // [rsp+20h] [rbp-20h]
char* enc_flag; // [rsp+28h] [rbp-18h]
if (strlen(flag) != 28) {
return 1;
}
sub_127D(flag);
str1 = (char*) sub_121F(flag, 0, 14);
str2 = (char*) sub_121F(flag, 14, 14);
if (strlen(str1) != 14 || strlen(str2) != 14) {
return 1;
}
enc_str1 = (char*) sub_12E4(str1, 2);
enc_str2 = (char*) sub_12E4(str2, 3);
len1 = strlen(enc_str1);
len2 = strlen(enc_str2);
enc_flag = (char*) malloc(len1 + len2 + 1);
*enc_flag = '\0';
strcat(enc_flag, enc_str1);
strcat(enc_flag, enc_str2);
ret = strcmp("RV{r15]_vcP3o]L_tazmfSTaa3s0", enc_flag) != 0;
free(enc_flag);
return ret;
}
After renaming variable names, it seems pretty clear what the function is doing. First, it checks whether the length of the input flag is 28 bytes long. Then, it applies function sub_127D on the flag, which is just four character transpositions:
void __fastcall sub_11C9(char* str, int a, int b) {
char tmp; // [rsp+1Fh] [rbp-1h]
tmp = str[a];
str[a] = str[b];
str[b] = tmp;
}
void __fastcall sub_127D(char* str) {
sub_11C9(str, 0, 12);
sub_11C9(str, 14, 26);
sub_11C9(str, 4, 8);
sub_11C9(str, 20, 23);
}
Notice that the inverse of a transposition is the same transposition. For instance, if we swap index 0 with index 12 and then swap again index 0 with index 12, we get to the original situation. Also, notice that the order of the transpositions does not matter since any of the transpositions share indices. As a result, the inverse functions of sub_11C9 and sub_127D are the same functions.
Next, it splits the flag into two parts and generates copies with sub_121F:
char* __fastcall sub_121F(char* str, int index, int size) {
char* dest; // [rsp+18h] [rbp-8h]
dest = (char*) malloc(size + 1);
strncpy(dest, &str[index], size);
dest[size] = '\0';
return dest;
}
After that, function sub_12E4 is aplied on both parts:
char *__fastcall sub_12E4(char* str, char xor_key) {
int k; // [rsp+1Ch] [rbp-84h]
int i; // [rsp+20h] [rbp-80h]
int j; // [rsp+24h] [rbp-7Ch]
char* new_str; // [rsp+28h] [rbp-78h]
_DWORD xor_indices[8]; // [rsp+30h] [rbp-70h]
_DWORD swap_indices[14]; // [rsp+50h] [rbp-50h]
char tmp[14]; // [rsp+8Ah] [rbp-16h] BYREF
unsigned __int64 canary; // [rsp+98h] [rbp-8h]
canary = __readfsqword(0x28u);
swap_indices[0] = 9;
swap_indices[1] = 12;
swap_indices[2] = 2;
swap_indices[3] = 10;
swap_indices[4] = 4;
swap_indices[5] = 1;
swap_indices[6] = 6;
swap_indices[7] = 3;
swap_indices[8] = 8;
swap_indices[9] = 5;
swap_indices[10] = 7;
swap_indices[11] = 11;
swap_indices[12] = 0;
swap_indices[13] = 13;
xor_indices[0] = 2;
xor_indices[1] = 4;
xor_indices[2] = 6;
xor_indices[3] = 8;
xor_indices[4] = 11;
xor_indices[5] = 13;
new_str = (char*) malloc(0xFu);
for (k = 1; k <= 8; ++k) {
for (i = 0; i <= 13; ++i) {
tmp[i] = str[swap_indices[i]];
}
strncpy(str, tmp, 0xEu);
}
for (j = 0; j <= 5; ++j ) {
str[xor_indices[j]] ^= xor_key;
}
strncpy(new_str, str, 0xEu);
new_str[14] = 0;
return new_str;
}
This function looks more interesting, but there is nothing difficult. First it just swaps indices according to an array of indices, a total of 8 times. Then, it applies XOR with a given key (2 for the first part and 3 for the second part) on specific indices.
Normally, when reversing an algorithm, we need to do the same steps, but in reverse order. However, the two operations are independent, so we can leave them in the same order. Also, the order of the for loops are fine, there is no need to reverse them since they just iterate the given arrays and all operations are independent. The only thing that needs to be reversed is tmp[i] = str[swap_indices[i]] to tmp[swap_indices[i]] = str[i].
Solution
So, considering everything, we can write a C script to reverse the encryption, reusing functions sub_11C9 and sub_127D and writing the reverse function rev_sub_12E4, which is slightly different to sub_12E4:
#include <stdio.h>
#include <string.h>
void sub_11C9(char* str, int a, int b) {
char tmp = str[a];
str[a] = str[b];
str[b] = tmp;
}
void sub_127D(char* str) {
sub_11C9(str, 0, 12);
sub_11C9(str, 14, 26);
sub_11C9(str, 4, 8);
sub_11C9(str, 20, 23);
}
void rev_sub_12E4(char* str, char xor_key) {
int swap_indices[] = {9, 12, 2, 10, 4, 1, 6, 3, 8, 5, 7, 11, 0, 13};
int xor_indices[] = {2, 4, 6, 8, 11, 13};
char tmp[14];
for (int k = 1; k <= 8; k++) {
for (int i = 0; i <= 13; i++) {
tmp[swap_indices[i]] = str[i];
}
memcpy(str, tmp, 14);
}
for (int j = 0; j <= 5; j++) {
str[xor_indices[j]] ^= xor_key;
}
}
int main() {
char flag[] = "RV{r15]_vcP3o]L_tazmfSTaa3s0";
rev_sub_12E4(flag, 2);
rev_sub_12E4(flag + 14, 3);
sub_127D(flag);
printf("HTB{%s}\n", flag);
}
Flag
If we compile and run the solution program, we will get the flag:
$ gcc solve.c
$ ./a.out
HTB{cRyPto_r3V_15_aLways_aWeS0m3}