CTF-PWN

BROP

2019-01-13  本文已影响37人  clive0x

BROP对存在栈溢出的ELF进行指令盲注,Papper描述攻击可谓充满了艺术。要满足BROP,需要几个条件:1.进程crash后可以自动重启,类似于httpd、nginx等Daemned服务。2.进程通过fork重启、而非execve(),fork clone父进程状态,从而多次restart间能保持状态,如canary不变等。

指令盲注基于几个特性:

1.Stop gadgets,如sleep、loop代码,通过连接timeout来判定(non loss connection)。

2.trap gadgets,即crash gadgets,如 \0 pointer dereferences。

3.Probe  gadgets,当时正在探测。

盲注判断条件:

1.正常返回,nocrash。

2.超时返回,inf,即为stop gadgets。

3.丢失连接,crashed,即为stop gadgets。

攻击目标最终要拿到shell,即要指行execv('/bin/sh',0),需要能够ret2plt执行(如果有execv),或者syscall execv。X86-64位机器下需要rax传syscall number,rdi、rsi、rdx传参。Paper通过 __libc_csu_init函数指令偏移解决了rdi和rsi问题:

pop RDX;ret在文件中罕见,使用strcmp系列函数来指定rdx:rdx保存cmp的内存字节数。

最后只缺RAX,这个使用ret2plt来绕过。 plt区域判断比较有艺术性,特征如下:

1.每个plt长20 bytes。

2.+6和+b两路径执行不会挂,+b为上面下箭头处,自己在栈上放plt num。

Paper exp使用ruby编写,有如下几个过程:

1.find_overflow_len,获取溢出长度,代码比较简单,暴力猜测。

2.find_rip 获取原RIP,同上,猜测各字节内容,单字节范围在(0-255)间,捕获inf和nocrash两种情况,inf即为stop gadget,nocrash为正确内容,另外rip有个判断条件

[0x400000,0x400000 + 0x600000]

内存:AAAAAAAAA + canary + rip。在找到rip之前,已经把canary找到了。有一段canary检查代码,又是一些特征的运用,文章作者几乎是二进制层面的hacker,canary8字节,最后一字节不为0。

3.find_inf 查找stop gadgets,即未断连接,但是超时返回gadget,从0x400000 + 0x1000,即0x401000开始,每隔0x10字节发送['AAAA'*olen,canary,addr,addr,DEATH]进行盲注,选择inf返回的addr,另外通过paranoid_inf()二次验证,二次验证规则如下:

验证5次,从inf addr + 0x10开始,每次地址+ 0x10,发送

['AAAA'*olen,canary,addr,addr +6,inf,inf] 注,本次发送未判断,及下面

['AAAA'*olen,canary,addr,addr +6,DEATH],用判断plt逻辑来查找plt。在前面也提到了,对于plt,plt + 0,plt + 6,plt+b都会正常执行,不会死。

4.find_depth,查找嵌套函数调用深度。

i in 1..30层探测后面有几个ret指令,发送['AAAA'*olen,canary, @plt*i],通过正常返回来判断(no_crash),如5层调用堆栈如下

[canary,rip,rip,rip,rip,rip,others],探测时栈变成[canary,plt,plt,plt,plt,plt,others] 这个地方有个bug,代码执行plt及其后指令,有可能其后就有crash指令

5.find_gadget 查找BROP,即__libc_csu_init6个连续pop,

addr 初始值设为@plt + 0x200(plt section或者text section),

while true 循环,addr + 7开始,每次探测地址+7(原因在后面有提)

一、通过check_instr() 来验证是否是六个brop。

a:depth为0时,发送

['AAAA'*olen,canary,addr,6个@plt,inf,inf,death],通过返回值为inf来检测(probe addr为6个pop,会把6个@plt弹出,Paper用6个death,对应exp用6个plt,6个pop并ret之后来到inf)。

b,当depth> 0时,发送

['AAAA'*olen,canary,addr,6个@plt,@plt直到depth],通过返回值为no_crash判断。这个地方和find_depth有同样的问题。

二、通过verify_gadget()做二次验证,同时设置RDI:

a.通过get_dist()函数探测left:pop rsp和right:pop rdi,这也是Paper最难理解的部分,上面提到了+7,下面解释为啥是+7,

6个连续pop+ret共11 bytes,如下:

加7是想让addr落到 pop rbp(第6个字节)处,

以depth=0为例,

a1:left探测(图pop rbp往上), 发送

['AAAA'*olen,canary,(41 rex.B),6个@plt,inf,inf,death] 返回inf,正常

['AAAA'*olen,canary,(5c pop rsp),6个@plt,inf,inf,death],pop rsp会令进程crash,与期望的inf相违背,异常,left为1。

a2:right探测(图pop rbp往下),发送

['AAAA'*olen,canary,(41 rex.B),6个@plt,inf,inf,death]正常

['AAAA'*olen,canary,(5e pop rsi),6个@plt,inf,inf,death]正常

['AAAA'*olen,canary,(41 rex.B),6个@plt,inf,inf,death]正常

['AAAA'*olen,canary,(5f pop rdi),6个@plt,inf,inf,death]异常

pop rdi会令进程crash,这里有疑问,rdi不像rsp那样破坏堆栈,怎么会crash?

right为3.rex.b指令为amd64新增,扩操作数长度,单独执行不会crash,另有rex.w等。

通过left(1) + right(3) == 3做进一步校验,

获取ret: = gadget + right + 2,发送

['AAAA'*olen,canary,6个@ret,inf,inf,death],通过返回值inf做进一步校验。

获取rdi = ret -1 ,通过check_rdi_bad_inf()检验,发送

['AAAA'*olen,canary,death]让程序挂(why?),通过test_vsyscall()验证,发送

['AAAA'*olen,canary,(time = VSYSCALL + 0x400),inf,inf,death]是否返回inf进一步校验

发送'AAAA'*olen,canary,death]让程序挂(why?),

通过find_writable()查找到写内存区域:

add从rip开始,循环检测,每次+0x10000,发送

['AAAA'*olen,canary,pop rdi,addr,(time = VSYSCALL + 0x400),inf,inf,death],判断是否为inf来验证是否可写,

vsyscall + 400 time函数定义:time_t time(time_t *tloc)

通过vsyscall把返回的时间存到addr(pop rdi,addr实现参数)

通过check_rdi()做进一步校验,发送

['AAAA'*olen,canary,brop,6个death,inf,inf,death],判断是否为inf校验。

通过paranoia_checks()做进一步检验,发送

['AAAA'*olen,canary,pop rdi,0,pop rsi,0,0,inf,inf,death] 判断是否为inf校验。

6.find_strcmp ,获取strcmp函数,设置rdx.

声明:int strcmp(const char *s1, const char *s2);

利用good/bad参数组合来校验,good参数:rip,俩bad参数 300和500,原理:

strcmp(bad,bad)/strcmp(bad,good)/strcmp(good,bad)都报错,

strcmp(good,good)成功返回。

另外还利用了vsyscall page最后一个字节:

strcmp(VSYSCALL + 0x1000 - 1,good),超vsyscall page时正常返回,不会报错。

并把good地址存为strcmp_addr地址。

7.find_write(),查找write()函数

plt num 0至300循环探测,

7.1 max_fd上设置初始调用参数,write(fd,addr,len)相关参数

len参数通过strcmp设置:[@strcmp,@strcmp_addr,@strcmp_addr]

设置函数和第一二个参数:[pop rdi,max_fd,pop rsi,@strcmp_addr,0,@plt + 0xb,@write_plt_num]

注:plt+0xb进入plt第三条指令,直接jmp到plt start,后栈上存在@write_plt_num

7.2 chain多个socket write, for fd in [0,max_fd-1]:

设置rdi:[fd]。

设置调用函数[@plt + 0xb,@write_plt_num]

最后的发送rop为[@strcmp,@strcmp_addr,@strcmp_addr,pop rdi,max_fd,pop rsi,@strcmp_addr,0,@plt + 0xb,@write_plt_num, max_fd-1个 (pop rdi ,fd,@plt + 0xb,@write_plt_num),DEATH]

表示max_fd个write函数串起来向客户端写东西,总有一个会成功。

后面这个connection用一个select(conns,nil,nil)来做返回测试。

只要客户端接收到服务端的数据,即探测到write函数。这块牛B在探测多个fds。

8.find_fd,获取打开的句柄

循环10次,构造{fd:count(fd)} map,可用fd通过下面的do_find_fd实现:

do_find_fd  从20往下至0循环探测,发送

[@strcmp,@strcmp_addr,@strcmp_addr,pop rdi,fd,pop rsi,@strcmp_addr,0,@plt + 0xb,@write_plt_num,death],即write(fd,strcmp_addr,len),通过检测是否返回值来验证可用的fd。

最后保存被验证次数最多的fd,及最小、最大的fd。

另外设置@max_fd = 最大fd + 3

9.find_good_rdx,在比较内容小于16或者没找到strcmp \0地址的情况下,查找相对好的strcmp_addr,即cmp的地址内容足够长。

探测addr从strcmp_addr开始,循环探测,当探测到 \0时,addr + 8(64位数)继续,未找到比16大的场景时,addr +(探测到的长度+1) +1即跳过\0

[@strcmp,@strcmp_addr,@strcmp_addr,pop rdi,max_fd,pop rsi,@strcmp_addr,0,@plt + 0xb,@write_plt_num, max_fd-1个 (pop rdi ,fd,@plt + 0xb,@write_plt_num),DEATH]

根据read响应来判断,当read响应为0或者响应长度>16时,分别更新strcmp_zero地址和strcmp_addr。更长的响应代表找到更好的rdx。

10,dump_bin()方法,从服务端内存dump bin

addr 从0x400000循环,每次addr + dump_len

10.1 dump bin

fd in [0,max_fd] chain write,即write(fd0,addr,strcmp_len)|write(fd1,addr,strcmp_len)|...|write(max_fd,addr,strcmp_len)

注:本地没有在rop尾加death。

dump内容写本地文件。

10.2 analyze_bin,分析bin

10.2.1分析更长的字符串,更新strcmp_addr,以找到更大的rdx。

10.2.2分析dynamic string section,二进制正则

/[[:alnum:]]{4,}\0[[:alnum:]]{4,}/ 即00([数据或者字符]{4,}0[数据或者字符]{4,})

string以\0结束,查找多个string.

10.2.3 find_sym分析查找dynamic symbol section,正则@bin.rindex(/\0{24}/, @dynstr)

@dynstr为10.2.2分析结果,ELF文件 dyn sym section在dyn str section正上面。

上术正则表示dyn str 上面第一个24个0区域

Elf64_Sym第一项全0,每项长度24bytes。

10.2.4 dump_sym,解析dynamic symbol table

while 循环,循环起始地址是dynamic sym,结束条件是地址<dynamic str

数据解析成Elf64_sym结构数组。

10.2.5 dump_rel,解析relocation 结构

rela section 在dynstr之下

idx = @bin.index(/(.{8}\07\0\0\0.{4}\0{8}){3}/, @dynstr)

.{8}代表Elf64_Rela结构的r_offset成员,r_info由dynsym 索引和type组成

#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

故\07\0\0\0表示 type为7即R_X86_64_JUMP_SLOT类型,.{4}表示sym ,即dynsym索引

\0{8}表示r_addend,R_X86_64_JUMP_SLOT relocation修正值为symbol value,无须r_addend。

num 即为dynsym num,这里需要提的是relocation table,做为桥梁把symbol定义和symbol引用结合在一块。symbol引用即为r_offset,也就是got地址(Dynamic通过got写引用)。symbol定义在dynsym中,通过 rela section的sh_link指向对应dynsym table,见https://www.intezer.com/executable-and-linkable-format-101-part-3-relocations/

最后构造对应的pltf map,这里relocation函数对应plt num。

10.2.6 find_gadgets,查找dump bin中的gadgets

即syscall、pop rax,pop rdx,pop rsi

10.3 build_exp_rop()构造 exp

用到如下函数

a

10.3.1 构造sleep函数调用 sleep(delay) or usleep(1000 * 1000 * delay)

10.3.2   构造read(expfd,writable) & write(expfd,writable)调用

注:writable为@got_end + 100,进入data section

10.3.3 dup fd to 0,1,2

dup2存在时构造set_plt(rop, dup2, expfd, fd)或者 close and fcntl存在时

即把网络fd map到std_in、std_out、std_err上,从而完成read和write操作。

10.3.4,执行execve('/bin/sh',0,0),通过execve plt或者syscall execve

最终的rop+death。

11.正式exp。

11.1通过10.3生成exp rop

11.2生成50个链接

11.3发送11.1生成的exp,发送链接放入11.2中,共51个链接

11.4往51个链接发送'/bin/sh'字串,以便写入服务端data section中

11.5 find_sock()找到有影响的链接,这时候服务端已经read数据到data section,同时会write 同一区域数据到客户端,需判断是否包括'/bin/sh'字串。

11.6到此可以输入shell command。

.............................................................DONE...................................................……

上一篇下一篇

猜你喜欢

热点阅读