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 求解时需要对两点进行改动:

  1. 在 ptrace 上下钩子排除它的干扰 否则 angr 会添加一个其返回值不确定的符号从而导致 opcode 不固定爆出多余分支
  2. 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}