RITSECCTF2022
TagAlong #
执行后提示:
something is tagging along this PE file...
47
根据提示 flag 应该藏在 PE 文件里面,用 ResourceHacker 打开之
这里面有很多 0x47,而一般来说一个二进制里最多的是 0,将这一整段 XOR 0x47 得到 flag
b'\xfcH\x83\xe4\xf0\xe8\xc0\x00\x00\x00AQAPRQVH1\xd2eH\x8bR`H\x8bR\x18H\x8bR H\x8brPH\x0f\xb7JJM1\xc9H1\xc0\xac<a|\x02, A\xc1\xc9\rA\x01\xc1\xe2\xedRAQH\x8bR \x8bB<H\x01\xd0\x8b\x80\x88\x00\x00\x00H\x85\xc0tgH\x01\xd0P\x8bH\x18D\x8b@ I\x01\xd0\xe3VH\xff\xc9A\x8b4\x88H\x01\xd6M1\xc9H1\xc0\xacA\xc1\xc9\rA\x01\xc18\xe0u\xf1L\x03L$\x08E9\xd1u\xd8XD\x8b@$I\x01\xd0fA\x8b\x0cHD\x8b@\x1cI\x01\xd0A\x8b\x04\x88H\x01\xd0AXAX^YZAXAYAZH\x83\xec AR\xff\xe0XAYZH\x8b\x12\xe9W\xff\xff\xff]H\xba\x01\x00\x00\x00\x00\x00\x00\x00H\x8d\x8d\x01\x01\x00\x00A\xba1\x8bo\x87\xff\xd5\xbb\xf0\xb5\xa2VA\xba\xa6\x95\xbd\x9d\xff\xd5H\x83\xc4(<\x06|\n\x80\xfb\xe0u\x05\xbbG\x13roj\x00YA\x89\xda\xff\xd5msg * RS{SH3LLCODE_T4GS_4LONG}\x00'
RS{SH3LLCODE_T4GS_4LONG}
Soup #
以 arg1 为 key,arg2 为明文做 RC4 加密
问题在于我们不知道 key,根据题目名字尝试 soup, Soup, SOUP 为 key
RS{BR0CC0L1_CH3DD@R}
DataFun #
函数 sub_1588 中有一个未被修复的跳转表,修复之可以看出这是一个虚拟机
综合位于 main 函数中初始化的函数,以及 sub_1588 中对 vm 进行操作的小函数,可以复原出 vm 的数据结构
struct vm_t
{
int sz;
int field_4;
unsigned __int8 *buf;
int top;
int field_14;
};
显然这是一个栈
opcode 长度为 139
需要控制输入,使得通关后“栈”上残留的值为 R3V3RS1NG_1S_E4SY
load 函数使用了 PTRACE_TRACEME 反调试
在有调试器的情况下会将 opcode XOR 0x36 而不是 0x37
这题的 opcode 并不复杂 可以考虑使用 angr 求解,在 angr 求解时需要对两点进行改动:
- 在 ptrace 上下钩子排除它的干扰 否则 angr 会添加一个其返回值不确定的符号从而导致 opcode 不固定爆出多余分支
- run 函数中遇到运算错误会跳转到一个纠错分支即在栈上插入 0xAA 这个多余的分支每取一次指令就会产生一次,因此每一个 state 经过一遍循环就会导出两个后继,时间复杂度从无分支时的 O(n) 变成了 O(2^n) 这显然是跑不完的
共有 38 次读取单字节的操作即输入应长 38
将 stdin 设置为 38 字节的位符号,并在 ptrace 上下钩子 MutePtrace,总是返回 0
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
import angr
import claripy
import logging
logging.getLogger('angr.sim_manager').setLevel(logging.DEBUG)
base_addr = 0x400000
proj = angr.Project(
"data_fun", main_opts={"base_addr": base_addr}, auto_load_libs=False
)
class MutePtrace(angr.SimProcedure):
def run(self, *args, **kwargs):
print("ptrace muted")
return claripy.BVV(0, 32)
proj.hook_symbol("ptrace", MutePtrace(), replace=True)
flags = [claripy.BVS("flag%d" % i, 8) for i in range(38)] + [claripy.BVV(b"\n")]
flag = claripy.Concat(*flags)
在执行 run 函数的过程中由于 opcode 固定 不会产生除了纠错分支外的其他分支
所以一旦遇到大于一个分支的状态且地址为纠错分支的地址 (0x165C, 0x1748, 0x182B) 就消减掉它(因为只是求任意一个通关解,所以总是可以期待在不出现纠错分支的情况下存在这样的解)
除此之外还需要规避逻辑上输出失败的分支 (0x1A5F)
begin = proj.factory.entry_state(stdin=flag, add_options=angr.options.unicorn)
simgr = proj.factory.simgr(begin)
avoid_addrs = [0x165C, 0x1748, 0x182B]
true_addr = 0x1A45
false_addr = 0x1A5F
def step_func(lsm):
lsm.stash(
filter_func=lambda state: state.addr - base_addr in avoid_addrs,
from_stash="active",
to_stash="avoid",
)
lsm.stash(
filter_func=lambda state: state.addr - base_addr == false_addr,
from_stash="active",
to_stash="avoid",
)
lsm.stash(
filter_func=lambda state: state.addr - base_addr == true_addr,
from_stash="active",
to_stash="found",
)
return lsm
simgr.explore(step_func=step_func, until=lambda lsm: len(lsm.found) > 0)
end = simgr.found[0]
solved = end.solver.eval(flag, cast_to=bytes)
最后得到一组合法输入
b'R6\xfd\x7f\xaa3\x00RJ\xf76\xdf\xad\x1d\x07_\x80\x801b\x11\xe0\x0c\xbdA\x01\x02\x03\x04\x05\x06ik9SH\xf9\x00\n'
本地验证
import subprocess
p = subprocess.Popen("./data_fun", stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
stdout, stderr = p.communicate(solved + b"\n")
print(stdout.decode())
和提供的靶机互动并得 flag
RS{R3V3RS1NG_4ND_ST4CK5_H0W_E4SY}
SSHBackdoor #
Get back into the server as user "alan".
由标准 sshd 魔改而来,文件很大,所以先看字符串
RITSEC 唯一引用在函数 sub_12200 是个循环 OTP
sub_12200 的唯一引用在 sub_7ECD0
该函数下面是一堆 krb5 开头的调用,显然是 SSH 的 Kerberos 流程
然而如果满足某个条件该函数可以提前结束返回 1
逆向 sub_120E0 可以发现它是一个标准的 RC4
密钥长度 10 密文长度 31
密钥生成函数是 sub_12290
知道 v3 是 22 之后可以马上猜出 sub_14ac0 为 TCP 连接
从而可以知道密钥是连接 SSH 之后返回数据的前 10 个字符
nc 127.0.0.1 22 | head -c 10
SSH-2.0-Op
根据以上信息写解密函数
from Crypto.Util.number import bytes_to_long as b2l, long_to_bytes as l2b
# RC4 Impl
def ksa(key):
j, sbox = 0, list(range(256))
for i in range(256):
j = (j + key[i % len(key)] + sbox[i]) & 255
sbox[i], sbox[j] = sbox[j], sbox[i]
return sbox
def prga(sbox):
i, j = 0, 0
while True:
i = (i + 1) & 255
j = (j + sbox[i]) & 255
sbox[i], sbox[j] = sbox[j], sbox[i]
s = sbox[(sbox[i] + sbox[j]) & 255]
yield s
s31=b'\xc7\x97h\xc7\xf2\xad\x95\x0b\x98\x15\xd1\xbc\x15Il\xe3#\x96\xa7OA\xae\xd6H\x11O\xb4f\xb6\x83\xeb'
b10=b'SSH-2.0-Op'
K=b'RITSEC'
k10=bytes(K[i%6]^b10[i] for i in range(10))
rc4=prga(ksa(k10))
passwd=bytes(s31[i]^next(rc4) for i in range(31))
print(passwd)
# VEGA INTERNATIONAL NIGHT SCHOOL
RS{psych1c_ch45m5_4q41t_y0u}