二进制安全之栈溢出

本文主要介绍二进制安全的栈溢出内容。

栈基础

内存四区

  • 代码区(.text):这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令执行。
  • 数据区(.data):用于存储全局变量和静态变量等。
  • 堆区:动态地分配和回收内存,进程可以在堆区动态地请求一定大小的内存,并在用完后归还给堆区。地址由高到低生长
  • 栈区:用于动态地存储函数之间的调用关系,以保证被调用函数在返回时恢复到母函数中继续执行;此外局部变量也存储在栈区。地址由低到高生长
<code>BSS段:(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域,属于静态内存分配。/<code>

栈的概念

  • 一种数据结构,数据存储方式为先进后出,压栈(push)和出栈(pop)
  • 每个程序都有自己的进程地址空间,进程地址空间中的某一部分就是该程序的栈,用于保存函数调用信息和局部变量
  • 程序的栈是从进程空间的高地址向低地址增长的,数据是从低地址向高地址存放的
二进制安全之栈溢出

函数调用

  • 函数调用经常嵌套,在同一时刻,堆栈中会有多个函数的信息。

栈帧

  • 每个未完成运行的函数占用一个独立的连续区域,称作栈帧。
二进制安全之栈溢出

基本流程

<code>;调用前
push arg3 ;32位esp-4,64位esp-8
push arg2
push arg1
call func ;1. 压入当前指令的地址,即保存返回地址 2. jmp到调用函数的入口地址
push ebp ;保存旧栈帧的底部,在func执行完成后在pop ebp
mov ebp,esp ;设置新栈帧的底部
sub esp,xxx ;设置新栈帧的顶部/<code>
二进制安全之栈溢出

详细流程

<code>int func_b(int b1,int b2)
{
int var_b1,var_b2;
var_b1 = b1+b2;
var_b2 = b1-b2;
return var_b1 * var_b2;
}
int func_a(int a1,int a2)
{
int var_a;
var_a = fuc_b(a1+a2);
return var_a;
}
int main(int argc,char** argv,char **envp)
{
int var_main;
var_main = func_A(4,3);
return 0;
}/<code>


二进制安全之栈溢出

参数传递

x86

<code>通过栈传参

先压入最后一个参数/<code>

x64

<code>rdi rsi rdx rcx r8 r9 接收后六个参数

之后的参数通过栈传参/<code>

64位的利用方式

<code>构造rop链

ROPgadget –binary level3_x64 –only ‘pop|ret’

# Gadgets information

0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop r14 ; pop r15 ; ret
0x00000000004006b2 : pop r15 ; ret
0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400550 : pop rbp ; ret
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret
0x0000000000400499 : ret
依次找pop rdi,pop rsi..,pop r9 ,这些寄存器里面存放的是参数,可以通过pop覆盖其中的内容/<code>

栈溢出

栈溢出指的是程序向栈中某个变量写入的字节数超过了这个变量本身申请的字节数,因而导致栈中与之相邻的变量的值被改变。

栈溢出目的

  • 破坏程序内存结构
  • 执行system(/bin/sh)
  • 执行shellcode

栈溢出思路

判断溢出点

<code>常见的危险函数:

输入:gets scanf vscanf

输出:sprintf

字符串:strcpy strcat bcopy/<code>

判断padding

<code>计算我们所要操作的地址和所要覆盖的地址的距离

IDA静态分析中常见的三种索引方式


a. 相对于栈基地址的索引,通过查看EBP相对偏移获得 char name[32]; [esp+0h] [ebp-28h] ==> 0×28+0×4

b. 相对于栈顶指针的索引,需要加上ESP到EBP的偏移,然后转换为a方式

c. 直接地址索引,相当于直接给出了地址/<code>

覆写内容

<code>覆盖函数返回地址

覆盖栈上某个变量的内容,如局部变量和参数/<code>

Ret2text

<code>返回到某个代码段的地址,如.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"要求我们控制程序执行程序本身已有的代码/<code>

Ret2shellocde

<code>跳转到我们在栈中输入的代码,一般在没有开启NX保护的时候使用.

ret2shellcode的目标即在栈上写入布局好的shellcode,利用ret_address返回到shellcode处执行代码。/<code>

Ret2syscal

<code>让程序返回到系统调用,调用syscall或execve执行某个程序,对于静态编译的程序,没有libc,只好通过execve执行shellcode了。

syscall --->rax syscall 0x3b ==>execve rax

• --->rdi path ==> /bin/sh rdi

• --->rsi argv / rsi

• --->rdx env rdx

int execve(const char *filename, char *const argv[],char *const envp[]);
execve("/bin/sh",null.null) 等同于system("bin/sh")
syscall(32位程序为int80)会根据系统调用号查找syscall_table,execve对应的系统调用号是0x3b。

当我们给syscall的第一个参数即rax中写入0x3b时(32位程序为0xb),就会找到syscall_table[0x3b],即syscall_execve,然后通过execve启动程序。

找syscall和int 80的方法:ROPgadget –binary test –only ‘int/syscall’

静态编译的程序没有system等函数的链接支持,故一般利用ret2syscall构造栈溢出
/<code>

Ret2libc

<code>如找到system函数在动态链接库libc中的地址,将return的内容覆盖为该地址,跳转执行

leak出libc_addr + call system + 执行system(‘/bin/sh’)

难点:libc动态加载,每次基址都会变化,如何泄露libc的地址?

思路:got —> read_addr() —>libc

read_addr – libc_base = offsset (不变)

libc_base = read_addr – offset

bin/sh的来源 : 程序本身或libc或者写一个/bin/sh到bss段

binsh = libc.search(“/bin/sh”).next()/<code>

其它

<code>判断是否是否为动态编译

⚡ ⚙  ~/stack/day_4  ldd ret2text
linux-gate.so.1 => (0xf7f36000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d60000) -->libc的版本,也可以vmmap查看
/lib/ld-linux.so.2 (0xf7f38000)
判断libc的版本

a. 本地直接通过vmmap查看
b. 远程的根据函数后几位的偏移得到
libc-database
link:https://github.com/lieanu/libc-database.git
usage: ./find func_name offset
exemplify: ./find gets 5a0
effection:
➜ libc-database git :( master) ./find gets 5a0
archive-eglibc (id libc6_2.17-93ubuntu4_i386)
c: 5a0怎么来的?
.got.plt:0804A010 off_804A010 dd offset gets

pwndbg> x/20gz 0x0804a010
0x804a010 <gets>: 0x08048476f7e643e0 0x08048496f7e64ca0
0x804a020 <__gmon_start__>: 0x080484b6080484a6 0xf7e65360f7e1d540
0x804a030 <rand>: 0x080484f6080484e6 0x0000000000000000
0x804a040 <stdin>: 0x00000000f7fb75a0 0x0000000000000000
0x804a050: 0x0000000000000000 0x0000000000000000
0x804a060 <stdout>: 0x00000000f7fb7d60 0x0000000000000000
0x804a070: 0x0000000000000000 0x0000000000000000

0x804a080: 0x0000000000000000 0x0000000000000000
0x804a090: 0x0000000000000000 0x0000000000000000
0x804a0a0: 0x0000000000000000 0x0000000000000000
64位程序和32位程序的区别

1. 传参方式
64位:rdi rsi rdx rcx r8 r9
32位:通过栈传参
2. syscall & int 80/<stdout>/<stdin>/<rand>/<gets>/<code>

栈空间布局

<code>// 伪代码
A(int arg_a1,int arg_a2)
B(int arg_b1,int arg_b2,int arg_b3)
C()
-------------------------------------
// B的压栈流程
---> ESP //指向栈顶,随着压栈不断抬高
buf[128] //局部变量
EBP //保存旧栈帧的底部,4字节
return //这是B的返回地址,即C
arg_b1
arg_b2
arg_b3
-->EBP //指向当前栈帧的底部,随着压栈不断抬高,指向旧栈帧/<code>

栈溢出原理

  • 当局部变量buf超过128字节,会向下覆盖EBP,return以及参数的内容。
  • 构造return
<code>将buf 的 132到136字节的空间输入shellcode的地址

会跳转执行shellcode/<code>

保护机制

NX

  • 保护原理
<code>堆栈不可执行保护,bss段也不可执行,windows下为DEP,可通过gcc -z execstack关闭

开启NX后再把return的内容覆盖为一段shellcode,在开启NX的时候,不能执行。/<code>

绕过原理 : 32位

<code>实现A函数执行的方法,即构建ROP链

return —> fake_addr —> A

将B的参数从arg_b2到arg_b3也覆盖成A的参数
/<code>


<code>// 伪代码
A(int arg_a1,int arg_a2)
B(int arg_b1,int arg_b2,int arg_b3)
C(int arg_c1,int arg_c2)
-------------------------------------

// B的压栈流程
---> ESP\t\t\t\t\t\t\t\t
buf[128]\t\t\t\t\t
EBP \t\t\t \t\t\t
return\t\t\t\t\t\t//把return的内容覆盖为A的地址
arg_b1\t\t\t\t\t\t//程序在调用A函数的时候,把下一个栈数据当作A的返回地址,因此需要在再下一条语句的时候开始覆盖参数
arg_b2 arg_a2\t\t //将B的参数用A的参数覆盖掉
arg_b3\targ_a1\t\t
\t-->EBP\t\t\t\t\t\t\t/<code>


<code>借鉴上面的方法,在调用A之后,再调用C,构建ROP链

这时不能把系统认为的A的返回地址的arg_b1覆盖为C的返回地址,不然会向上覆盖arg_a2和arg_a2,导致A无法正常执行。

这时需要再找一个return语句,程序里面通常含有pop-pop-ret的链

ROPgadget –binary –only ‘pop|ret’ : 自动寻找rop链

Gadgets information
============================================================
0x00000000004006ac : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006ae : pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006b0 : pop r14 ; pop r15 ; ret\t\t\t//选择这个地址,代码段无NX
0x00000000004006b2 : pop r15 ; ret
0x00000000004006ab : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret
0x00000000004006af : pop rbp ; pop r14 ; pop r15 ; ret
0x0000000000400550 : pop rbp ; ret
0x00000000004006b3 : pop rdi ; ret
0x00000000004006b1 : pop rsi ; pop r15 ; ret
0x00000000004006ad : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret

0x0000000000400499 : ret
将arg_b1覆盖为addr_pop_pop_ret的地址:4006b0

此时将将arg_a1 pop到r14,arg_a2 pop到r15,然后ret

将ret的内容覆盖为C的入口地址,即可!

程序执行如下代码:

// 伪代码
A(int arg_a1,int arg_a2)
B(int arg_b1,int arg_b2,int arg_b3)
C(int arg_c1,int arg_c2)
-------------------------------------
// B的压栈流程
---> ESP\t\t\t\t\t\t\t\t
buf[128]\t\t\t\t\t
EBP \t\t\t \t\t\t
return\t\t\t\t\t\t//-->fake_addr_A
arg_b1\t\t\t\t\t\t//-->4006b0 addr_pop_pop_ret
arg_b2 arg_a1\t\t //pop r14
arg_b3\targ_a2\t\t //pop r15
ret \t\t\t\t\t\t\t// --->fake_addr_C
0\t\t\t\t\t\t\t\t\t// --->C的返回地址,现在没用了
arg_c1
arg_c2
\t-->EBP\t/<code>

完整流程

<code>使用buf将栈空间覆盖

在B退出的时候ret到A

依次取覆盖之后的A的两个参数,执行A函数

返回到pop_pop_ret的地址

将ret的地址覆盖为C的地址


将C的返回地址置空

写入C的参数

执行C函数/<code>

总结

<code>A函数的功能通常时”/bin/sh”

C函数的功能为system

上述流程执行完则可以达到反弹shell的目的

由于程序不在栈上执行而是在代码段中执行,所有可以绕过NX保护机制。/<code>

Canary(金丝雀)

  • 保护原理
<code>开启canary后,会在程序的EBP与ESP之间的位置随机插入一段md5值,占4字节或8字节。

canary为一段以 /0 结尾的一串md5值,如123456/0,起截断作用,防止打印。

在程序return之前与内核地址[fs:0x28]异或校验md5值

异或结果为1时报错退出,为0时正常ret。

几种思路

如果能在栈中拿到md5值,在指定位置可以精准覆盖之。

将从内核中取的md5值,设置为自己定义的值,覆盖的时候覆盖自己定义的值。/<code>

gcc开启canary

<code>参数:-fstack-protector :启用保护,不过只为局部变量中含有数组的插入保护

参数:-fstack-protector-all :为所有函数插入保护

参数:-fstack-protector-strong -fstack-protector-explicit :只对明确有stack-protect 属性的函数启用保护

参数:-fo-stack-protector :禁用保护/<code>

3种利用方法利用

<code>覆盖canary的最后一个字节

利用栈溢出将”\\0″覆盖掉,则可以将canary打印出来。

smash

leak stackguard — top/<code>

查看开启的保护机制

<code>⚡ > ~/stack/day_1> checksec leak_canary
[*] '/root/stack/day_1/leak_canary'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found

NX: NX enabled
PIE: No PIE (0x8048000)/<code>

PIE

  • 保护原理
<code>让程序能装载在随机的地址,主要是代码段的地址随机化,改变的是高位的基地址。

gdb中使用show proc info 可以显示代码段的基地址

–enabled-default-pie开启 -no-pie关闭/<code>
  • 通常与ALSR联合使用
二进制安全之栈溢出


ALSR

  • 保护原理
<code>每次加载程序,使其地址空间分布随机化,即使可执行文件开启PIE保护,还需要系统开启ASLR才会真正打乱基址。主要是堆栈和libc的地址随机化。

修改/proc/sys/kernel/randommize_va_space来控制ASLR的开关。/<code>

栈溢出进阶

pwntools

<code># Pwntools环境预设
from pwn import *
context.arch = "amd64/i386"\t\t\t\t\t\t\t\t#指定系统架构
context.terminal = ["tmux,"splitw","-h"]\t #指定分屏终端
context.os = "linux"\t\t\t\t\t\t\t\t #context用于预设环境

# 库信息
elf = ELF('./PWNME')\t\t\t\t\t\t# ELF载入当前程序的ELF,以获取符号表,代码段,段地址,plt,got信息
libc = ELF('lib/i386-linux-gnu/libc-2.23.so')\t # 载入libc的库,可以通过vmmap查看
/*
首先使用ELF()获取文件的句柄,然后使用这个句柄调用函数,如
>>> e = ELF('/bin/cat')
>>> print hex(e.address)\t# 文件装载的基地址

>>> print hex(e.symbols['write']) # plt中write函数地址
>>> print hex(e.got['write']) \t # GOT表中write符号的地址
>>> print hex(e.plt['write']) \t\t# PLT表中write符号的地址
*/

# Pwntools通信
p = process('./pwnme')\t\t\t\t\t\t# 本地 process与程序交互
r = remote('exploitme.example.com',3333) \t\t # 远程

# 交互
recv()\t\t\t# 接收数据,一直接收
recv(numb=4096,timeout=default)\t# 指定接收字节数与超时时间
recvuntil("111")\t # 接收到111结束,可以裁剪,如.[1:4]
recbline()\t\t# 接收到换行结束
recvline(n)\t\t# 接收到n个换行结束
recvall()\t\t\t# 接收到EOF
recvrepeat(timeout=default) #接收到EOF或timeout
send(data)\t\t# 发送数据
sendline(data)\t\t# 发送一行数据,在末尾会加\\n
sendlineafter(delims,data) # 在程序接收到delims再发送data
r.send(asm(shellcraft.sh()))\t\t\t\t\t\t # 信息通信交互
r.interactive()\t\t\t\t\t\t\t\t # send payload后接收当前的shell

# 字符串与地址的转换
p64(),p32() #将字符串转化为ascii字节流
u64(),u32() #将ascii的字节流解包为字符串地址 /<code>

got & plt

在IDA中选择view-open subview - segment可以直接查看到got和plt段

<code>.plt:08048440 ; __unwind { 

.plt:08048440 push ds:dword_804A004
.plt:08048446 jmp ds:dword_804A008\t\t;804A008是got表的地址
.plt:08048446 sub_8048440 endp
/<code>


<code>plt段的某个地址存放着指令 jmp got/<code>


<code>.got.plt:0804A00C off_804A00C     dd offset printf        ; DATA XREF: _printf↑r
.got.plt:0804A010 off_804A010 dd offset gets ; DATA XREF: _gets↑r
.got.plt:0804A014 off_804A014 dd offset time ; DATA XREF: _time↑r
.got.plt:0804A018 off_804A018 dd offset puts ; DATA XREF: _puts↑r
.got.plt:0804A01C off_804A01C dd offset system ; DATA XREF: _system↑r
.got.plt:0804A020 off_804A020 dd offset __gmon_start__
.got.plt:0804A020 ; DATA XREF: ___gmon_start__↑r
.got.plt:0804A024 off_804A024 dd offset srand ; DATA XREF: _srand↑r
.got.plt:0804A028 off_804A028 dd offset __libc_start_main
.got.plt:0804A028 ; DATA XREF: ___libc_start_main↑r
.got.plt:0804A02C off_804A02C dd offset setvbuf ; DATA XREF: _setvbuf↑r
.got.plt:0804A030 off_804A030 dd offset rand ; DATA XREF: _rand↑r
.got.plt:0804A034 off_804A034 dd offset __isoc99_scanf/<code>


<code>got段中存放着程序中函数的地址,可以避免每次调用某个函数的时候去libc库中寻找。/<code>


函数调用流程

<code>找到plt表.plt表存放指令

跳转到got表,got表存放地址,不能填在return的位置

找到对应的func_addr


没有的时候跳转到libc中取出函数,并缓存到got表/<code>


plt2leakgot

<code>plt["write"](1,got("write"),4)

通过plt的write函数leak出got的地址/<code>


libc_csu_init

  • 在所有的64位程序中都含有libc_csu_init函数
<code>.text:0000000000400650 __libc_csu_init proc near               ; DATA XREF: _start+16↑o
.text:0000000000400650 ; __unwind {
.text:0000000000400690
.text:0000000000400690 loc_400690: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400690 mov rdx, r13\t\t\t\t// 4. 利用点,将r13给到了rdx
.text:0000000000400693 mov rsi, r14\t\t\t\t// 5. 控制rsi
.text:0000000000400696 mov edi, r15d\t\t\t// 6. 控制rdi的低四位
.text:0000000000400699 call qword ptr [r12+rbx*8]\t//7. 给rbx赋0,相当于call [r12]
.text:000000000040069D add rbx, 1
.text:00000000004006A1 cmp rbx, rbp
.text:00000000004006A4 jnz short loc_400690
.text:00000000004006A6
.text:00000000004006A6 loc_4006A6: ; CODE XREF: __libc_csu_init+36↑j
.text:00000000004006A6 add rsp, 8
.text:00000000004006AA pop rbx\t\t\t\t\t//1. 控制函数从这里执行
.text:00000000004006AB pop rbp
.text:00000000004006AC pop r12\t\t\t\t\t//8. 给r12添一个main_addr
.text:00000000004006AE pop r13\t\t\t\t\t//2. 通过栈控制r13
.text:00000000004006B0 pop r14

.text:00000000004006B2 pop r15
.text:00000000004006B4 retn\t\t\t\t\t\t\t //3. ret到 mov rdx, r13
.text:00000000004006B4 ; } // starts at 400650
.text:00000000004006B4 __libc_csu_init endp
//实现通过栈控制rdx/<code>


  • 目的
    • 在64位提供三个参数的用法


  • 利用原理
<code>程序在启动main函数之前,都由glibc的标准c库启动,即由libc_csu_init启动main函数

libc_csu_init可以获得一个有4个参数调用的地方,比如系统调用函数syscall

如syscall—>rax syacall 0x3b ==>execve

—>rdi path\t“/bin/sh”

—>rsi argv

—>rdx env

通过syscall调用execve,执行execve(“/bin/sh”,null,null),等价于system(“/bin/sh”)

syscall(32位程序为int80)会根据系统调用号查找syscall_table,execve对应的系统调用号是0x3b。


当我们给syscall的第一个参数即rax中写入0x3b时(32位程序为0xb),就会找到syscall_table[0x3b],即syscall_execve,然后通过execve启动程序。

找syscall和int 80的方法:ROPgadget –binary test –only ‘int/syscall’/<code>


ret2_dl_runtime_resolve

<code>解决32位无输出函数的情况,64位使用IOfile的方式。

找对应的plt中的地址

跳转到对应的got表中,got中如果有,则执行对应函数

如果跳转到的got对应位置没有值,got会向后累加一个地址,然后跳转到plt[0],压入两个参数,一个是index,一个是与DYNAMIC有关的参数,称为link_map(动态链接用到的名字,如puts),然后再调用dl_runtime_resolve(link_map_obj,reloc_index)

push cs:qword_602008\t\t\t
.plt:00000000004007A6 jmp cs:qword_602010
dl_runtime_resolve实际上是一个解析实际地址的函数,根据函数名称做解析,然后写回到plt的index对应的got

调用完之后,会根据参数调用解析出来的地址,比如解析出来的puts函数,那么会调用puts函数,并且写入puts_got中

结束后,接着运行程序


因此,我们向DYNAMIC中写入puts字符串就可以了/<code>


分享到:


相關文章: