- [N1CTF 2021]babyrust
- [N1CTF 2021]Py
- [L3HCTF 2021]double-joy
- [L3HCTF 2021]Load
- [hack.lu]pycoin
- [hack.lu]atareee
TODO:
- [L3HCTF 2021]idaaaaaaaa
[N1CTF]babyrust
题目直接给了Rust源码,之前没有见过,现查文档学习
首先main函数里只是调用了一个 check!
let result = check!(@s /*your answer*/);
main
上面就是 check
的实现
macro_rules! check {
(@s n1ctf{$Never:tt}) => {
check!(stringify!($Never))
};
macro_rules!
是个宏定义,里面的 @s
,@e
应该是类似字符串匹配的东西
一开始的 (@s n1ctf{$Never:tt})
就把flag中去除 n1ctf{}
的部分存到了 $Never
变量中,stringify!
将其转换为字符串常量
stringify!
为 Rust 内置宏。其接收一个 Rust 表达式,如 1 + 2 , 然后在编译时将表达式转换为一个字符串常量,如 “1 + 2” 。
接下来的宏定义有很多类似这个的:
(@e ($Never:expr,$Gonna:expr,$Give:expr); (Never gonna give you up $($code:tt)*)) => {
$Give += true as usize;
check!(@e ($Never,$Gonna,$Give); ($($code)*));
};
理解一下大概就是根据传入的 code
字符串开头是否为 Never gonna give you up
,来判断是否进入这个定义,所以判断是一个根据字符串内容实现的Rust的简单vm,指令就是对 $Never, $Gone, $Give
三个变量进行变化
为了快速分析,直接在各个指令上加个输出,然后编译运行,会发现整体逻辑大概就是:
$Gonna = $Never[$Give]; // 将当前位置数据取出
$Gonna += true as u8; // 每次加1,执行多次
$Gonna -= true as u8; // 每次减1,执行多次
$Never[$Give] = $Gonna; // 将计算结果存回去
$Give += true as usize; // index + 1
于是修改代码如下:
#![recursion_limit="8192"] // 添加在开头
// 宏定义如下两个定义添加输出,可以打印出明文和密文
(@e ($Never:expr,$Gonna:expr,$Give:expr); (Never gonna say goodbye $($code:tt)*)) => {
$Gonna = $Never[$Give];
println!("Gonna = Never[Give: {}]: {}", $Give, $Never[$Give]);
check!(@e ($Never,$Gonna,$Give); ($($code)*));
};
(@e ($Never:expr,$Gonna:expr,$Give:expr); (Never gonna tell a lie and hurt you $($code:tt)*)) => {
$Never[$Give] = $Gonna;
println!("Never[Give: {}] = Gonna: {}", $Give, $Gonna);
check!(@e ($Never,$Gonna,$Give); ($($code)*));
};
// 随便设置一个容易计算的明文
let result = check!(@s n1ctf{00000000000000000000000000000000}/*your answer*/);
加密只有加减法,所以偏移始终相等
cipher = [
148, 59, 143, 112, 121, 186, 106, 133, 55, 90, 164, 166, 167, 121, 174, 147,
148, 167, 99, 86, 81, 161, 151, 149, 132, 56, 88, 188, 141, 127, 151, 63
]
fake_cipher = [
131, 53, 124, 109, 118, 165, 89, 131, 50, 83, 163, 149, 165, 104, 153, 145,
142, 149, 77, 69, 60, 154, 133, 128, 115, 54, 69, 168, 133, 105, 146, 59
]
for c, f in zip(cipher, fake_cipher):
print (chr(c - f + 48), end='')
# A6C33EA2571A2AE26BFAE7BEA2CD8F54
[N1CTF]Py
首先解包elf,能在目录下得到两个pyc
修改文件头,0a5n.py
为
import L
from var import *
def check_format(flag):
if len(flag) != 28:
return False
for i in flag:
if i not in '0123456789abcdef':
return False
return True
v1 = L.c1(v1, v2, v3)
v6 = L.c2(v1, v4, v5)
k = input('flag:')
if check_format(k) == True:
v2 = L.f3(k)
v3 = v2 - v6
if v3.a2 == g1 and v3.a3 == g2:
print('Congratulations! n1ctf{%s}' + k)
L.py 中有乱码,还原字节码能得到两个exec
z = ''.join([chr(i ^ 2) for i in z])
exec(z)
这里实际还原出来的是 <<
运算,根据z中的数据猜测实际为 ^
能得到一个smc
key = 0
libc = ctypes.CDLL("libc.so.6")
_ptrace = libc.ptrace
key=_ptrace(0, 0, 1, 0)
_memcpy = libc.memcpy
key += 1
address=id(f1.__code__.co_code)+bytes.__basicsize__-1
codes=list(f1.__code__.co_code)
for i in range(len(codes)):codes[i]^=key
codes=bytearray(codes)
buff=(ctypes.c_byte*len(codes)).from_buffer(codes)
_memcpy(ctypes.c_char_p(address),ctypes.cast(buff,ctypes.POINTER(ctypes.c_char)),ctypes.c_int(len(codes)))
手动patch一下pyc文件,uncompyle6反编译后自己修复一下变量名,发现很多函数的逻辑很奇怪,根据刚才异或运算被解释成了左移运算,题目中的vm可能对基础运算符的opcode进行了相互的调换
通过使用的参数和函数的形式,猜测应该是个ECC算法,对其进行还原(这里只猜到了opcode被替换,但没有想到去哪里查看新的opcode)
肉眼还原了一下运算:把 &
换成 |
, |
换成 <<
,<<
换成 ^
,^
换成 +
,+
换成 %
,%
换成 -
,-
换成 *
,**
换成 //
,>>
换成 &
0a5n.py:
import L
from var import *
def check_format(flag):
if len(flag) != 28:
return False
for i in flag:
if i not in '0123456789abcdef':
return False
return True
v1 = L.c1(v1, v2, v3)
v6 = L.c2(v1, v4, v5)
k = input('flag:')
if check_format(k) == True:
v2 = L.f3(k)
v3 = v2 * v6
if v3.a2 == g1 and v3.a3 == g2:
print('Congratulations! n1ctf{%s}' % k)
L.py:
def inv_mod(b, p):
if b < 0 or p <= b:
b = b % p
c, d = b, p
uc, vc, ud, vd, temp = 1, 0, 0, 1, 0
while c != 0:
temp = c
q, c, d = d // c, d % c, temp
uc, vc, ud, vd = ud - q * uc, vd - q * vc, uc, vc
assert d == 1
if ud > 0:
return ud
else:
return ud + p
def leftmost_bit(x):
assert x > 0
result = 1
while result <= x:
result = 2 * result
return result // 2
class Curve(object): # c1
def __init__(self, p, a, b):
var4 = p
var4 ^= 0x10000000000000000000000000000000000000000L
self.p = var4
var5 = a
var5 -= 1
var5 //= 2
self.a = var5
var6 = b
var6 //= 2
var6 += 1
self.b = var6
def s1(self, x, y): # 判断是否在曲线上
return (y * y) - (x * x * x + self.a * x + self.b) % self.p == 0
class Point(object): # c2
def __init__(self, curve: Curve, x, y, order=None):
self.curve = curve
self.x = x
self.y = y
self.order = order
if self.a1:
assert self.a1.s1(x, y)
if order:
assert self * order == g1
def __eq__(self, other):
if self.curve == other.curve and self.x == other.x and self.y == other.y:
return True
else:
return False
def __add__(self, other):
if other == g1:
return self
if self == g1:
return other
assert self.curve == other.curve
if self.x == other.x:
if (self.y + other.y) % self.curve.p == 0:
return g1
return self.s1()
p = self.curve.p
l = other.y % self.y - inv_mod(other.x % self.x, p) + p
x3 = (l * l - self.x - other.x) % p
y3 = (l * (self.x - x3) - self.y) % p
return Point(self.curve, x3, y3)
def __mul__(self, other):
e = other
if self.order:
e = e + self.order
if e == 0:
return g1
if self == g1:
return g1
e3 = 3 * e
negative_self = Point(self.curve, self.x, -self.y, self.order)
i = leftmost_bit(e3) ** 2
result = self
while i > 1:
result = result.s1()
if e3 & i != 0 and e & i == 0:
result = result + self
if e3 & i == 0 and e & i != 0:
result = result + negative_self
i = i // 2
return result
def __rmul__(self, other):
return self * other
def s1(self): # double函数
if self == g1:
return g1
p = self.curve.p # 曲线的p
a = self.curve.a # 曲线的a
l = (3 * self.x * self.x + a) * inv_mod(2 * self.y, p) % p # 加法的lambda
x3 = (l * l) - (2 * self.x) % p # 加法的x_3
y3 = ((l * (self.x - x3)) - self.y) % p # 加法的y_3
return Point(self.curve, x3, y3)
g1 = Point(None, None, None) # g1是INFINITY
def f3(var0):
var1 = 0
for i in var0[::-1]:
var1 = (var1 << 4) | int(i, 16)
return var1
接下来只需要寻找 from var import *
中的 var
即可
根据pyinstxtractor.py的报错,发现 magic number 和 python3.5 差了1,于是找到报错的位置,将工具对 MAGIC_NUMBER
的检查去掉
if pyc_magic != pycHeader:
print('[!] Warning: This script is running in a different Python version than the one used to build the executable.')
print('[!] Please run this script in Python{0} to prevent extraction errors during unmarshalling'.format(self.pyver))
print('[!] Skipping pyz extraction')
# return
用python3.5进行解包,可以得到 var.pyc.encrypt
手动解密
import zlib
import tinyaes
key = 'nu1lnu1lnu1lnu1l'
obj = open('var.pyc.encrypted', 'rb').read()
cipher = tinyaes.AES(key.encode(), obj[:16])
obj = cipher.CTR_xcrypt_buffer(obj[16:])
obj = zlib.decompress(obj)
open('var.pyc', 'wb').write(obj)
得到ECC的曲线和点
p = 0xfffffffffffffffffffffffffffffffeffffac73
a = 0xfffffffffffffffffffffffffffffffeffffac71
b = 0x21
Px = 0xf6f8b692899e1b4c5c82580820c2c7cb5597e12e
Py = 0xafb7be2af28b649dab76337b42ee310119413529
Qx = 0x4945e0d8dc57e88d5949f84bf09943f572dbebb1
Qy = 0xb1bf040fe1939c7144341d3af61f36d63f47e272
上网抄了个sage实现的Pohlig-Hellman进行求解
p = 0xfffffffffffffffffffffffffffffffeffffac73
a = 0xfffffffffffffffffffffffffffffffeffffac71
b = 0x21
P = (0xf6f8b692899e1b4c5c82580820c2c7cb5597e12e, 0xafb7be2af28b649dab76337b42ee310119413529)
Q = (0x4945e0d8dc57e88d5949f84bf09943f572dbebb1, 0xb1bf040fe1939c7144341d3af61f36d63f47e272)
F = FiniteField(p)
E = EllipticCurve(F, [a, b])
P = E.point(P)
Q = E.point(Q)
print(factor(P.order()))
primes = [2^6, 5, 17, 79, 4457, 40591, 585977563, 1460624777797, 5490618741917]
dlogs = []
for fac in primes:
t = int(P.order()) // int(fac)
dlog = discrete_log(t*Q,t*P, operation="+")
dlogs += [dlog]
print("factor: "+str(fac)+", Discrete Log: "+str(dlog))
crt(dlogs, primes)
得到的结果计算十六进制并反转就是最后的flag
赛后查看了一下官方的WP,发现opcode就在 opcode.pyc
里面,因为一开始没有解包出来 opcode.pyc
所以就没有想到这里
"""
opcode module - potentially shared between dis and other modules which
operate on bytecodes (e.g. peephole optimizers).
"""
__all__ = [
'cmp_op', 'hasconst', 'hasname', 'hasjrel', 'hasjabs',
'haslocal', 'hascompare', 'hasfree', 'opname', 'opmap',
'HAVE_ARGUMENT', 'EXTENDED_ARG', 'hasnargs']
try:
from _opcode import stack_effect
__all__.append('stack_effect')
except ImportError:
pass
cmp_op = ('<', '<=', '==', '!=', '>', '>=', 'in', 'not in', 'is', 'is not', 'exception match',
'BAD')
hasconst = []
hasname = []
hasjrel = []
hasjabs = []
haslocal = []
hascompare = []
hasfree = []
hasnargs = []
opmap = {}
opname = [
''] - 256 # 这里也改了,应该是 *
for op in range(256):
opname[op] = '<%r>' + (op,)
del op
def def_op(name, op):
opname[op] = name
opmap[name] = op
def name_op(name, op):
def_op(name, op)
hasname.append(op)
def jrel_op(name, op):
def_op(name, op)
hasjrel.append(op)
def jabs_op(name, op):
def_op(name, op)
hasjabs.append(op)
def_op('POP_TOP', 1)
def_op('ROT_TWO', 2)
def_op('ROT_THREE', 3)
def_op('DUP_TOP', 4)
def_op('DUP_TOP_TWO', 5)
def_op('NOP', 9)
def_op('UNARY_POSITIVE', 10)
def_op('UNARY_NEGATIVE', 11)
def_op('UNARY_NOT', 12)
def_op('UNARY_INVERT', 15)
def_op('BINARY_MATRIX_MULTIPLY', 16)
def_op('INPLACE_MATRIX_MULTIPLY', 17)
def_op('BINARY_POWER', 26) #define BINARY_FLOOR_DIVIDE 26
def_op('BINARY_MULTIPLY', 24) #define BINARY_SUBTRACT 24
def_op('BINARY_MODULO', 23) #define BINARY_ADD 23
def_op('BINARY_ADD', 65) #define BINARY_XOR 65
def_op('BINARY_SUBTRACT', 22) #define BINARY_MODULO 22
def_op('BINARY_SUBSCR', 25)
def_op('BINARY_FLOOR_DIVIDE', 19) #define BINARY_POWER 19
def_op('BINARY_TRUE_DIVIDE', 20) #define BINARY_MULTIPLY 20
def_op('INPLACE_FLOOR_DIVIDE', 28)
def_op('INPLACE_TRUE_DIVIDE', 29)
def_op('GET_AITER', 50)
def_op('GET_ANEXT', 51)
def_op('BEFORE_ASYNC_WITH', 52)
def_op('INPLACE_ADD', 55)
def_op('INPLACE_SUBTRACT', 56)
def_op('INPLACE_MULTIPLY', 57)
def_op('INPLACE_MODULO', 59)
def_op('STORE_SUBSCR', 60)
def_op('DELETE_SUBSCR', 61)
def_op('BINARY_LSHIFT', 66) #define BINARY_OR 66
def_op('BINARY_RSHIFT', 27) #define BINARY_TRUE_DIVIDE 27
def_op('BINARY_AND', 63) #define BINARY_RSHIFT 63
def_op('BINARY_XOR', 62) #define BINARY_LSHIFT 62
def_op('BINARY_OR', 64) #define BINARY_AND 64
def_op('INPLACE_POWER', 67)
def_op('GET_ITER', 68)
def_op('GET_YIELD_FROM_ITER', 69)
def_op('PRINT_EXPR', 70)
def_op('LOAD_BUILD_CLASS', 71)
def_op('YIELD_FROM', 72)
def_op('GET_AWAITABLE', 73)
def_op('INPLACE_LSHIFT', 75)
def_op('INPLACE_RSHIFT', 76)
def_op('INPLACE_AND', 77)
def_op('INPLACE_XOR', 78)
def_op('INPLACE_OR', 79)
def_op('BREAK_LOOP', 80)
def_op('WITH_CLEANUP_START', 81)
def_op('WITH_CLEANUP_FINISH', 82)
def_op('RETURN_VALUE', 83)
def_op('IMPORT_STAR', 84)
def_op('YIELD_VALUE', 86)
def_op('POP_BLOCK', 87)
def_op('END_FINALLY', 88)
def_op('POP_EXCEPT', 89)
HAVE_ARGUMENT = 90
name_op('STORE_NAME', 90)
name_op('DELETE_NAME', 91)
def_op('UNPACK_SEQUENCE', 92)
jrel_op('FOR_ITER', 93)
def_op('UNPACK_EX', 94)
name_op('STORE_ATTR', 95)
name_op('DELETE_ATTR', 96)
name_op('STORE_GLOBAL', 97)
name_op('DELETE_GLOBAL', 98)
def_op('LOAD_CONST', 100)
hasconst.append(100)
name_op('LOAD_NAME', 101)
def_op('BUILD_TUPLE', 102)
def_op('BUILD_LIST', 103)
def_op('BUILD_SET', 104)
def_op('BUILD_MAP', 105)
name_op('LOAD_ATTR', 106)
def_op('COMPARE_OP', 107)
hascompare.append(107)
name_op('IMPORT_NAME', 108)
name_op('IMPORT_FROM', 109)
jrel_op('JUMP_FORWARD', 110)
jabs_op('JUMP_IF_FALSE_OR_POP', 112)
jabs_op('JUMP_IF_TRUE_OR_POP', 111)
jabs_op('JUMP_ABSOLUTE', 113)
jabs_op('POP_JUMP_IF_FALSE', 114)
jabs_op('POP_JUMP_IF_TRUE', 115)
name_op('LOAD_GLOBAL', 116)
jabs_op('CONTINUE_LOOP', 119)
jrel_op('SETUP_LOOP', 120)
jrel_op('SETUP_EXCEPT', 121)
jrel_op('SETUP_FINALLY', 122)
def_op('LOAD_FAST', 124)
haslocal.append(124)
def_op('STORE_FAST', 125)
haslocal.append(125)
def_op('DELETE_FAST', 126)
haslocal.append(126)
def_op('RAISE_VARARGS', 130)
def_op('CALL_FUNCTION', 131)
hasnargs.append(131)
def_op('MAKE_FUNCTION', 132)
def_op('BUILD_SLICE', 133)
def_op('MAKE_CLOSURE', 134)
def_op('LOAD_CLOSURE', 135)
hasfree.append(135)
def_op('LOAD_DEREF', 136)
hasfree.append(136)
def_op('STORE_DEREF', 137)
hasfree.append(137)
def_op('DELETE_DEREF', 138)
hasfree.append(138)
def_op('CALL_FUNCTION_VAR', 140)
hasnargs.append(140)
def_op('CALL_FUNCTION_KW', 141)
hasnargs.append(141)
def_op('CALL_FUNCTION_VAR_KW', 142)
hasnargs.append(142)
jrel_op('SETUP_WITH', 143)
def_op('LIST_APPEND', 145)
def_op('SET_ADD', 146)
def_op('MAP_ADD', 147)
def_op('LOAD_CLASSDEREF', 148)
hasfree.append(148)
jrel_op('SETUP_ASYNC_WITH', 154)
def_op('EXTENDED_ARG', 144)
EXTENDED_ARG = 144
def_op('BUILD_LIST_UNPACK', 149)
def_op('BUILD_MAP_UNPACK', 150)
def_op('BUILD_MAP_UNPACK_WITH_CALL', 151)
def_op('BUILD_TUPLE_UNPACK', 152)
def_op('BUILD_SET_UNPACK', 153)
del def_op
del name_op
del jrel_op
del jabs_op
这下就很舒服了(如果全都改乱了,就得写个脚本全改回去了,不过这样的话这个程序是不是也看不懂了)
[L3HCTF 2021]double-joy
恢复jmp表,发现是个vm,写一下反汇编
opcode = []
i = 0
index = 0
while i < len_op:
if i > 590:
break
cur_op = opcode[i]
if cur_op == 0:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] += memory[{index}]")
i += 1
elif cur_op == 1:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] = memory[{index}] - memory[{index - 1}]")
i += 1
elif cur_op == 2:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] *= memory[{index}]")
i += 1
elif cur_op == 3:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] = memory[{index}] / memory[{index - 1}]")
i += 1
elif cur_op == 4:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] %= memory[{index}] % memory[{index - 1}]")
i += 1
elif cur_op == 5:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] &= memory[{index}]")
i += 1
elif cur_op == 6:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] |= memory[{index}]")
i += 1
elif cur_op == 7:
index = index - 1
print ("%04x: " % i, f"memory[{index-1}] ^= memory[{index}]")
i += 1
elif cur_op == 8:
print ("%04x: " % i, f"memory[memory[{index-2}]] = memory[{index-1}]")
# print ("%04x: " % i, f"index -= 2")
index = index - 2
i += 1
elif cur_op == 9:
print ("%04x: " % i, f"memory[{index - 1}] = memory[memory[{index - 1}]]")
i += 1
elif cur_op == 10: #和case 11的情况一样
print ("%04x: " % i, f"memory[{index - 1}] = (1 if memory[{index - 1}] == 0 else 0)")
i += 1
elif cur_op == 11:
print ("%04x: " % i, f"memory[{index - 1}] = (1 if memory[{index - 1}] < 0 else 0)")
i += 1
elif cur_op == 12: #这个再仔细看看,不确定性有点高,脑袋有点糊
print ("%04x: " % i, f"memory[{index - 1}], memory[{index - 2}] = memory[{index - 2}], memory[{index - 1}]")
i += 1
elif cur_op == 13:
print("%04x: " % i, "index -= 1 # index = {index}")
index -= 1
i += 1 elif cur_op == 14:
print ("%04x: " % i, f"memory[{index}] = {int.from_bytes(opcode[i + 1: i + 5], 'little', signed=False)}") # print ("%04x: " % i, "i += 5")
index += 1
i += 5
elif cur_op == 15:
print("%04x: " % i, f"i = {hex((int.from_bytes(opcode[i + 1: i + 5], 'little', signed=False) + i + 5) & 0xffffffff)}")
# i = int.from_bytes(opcode[i + 1: i + 5], 'little', signed=False) + i + 5
i += 5 elif cur_op == 16:
print ("%04x: " % i, f"i += 5 + ({int.from_bytes(opcode[i + 1: i + 5], 'little', signed=False)} if memory[{index - 1}] != 0 else 0)")
index = index - 1
i += 5 elif cur_op == 17:
print("%04x: " % i, f"index += {int.from_bytes(opcode[i + 1: i + 5], 'little', signed=False)}")
# print("%04x i += 5" % i)
index += int.from_bytes(opcode[i + 1: i + 5], 'little', signed=False)
i += 5
elif cur_op == 18:
print("%04x: " % i, f"return (memory[{int.from_bytes(opcode[i + 1: i + 5], 'little', signed=False)}])")
i += 5
else:
print ("Error @ %04x" % i)
动调发现,总共有两个vm,flag的内存被共享,其他的状态分别保留,两个vm依次执行(能看到中间某个循环末尾有个return)分别是xtea加密和tea加密,相当于同时进行xtea和tea加密了写个求解脚本
需要注意是程序中为int类型,运算需要和题目中的一致(不能用位运算)
#include <stdio.h>
#include <stdint.h>
void decipher(int32_t* v) {
unsigned int i;
int const xtea_k[4] = {18764, 28534, 25888, 17237};
int v0, v1, xtea_delta=123456789, xtea_sum=987654321 + xtea_delta * 100;
int tea_k[4] = {21332, 20301, 8308, 25953};
int tea_delta=22334455; /* a key schedule constant */
int tea_sum=1592647341 + tea_delta * 100; /* set up */
for (int j = 8; j >= 0; j -= 2){
v0 = v[j]; v1 = v[j + 1];
for (i = 0; i < 20; i++) {
v1 -= ((v0<<4) + tea_k[2]) ^ (v0 + tea_sum) ^ ((v0 / 32) + tea_k[3]);
v0 -= ((v1<<4) + tea_k[0]) ^ (v1 + tea_sum) ^ ((v1 / 32) + tea_k[1]);
tea_sum -= tea_delta;
if (i == 19 && j == 0){
v0 ^= 0x1010101;
v1 ^= 0x1010101;
}
v1 -= (((v0 << 4) ^ (v0 / 32)) + v0) ^ (xtea_sum + xtea_k[(xtea_sum / 2048) & 3]);
xtea_sum -= xtea_delta;
v0 -= (((v1 *16 ) ^ (v1 / 32)) + v1) ^ (xtea_sum + xtea_k[xtea_sum & 3]);
if (i == 19 && j == 0){
v0 ^= 0x1010101;
v1 ^= 0x1010101;
}
} /* end cycle */
v[j]=v0; v[j + 1]=v1;
}
}
int main(){
int v[10] = {0xAEE0FAE8, 0xFC3E4101, 0x167CAD92, 0x51EA6CBE, 0x242A0100, 0x1511A1B, 0x514D6694, 0x2F5FBFEB, 0x46D36398, 0x79EEE3F0};
decipher(v);
for (int i = 0; i < 10; i+=2)
printf("%x %x\n",v[i],v[i + 1]);
return 0;
}
// [0x6c427530, 0x4d765f65, 0x7431575f, 0x4f645f68, 0x65496275, 0x4145545f, 0x7d]
// L3HCTF{D0uBle_vM_W1th_dOubIe_TEA} // 第一组失败了,猜测是flag头和D
[L3HCTF 2021]Load
这题使用了进程镂空技术(找时间研究室一下这一系列的技术),需要先把PE文件提取出来
动调进入镂空的函数,会发现一个对PE的比较,网上找可以找到MZ头(name字符串下面),dump下来即可
分析PE
首先提取了flag的中间部分,随后将其两两一组转化成16进制,共13字节
401070函数是将13字节拆成9和4字节分别存入src和v27,分别是33和22的矩阵
401370函数较大,且调用了一个递归函数
v11 = mul_matrix_4012A0(v17, v6) * a1[v5];
if ( (v5 & 1) != 0 )
v4 -= v11;
else
v4 += v11;
分析发现函数计算递归后与第一行的一个数字相乘,并根据奇偶进行加法和减法,重写该函数并验证后发现是求解矩阵行列式
再分析401370函数时发现,函数使用了一个数字除以行列式,猜测是用来求解逆矩阵的
调用完401370后就是验证部分,只需要满足所有等式即可
使用在线网站求解逆矩阵
flag = [-8, 18, -9, 6, -13, 6, -1, 2, -1, 13, -3, -30, 7]
for f in flag:
print ('%02x' % (f & 0xff), end='')
# f812f706f306ff02ff0dfde207
[hack.lu]pycoin
先使用uncompyle6反编译,发现执行了一串marshal字节码
将该字节码输出到文件,然后根据题目给的pyc补全文件头
再次反编译发现有花指令,开头和中间各有一个 jump_forward,中间还有两个连续的 rot_tow
这里尝试了一下新的patch方法:将花指令全替换成nop,但该方法的问题在于只能使用pycdc进行反编译,并不支持uncompyle6
个人感觉uncompyle6反编译效果更好,但适用的版本范围更窄,3.7及之前的版本较为合适 pycdc一直更新最新版本,但反编译效果较差,而且在细节上容易出问题,新版本只能使用pycdc,但最好结合pycdas的结果进行分析
另一种传统的patch方法就行把opcode去掉,然后把所有jmp修改到正确位置
patch后的结果如下:
最后反编译的结果为
from hashlib import md5
k = str(input('please supply a valid key:')).encode()
correct = len(k) == 16 and k[0] == 102 and k[1] == k[0] + 6 and k[2] == k[1] - k[0] + 91 and k[3] == 103 and k[4] == k[11] * 3 - 42 and k[5] == sum(k) - 1322 and k[6] + k[7] + k[10] == 260 and int(chr(k[7]) * 2) + 1 == k[9] and k[8] % 17 == 16 and k[9] == k[8] * 2 and md5(k[10] * b'a').digest()[0] - 1 == k[3] and k[11] == 55 and k[12] == k[14] / 2 - 2 and k[13] == k[10] * k[8] % 32 * 2 - 1 and k[14] == (k[12] ^ k[9] ^ k[15]) * 3 - 23 and k[15] == 125
print(f"valid key! {k.decode()}" if correct else 'invalid key :(')
然后一个z3就完事(tm k[5]约束了个寂寞)
from z3 import *
s = Solver()
k = [BitVec('k%d' % i, 8) for i in range(16)]
s.add(k[0] == 102)
s.add(k[1] == k[0] + 6)
s.add(k[2] == (k[1] - k[0]) + 91)
s.add(k[3] == 103)
s.add(k[4] == k[11] * 3 - 42)
s.add(k[11] == 55)
s.add(k[10] == 101)
s.add(k[15] == 125)
s.add(k[5] == sum(k) - 1322)
s.add(k[6] + k[7] + k[10] == 260)
s.add(k[7] > 0x30)
s.add(k[7] < 0x40)
s.add((k[7] - 0x30) * 11 + 1 == k[9])
s.add(k[8] % 17 == 16)
s.add(k[9] == k[8] * 2)
s.add(k[12] == k[14] / 2 - 2)
s.add(k[13] == (k[10] * k[8] % 32) * 2 - 1)
s.add(k[14] == (k[12] ^ k[9] ^ k[15]) * 3 - 23)
if s.check():
model = s.model()
for i in range(16):
if i == 5: # 这个位置根本没有约束,最后官方直接给出来了
continue
print (chr(model[k[i]].as_long()), end='')
else:
print ("No result")
[hack.lu]atareee
atareee.atstate2
是内存状态,对其进行binwalk,可以得到 memory.bin
和 savestate.json
文件
根据分析可以得知是 6052
架构,用Ghidra打开,根据题目中提示的 @061A
找到验证逻辑的地址
加密函数和验证函数如下:
void FUN_529e(void) {
byte bVar1;
byte bVar2;
byte bVar3;
byte in_C;
bVar3 = 0;
do {
write_1(PORTA,bVar3);
bVar1 = read_1(PORTA);
if ((bVar1 & 1) == 0) {
BYTE_ARRAY_5234[bVar3] = BYTE_ARRAY_50c2[bVar3] ^ BYTE_ARRAY_50c2[(byte)(bVar3 + 1)];
bVar2 = read_1(PORTA);
BYTE_ARRAY_5234[bVar3] = BYTE_ARRAY_5234[bVar3];
bVar1 = BYTE_ARRAY_5234[bVar3];
BYTE_ARRAY_5234[bVar3] = bVar1 << 1 | CARRY1(bVar2,BYTE_ARRAY_50c2[bVar3]);
}
else {
BYTE_ARRAY_5234[bVar3] = BYTE_ARRAY_50c2[bVar3] ^ BYTE_ARRAY_5219[(byte)(bVar3 - 1) + 1];
bVar1 = BYTE_ARRAY_5234[bVar3];
BYTE_ARRAY_5234[bVar3] = bVar1 << 1 | in_C;
}
if ((bool)(bVar1 >> 7)) {
BYTE_ARRAY_5234[bVar3] = BYTE_ARRAY_5234[bVar3] + '\x01';
}
bVar3 = bVar3 + 1;
in_C = 0x19 < bVar3;
} while (bVar3 != 0x1a);
return;
}
undefined FUN_52e8(void) {
byte bVar1;
bVar1 = 0;
do {
if (BYTE_ARRAY_5200[bVar1] != BYTE_ARRAY_5234[bVar1]) {
FUN_531d();
bVar1 = 0;
do {
BYTE_ARRAY_509a[bVar1] = s__NICE_TRY,_MAYBE_NEXT_TIME_ITS_C_5276[bVar1];
bVar1 = bVar1 + 1;
} while (bVar1 != 0x28);
return 0;
}
bVar1 = bVar1 + 1;
} while (bVar1 != 0x1a);
bVar1 = 0;
do {
BYTE_ARRAY_509a[bVar1] = s__GOOD_JOB!_ENJOY_THOSE_REVERSING_524e[bVar1];
bVar1 = bVar1 + 1;
} while (bVar1 != 0x28);
return 0;
}
通过修改 savestate.json
的PC位置(EIP)即可进行动调,发现输入被存入 50c2
中,复现该函数并爆破即可得到flag
target = [
0x14, 0x1E, 0xC, 0xE0,
0x30, 0x5C, 0xCE, 0xF0,
0x36, 0xAE, 0xFC, 0x39,
0x1A, 0x91, 0xCE, 0xB4,
0xC4, 0xE, 0x18, 0xF3,
0xC8, 0x8E, 0xA, 0x85,
0xF6, 0xbd
]
array_50c2 = [
0xD9, 0x50, 0x48, 0xB9,
0xD8, 0x50, 0x48, 0x60,
0x46, 0x54, 0x43, 0x44,
0x45, 0x49, 0x50, 0x55,
0x52, 0x53, 0x4C, 0x47,
0x58, 0x51, 0xF3, 0x50,
0x8, 0x51, 0x10
]
array_5219 = [
0xBD, 0x43, 0x11, 0x37,
0xF2, 0x69, 0xAB, 0x2C,
0x99, 0x13, 0x12, 0xD1,
0x7E, 0x9A, 0x8F, 0xE,
0x92, 0x37, 0xF4, 0xAA,
0x4D, 0x77, 0x3, 0x89,
0xCA, 0xFF,
]
array_5234 = [0 for _ in range(0x1a)]
in_C = 0
j = 0
for i in range(0x19, -1, -1):
in_C = 1 if (0x19 < i + 1) else 0
for j in range(0x30, 0x60):
array_50c2[i] = j
var1 = i
var2 = 0
if var1 & 1 == 0:
array_5234[i] = array_50c2[i] ^ array_50c2[i + 1]
var2 = i
var1 = array_5234[i]
array_5234[i] = ((var1 << 1) | ((array_50c2[i] + i) >> 7)) & 0xff
else:
array_5234[i] = array_50c2[i] ^ array_5219[i]
var1 = array_5234[i]
array_5234[i] = ((var1 << 1) | in_C) & 0xff
if var1 >> 7 != 0:
array_5234[i] = array_5234[i] + 1
if array_5234[i] == target[i]:
print (chr(j), end= '')
break
else:
print ("no")