BinCrypt Breaker
6 minutos de lectura
Se nos proporciona un binario llamado checker y otro archivo llamado 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
Descompilación
Si abrimos el binario checker en IDA, vemos la siguiente función main:
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;
}
}
Simplemente llama a decrypt, que devuelve un descriptor de archivo, y luego ejecuta el archivo con fexecve. Esta es la función 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;
}
Como se puede ver, decrypt abre el archivo file.bin, aplica XOR con 0xab a cada byte y guarda la salida en otro archivo, cuyo descriptor es devuelto y luego ejecutado en main. Por lo tanto, podemos descifrar este file.bin y obtener dicho archivo oculto:
$ 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
Binario oculto
Como se puede ver, es otro binario ELF, así que vamos a descompilarlo con IDA. Esta es la función 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;
}
Esto es solo un checker. Se nos solicita introducir la flag (sin HTB{}) y la función sub_14A1 verificará si es correcta o no:
_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;
}
Después de renombrar las variables, parece bastante claro lo que hace la función. Primero, verifica si la longitud de la flag de entrada es de 28 bytes. Luego, aplica la función sub_127D a la flag, que simplemente realiza cuatro transposiciones de caracteres:
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);
}
Nétese que la inversa de una transposición es la misma transposición. Por ejemplo, si intercambiamos el índice 0 con el índice 12 y luego volvemos a intercambiar el índice 0 con el índice 12, volvemos a la situación original. Además, obsérvese que el orden de las transposiciones no importa ya que ninguna de las transposiciones comparte índices. Como resultado, las funciones inversas de sub_11C9 y sub_127D son las mismas funciones.
Luego, se divide la flag en dos partes y se generan copias con 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;
}
Después de eso, se aplica la función sub_12E4 a cada parte:
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;
}
Esta función parece más interesante, pero no hay nada difícil. Primero intercambia índices de acuerdo con un array de índices, un total de 8 veces. Luego, aplica XOR con una clave dada (2 para la primera parte y 3 para la segunda parte) en índices específicos.
Normalmente, al invertir un algoritmo, necesitamos hacer los mismos pasos, pero en orden inverso. Sin embargo, las dos operaciones son independientes, por lo que podemos dejarlas en el mismo orden. Además, el orden de los bucles for está bien, no es necesario invertirlos ya que solo iteran sobre los arrays dados y todas las operaciones son independientes. Lo único que necesita invertirse es tmp[i] = str[swap_indices[i]] a tmp[swap_indices[i]] = str[i].
Solución
Entonces, considerando todo, podemos escribir un script en C para invertir el cifrado, reutilizando las funciones sub_11C9 y sub_127D y escribiendo la función inversa rev_sub_12E4, que es ligeramente diferente a 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
Si compilamos y ejecutamos el programa de solución, obtendremos la flag:
$ gcc solve.c
$ ./a.out
HTB{cRyPto_r3V_15_aLways_aWeS0m3}