Encryption Bot
7 minutos de lectura
Se nos proporciona un binario llamado chall
y la salida flag.enc
:
$ file chall
chall: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=581371f680358611dc4e8d77b03858bd4c780174, for GNU/Linux 3.2.0, stripped
$ cat flag.enc
9W8TLp4k7t0vJW7n3VvMCpWq9WzT3C8pZ9Wz
$ xxd flag.enc
00000000: 3957 3854 4c70 346b 3774 3076 4a57 376e 9W8TLp4k7t0vJW7n
00000010: 3356 764d 4370 5771 3957 7a54 3343 3870 3VvMCpWq9WzT3C8p
00000020: 5a39 577a Z9Wz
Ingeniería inversa
Si lo abrimos en Ghidra, veremos esta función main
(renombrada desde FUN_0010163e
, ya que el binario no tiene símbolos, stripped):
int main() {
FILE *fp;
char text[32];
banner();
printf("\n\nEnter the text to encrypt : ");
__isoc99_scanf("%s", text);
fp = fopen("data.dat", "r");
if (fp != NULL) {
system("rm data.dat");
}
putchar(L'\n');
test_length(text);
dummy();
weird(text);
enc();
system("rm data.dat");
putchar(L'\n');
return 0;
}
Básicamente, pide que el texto para cifrar. Luego, abre un archivo llamado data.dat
. Si el archivo ya existe (el proceso de abrir no falla), el programa lo elimina.
Después de eso, la variable text
se pasa a test_length
(renombrada desde FUN_001015dc
):
void test_length(char *string) {
size_t length;
length = strlen(string);
if ((int) length != 27) {
puts("I\'m encrypt only specific length of character.");
puts("(-_-) Find it (-_-)");
/* WARNING: Subroutine does not return */
exit(1);
}
}
Como dice el nombre de la función, verifica que la longitud del texto de entrada sea de 27 caracteres.
Pasemos a una función que renombré como weird
desde FUN_0010131d
:
int weird(char *string) {
size_t length;
ulong ii;
undefined local_868[2000];
int string_int[31];
int i;
i = 0;
while (true) {
ii = (ulong) i;
length = strlen(string);
if (length <= ii) break;
string_int[i] = (int) string[i];
encode(string_int[i], local_868);
i = i + 1;
}
dummy();
return 0;
}
Obsérvese que hay algunas funciones que renombré como dummy
porque estaban vacías:
void dummy() {
return;
}
Centrándonos en weird
, la función solo toma los caracteres de string
(que era el texto de entrada) y los transforma en valores enteros. Luego, los valores enteros se pasan a encode
(renombrada desde FUN_001011d9
) uno por uno:
int encode(int plaintext_byte) {
int pt;
uint bits[20];
FILE *fp;
int j;
int i;
fp = fopen("data.dat", "a");
pt = plaintext_byte;
for (i = 0; i < 8; i++) {
bits[i] = pt % 2;
pt /= 2;
}
for (j = 7; j > -1; j--) {
fprintf(fp, "%d", (ulong) bits[j]);
}
fclose(fp);
return 0;
}
Aquí, la función toma el carácter como entero y obtiene su representación binaria (primer bucle for
). Luego, los bits se escriben en el archivo data.dat
.
Solo para verificarlo, escribamos la función anterior en Python:
$ python3 -q
>>> pt = ord('H')
>>> pt
72
>>> f'{pt:08b}'
'01001000'
>>>
>>> bits = []
>>>
>>> for _ in range(8):
... bits.append(pt % 2)
... pt //= 2
...
>>> bits
[0, 0, 0, 1, 0, 0, 1, 0]
>>>
>>> for i in range(7, -1, -1):
... print("%d" % bits[i], end='')
...
01001000
Perfecto, entonces encode
solo imprime la representación binaria de un carácter en data.dat
.
Después de llamar a weird
, hay una función renombrada a enc
(desde FUN_001014ba
):
void enc() {
int c;
int bits[400];
int n;
char cc;
FILE *fp;
int j;
int res;
int ii;
int i;
fp = fopen("data.dat", "r+");
dummy();
for (i = 1; i < 217; i++) {
c = fgetc(fp);
cc = (char) c;
if (cc == '0') {
bits[i - 1] = 0;
} else if (cc == '1') {
bits[i - 1] = 1;
}
if (i != 0) {
if (i % 6 == 0) {
res = 0;
ii = i;
for (j = 0; ii = ii + -1, j < 6; j = j + 1) {
n = two_pow(j);
res = res + bits[ii] * n;
}
print_enc(res);
}
}
}
fclose(fp);
}
Esta función utiliza el archivo data.dat
, que contiene el texto de entrada en representación binaria. Está leyendo el archivo carácter a carácter, y establece un vector llamado bits
a 0
o 1
. Cuando el índice del carácter (bit) es un múltiplo de 6
, la función realiza más operaciones:
if (i % 6 == 0) {
res = 0;
ii = i;
for (j = 0; ii = ii + -1, j < 6; j = j + 1) {
n = two_pow(j);
res = res + bits[ii] * n;
}
print_enc(res);
}
Hay una función llamada two_pow
(FUN_001013ab
) ya que two_pow(n)
devuelve $2^n$:
int two_pow(int n) {
int i;
int ret;
ret = 1;
for (i = n; i != 0; i--) {
ret *= 2;
}
dummy();
return ret;
}
Solamente porque:
$$ 2^n = \prod_{i=1}^n 2 $$
Entonces, el bucle for
de la función enc
en realidad transforma los bits en un número decimal, que se guarda en res
. Ese bucle for
podría expresarse en términos matemáticos como:
$$ \mathrm{res} = \sum_{j=0}^{6-1} \mathrm{bits}[j] \cdot 2^j $$
Finalmente, print_enc
(FUN_001013e9
):
undefined8 print_enc(int res) {
long i;
undefined8 *puVar1;
undefined8 local_198;
undefined8 local_190;
undefined8 local_188;
undefined8 local_180;
undefined8 local_178;
undefined8 local_170;
undefined8 local_168;
undefined8 local_160;
undefined8 local_158[42];
local_198 = 0x5958575655545352;
local_190 = 0x363534333231305a;
local_188 = 0x4544434241393837;
local_180 = 0x4d4c4b4a49484746;
local_178 = 0x6463626151504f4e;
local_170 = 0x6c6b6a6968676665;
local_168 = 0x74737271706f6e6d;
local_160 = 0x7a7978777675;
puVar1 = local_158;
for (i = 42; i != 0; i--) {
*puVar1 = 0;
puVar1 = puVar1 + 1;
}
putchar((int) *(char *) ((long) &local_198 + (long) res));
return 0;
}
En esta función tenemos una cadena de texto en formato hexadecimal. Podemos imprimirlo como bytes usando pwntools
(en formato little-endian):
$ python3 -q
>>> from pwn import p64
>>> string = b''.join(map(p64, [0x5958575655545352, 0x363534333231305a, 0x4544434241393837, 0x4d4c4b4a49484746, 0x6463626151504f4e, 0x6c6b6a6968676665, 0x74737271706f6e6d, 0x7a7978777675]))
>>> string
b'RSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQabcdefghijklmnopqrstuvwxyz\x00\x00'
La función print_enc
toma el valor en res
y lo usa como un offset para imprimir un solo carácter de la cadena anterior.
Análisis estático
Vamos a echar un vistazo a la flag cifrada:
9W8TLp4k7t0vJW7n3VvMCpWq9WzT3C8pZ9Wz
El carácter 9
aparece en el índice 18
:
>>> string.index(b'9')
18
Y este número se obtuvo de 6 bits:
>>> f'{18:06b}'
'010010'
Luego, W
está en la posición 5
:
>>> string.index(b'W')
5
>>> f'{5:06b}'
'000101'
Hagamos otro más:
>>> string.index(b'8')
17
>>> f'{17:06b}'
'010001'
Si nos unimos a las salidas binarias, reconstruiremos data.dat
:
010010000101010001
Separando los bits en trozos de 8 bits (1 byte), obtendremos parte de la flag:
Binary: 01001000 01010100 01
Hex: 48 54
Byte: H T
Análisis dinámico
En este punto, podemos intentar ejecutar el binario con una flag de prueba (con 27 caracteres):
$ ./chall
####### ######
# # # #### ##### # # ##### ##### # #### # # # # #### #####
# ## # # # # # # # # # # # # # ## # # # # # #
##### # # # # # # # # # # # # # # # # ###### # # #
# # # # # ##### # ##### # # # # # # # # # # # #
# # ## # # # # # # # # # # # ## # # # # #
####### # # #### # # # # # # #### # # ###### #### #
Enter the text to encrypt : HTB{AAAAAAAAAAAAAAAAAAAAAA}
9W8TLqWS7BWS7BWS7BWS7BWS7BWS7BWS7BWz
En comparación con flag.enc
se ve muy similar:
Test: 9W8TLqWS7BWS7BWS7BWS7BWS7BWS7BWS7BWz
flag.enc: 9W8TLp4k7t0vJW7n3VvMCpWq9WzT3C8pZ9Wz
Aquí, descubrí que el binario solo está codificando los bits en una especie de Base64. Es base 64 porque se necesitan 6 bits ($2^6 = 64$ combinaciones). Pero no es la codificación Base64 habitual ya que el alfabeto es diferente.
Flag
En este punto, tomé el proceso mostrado antes y encontré la flag en representación binaria. Después de eso, lo único que hay que hacer es ponerlo en bytes:
$ python3 -q
>>> from pwn import p64
>>>
>>> string = b''.join(map(p64, [0x5958575655545352, 0x363534333231305a, 0x4544434241393837, 0x4d4c4b4a49484746, 0x6463626151504f4e, 0x6c6b6a6968676665, 0x74737271706f6e6d, 0x7a7978777675]))
>>>
>>> with open('flag.enc', 'rb') as f:
... enc = f.read()
...
>>> enc
b'9W8TLp4k7t0vJW7n3VvMCpWq9WzT3C8pZ9Wz'
>>>
>>> bits = ''
>>> for c in enc:
... index = string.index(bytes([c]))
... bits += f'{index:06b}'
...
>>> bits
'010010000101010001000010011110110011001101101110010000110111001001111001011100000101010000110001001100000100111001011111010101110011000101110100010010000101111101000010001100010101010001110011001000010010000101111101'
>>> hex(int(bits, 2))
'0x4854427b336e437279705431304e5f573174485f4231547321217d'
>>> bytes.fromhex(hex(int(bits, 2))[2:])
b'HTB{3nCrypT10N_W1tH_B1Ts!!}'