This page looks best with JavaScript enabled

“西湖论剑” 2023 Reverse WriteUp

 ·  ☕ 6 min read · 👀... views

校队参赛,Re方向拿了一个一血一个二血。这次Re题目质量还可以,没有那种特别恶心的乱七八糟算法来恶心人,考的都是一些针对性的知识点。

Dual personality

经典天堂之门反调试题。算法很简单,主要依赖天堂之门干扰动态调试,花指令+SMC反IDA静态分析。整体处理方法:CE调试+对比各部分加密前后内容,对照CE代码修复IDA代码段进行静态分析。

加密过程分三个阶段

第一部分

scanf后下硬断直接追出来是这一部分

1

然后这里有个注意点是 dword_407058 这个常量,它会在前面的x64代码中被赋予一个初始值,并有一个反调试。可以跟随到前面的天堂之门跳转中,看到x64代码检测了PEB.BeingDebugged位,若该位为0则会赋予初始值0x5DF966AE,这个才是正确的初始值
1

还原出加密流程的伪代码

1
2
3
4
5
6
7
key = 0x3CA7259D
flag = [0x64636261, 0x68676665, 0x6C6B6A69, 0x706F6E6D, 0x74737271, 0x78777675, 0x31307A79, 0x35343332]
for i in range(len(flag)):
    flag[i] = (flag[i] + key) & 0xFFFFFFFF
    key ^= flag[i]
    print("flag[%d] = %s" % (i, hex(flag[i])))
    print("key =", hex(key))

马上可以写出这一部分的exp脚本

1
2
3
4
5
6
7
8
9
for (int i = 0; i < 8; ++i){
    DWORD bak = ans[i];
    ans[i] = (ans[i] - key) & 0xFFFFFFFF;
    key ^= bak;
}

BYTE *p = (BYTE *)ans;
for(int i=0;i<0x20;++i)
    printf("%c", *(p+i));

第二部分

然后进入第二部分,对应这个call
2

这里由于段选择子被切换调试不了,对着该函数修复代码段后F5大致还原出函数内容
3

修出来的逻辑很乱看不太明白,就直接对照变换前后内容写了转化脚本
4
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
def rolCustomBit(v,bit,shift):
    shift &= (bit-1)
    if shift == 0:
        return v
    HightBit = v >> (bit - shift)
    LowBit = v << shift
    l = [x for x in range(4, bit + 1) if x % 4 == 0]
    LCount = len(l)
    _ = '0x' + 'F' * LCount
    FfValue = int(_, 16)
    Value = (HightBit | LowBit) & FfValue
    return Value
def rorCustomBit(v, Bytebit, shift):
    shift &= (Bytebit - 1)  # 按照bit值 来进行设置移动位数
    if shift == 0:
        return v
    a1 = (v >> shift)  # 右移shift位 空出高位shift位
    a2 = (v << ((Bytebit) - shift))  # 计算出剩下要移动的位数
    l = [x for x in range(4, Bytebit + 1) if x % 4 == 0]
    LCount = len(l)
    _ = '0x' + 'F' * LCount
    FfValue = int(_, 16)
    value = (a2 | a1) & FfValue
    return value
flag=[0xA88D38AE1E1B36E0,0x34065C283209F8B8,0x140A04D73A0730AA,0x848BC435808F8217,]
for i in range(len(flag)):
    if i==0:
        print(hex(rorCustomBit(flag[i],64,12)))
    if i==1:
        print(hex(rorCustomBit(flag[i],64,34)))
    if i==2:
        print(hex(rorCustomBit(flag[i],64,56)))
    if i==3:
        print(hex(rorCustomBit(flag[i],64,14)))

第三部分

通过切段选择子jmp到sub_401291初始化异或的key
6

然后回来继续执行,此时段选择子没有恢复所以依然无法调试,但可以通过IDA修复代码段看出做了什么
7

IDA对0x40146E建立函数,可以看到就是使用前面获得的四字节key异或
8

完事后memcpy校验flag。故可以写出第三部分exp

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DWORD right_key[8] = {0xE20F4FAA, 0x549941E4, 0x7E842B2C, 0x788B8FBC, 0x5E8873D3, 0x708547AE, 0xCE09B331, 0xCA0DF513};
BYTE key_part3[] = {0x4, 0x77, 0x82, 0x4A};
int main() {
    BYTE *p = (BYTE *)right_key;
    for (int i = 0; i < 0x20; ++i){
        *(p+i) ^= key_part3[i%4];
        printf("0x%02X\n", *(p+i));
    }
    for (int i = 0; i < 0x8; ++i)
        printf("0x%X, ", right_key[i]);
    return 0;
}

最终得到flag:6cc1e44811647d38a15017e389b3f704

Berkeley

队友拿了一血,太猛了!

程序加载了一段bpf,其中包含两个函数LBB0_1和LBB0_2,分别注册为check_flag的uprobe和uretprobe,因此check_flag中的代码实际上是没用的,真正的校验在这两个函数里。将bpf代码提出出来。
Berkeley1

用ghidra以及Nalen98/eBPF-for-Ghidra插件将bpf文件打开。两个函数反编译结果如下
Berkeley2
Berkeley3

写出伪代码如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
int check(char*flag){
 unsigned char output[32];
 for (int i=0; i<256; i++) {
  unsigned char uc1 = flag[i/8];
  unsigned char uc2 = ~(flag[i/8] + arr[i%8]);
  output[i] = key[uc1 ^ uc2];
 }
 for (int i=0; i<256; i++) {
  output[i] = key[output[i] ^ key[i]];
 }
 for (int i=0; i<256; i++) {
  if(output[i]!=cipher[i]){
   printf("error!");
   exit(1);
  }
 }
}

其中arr是在访问地址为0的地方,我并不知道他是什么,但flag是逐字节检验的,所以patch bpf中校验的字符数量,然后爆破。下图为patch位置:
Berkeley4

bpf_trace_printk的输出可以通过/sys/kernel/debug/tracing/trace_pipe管道读取

写出exp如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
import os, string
from pwn import *
with open('Berkeley','rb') as f:
    c=f.read()
flag=''
p=process(['cat', '/sys/kernel/debug/tracing/trace_pipe'])
for i in range(len(flag)*8+8,0x108,8):
    with open(f'a/test{i}','wb') as f:
        f.write(c[:197468])
        f.write(i.to_bytes(2,'little',signed=False))
        f.write(c[197470:])
    os.chmod(f'a/test{i}',0o777)
    for ch in string.printable:
        os.system(f'a/test{i} "{(flag+ch).ljust(32)}" > /dev/null')
        buf=p.recvuntil('\n',timeout=1)
        if b'Right' in buf:
            print(ch,end='')
            flag+=ch
            break
print()

EasyVT

拿了个二血顺便review了一下VT。

我只能说出题人牛逼,题目用MD模式编译还不给链接库,合着就是让人静态呗,那出VT有啥意义,费这么大功夫还不如第一题的天堂之门有用(×
EasyVT1

首先,可以很容易的看出这个题是在周壑的 VT_Learn 项目上改的(如果学过他的VT课程的话),甚至连pool tag的名字都没改
EasyVT1
EasyVT2

其实大部分东西都没什么用,就是最最简单基本的VT配置,在SetupVMCS函数(sub_402240)中主要关注HOST_RIP的设置。这个域设置VMMEntryPoint(sub_401C10),也就是当VmExit发生时,会先进入到这个位置,做状态的保存恢复和VM处理函数的Dispatch分发。
EasyVT3

所以对于VM事件的核心分发函数在sub_401C90中,并根据宏可以还原出符号
EasyVT4

然后回过头来看一下Guest.exe。三环程序非常简单,输入32字节,分四组,每组8字节。从Vmxon开始到Vmoff结束,每条vm指令都会发起一次VmExit进入Host进行对应的处理。若四次均校验通过则为flag
EasyVT5

从三环程序可以看出每组输入经过的Host处理流程都是一样的,Vmxon-Vmclear-Vmptrld-Vmwrite-Vmlaunch-Vmread-Vmcall-Vmptrst-Vmresume-Vmoff,总共10步。然后再去分析驱动程序,可以知道vmxon~vmread 在读取配置数据,vmcall~vmresume在处理加密,vmxoff判断运算结果。实际上,说白了,这道题跟VT就没什么关系,只是靠VT把正常的一个输入、加密、校验流程拆成了10步而已

然后就是从后往前一步一步看,先是魔改的TEA解密,然后是一个RC4,完事。其中穿插着各种换位,懒得分析交换规则了,直接手动换一换就出了

TEA解密

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

DWORD ans[] = {0x5C073994, 0x0D805CB3, 0x87DDA586, 0x317FB8E, 0x6520EF29, 0x5A4987AF, 0x0EB2DC2A4, 0x38CF470E};
DWORD g_tea_sum = 0x20000000;
DWORD TEA_delta = 0xC95D6ABF;
DWORD TEA_key[] = {0x00102030, 0x40506070, 0x8090A0B0, 0xC0D0E0F0};
DWORD tea_k0, tea_k1, tea_k2, tea_k3;
DWORD tea_v0, tea_v1;

void vmhandler_vmresume()
{
    unsigned int i;

    tea_k2 = TEA_key[1];
    tea_k3 = TEA_key[3];
    tea_k0 = TEA_key[2];
    tea_k1 = TEA_key[0];
    for ( i = 0; i < 0x20; ++i )
    {
        tea_v1 -= (tea_k3 + ((unsigned int)tea_v0 >> 5)) ^ (g_tea_sum + tea_v0) ^ (tea_k2 + 16 * tea_v0);
        tea_v0 += (tea_k1 + ((unsigned int)tea_v1 >> 5)) ^ (g_tea_sum + tea_v1) ^ (tea_k0 + 16 * tea_v1);
        g_tea_sum -= TEA_delta;
    }
}

void rc4_encrypt () {  

    g_tea_sum = 0x20000000;
    TEA_delta = 0xC95D6ABF;

    tea_k2 = TEA_key[1];
    tea_k3 = TEA_key[3];
    tea_k0 = TEA_key[2];
    tea_k1 = TEA_key[0];  

    g_tea_sum -= 0x20*TEA_delta;
    for (int i=0; i < 0x20; i++) {    
        g_tea_sum += TEA_delta;  
        tea_v0 -= ((tea_v1<<4) + tea_k0) ^ (tea_v1 + g_tea_sum) ^ ((tea_v1>>5) + tea_k1);  
        tea_v1 += ((tea_v0<<4) + tea_k2) ^ (tea_v0 + g_tea_sum) ^ ((tea_v0>>5) + tea_k3);  
    }                                           
}  

int main() {

    tea_v0 = 0x0D805CB3;
    tea_v1 = 0x5C073994;
    rc4_encrypt();
    printf("0x%X 0x%X\n", tea_v0, tea_v1);

    tea_v0 = 0x317FB8E;
    tea_v1 = 0x87DDA586;
    rc4_encrypt();
    printf("0x%X 0x%X\n", tea_v0, tea_v1);

    tea_v0 = 0x5A4987AF;
    tea_v1 = 0x6520EF29;
    rc4_encrypt();
    printf("0x%X 0x%X\n", tea_v0, tea_v1);

    tea_v0 = 0x38CF470E;
    tea_v1 = 0x0EB2DC2A4;
    rc4_encrypt();
    printf("0x%X 0x%X\n", tea_v0, tea_v1);

    return 0;
}

RC4解密

  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
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
#include<cstdio>
#include<cstring>
#include<windows.h>

#define MAX 65534
int S[256]; //向量S
char T[256];    //向量T
int KeyStream[MAX]; //密钥
BYTE CryptoText[MAX];

BYTE Key[] = {48, 52, 101, 53, 50, 99, 55, 101, 51, 49, 48, 50, 50, 98, 48, 98};

int GetLength(BYTE* list) {
    int cnt = 0;
    for(int i=0;i<MAX;++i) {
        if(list[i] == '\x00')
            break;
        cnt++;
    }
    return cnt;
}

void init_S()
// 初始化S;
{
    for (int i = 0; i < 256; i++) {
        S[i] = i;
    }
}

void init_Key(BYTE* key) {    // 拓展密钥

    int d, keylen = 16;
    for (int i = 0; i < 256; i++) {   //初始化T[]
        T[i] = key[i % keylen];           
    }

}

void  permute_S()
{
    // 置换S;
    int temp;
    int j = 0;
    for (int i = 0; i < 256; i++) {
        j = (j + S[i] + T[i]) % 256;
        temp = S[i];
        S[i] = S[j];
        S[j] = temp;
    }
}

void create_key_stream(BYTE* text, int textLength)
{
    // 生成密钥流
    int i, j;
    int temp, t, k;
    int index = 0;
    i = j = 0;
    while (textLength--) {   //生成密钥流
        i = (i + 1) % 256;
        j = (j + S[i]) % 256;
        temp = S[i];
        S[i] = S[j];
        S[j] = temp;
        t = (S[i] + S[j]) % 256;
        KeyStream[index] = S[t];
        index++;
    }

}

void Rc4EncryptText(BYTE* text)
{

    int textLength = GetLength(text);
    printf("%d\n", textLength);

    init_S();
    init_Key(Key);
    permute_S();
    create_key_stream(text, textLength);
    int plain_word;

    // 每次进vm会刷新状态
    // 所以总是用密钥流的前8字节解密
    for (int i = 0; i < textLength; i++) {
        CryptoText[i] = char(KeyStream[i%8] ^ text[i]);     
    }

}

int main() {

    DWORD text[] = {
        0xB89C12D5, 0xB17E7A2C,
        0xBF9842D1, 0xE6257321,
        0xEDCD45D0, 0xB2262921,
        0xB99B49DC, 0xBA722D2C
    };
    Rc4EncryptText((BYTE*)text);

    // 四个四个调换顺序
    for(int i = 0; i < 4; i++)
        for(int j = 1; j >= 0; j--)
            for(int k = 0; k<4;++k)
                printf("%c", CryptoText[i*8 + j*4 + k]);

    return 0;

}
Share on

Qfrost
WRITTEN BY
Qfrost
CTFer, Anti-Cheater, LLVM Committer