https://david942j.blogspot.com/2017/02/project-one-gadget-in-glibc.html
one-gadget 是glibc里调用execve('/bin/sh', NULL, NULL)
的一段非常有用的gadget。在我们能够控制ip(也就是pc)的时候,用one-gadget来做RCE(远程代码执行)非常方便,比如有时候我们能够做一个任意函数执行,但是做不到控制第一个参数,这样就没办法调用system("sh")
,这个时候one gadget就可以搞定了。我之前每次都是用IDA去手动找的,哪怕我原来还找过,所以我就决定写个好用的工具来避免再手动去找。
最后做出来的工具是one_gadget,工具不仅可以找到one gadget还可以把需要满足的条件也给出来。
这篇文章主要讲讲one_gadget都干了点什么。
one_gadget的代码可以在这里找到。
代码包装成了一个ruby gem,在命令行里用gem install one_gadget
就可以安装。
首先,一个潜在的gadget需要满足以下几个条件:
为了说的更明白点,看看下面这一段汇编,这时libc-2.23用objdump
出来的内容:
; 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>
第45271行相当于rdi = libc_base + 0x18c177
,而libc_base + 0x18c177
正好就是'/bin/sh'的字符串。
用strings
很容易把字符串的偏移量拿出来:
至于这个gadget的约束,注意一下45278行的rsi = rsp + 0x30
,从这就可以看出其实最后结果是调用的execve('/bin/sh', rsp + 0x30, environ)
,这就需要[rsp + 0x30] == NULL
。
Gadget 0x4526a:
execve('/bin/sh', rsp + 0x30, environ)
所以找gadget的策略并不麻烦:
execve
的备选都去了lea rsi, [rsp+0x??]
的汇编就是约束条件这个简单的策略在glibc-2.19和glibc-2.23能找到3个one gadget,如下:
; 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)
由于这些gadget的约束只要求stack上的一些特定位置值为0,所以非常有用。
但是,在32位的libc上,这办法完全用不了。
下面我们来看下一个32位libc的潜在one gadget长啥样:
; 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>
32和64主要有这两点区别:
[<reg> - 0x??]
来访问只读数据的下面我们来看下为什么这两点不一样的地方会导致one gadget在32位的libc上会很难去找,也很难用。
在64位libc里访问data段是用rip相对偏移去访问的,而在32位libc里,汇编大概长这样:
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
在不同的函数里可能会用不同的寄存器为基础去访问数据,比如fexecve
的前6行:
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
在执行了add ebx, 0x101957
之后,ebx
就是libc_base + 0xb06a9 + 0x101957 = libc_base + 0x1b2000
,这里0x1b2000
是dynamic tag pltgot的值:
$ readelf -d libc.so.6 | grep PLTGOT
0x00000003 (PLTGOT) 0x1b2000
在我们找one gadget的时候,本不应该在一个函数前几行出现的是,所有的32位one gadget都有一个约束要求特定寄存器(一般是ebx
或者esi
)指向libc的GOT区域。
这个约束看起来非常强,因为ebx
和esi
在x86里是callee safe的,也就是说在一段程序返回之前会被pop回来,但是在实际当中,由于esi
或者rdi
已经在main
里被赋值为了需要的值,也就是在__libc_start_main
里设置的,所以这个条件还是有可能被满足的。
在32位里,参数被放在了[esp], [esp+4], [esp+8]
. 这里有两种方法来做到,一种是直接用mov
来设置这些值,另外一种是使用push
指令。两种指令在找gadget的时候都需要被考虑到,这样就比64位复杂一些,不过还好不是太难。
在我找到这段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>
第一眼看过去我们可能会觉得这段gadget会调用execve('/bin/sh', esp+0x2c, environ)
,但是这其实是不对的。在3ac88行把argv设置为了esp+0x2c
,esp
的值在3ac6f: add esp, 0xc
和3ac86: push DWORD PTR [eax]
被改动了,所以这段gadget的真实结果是调用了execve('/bin/sh', esp+0x34, environ)
由于这种比较复杂的gadget,我决定不用基于规则的策略来找gadget,而是使用符号执行。
我用ruby实现了一个非常简单的符号执行来找one gadget。由于我们根本没有考虑条件跳转,所以非常简单。我们要做的只是去找gadget的正确约束要求,比如说下面这一段汇编:
mov edx, [eax]
lea esi, [edx + 4]
push esi
call func
如果我们想要func
的第一个参数是0,那么真正的的约束就是[[eax]+4]
等于0.
为了解决这个问题,我们只需要把每个寄存器和每个栈slot都设置为符号变量,符号化的含义可以在wiki里找到。
通过符号执行,我们就可以成功去解析出one gadget的约束,另外我们还可以从glibc里任意位置开始尝试符号执行,看最后函数是不是可以做到`execve('/bin/sh', argv, environ)'.
one_gadget工具还在开发当中,在1.3.1版本中,在glibc-2.23可以找到很多one gadget。在去掉了一些重复或者很难达到的约束之后,64位里有6个one gadget,32位里有3个one gadget可以找到:
64bit libc-2.23.so
32bit libc-2.23.so
我也试了不同版本的libc,比如在glibc-2.19上的64位和32位,分别可以找到6段和4段。
如果有任何建议,非常欢迎与我联系,感谢您的阅读。