参赛ID:1K0CT, Team : 4W3, 最终排名:88th(公开赛道)
做到flocto师傅的题真的每次都要有感而发一下 真正能让我全身心投入的赛题 给一个陌生的考点 但是不至于让你寸步难行 有种接触新东西的时候那个东西对你欲拒还迎激发探索欲的美 可能这就是flocto师傅出的题的魅力吧
revtale-1 | GM逆向
和LACTF那次不同的是这次就是纯粹的套GM壳检测flag而不是游戏逆向 没有魔改data文件 直接用UndertaleModTool
看代码即可:
1 | pk = [95, 119, 51] |
对flag进行了分段检测 直接解出每段的flag拼接即可amateursCTF{ggez_w3_l0v3_vm_b33f}
cplusplus | 代码理解
64位无壳 主函数(已重命名变量和函数):
1 | __int64 __fastcall main(int a1, char **a2, char **a3) |
/dev/urandom
是Linux系统的一个随机数发生器 程序从里面取了两次1byte数据存入random中 用这两个1byte随机数对flag进行加密 处理函数:
1 | unsigned __int64 __fastcall add(__int64 a1) |
比较有迷惑性的是add(arg)
函数(已重命名) 实际上就是lambda arg:arg + 1
剩下的难点就是找到这两个随机数 好在它们的长度只有1byte 直接爆破即可:
1 |
|
typo | pyre
题目直接给了一个混淆变量名和函数名的.py
:
1 | import random as RrRrRrrrRrRRrrRRrRRrrRr |
改一下变量和函数名程序的逻辑就很清晰了:
1 | import random as rand |
用一个随机的函数表来对flag进行操作然后Base16 -> Base17 每一个操作都是可逆的 只需要把它们写成逆操作再逆序执行加密时的函数就能逆向整个加密过程:
1 | import random as rd |
dill-with-it | py-deserialized vuln
main.py
:
1 | # Python 3.10.12 |
给出了一串序列化的python代码 正常的序列化python object
可以通过dill库来完全还原源代码(要使用对应的python版本的dill或pickle库):
1 | def Python_object(): |
但是这道题利用了反序列化的漏洞 因为反序列化实际上就是用PVM解释序列化的python代码 当序列化的类中出现类似__reduce__()
的魔术方法 且其返回值形式为(callable, (arg1, arg2, ...))
时 反序列化由这个类实例化的对象的序列化字符串时会直接执行callable(arg1, arg2, ...)
此时load(s)返回的就不是一个python object而是执行这个函数得到的返回值 例如:
1 | import dill |
其中pickletools.dis(serialized)
可以用来打印可读性更高的PVM opcode 该版本的opcode释义如下:
MARK | = b’(‘ | # push special markobject on stack | |||||||||
---|---|---|---|---|---|---|---|---|---|---|---|
STOP | = b’.’ | # every pickle ends with STOP | |||||||||
POP | = b’0’ | # discard topmost stack item | |||||||||
POP_MARK | = b’1’ | # discard stack top through topmost markobject | |||||||||
DUP | = b’2’ | # duplicate top stack item | |||||||||
FLOAT | = b’F’ | # push float object; decimal string argument | |||||||||
INT | = b’I’ | # push integer or bool; decimal string argument | |||||||||
BININT | = b’J’ | # push four-byte signed int | |||||||||
BININT1 | = b’K’ | # push 1-byte unsigned int | |||||||||
LONG | = b’L’ | # push long; decimal string argument | |||||||||
BININT2 | = b’M’ | # push 2-byte unsigned int | |||||||||
NONE | = b’N’ | # push None | |||||||||
PERSID | = b’P’ | # push persistent object; id is taken from string arg | |||||||||
BINPERSID | = b’Q’ | # | “ | “ | “ | ; | “ | “ | “ | “ | stack |
REDUCE | = b’R’ | # apply callable to argtuple, both on stack | |||||||||
STRING | = b’S’ | # push string; NL-terminated string argument | |||||||||
BINSTRING | = b’T’ | # push string; counted binary string argument | |||||||||
SHORT_BINSTRING= b’U’ | # | “ | “ | ; | “ | “ | “ | “ < 256 bytes | |||
UNICODE | = b’V’ | # push Unicode string; raw-unicode-escaped’d argument | |||||||||
BINUNICODE | = b’X’ | # | “ | “ | “ | ; counted UTF-8 string argument | |||||
APPEND | = b’a’ | # append stack top to list below it | |||||||||
BUILD | = b’b’ | # call setstate or dict.update() | |||||||||
GLOBAL | = b’c’ | # push self.find_class(modname, name); 2 string args | |||||||||
DICT | = b’d’ | # build a dict from stack items | |||||||||
EMPTY_DICT | = b’}’ | # push empty dict | |||||||||
APPENDS | = b’e’ | # extend list on stack by topmost stack slice | |||||||||
GET | = b’g’ | # push item from memo on stack; index is string arg | |||||||||
BINGET | = b’h’ | # | “ | “ | “ | “ | “ | “ | ; | “ | “ 1-byte arg |
INST | = b’i’ | # build & push class instance | |||||||||
LONG_BINGET | = b’j’ | # push item from memo on stack; index is 4-byte arg | |||||||||
LIST | = b’l’ | # build list from topmost stack items | |||||||||
EMPTY_LIST | = b’]’ | # push empty list | |||||||||
OBJ | = b’o’ | # build & push class instance | |||||||||
PUT | = b’p’ | # store stack top in memo; index is string arg | |||||||||
BINPUT | = b’q’ | # | “ | “ | “ | “ | “ ; | “ | “ 1-byte arg | ||
LONG_BINPUT | = b’r’ | # | “ | “ | “ | “ | “ ; | “ | “ 4-byte arg | ||
SETITEM | = b’s’ | # add key+value pair to dict | |||||||||
TUPLE | = b’t’ | # build tuple from topmost stack items | |||||||||
EMPTY_TUPLE | = b’)’ | # push empty tuple | |||||||||
SETITEMS | = b’u’ | # modify dict by adding topmost key+value pairs | |||||||||
BINFLOAT | = b’G’ | # push float; arg is 8-byte float encoding | |||||||||
TRUE | = b’I01\n’ | # not an opcode; see INT docs in pickletools.py | |||||||||
FALSE | = b’I00\n’ | # not an opcode; see INT docs in pickletools.py | |||||||||
# Protocol 2 | |||||||||||
PROTO | = b’\x80’ | # identify pickle protocol | |||||||||
NEWOBJ | = b’\x81’ | # build object by applying cls.new to argtuple | |||||||||
EXT1 | = b’\x82’ | # push object from extension registry; 1-byte index | |||||||||
EXT2 | = b’\x83’ | # ditto, but 2-byte index | |||||||||
EXT4 | = b’\x84’ | # ditto, but 4-byte index | |||||||||
TUPLE1 | = b’\x85’ | # build 1-tuple from stack top | |||||||||
TUPLE2 | = b’\x86’ | # build 2-tuple from two topmost stack items | |||||||||
TUPLE3 | = b’\x87’ | # build 3-tuple from three topmost stack items | |||||||||
NEWTRUE | = b’\x88’ | # push True | |||||||||
NEWFALSE | = b’\x89’ | # push False | |||||||||
LONG1 | = b’\x8a’ | # push long from < 256 bytes | |||||||||
LONG4 | = b’\x8b’ | # push really big long | |||||||||
_tuplesize2code = [EMPTY_TUPLE, TUPLE1, TUPLE2, TUPLE3] | |||||||||||
# Protocol 3 (Python 3.x) | |||||||||||
BINBYTES | = b’B’ | # push bytes; counted binary string argument | |||||||||
SHORT_BINBYTES = b’C’ | # | “ | “ | ; | “ | “ | “ | “ < 256 bytes | |||
# Protocol 4 | |||||||||||
SHORT_BINUNICODE = b’\x8c’ | # push short string; UTF-8 length < 256 bytes | ||||||||||
BINUNICODE8 | = b’\x8d’ | # push very long string | |||||||||
BINBYTES8 | = b’\x8e’ | # push very long bytes string | |||||||||
EMPTY_SET | = b’\x8f’ | # push empty set on the stack | |||||||||
ADDITEMS | = b’\x90’ | # modify set by adding topmost stack items | |||||||||
FROZENSET | = b’\x91’ | # build frozenset from topmost stack items | |||||||||
NEWOBJ_EX | = b’\x92’ | # like NEWOBJ but work with keyword only arguments | |||||||||
STACK_GLOBAL | = b’\x93’ | # same as GLOBAL but using names on the stacks | |||||||||
MEMOIZE | = b’\x94’ | # store top of the stack in memo | |||||||||
FRAME | = b’\x95’ | # indicate the beginning of a new frame | |||||||||
# Protocol 5 | |||||||||||
BYTEARRAY8 | = b’\x96’ | # push bytearray | |||||||||
NEXT_BUFFER | = b’\x97’ | # push next out-of-band buffer | |||||||||
READONLY_BUFFER | = b’\x98’ | # make top of stack readonly |
结合此表可以看出__reduce__()
漏洞实现的底层原理是这样的指令结构:
1 | # MEMOIZE/GET/... 用以确保栈上一定有接下来REDUCE要使用的函数 |
有了这些基础知识再看这题 先用dis打印出可读性较高的指令:
1 | 0: \x80 PROTO 4 |
一开始是先用types.CodeType
和types.FunctionType
来定义了一个函数并在MEMO中以赋以编号0 翻译为python代码大致为:
1 | code = types.CodeType( |
其中的PYC字节码翻译为python代码含义大致为(用这种方式定义函数中使用别的函数时LOAD_GLOBAL x
中的x
就是要使用的函数在预备函数列表中的下标 在这里就是('int', 'from_bytes', 'bin', 'range', 'len', 'chr')[x]
):
1 | def decodea(arg): |
再看下面的PVM字节码:
1 | 324: g GET 0 |
大量使用了一开始定义的函数 且传入的参数为编码过的字节串 通过这个函数来解码的到以下内容:
1 | b'\x01.\xce\x966' : bytearray(b'list\x01') |
这样整个加密的流程就很简单易懂了 以下是大致加密流程及解密过程:
1 | # encrypt |
除了这种静态分析的做法还有一种可以动调的做法 使用pickledbg
gogogaga | Go语言逆向 | Go多线程调试
IDA8.3打开用于静态分析 检查函数:
1 | // main.checkKey |
整体逻辑是先检测key的格式是否是?????-?????-?????-?????-?????
然后检测?
的范围是否为[A-Z0-9]
如果满足这两个条件就启动5个线程分别检测5段key
线程函数1:
1 | // main.Monday |
通过信道check
发送检测是否通过的信号elem
简单的base64编码 求出来的字节串是小端序 所以要逆转一下编码 得到第一个片段LARRY
线程函数2:
1 | // main.Tuesday |
检测每一位是否都是数字字符 然后将对应的数字求和结果需要为35 直接77777
线程函数3:
1 | // main.Wednesday |
检测key是否全部都是大写字母 在runtime_hmap h
为每个字母建立一个映射 如果检测到当前字母在map中已经是存在的key就退出 通过这层检测后将每个字母异或0x60然后调用线程函数2 因为不能用重复的字母 所以这段key可以为UVWXY
线程函数4:
1 | // main.Thursday |
根据每个字符的有效位对两个计数器进行操作 要求最后这两个计数器到达一定的数值 简单分析一下可以知道如果字符在A-Z范围内不可逆达到这种结果 直接用数字字符爆破:
1 |
|
线程函数5:
1 | void __golang main_Friday(string key, chan_bool check) |
初始化一个6 * 7的矩阵 第0列的值初始化为0-7 第1行初始化为0-5 然后要求处理完后matrix[5][6]的值是3 可以直接拼出key来KNLCC
要动调类似的不对同一个数据操作所以多线程并行的程序 需要在每个线程函数的入口下断点 然后在动调时切换到除主函数以外的线程函数的界面 发现在等待运行的线程锁内 进行步出会到达某个函数的入口断点 这时候对除了主线程以外的线程暂停 否则可能会因为其他线程先处理完数据而程序因为发送到信道的信号直接退出
patchflag | Windows内核逆向
剩下的题目待复现…