系统调用
today
新的知识又增长了,发现了getshell
的另一种方式:syscall
和srop
。故事还要源于…(此处省略万字输出)
(note:本作者这次有点懒,没有写AT&T汇编,而是一律用了intel汇编,请悉知)
可能是之前汇编基础不太好吧,竟没有发现syscall
这么好用的指令,只要再把/bin/sh
传参就能直接打开一个shell,真是妙蛙。但是在系统调用之前要做很多的事情,诸如各类参数传递,以64位的来说,我们要先知道我们要执行的函数系统调用号为59。我也是翻过libc
库的,发现system
函数实现里面有一段竟然是直接执行execve("/bin/sh")
实属意外了,这是在我有次反汇编libc
库的时候发现的,我原来一直是只用system
函数getshell
的,没想到system
内部是通过这样的系统调用来打开shell
的。
那这得学啊,这是基础的基础啊。
这边给出一下64位Linux的各个系统调用号,这个在Linux的/usr/include/asm/unistd.h
下有,我这里截取部分。
1 |
emm,经过多方取证呢,发现打开shell不仅要给第一个参数传上/bin/sh
字符串,第二第三个字符串都需要置零。64位呢前六个参数是依次给rdi,rsi,rdx,rcx,r8,r9
这六个寄存器的。那么我们传参过程就比较艰难,注入代码首先肯定不现实,现在题目基本都是NX保护的挺到位的。所以要去程序里面寻找gadget
,由于pop %rbx
这个gadget
巨难找,在实际应用要懂得变通,比如先给一个寄存器我想要的值然后再mov
给rdx
,有一样的效果。
由此就衍生出来一个return to __libc_csu_init
简称ret2csu
的方法来,其实这个我个人认为没有必要单独拿出来讲,因为就是因为这个特殊的函数基本蕴含了很多时候我们需要的gadget
才会被如此传唱的,会了基本的rop
攻击就行,ret2csu
的原理跟这个就一模一样。
一般的payload就是
1 | payload=fill_data+fake_rbp+(mov rax,59)+pop_rdi+bin_sh_addr+pop_rsi+0+pop_rdx+0 |
这是一般的payload,如果找不到对应的gadget,可以自己适当变通。
buuctf:ciscn_s_3
分析程序
64位无壳的elf程序,扔进IDA
竟发现plt
表只有可怜的两项,并且实用性也不大的那种。在一般的ret2libc
中我们一般通过一个输出函数(puts,write,printf)
来泄露地址,然而这里没有可以利用的输出函数,如果你想模仿下面输出一个plt
表项内容也可以,不拦你,只是你费尽心思用系统调用的puts
或者write
为何不直接用execve
直接getshell
呢?所以我们就确定我们采用syscall
的方式去getshell
。
寻找漏洞
这一个可以说很明显的一个栈溢出漏洞了,read
0x400
字节的数据,且缓冲区特别小。
第一次溢出
由于程序并未直接提供/bin/sh
字符串,因此我们要靠自己,然后最后填上一个main
实现二次溢出,第一次写/bin/sh
,第二次执行execve("/bin/sh")
。
这里提出一点我自己的看法,我认为这个题目有点问题,vlun函数很可能存在栈不平衡的情况,理由有两点
1.程序正常执行(无溢出)无法正常结束
2.给24
字节大小的数据可以直接覆盖rip
,因为我的理解是缓冲区大小16
字节再加上一个rbp 8
字节应该24
个字节才刚刚覆盖到rbp
的位置,而无法覆盖返回地址的。
而且由于我们要调用栈上的数据,且栈是动态的,我们无法准确得知我们写的/bin/sh
字符串的所在位置,因此我们第一次溢出可谓是身负重担,不仅要提供/bin/sh
字符串还要泄露栈的地址。由于程序只有下面的
1 | sys_write(1u, buf, 0x30uLL); |
因此我们必须通过这个函数泄露点什么。
想想当时程序调用的栈帧是什么样的,什么内容是跟栈的地址有关的。
1 | 低地址↑ |
如此我们也可以清楚看到,首先main的rbp是肯定被我们覆盖了的(PS 虽然现在好像是没有的),然后后面还有一个调用main函数的那个函数的rbp,那个可以确定栈的地址,能输出这个之后我们就可以开始算偏移了。
gdb调试算算rbp与它们之间的偏移,这里需要注意由于栈不平衡的原因,我们需要手动调一下rip让程序不要异常退出,然后我们调试到leave指令可以观察到此时的栈。
在地址0x7fffffffde20的地方存了__libc_start_main函数的rbp=0x7fffffffdf28,那么计算一下偏移就是264,当前的rsp是main函数的rsp是vlun函数的rbp,所以我们泄露出来的这个地址与vlun函数的rbp偏移了264字节,然后再有16字节的缓冲区,如果我们再缓冲区开始写入/bin/sh
字符串的话那么偏移就有280字节的大小。
然而这些应该是第二次溢出考虑的问题,这里只是分析一下泄露地址的可行性。
第一次的payload比较简单
1 | payload=b'/bin/sh\0'*2+p64(main) |
然后接受准备接收rbp,但是在改rip的过程中发现这里还是有0x20个字节才能到rbp的,但是我能直接略过rbp覆盖rip就有点离谱,就不知道main的rbp是存在哪了,也许是存在rip后面了。这个我也不太能讲的清楚,希望有师傅明白的话能为我解答一下,感激不尽。
1 | p.sendline(payload) |
那么第一次溢出完美的构造了/bin/sh
字符串并且获得了它的地址。
第二次溢出
第二次溢出要准备的东西就有点多了。
寻找gadget
我们需要的gadget有
1 | pop rax |
经过努力的寻找我们找到了mov rax,59用于代替pop rax,syscall本来就有,pop rdi很好找,pop rsi也有,唯独就是pop rdx找不着,但是关于rbx的gadget可以看到有一个mov rdx,r13,在libc_csu_init函数有一堆的pop寄存器的操作,我们也可以很容易找到pop r13,那么pop r13和mov rdx,r13两个一结合不就是pop rdx嘛,对吧。
但是需要注意的是,mov rdx,r13后面跟的并不是ret,所以在执行mov rdx,13这个gadget时候,要把rip及时地劫持回来。它运行到后面之后会执行call ptr[r12+rbx*8],并且,rbx,rbp,r12~r15都是我们可以随意改的。
rbx我们置零,r12我们放在我们payload上面的其中一个地方能让它接着执行就可以了。
那么我们先写payload
1 | payload=b'/bin/sh\0'*2+p64(pop_rbx_rbp)+p64(0)*2+p64(bin_sh+0x50)+p64(0)*3+p64(mov_rdx)+p64(mov_rax)+p64(pop_rdi)+p64(bin_sh)+p64(syscall) |
就是原本传给r12的偏移我用的是+0x48的,结果给报错了,反正这个题挺神奇的,就是感觉少了八字节的数据在里面但是好像又没少,希望有师傅能为我解答一下这个困惑,但是我一般遇到这种情况就偏移多8少8都试试看,如果是32位那就上下偏移4看看。
最后给师傅们看看结果吧
exp
1 | from pwn import * |