BROP
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
用到如下函数
a10.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...................................................……