乱七八糟的考试终于结束了,这次是时隔半年重新回来和AAA的队友们一起打比赛,又回归了熬夜的生活。这次比赛6个Reverse做出5个,剩下的一个RL_Env貌似是个机器学习题,真不愧是自由而无用的灵魂,我甚至都没有看懂题目在做什么。

stream

rust题目,还好有符号,拖到IDA里打开,std::rt::lang_start_internal的第一个参数是rust里的主函数。

 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
do
    {
      v5 = v1;
      v6 = v2 % v1;
      *((_BYTE *)&v13 + (v4 & 0x1F)) = *(_BYTE *)(v3 + v6);
      *(_OWORD *)&v9[16] = v14;
      *(_OWORD *)v9 = v13;
      rand_chacha::guts::init_chacha::ha09fa0e953c784d5(v19, v9, "", 8LL);
      *(_OWORD *)&v9[40] = v20;
      *(_OWORD *)&v9[24] = v19[1];
      *(_OWORD *)&v9[8] = v19[0];
      v15 = *(_OWORD *)v9;
      v16 = *(_OWORD *)&v9[16];
      v17 = *(_OWORD *)&v9[32];
      v18 = *((_QWORD *)&v20 + 1);
      v21 = 64LL;
      v22[15] = 0LL;
      v22[14] = 0LL;
      v22[13] = 0LL;
      v22[12] = 0LL;
      v22[11] = 0LL;
      v22[10] = 0LL;
      v22[9] = 0LL;
      v22[8] = 0LL;
      v22[7] = 0LL;
      v22[6] = 0LL;
      v22[5] = 0LL;
      v22[4] = 0LL;
      v22[3] = 0LL;
      v22[2] = 0LL;
      v22[1] = 0LL;
      v22[0] = 0LL;
      v24 = *((_QWORD *)&v20 + 1);
      v23[2] = *(_OWORD *)&v9[32];
      v23[1] = *(_OWORD *)&v9[16];
      v23[0] = *(_OWORD *)v9;
      rand_chacha::guts::refill_wide::h2ceeff1c2be61c9f((char *)v23 + 8, 10LL, v22);
      v21 = 1LL;
      *(_BYTE *)(v3 + v6) ^= LOBYTE(v22[0]);
      ++v4;
      v2 += 7LL;
      v1 = v5;
    }
    while ( v5 != v4 );

读入文件后主要加密过程在这个循环中完成。试着写了两行rust,发现rand_chacha::guts::{init_chacha, refill_wide}两个函数都不是pub的,说明并非直接调用这两个函数,而是编译时内联了进来。看guts.rs的源码可知refill_wide的第二个参数是round,可以发现10轮的是同一个包里的ChaCha20Rng。IDA调试一下可知生成的随机字节填充在v22的buffer里,最后*(_BYTE *)(v3 + v6) ^= LOBYTE(v22[0]);表示只用了第一个字节异或到原先的明文上。上面的init_chachaChaCha20Rng::from_seed展开而成,key是v9开始的32字节。*((_BYTE *)&v13 + (v4 & 0x1F)) = *(_BYTE *)(v3 + v6);表示每次从明文取一个字节,循环放入key中,最终生成的伪随机字节重新和这个字节异或,那么每轮只有256种可能性,可以爆破。从明文取字节的顺序是4,11,18…最开始的时候key全为0,只放入一个明文字节爆破第一个字节后继续爆破第二个,依次类推,最后有多解情况,把flag限制在ascii内减少尝试。

由于我不会rust,又没有找到合适的等价实现,就用rust包装了一个随机数生成程序,主程序用Python实现。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
use std::env;
use rand_chacha::rand_core::{RngCore,SeedableRng};
use rand_chacha::ChaCha20Rng;

fn main() {
    let mut key:[u8;32]=[0;32];
    let args: Vec<String> = env::args().collect();
    let arg1=&args[1];
    for i in 0..32 {
        let x = u8::from_str_radix(&arg1[i*2..i*2+2], 16).unwrap();
        key[i]=x;
    }
    let mut result=[0u8;256];
    let mut rng=ChaCha20Rng::from_seed(key);
    rng.fill_bytes(&mut result);
    println!("{}",result[0]);
}
 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
import copy
import os

TARGET = [154, 122, 66, 146, 60, 227, 27, 164, 184, 114, 150, 47, 1, 152, 94, 41, 34, 245, 154, 20, 13, 76,
          24, 42, 56, 65, 212, 243, 169, 232, 96, 238, 218, 3, 220, 28, 161, 134, 18, 254, 24, 66, 137, 128, 145, 220]

def do_rust(key):
    s = ''.join(map(lambda x: '{:02x}'.format(x), key))
    p = os.popen('./target/debug/st {}'.format(s), 'r')
    return int(p.readline())

def solve(s, index, pos, key):
    if(index == 46):
        print(''.join(map(chr, s)))
        return
    for x in range(256):
        newkey = copy.copy(key)
        newkey[index % 32] = x
        r = do_rust(newkey)
        if(x ^ r == TARGET[pos % 46]):
            print(index)
            news = copy.copy(s)
            news[pos % 46] = x
            solve(news, index+1, pos+7, newkey)

solve([0]*46, 0, 4, [0]*32)

flag:*ctf{EbXZCOD56vEHNSofFvRHG7XtgFJXcUXUGnaaaaaa}

wherekey

从网上找到一个libc 2.31的signature安上去,主函数花指令处jnz改为jmp可以反汇编。发现这个程序中间还有一个listen和send,自己给自己发送数据来增加工作量。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
    v2[dword_4C8570 / 64] |= 1LL << (dword_4C8570 % 64);
    v2[dword_4C8520 / 64] |= 1LL << (dword_4C8520 % 64);
    sub_44E080(1024LL, v2, 0LL, 0LL, 0LL);
    if ( (v2[dword_4C8520 / 64] & (1LL << (dword_4C8520 % 64))) != 0 )
      sub_40223C();
    if ( (v2[dword_4C8570 / 64] & (1LL << (dword_4C8570 % 64))) != 0 )
    {
      v0 = ((__int64 (__fastcall *)(void *))((char *)&sub_401D44 + 1))(&unk_4C5120);
      sub_411D40(v0);
      sub_402072();
      v1 = ((__int64 (__fastcall *)(void *))((char *)&sub_401D44 + 1))(&unk_4C5130);
      sub_411D40(v1);
    }

上面的或操作是宏FD_SET(fd, set)展开后的代码,下面的与操作是FD_ISSET宏,都是和select一起使用的。tty收到数据后交给sub_402072()处理,看出发送的数据包应该是5个5字节的。看sub_40223C()处理socket收到的数据包,该数据包又交给sub_4022DE(__int64 a1)处理,每5个字节和’flag{are_you_sure_friend}‘抠出来的字节相乘,实际上是一个线性方程组,常量字符串是系数矩阵,共有5组线性方程组。去掉花指令后找到比较对象是unk_4C5150。翻出自己的陈年老代码用高斯消去法找出逆矩阵,再分别和5个向量相乘,即可得出明文。

 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
FL = 'flag{are_you_sure_friend}'

EP = [[ord(FL[5*i+j]) for i in range(5)]for j in range(5)]

INV = [0 for i in range(257)]

for i in range(257):
    for j in range(257):
        if (i*j) % 257 == 1:
            INV[i] = j
            break

EXT = [EP[i]+[1 if i == j else 0 for j in range(5)]for i in range(5)]

def show(martix):
    for i in range(5):
        print(martix[i])

def add(martix, k, line1, line2):
    for i in range(10):
        martix[line2][i] = (k*martix[line1][i]+martix[line2][i]) % 257

def mul(martix, x, line):
    for i in range(10):
        martix[line][i] = (martix[line][i]*x) % 257

def swap(martix, line1, line2):
    for i in range(20):
        martix[line1][i], martix[line2][i] = martix[line2][i], martix[line1][i]

def mulline(martix, x):
    y = [0]*5
    for i in range(5):
        for j in range(5):
            y[i] += martix[i][j]*x[j]
            y[i] = (y[i]) % 257
    return y

for i in range(5):
    if INV[EXT[i][i]] == 0:
        for k in range(i+1, 5):
            if INV[EXT[k][i]] != 0:
                swap(EXT, i, k)
                break
        else:
            raise ValueError
    mul(EXT, INV[EXT[i][i]], i)
    for j in range(5):
        if j == i:
            continue
        add(EXT, (-EXT[j][i]) % 257, i, j)

DP = [EXT[i][5:] for i in range(5)]

show(DP)

QUESTION = [0x38, 0x6D, 0x4B, 0x4B, 0xB9, 0x8A, 0xF9, 0x8A, 0xBB, 0x5C, 0x8A, 0x9A,
            0xBA, 0x6B, 0xD2, 0xC6, 0xBB, 0x05, 0x90, 0x56, 0x93, 0xE6, 0x12, 0xBD, 0x4F]

s = ''
for i in range(5):
    q = QUESTION[i*5:i*5+5]
    s += ''.join(map(chr, mulline(DP, q)))
print(s)

flag:*CTF{Ha23_f0n_9nd_G0od-1uck-OH}

ChineseGame

同样是静态链接,找到libc和libstdc++的signature安上。有一个10元素的链表,链表节点存放了一个数,只需要关心这个数是否大于100,若将大于100简记作+,小于100为-,初始状态是+-++++++++,最后要求全为-。输入内容是01字符串,分别用来选两种操作,冥冥之中感觉像是九连环游戏。每种操作都有一个位置参数,从dword_5D5140开始取。两种操作的内容是:如果该位置后面节点的状态是+---…(或者根本没有后面的节点),则将该位置变为+/-,否则什么都不干。经队友提示,发现dword_5D5140的位置参数恰好是正确解法需要操作的位置,所以只需找出进行此操作时是由+翻转到-还是-翻转到+即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
a = [1, 3, 1, 2, 1, 5, 1, 2, 1, 3, 1, 2, 1, 4, 1, 2, 1, 3,
    ...
     1, 2, 1, 3, 1, 2, 1]

s = ''
init = [1, 0]+[1]*8
for x in a:
    x = 10-x
    if(init[x] == 1):
        s += '0'
    else:
        s += '1'
    init[x] = 1-init[x]
print(init,s)

flag:*CTF{4ncient_G4me_Fr0m_4ncient_Ch1na!}

Favourite Architecure flag0

加入此题之前,队友已经逆向了一部分。flag长度为0x59,前0x29字节进行Chacha20加密(从函数的密钥扩展中的常量字符串’expand 32-byte k'看出。同样由D. J. Bernstein设计的Salsa20、Ed25519、Ed448都有特殊的字符串),提取固定的key和ciphertext解密即可。后0x30字节是TEA加密,但被修改成了16轮,维基百科上有如下示例代码:

 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
#include <stdint.h>

void encrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0, i;           /* set up */
    uint32_t delta=0x9e3779b9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i < 32; i++) {                       /* basic cycle start */
        sum += delta;
        v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);  
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

void decrypt (uint32_t* v, uint32_t* k) {
    uint32_t v0=v[0], v1=v[1], sum=0xC6EF3720, i;  /* set up */
    uint32_t delta=0x9e3779b9;                     /* a key schedule constant */
    uint32_t k0=k[0], k1=k[1], k2=k[2], k3=k[3];   /* cache key */
    for (i=0; i<32; i++) {                         /* basic cycle start */
        v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
        v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
        sum -= delta;                                   
    }                                              /* end cycle */
    v[0]=v0; v[1]=v1;
}

decrypt中的sum=0xC6EF3720是加密时sum每轮加上delta得出的累积值,若轮数减少为16轮,则sum=(0x9e3779b9*16)&0xffffffff=0xe3779b90,修改后解密即可。

flag:flag{have_you_tried_ghidra9.2_decompiler_if_you_have_hexriscv_plz_share_it_with_me_thx:P}

1rep

这是一个perlcc编译perl产生的程序,自己搞了一个perl脚本试着编译了一下,发现所有的这种程序的主函数都依次调用如下函数:

1
2
3
4
5
6
  perl_init(v20);
  perl_init1(v20);
  dl_init(v20);
  perl_init2(v20);
  *((_DWORD *)&_0 - 28) = perl_run(v20);
  destruct_main(v20);

从自己编译的源码中看出,v20是一个PerlInterpreter*,说明这种编译器实际上不是像cython那样把脚本代码转为等价的C代码,而是预先设置好变量、字节码之后,整体交给perl虚拟机运行,此处的perl虚拟机实例就是这个PerlInterpreter对象。最初的解题思路是借助调试器的appcall机制,设法从这个虚拟机实例里提取字节码。发现libperl里有一个perl_dump_all(),gdb在perl_run设好断点后,将rip改为perl_dump_all的地址并继续,结果dump出一大堆optree,依旧没法看。后来发现perl的B::Deparse模块有反编译功能,参考StackOverflow上的提问,利用Perl_eval_pvPerlInterpreter里注入代码运行。

1
use B::Deparse; B::Deparse->compile->()

但是注入以上代码后,只反编译了开始的一小点检查。flag去掉外壳后,里面16字节交给kwvIJu检查。再注入B::Deparse的其他功能反编译特定函数:

1
2
3
4
use B::Deparse;
$deparse = B::Deparse->new("-sC");
$body = $deparse->coderef2text(\&func);
print "$body\n";

不知道为什么kwvIJu没法反编译,但是试着反编译别的函数发现,所有函数都是检查当前flag的首字符(位于0-f范围),然后根据此字符switch到16个不同的分支,每个分支再调用不同的其他函数,调用的函数用来处理去掉首字符后剩下的flag。观察IDA中存放字符串的地方,发现.rodata:00000000032E6CAF夹杂着一个Correct,试着反汇编上方的afRNDz,发现这个函数是打印Correct,所以需要找出是哪个函数经switch语句调用了afRNDz。试着再反汇编上方的xmecUK,发现它调用了afRNDz,再继续往上翻找,可以确定flag的最后14字节,但是后面再往上就找不到调用这一串函数的函数了,只好爆破最前面两字节,侥幸拿到flag。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from pwn import *

for x in range(16):
    for y in range(16):
        s = '*ctf{'+hex(x)[2:]+hex(y)[2:]+'4bb0982b39baa2}'
        assert(len(s) == 22)
        r = process('./bin')
        r.sendline(s)
        t = r.recvall()
        print(t)
        if 'Correct' in t:
            print(s)

flag:*ctf{7a4bb0982b39baa2}