Two easy rated challenges from the HackTheBox CTF event. In this article we look into the Step-by-step breakdown of how to approach and solve these challenges.
The original binaries are saved here.
Endlesscycle

After de-compilation and renaming I end-up with this code:
int main(int argc,char **argv) {
int iVar1;
char *pcVar2;
int i;
int i2;
pcVar2 = (char *)mmap((void *)0,158,7,33,-1,0);
/* 3486694491 */
srand(UINT_001042b8);
for (_i = 0; _i < 158; _i = _i + 1) {
for (_i2 = 0; _i2 < (ulong)(long)INT_ARRAY_00104040[_i]; _i2 = _i2 + 1) {
rand();
}
iVar1 = rand();
pcVar2[_i] = (char)iVar1;
}
/* construct function using rand */
iVar1 = (*(code *)pcVar2)();
if (iVar1 == 1) {
puts("You catch a brief glimpse of the Dragon\'s Heart - the truth has been revealed to you");
}
else {
puts("The mysteries of the universe remain closed to you...");
}
return 0;
}
This binary is initialized with srand and creates a function based on the rand function and it should be possible to recreate this function by setting the srand (for example in python), but I choose the easiest way by examining it in GDB.
=> 0x7ffff7fbf000: push rbp // init
0x7ffff7fbf001: mov rbp,rsp
0x7ffff7fbf004: push 0x101213e
0x7ffff7fbf009: xor DWORD PTR [rsp],0x1010101
// 0x101213e ^ 0x101213e
0x7ffff7fbf010: movabs rax,0x67616c6620656874
0x7ffff7fbf01a: push rax
0x7ffff7fbf01b: movabs rax,0x2073692074616857
0x7ffff7fbf025: push rax
0x7ffff7fbf026: push 0x1
0x7ffff7fbf028: pop rax
0x7ffff7fbf029: push 0x1
0x7ffff7fbf02b: pop rdi
0x7ffff7fbf02c: push 0x12
0x7ffff7fbf02e: pop rdx
0x7ffff7fbf02f: mov rsi,rsp
0x7ffff7fbf032: syscall // write 18 bytes from stack f01b, f010
// What is the flag?
0x7ffff7fbf034: sub rsp,0x100 // 256 bytes reserved for buffer
0x7ffff7fbf03b: mov r12,rsp
0x7ffff7fbf03e: xor eax,eax
0x7ffff7fbf040: xor edi,edi
0x7ffff7fbf042: xor edx,edx // cleaning
0x7ffff7fbf044: mov dh,0x1
0x7ffff7fbf046: mov rsi,r12
0x7ffff7fbf049: syscall // read into r12
0x7ffff7fbf04b: test rax,rax // if read is not empty
0x7ffff7fbf04e: jle 0x7ffff7fbf082
// for loop sliding 4 bytes with xor
0x7ffff7fbf050: push 0x1a // 26
0x7ffff7fbf052: pop rax
0x7ffff7fbf053: mov rcx,r12 // rcx = r12
0x7ffff7fbf056: add rax,rcx // rax += rcx + 26
0x7ffff7fbf059: xor DWORD PTR [rcx],0xbeefcafe
0x7ffff7fbf05f: add rcx,0x4 // rcx += 4
0x7ffff7fbf063: cmp rcx,rax // if rcx < rax
0x7ffff7fbf066: jb 0x7ffff7fbf059
0x7ffff7fbf068: mov rdi,r12
0x7ffff7fbf06b: lea rsi,[rip+0x12] # 0x7ffff7fbf084
0x7ffff7fbf072: mov rcx,0x1a
0x7ffff7fbf079: cld
// comparing rdi (r12, input) with rsi (0x1a bytes = 26 bytes)
0x7ffff7fbf07a: repz cmps BYTE PTR ds:[rsi],BYTE PTR es:[rdi]
0x7ffff7fbf07c: sete al
0x7ffff7fbf07f: movzx eax,al
0x7ffff7fbf082: leave
0x7ffff7fbf083: ret
You can notice on the end there is a comparison RDI with RSI, where RDI is input. I dumped the memory:
// 0x7ffff7fbf084: memory dump
0x7ffff7fbf084: 0xb6 0x9e 0xad 0xc5 0x92 0xfa 0xdf 0xd5
0x7ffff7fbf08c: 0xa1 0xa8 0xdc 0xc7 0xce 0xa4 0x8b 0xe1
0x7ffff7fbf094: 0x8a 0xa2 0xdc 0xe1 0x89 0xfa 0x9d 0xd2
0x7ffff7fbf09c: 0x9a 0xb7
When reconstructing the behavior in C it should look something like this:
int main() {
char buffer[256];
char message[] = "What is flag?";
char expected[] = "..."; // 0x7ffff7fbf084
ssize_t bytes_read;
// Print message
write(STDOUT_FILENO, message, sizeof(message) - 1);
// Read user input
bytes_read = read(STDIN_FILENO, buffer, 256);
if (bytes_read <= 0) {
return 0;
}
// XOR first few bytes with 0xBEEFCAFE
for (int i = 0; i < bytes_read - 4; i += 4) {
*(int *)(buffer + i) ^= 0xBEEFCAFE;
}
// Compare with expected string
if (memcmp(buffer, expected, 26) == 0) {
puts("You got the flag!");
return 1;
}
puts("Try again...");
return 0;
}
So now we can get the hands on python and reverse it:
import struct
# copied from memory dump
encrypted_data = [
0xb6, 0x9e, 0xad, 0xc5, 0x92, 0xfa, 0xdf, 0xd5,
0xa1, 0xa8, 0xdc, 0xc7, 0xce, 0xa4, 0x8b, 0xe1,
0x8a, 0xa2, 0xdc, 0xe1, 0x89, 0xfa, 0x9d, 0xd2,
0x9a, 0xb7
]
# the key that is used
xor_key = 0xBEEFCAFE
original_bytes = bytearray()
for i in range(0, len(encrypted_data), 4):
# XOR is applied to 4-byte chunks
chunk = encrypted_data[i:i+4]
while len(chunk) < 4:
chunk.append(0)
# unpack it into int and back
# (reverse of XOR is another XOR with same value)
encrypted_int = struct.unpack("<I", bytes(chunk))[0]
decrypted_int = encrypted_int ^ xor_key
original_bytes.extend(struct.pack("<I", decrypted_int))
original_input = original_bytes.decode(errors="ignore")
print("Original input:", original_input)
Next on the list is:
Impossimaze

When trying to make sense in the binary I’ve ended up with this code:
int main(int argc,char **argv) {
int iVar1;
uint uVar2;
uint max_y_2;
uint max_x_2;
ulong max_x;
char char_to_write;
int x;
int y;
int *char_index;
undefined4 in_register_0000003c;
long in_FS_OFFSET;
int half_x;
int max_y;
char position_x_y [24];
long canary;
canary = *(long *)(in_FS_OFFSET + 40);
/* newterm(getenv("TERM"), stdout, stdin); */
initscr(CONCAT44(in_register_0000003c,argc));
cbreak();
noecho();
curs_set(0);
keypad(window,1);
max_y = getmaxy(window);
max_x = getmaxx(window);
/* ensures half */
half_x = (int)(((uint)(max_x >> 31) & 1) + (int)max_x) >> 1;
max_y = max_y / 2;
x = 0;
do {
max_y_2 = getmaxy(window);
max_x_2 = getmaxx(window);
if (x == 260) {
half_x = half_x - (uint)(1 < half_x);
}
else if (x < 261) {
if (x == 258) {
max_y = max_y + 1;
}
else if (x == 259) {
max_y = max_y - (uint)(1 < max_y);
}
}
else {
half_x = half_x + (uint)(x == 261);
}
werase(window);
wattr_on(window,1048576,0);
wborder(window,0,0,0,0,0,0,0,0);
if (2 < (int)max_x_2) {
x = 1;
do {
y = 1;
if (2 < (int)max_y_2) {
do {
uVar2 = get_char(x,y);
if ((int)uVar2 < 61) {
char_to_write = 'A';
if ((int)uVar2 < 31) {
char_to_write = (-(uVar2 < 31) & 133U) + 86;
}
}
else {
char_to_write = (-(uVar2 - 61 < 120) & 202U) + 86;
}
iVar1 = wmove(window,y,x);
if (iVar1 != -1) {
waddch(window,(int)char_to_write);
}
y = y + 1;
} while (y != max_y_2 - 1);
}
x = x + 1;
} while (max_x_2 - 1 != x);
}
wattr_off(window,1048576,0);
wattr_on(window,2097152,0);
x = wmove(window,max_y,half_x);
if (x != -1) {
waddch(window,88);
}
wattr_off(window,2097152,0);
snprintf(position_x_y,16,"%d:%d",(ulong)max_y_2,(ulong)max_x_2);
x = wmove(window,0,0);
if (x != -1) {
waddnstr(window,position_x_y,4294967295);
}
if ((max_y_2 == 13) && (max_x_2 == 37)) {
wattr_on(window,524288,0);
wattr_on(window,2097152,0);
char_index = &INT_001040c0;
x = 6;
do {
y = x + 1;
x = wmove(window,6,x);
if (x != -1) {
/* add a character (with attributes) to a curses window and advance cursor */
waddch(window,(&DAT_00104120)[*char_index]);
}
char_index = char_index + 1;
x = y;
} while (y != 30);
wattr_off(window,2097152,0);
wattr_off(window,524288,0);
}
x = wgetch(window);
} while (x != 113);
endwin();
if (canary != *(long *)(in_FS_OFFSET + 40)) {
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}
return 0;
}
This binary is keeping track of window size, position x-y and moving cursor using wmove and waddch.
My approach was to search for printing any method, that may print out our flag. There were a few functions that caught my eye:
uVar2 = get_char(x,y);
if ((int)uVar2 < 61) {
char_to_write = 'A';
if ((int)uVar2 < 31) {
char_to_write = (-(uVar2 < 31) & 133U) + 86;
}
}
else {
char_to_write = (-(uVar2 - 61 < 120) & 202U) + 86;
}
waddch(window,(int)char_to_write);
// Write formatted output to sized buffer
snprintf(position_x_y,16,"%d:%d",(ulong)max_y_2,(ulong)max_x_2);
waddnstr(window,position_x_y,4294967295);
char_index = &INT_001040c0;
waddch(window,(&DAT_00104120)[*char_index]);
waddch is used add a character to a curses window and advance cursor.
So we got few options, we will try to eliminate it one by one by thinking what it does, the first is kind of complex shifting of the char depending on get_char function:
int get_char(int x,int y) {
return *(int *)(&DAT_00102020 +
(long)((x + *(int *)(&DAT_00102020 + (long)((y + 1337) % 256) * 4)) % 256) * 4);
}
But I stepped out and was thinking about the “char_to_write = ‘A’;”, it seemed weird to me, so I moved on.
Next we just printing position x-y, that’s clearly not what we are searching for.
Moving on we end-up here:
char_index = &INT_001040c0;
waddch(window,(&DAT_00104120)[*char_index]);
I noticed that the char_index variable is actually an array of ints and if some conditions are met then the char is printed.
So I tried to extract some of the non-null ints from the memory and added it to the DAT_00104120 and what we got here: “HTB{“, beginning of the flag and I knew that I was on the right path.
00104205 H
0010413c T
001041d8 B
001041d2 {
00104160 T
00104188 H
001041f2 3
001041aa _
00104170 c
001041a6 u
0010421c r
00104193 s
001041f2 3
001041aa _
0010420d i
00104193 s
001041aa _
00104165 b
0010421c r
00104176 o
001041eb k
001041f2 3
0010420f n
001041e0 }
# HTB{th3_curs3_is_brok3n}
Final words
Thanks for reading, if someone is interested in collaboration write me on discord (bitr13x) or email me (sw33tbit@protonmail.com).