2017年2月23日 星期四

[Project] The one-gadget in glibc

Introduction

One-gadgets are useful gadgets in glibc, which leads to call execve('/bin/sh', NULL, NULL). It's convenient to use it to get RCE (remote code execution) whenever we can only control PC (program counter). For example, sometimes the vulnerability only leads to an arbitrary function call without controlling the first argument, which forbids us to call system("sh"). But one-gadgets can do the magic in this situation. I used to use IDA-pro to find these gadgets every time, even I found it before. So I decided to stop doing such routine and develop an easy-to-use tool for it.

one_gadget is the product, it not only finds one-gadgets but also shows the constraints need to be satisfied.


This article records how one_gadget works.



Repository

The source code of one_gadget can be found here.
It's a ruby gem, type gem install one_gadget in command line to install it.

One Gadget

First of all, a potential gadget must satisfy:
  1. has accessed to the '/bin/sh' string.
  2. call the exec* family function.

To demonstrate clearly, consider the following assembly code, which is the result of objdump of libc-2.23:
; glibc-2.23 (64bit, 16.04 ubuntu, BuildID: 60131540dadc6796cab33388349e6e4e68692053)
   4526a: mov    rax,QWORD PTR [rip+0x37dc47] # 3c2eb8 <_IO_file_jumps@@GLIBC_2.2.5+0x7d8>
   45271: lea    rdi,[rip+0x146eff]           # 18c177 <_libc_intl_domainname@@GLIBC_2.2.5+0x197>
   45278: lea    rsi,[rsp+0x30]
   45278: mov    DWORD PTR [rip+0x380219],0x0 # 3c54a0 <__abort_msg@@GLIBC_PRIVATE+0x8c0>  
   45287: mov    DWORD PTR [rip+0x380213],0x0 # 3c54a4 <__abort_msg@@GLIBC_PRIVATE+0x8c4>
   45291: mov    rdx,QWORD PTR [rax]
   45294: call   cbbc0 <execve@@GLIBC_2.2.5>
Line 45271 is equivalent to rdi = libc_base + 0x18c177, and libc_base + 0x18c177 is exactly the string '/bin/sh'.

It's easy to find the string offset with command strings:

As the constraints of this gadget, notice line 45278 is rsi = rsp + 0x30. So we know the final result of this gadget is execve('/bin/sh', rsp+0x30, environ), which implies the constraint of this gadget is [rsp+0x30] = NULL.

Gadget 0x4526a:
    execve('/bin/sh', rsp+0x30, environ)

So the strategy of finding gadgets is simple:
  1. find assembly codes that access '/bin/sh' as one-gadget candidates.
  2. filter out candidates that not calling execve in a near line.
  3. The asm code looks like lea rsi,[rsp+0x??] is the constraint.
This simple strategy can find three one-gadgets in glibc-2.19 and glibc-2.23, listed as follows:
; glibc-2.19(64bit, 14.04 ubuntu, BuildID: cf699a15caae64f50311fc4655b86dc39a479789)
0x4647c execve('/bin/sh', rsp+0x30, environ)
0xe5765 execve('/bin/sh', rsp+0x50, environ)
0xe66bd execve('/bin/sh', rsp+0x70, environ)

; glibc-2.23(64bit, 16.04 ubuntu, BuildID: 60131540dadc6796cab33388349e6e4e68692053)
0x4526a execve('/bin/sh', rsp+0x30, environ)
0xef6c4 execve('/bin/sh', rsp+0x50, environ)
0xf0567 execve('/bin/sh', rsp+0x70, environ)
These gadgets are useful since their constraints are only certain value on stack to be zero.


While this simple strategy totally fails in a 32-bit libc.


Let's see what a potential one-gadget in a 32-bit libc looks like:
; glibc-2.23 (32bit, 16.04 ubuntu, BuildID: 926eb99d49cab2e5622af38ab07395f5b32035e9)
   3ac69: mov    eax,DWORD PTR [esi-0xb8]
   3ac6f: add    esp,0xc
   3ac72: mov    DWORD PTR [esi+0x1620],0x0
   3ac7c: mov    DWORD PTR [esi+0x1624],0x0
   3ac86: push   DWORD PTR [eax]
   3ac88: lea    eax,[esp+0x2c]
   3ac8c: push   eax
   3ac8d: lea    eax,[esi-0x567d5]
   3ac93: push   eax
   3ac94: call   b0670 <execve@@GLIBC_2.0>
There are two main differences between 32-bit and 64-bit:
  1. Data access: In 32-bit, it uses [<reg>-0x??] to access readonly data.
  2. Calling convention: In 32-bit, arguments are on the stack, while 64-bit uses registers.

Following discuss why these two differences make one-gadgets in 32-bit much harder to be found and used.

Data access method

64-bit libc uses the related offset of rip to access data segment. While in a 32-bit libc, there's assembly code looks like:
  11f995: mov    ebx,DWORD PTR [esp]
  11f998: ret
  11f999: mov    eax,DWORD PTR [esp]
  11f99c: ret
  11f99d: mov    edx,DWORD PTR [esp]
  11f9a0: ret
  11f9a1: mov    esi,DWORD PTR [esp]
  11f9a4: ret
  11f9a5: mov    edi,DWORD PTR [esp]
  11f9a8: ret
  11f9a9: mov    ebp,DWORD PTR [esp]
  11f9ac: ret
  11f9ad: mov    ecx,DWORD PTR [esp]
  11f9b0: ret
In different functions may use different register as the base to access data. For example, the first six lines of function fexecve is:
000b06a0 <fexecve@@GLIBC_2.0>:
   b06a0: push   ebp
   b06a1: push   edi
   b06a2: push   esi
   b06a3: push   ebx
   b06a4: call   11f995 <__frame_state_for@@GLIBC_2.0+0x375>
   b06a9: add    ebx,0x101957
   b06af: sub    esp,0x8c
After executing instruction add ebx,0x101957ebx will be set as libc_base+0xb06a9+0x101957=libc_base+0x1b2000, where 0x1b2000 is the value of dynamic tag PLTGOT:

$ readelf -d libc.so.6 | grep PLTGOT
 0x00000003 (PLTGOT)                     0x1b2000

Since we are finding one-gadgets, which should not appear in the first few lines of a function, that is, all 32-bit one-gadgets will have a constraint that certain register (usually ebx  or esi) points to the GOT area in libc.

This constraint seems really tough, since ebx and esi are callee safe in x86, which means their value will be pop-ed back before a routine returns. While in practice, the value of esi  or edi is already be the desired value in main, which was set in __libc_start_main. So this constraint still possible to be satisfied.

Calling convention

In 32-bit, the arguments are put in [esp], [esp+4], [esp+8]. There are two ways to do this, one is directly use mov to set these values, another is to use the push instruction. Two kinds of instructions need to be considered when finding gadgets, more complex than in 64-bit, but not hard.

All is well until I found this gadget:
   3ac69: mov    eax,DWORD PTR [esi-0xb8]
   3ac6f: add    esp,0xc
   3ac72: mov    DWORD PTR [esi+0x1620],0x0
   3ac7c: mov    DWORD PTR [esi+0x1624],0x0
   3ac86: push   DWORD PTR [eax]
   3ac88: lea    eax,[esp+0x2c]
   3ac8c: push   eax
   3ac8d: lea    eax,[esi-0x567d5]
   3ac93: push   eax
   3ac94: call   b0670 <execve@@GLIBC_2.0>
At first glance we may say this gadget will call execve('/bin/sh', esp+0x2c, environ), but this is incorrect. Before line 3ac88 sets argv to esp+0x2c, the value of esp has been changed by  3ac6f: add esp,0xc and 3ac86: push DWORD PTR [eax], thus the correct result of this gadget should be execve('/bin/sh', esp+0x34, environ).

Because of this complicated gadget, I decided not to use a rule-base strategy to find gadgets, but use symbolic execution instead.


Symbolic Execution

I implement an extremely simple symbolic execution on Ruby to find one-gadgets. It's simple because we don't need to consider the condition-branch. All we need is to show the correct constraints for this gadget. For example, consider this assembly:
mov edx, [eax]
lea esi, [edx + 4]
push esi
call func
If we want the first argument of func to be zero, the real constraint is [[eax] + 4] equals zero.

To deal with this, just set every registers and every stack slots as a symbolic variable, the meaning of symbolic can be found in the wiki page.

With SE, we can correctly resolve the constraints of one-gadgets. Furthermore, we can try to execute from any position in glibc, and check if it results in a function call like execve('/bin/sh', argv, environ).


Conclusion

The one_gadget tool is still under development, as version 1.3.1 it can find many one-gadgets in glibc-2.23. After removing duplicate or hard-to-reach constraints, six one-gadgets in 64-bit and three one-gadgets in 32-bit have been found, they are shown as follows:

64-bit libc-2.23.so




32-bit libc-2.23.so



I also tried different versions of libc, for example, one_gadget found six and four one-gadgets in glibc-2.19 64-bit and 32-bit, respectively.

Any suggestion is welcome, thanks for your reading :)

沒有留言:

張貼留言