One-gadget is an useful gadget in glibc, which leads to call (, , ). It's convenient to use it to get RCE (remote code execution) whenever we can only control ip (i.e. the program counter). For example, sometimes the vulnerability only leads to an arbitrary function call without controlling the first argument, which forbids us to call (). 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.
The source code of one_gadget can be found here.
It's a ruby gem, use gem install one_gadget in command line to install it.
First of all, a potential gadget must satisfy:
- has accessed to the /bin/sh string.
- call the exec* family function.
To demonstrate clearly, consider the following assembly code, which is the result of objdump of libc-2.23:
Line 45271 is equivalent to , and is exactly the string /bin/sh.
It's easy to find the string offset use command strings:
As the constraints of this gadget, notice line 45278 is . So we know the final result of this gadget is , which implies the constraint of this gadget is .
So the strategy of finding gadgets is simple:
- find assembly codes that access /bin/sh as one-gadget candidates.
- filter out candidates that not calling execve in a near line.
- The asm code looks like is the constraint.
This simple strategy can find three one-gadgets in glibc-2.19 and glibc-2.23, listed as following:
These gadgets are useful since their constraints are only certain value on stack to be zero.
While this simple strategy totally fails in 32bit libc.
Let's see what a potential one-gadget in 32bit libc looks like:
There are two main differences between 32bit and 64bit:
- Data access: In 32bit, it uses to access readonly data.
- Calling convention: In 32bit, arguments are on stack, while 64bit uses registers.
Following discuss why these two differences make one-gadgets in 32bit much harder to be found and used.
Data access method
In 64bit libc, it uses the related offset of rip to access data segment. While in 32bit libc, there's assembly code looks like:
In different functions may use different register as the base to access data. For example, the first six lines of function fexecve is:
After executing instruction , ebx 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
While we are finding one-gadgets, which should not possible appear in first few lines of a function, that is, all 32bit 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.
In 32bit, the arguments are put in . While there're two ways to do this, one is directly use mov to set these values, another is use push instruction. Two kinds of instruction need to be considered when finding gadgets, more complex than 64bit, but not hard.
All is well until I found this gadget:
At first glance we may say this gadget will call , while this is incorrect. Before line 3ac88 set argv to , the value of esp has been changed by and , thus the true result of this gadget is .
Because of this complicated gadget, I decided not to use a rule-base strategy to find gadgets, but use symbolic execution instead.
I implement a very simple symbolic execution in ruby to find one-gadgets. It's very simple because we don't need consider the condition-branch. All we need is to show the correct constraints for this gadget. For example, consider this assembly:
If we want the first argument of func is zero, the real constraint is eax equals zero.
To deal with this, just set every registers and every stack slots to a symbolic variable, meaning of symbolic can be found in the wiki page.
With SE, we can correctly resolve the constraints of one-gadgets. Further more, we can try start execution from any position in glibc, check if it can result in a function call like (, argv, environ).
The one_gadget tool is still under developing, as version 1.3.1 it can find many one-gadgets in glibc-2.23. After removing some duplicate or hard-to-reach constraints, six one-gadgets in 64bit and three one-gadgets in 32bit has be found, shown as follows:
I also tried difference versions of libc, for example, one_gadget found six and four one-gadgets in glibc-2.19 64bit and 32bit, respectively.
Any suggestion is welcome, thanks for your reading :).