Encryption Bot
7 minutes to read
We have a binary called chall
and the output 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
Reverse engineering
If we open it in Ghidra, we will see this main
function (renamed from FUN_0010163e
, since the binary is 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;
}
Basically, it asks for the text to encrypt. Then, it opens a file called data.dat
. If the file already exists (the open process does not fail), the program removes it.
After that, the text
variable is passed to test_length
(renamed from 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);
}
}
As the name of the function says, it verifies that the length of the input text is 27 characters long.
Let’s move on to a function I renamed as weird
from 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;
}
Notice that there are some functions that I renamed as dummy
because they were blank:
void dummy() {
return;
}
Focusing on weird
, the function only takes the characters of string
(which was the input text) and transforms them to integer values. Then, the integer values are passed to encode
(renamed from FUN_001011d9
) one by one:
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;
}
Here, the function takes the character as integer and obtains its binary representation (first for
loop). Then, the bits are written into the file data.dat
.
Just to verify it, let’s rewrite the above function in 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
Perfect, so encode
only prints the binary representation of a character into data.dat
.
After calling weird
, there is a function renamed to enc
(from 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);
}
This function uses the file data.dat
, which contains the input text in binary representation. It is reading the file character by character, and setting an array called bits
to either 0
or 1
. When the index of the character (bit) is a multiple of 6
, the function performs more operations:
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);
}
There is a function called two_pow
(FUN_001013ab
) because two_pow(n)
returns $2^n$:
int two_pow(int n) {
int i;
int ret;
ret = 1;
for (i = n; i != 0; i--) {
ret *= 2;
}
dummy();
return ret;
}
Just because:
$$ 2^n = \prod_{i=1}^n 2 $$
So, the previous for
loop in function enc
actually transforms bits into a decimal number, which is saved in res
. Such for
loop could be expressed in mathematical terms as:
$$ \mathrm{res} = \sum_{j=0}^{6-1} \mathrm{bits}[j] \cdot 2^j $$
Finally, there is 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;
}
In this function we have a string in hexadecimal format. We can print it as bytes using pwntools
(in little-endian format):
$ python3 -q
>>> from pwn import p64
>>> string = b''.join(map(p64, [0x5958575655545352, 0x363534333231305a, 0x4544434241393837, 0x4d4c4b4a49484746, 0x6463626151504f4e, 0x6c6b6a6968676665, 0x74737271706f6e6d, 0x7a7978777675]))
>>> string
b'RSTUVWXYZ0123456789ABCDEFGHIJKLMNOPQabcdefghijklmnopqrstuvwxyz\x00\x00'
The print_enc
function takes the value at res
and uses it as an offset to print a single character of the above string.
Static analysis
Let’s take a look again at the encrypted flag:
9W8TLp4k7t0vJW7n3VvMCpWq9WzT3C8pZ9Wz
Character 9
is at index 18
:
>>> string.index(b'9')
18
And this number was obtained from 6 bits:
>>> f'{18:06b}'
'010010'
Then, W
is at index 5
:
>>> string.index(b'W')
5
>>> f'{5:06b}'
'000101'
Let’s do another one:
>>> string.index(b'8')
17
>>> f'{17:06b}'
'010001'
If we join the binary outputs, we will be reconstructing data.dat
:
010010000101010001
Separating the bits in chunks of 8 bits (1 byte), we will be getting part of the flag:
Binary: 01001000 01010100 01
Hex: 48 54
Byte: H T
Dynamic analysis
At this point, we can try to run the binary with a test flag (with 27 characters):
$ ./chall
####### ######
# # # #### ##### # # ##### ##### # #### # # # # #### #####
# ## # # # # # # # # # # # # # ## # # # # # #
##### # # # # # # # # # # # # # # # # ###### # # #
# # # # # ##### # ##### # # # # # # # # # # # #
# # ## # # # # # # # # # # # ## # # # # #
####### # # #### # # # # # # #### # # ###### #### #
Enter the text to encrypt : HTB{AAAAAAAAAAAAAAAAAAAAAA}
9W8TLqWS7BWS7BWS7BWS7BWS7BWS7BWS7BWz
Compared to flag.enc
it looks very similar:
Test: 9W8TLqWS7BWS7BWS7BWS7BWS7BWS7BWS7BWz
flag.enc: 9W8TLp4k7t0vJW7n3VvMCpWq9WzT3C8pZ9Wz
Here, I found out that the binary is just encoding the bits in a kind of Base64. It is base 64 because it takes 6 bits ($2^6 = 64$ combinations). But it is not the usual Base64 encoding since the alphabet is a bit different.
Flag
At this point, I took the process shown before and found the flag in binary representation. After that, the only thing to do is print it as 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!!}'