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
int two_pow(int n) {
int i;
int ret;
ret = 1;
for (i = n; i != 0; i--) {
ret *= 2;
}
dummy();
return ret;
}
Just because:
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:
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 (
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!!}'