WHUCTF2025-Re复现

前言

WHU大二,在CTF中摸爬滚打的逆向小白一枚,前段时间参加的WHUCTF校赛,记录一下解题思路

同时这也是苯人的第一篇博客,还挺有纪念意义的,哈哈

贴两张最终战果:(队友tql)

题目

以下是对比赛题目的复现,写的太烂直接喷就好了,没必要划走(

签到题

64位PE,无壳

打开是一个C++程序

整体逻辑比较简单:

要求我们输入flag,然后对这个flag进行加密,最后再与所给的密文进行比对

加密的逻辑在48行,对每个字符的ascii值+10

将加密的逻辑反过来即可

给出EXP:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
v9 = [0]*19
v9[0] = 112
v9[1] = 118
v9[2] = 107
v9[3] = 113
v9[4] = 133
v9[5] = 118
v9[6] = 111
v9[7] = 126
v9[8] = 49
v9[9] = 125
v9[10] = 105
v9[11] = 124
v9[12] = 111
v9[13] = 128
v9[14] = 111
v9[15] = 124
v9[16] = 125
v9[17] = 111
v9[18] = 135
for i in v9:
print(chr(i-10),end='')

跳舞的小人

32位PE,无壳

打开集贸没有

题目提到tls回调函数,查了一下,发现tls函数是一种可以在程序EP之前执行的函数

尝试在函数列表中搜索tls,还真有。那么猜测主要逻辑就隐藏在这里

tls主要分成0和1两部分

tls0那部分是将byte_41C000[i] ^ 5的结果写入story.txt,这里直接写脚本打印:

1
2
3
4
5
str=[  
# 省略
]
for i in str:
print(chr(i^5),end='')

发现是siesta的悲伤小故事,告诉我们flag在图片中:

点开tls_1,将byte_41C318[i] ^ 0x20的结果写入图片,给出EXP:

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

unsigned char byte_F3C318[6918] = {
// 省略
};

int main()
{
const char *fileName = "secret.png";

// 打开文件(当前文件夹下)
FILE *stream = fopen(fileName, "wb");
if (!stream)
{
printf("无法打开文件: %s\n", fileName);
return 1;
}

// 写入数据
for (int i = 0; i < 6918; ++i)
{
fputc(byte_F3C318[i] ^ 0x20, stream);
}

fclose(stream);
printf("文件写入成功: %s\n", fileName);

return 0;
}

然后得到跳舞的小人图片,按照题目给的网站解密即可

login

下载附件,发现是apk文件,拖入jeb

定位到MainActivity函数,反编译,捕捉到关键逻辑:

image-20250331105501427

不熟悉Java,但大概理解就是,将Username存到v7_1,将Password存到v2_1,然后对这两部分进行验证

对Username的验证比较简单,结合前面给的enc和key差不多能猜出来这就是个tea加/解密,点进去一看确实是

并且这道题不需要对算法进行逆向(当时没理清逻辑,直接无脑逆,卡了好久……)直接顺着它的数据和解密就能拿到Username

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
#include <stdio.h>
void tea_decode(unsigned int ciphertext[2], unsigned int *key)
{
unsigned int cip0 = ciphertext[0];
unsigned int cip1 = ciphertext[1];
unsigned int sum = 0xC6EF3720;
for (int i = 0; i < 0x20; ++i)
{
cip0 -= (((cip1 << 4) ^ cip1 >> 5) + cip1) ^ (key[sum & 3] + sum);
sum += 1640531527;
cip1 -= (((cip0 << 4) ^ cip0 >> 5) + cip0) ^ (key[sum >> 11 & 3] + sum);
}
ciphertext[0] = cip0;
ciphertext[1] = cip1;
}

int main()
{
unsigned int key[] = {2, 0, 2, 5}; // 密钥
unsigned int ciphertext[4] = {
0xE8894BC5, 0x6D26C4FE, 0x72A27D59, 0x820FF773};
for (int i = 0; i < 2; i++)
{
tea_decode(ciphertext + 2 * i, key); // 以两个32位数据为单位来进行解密
}
for (int i = 0; i < 16; i++)
{
printf("%c", *((char *)ciphertext + i));
}
return 0;
}

得到用户名:this_isss_keey!!

猜测这应该是密码验证算法的密钥

继续来看对密码验证的逻辑

注意到verifyPasswordNative是一个native方法,也就是库文件里的方法。

这里我们对apk文件进行解包,找到lib文件夹下的libmyapplication.so文件

定位到verifyPasswordNative函数,密钥初始化和加密的主要逻辑如下:(关键变量名已修改)

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
v61[0] = _byteswap_ulong(v54) ^ 0xA3B1BAC6;
key1 = _byteswap_ulong(v14) ^ 0x56AA3350;
v61[1] = key1;
v19 = _byteswap_ulong(v15) ^ 0x677D9197;
v61[2] = v19;
key3 = _byteswap_ulong(v16) ^ 0xB27022DC;
v61[3] = key3;
for ( i = 0LL; i != 32; ++i )
{
key2 = v19;
v19 = key3;
v23 = *(_DWORD *)&byte_6E0[i * 4] ^ key1 ^ key2 ^ key3;
v24 = *(_DWORD *)&byte_6E0[i * 4] ^ key1 ^ key2 ^ key3;
v25 = (unsigned __int8)(byte_6E0[i * 4] ^ key1 ^ key2 ^ key3);
v26 = (byte_760[v23 >> 24] << 24) | (byte_760[BYTE2(v23)] << 16);
LODWORD(v23) = ((byte_760[BYTE1(v24)] << 8) | byte_760[v25]) + v26;
HIDWORD(v27) = (byte_760[BYTE1(v24)] << 8) | byte_760[v25];
LODWORD(v27) = v23;
key3 = v61[i] ^ (v27 >> 9) ^ v23 ^ (__PAIR64__(v23, v26) >> 19);
v61[i + 4] = key3;
rkey[i] = key3;
key1 = key2;
}
if ( v13 > 0 )
{
plaintext = (unsigned int *)v17;
cipertext = c;
do
{
v30 = _byteswap_ulong(*plaintext);
plaintext2 = _byteswap_ulong(plaintext[1]);
v32 = _byteswap_ulong(plaintext[2]);
plaintext3 = _byteswap_ulong(plaintext[3]);
for ( j = 0LL; j != 32; ++j )
{
plaintext1 = plaintext2;
plaintext2 = v32;
v32 = plaintext3;
v36 = plaintext3 ^ rkey[j] ^ plaintext1 ^ plaintext2;
v37 = byte_760[v36 >> 24]; // 最高字节
v38 = (v37 << 24) | (byte_760[BYTE2(v36)] << 16);// 最高两个字节
v39 = byte_760[BYTE1(v36)] << 8;
LODWORD(v36) = byte_760[(unsigned __int8)v36];
v40 = v36 | v39; // 最低两个字节
HIDWORD(v41) = v40 + v38;
LODWORD(v41) = v38;
plaintext3 = (v40 + v38) ^ (v41 >> 22) ^ v30 ^ (__PAIR64__(v40, v38) >> 16) ^ ((v37 >> 6) + 4 * (v40 + v38)) ^ (__PAIR64__(v36, v40 + v38) >> 8);// res ^ res>>22 ^ X ^ res<<2 | res>>30 ^ res>>8 | res<<24
v61[j + 4] = plaintext3; // res ^ res>>22 ^ X ^ res<<2 | res>>30 ^ res>>8 | res<<24
// 上一个密文、循环左移10位、不循环左移、循环左移16位、循环左移2位、循环左移24位
v30 = plaintext1;
}

首先对4个32位(4*4bytes == 16bytes,对应前面的this_isss_keey!!)的密钥进行了异或以及通过查表和移位进行密钥拓展

然后再用生成的轮秘钥对密文进行处理,共32轮

是什么算法呢?答案已经呼之欲出了。首先这是一个分组加密算法,同时还具有明显的密钥初始化、密钥和密文的S_box替换和移位操作,那么不就是SM4吗

这里直接用解密工具对密文解密,发现不对(我就知道

仔细阅读代码(一定要耐心,复杂代码尽量少依赖ai),发现有两处魔改:

  • 对密钥扩展表的每个数进行了+1
  • 加密时,改变了循环移位次数,18 ==> 16

从网上借用一下大佬的解密脚本,修改刚才的两处地方,解密得到结果(解密就是将密钥倒序使用)

代码参考:SM4加密算法原理以及C语言实现_sm4算法如何将一个字分成4个字节c语言-CSDN博客

解密脚本如下:

  • sm4.h:

    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
    #ifndef _SM4_H_
    #define _SM4_H_
    #include <stdlib.h>
    #include <stdio.h>
    #include <string.h>

    #define u8 unsigned char
    #define u32 unsigned long

    void four_uCh2uLong(u8 *in, u32 *out); // 四字节转换成u32

    void uLong2four_uCh(u32 in, u8 *out); // u32转换成四字节

    unsigned long move(u32 data, int length); // 左移,保留丢弃位放置尾部

    unsigned long func_key(u32 input); // 先使用Sbox进行非线性变化,再将线性变换L置换为L'

    unsigned long func_data(u32 input); // 先使用Sbox进行非线性变化,再进行线性变换L

    void print_hex(u8 *data, int len); // 无符号字符数组转16进制打印

    void encode_fun(u8 len, u8 *key, u8 *input, u8 *output); // 加密函数

    void decode_fun(u8 len, u8 *key, u8 *input, u8 *output); // 解密函数

    /******************************定义系统参数FK的取值****************************************/
    const u32 TBL_SYS_PARAMS[4] = {
    0xa3b1bac6,
    0x56aa3350,
    0x677d9197,
    0xb27022dc};

    /******************************定义固定参数CK的取值****************************************/
    const u32 TBL_FIX_PARAMS[32] = {

    0x70e16, 0x1c232a32, 0x383f464e, 0x545b626a, 0x70777e86, 0x8c939aa2, 0xa8afb6be, 0xc4cbd2da, 0xe0e7eef6, 0xfc030a12, 0x181f262e, 0x343b424a, 0x50575e66, 0x6c737a82, 0x888f969e, 0xa4abb2ba, 0xc0c7ced6, 0xdce3eaf2, 0xf8ff060e, 0x141b222a, 0x30373e46, 0x4c535a62, 0x686f767e, 0x848b929a, 0xa0a7aeb6, 0xbcc3cad2, 0xd8dfe6ee, 0xf4fb020a, 0x10171e26, 0x2c333a42, 0x484f565e, 0x646b727a};

    /******************************SBox参数列表****************************************/
    const u8 TBL_SBOX[256] = {

    0xd6, 0x90, 0xe9, 0xfe, 0xcc, 0xe1, 0x3d, 0xb7, 0x16, 0xb6, 0x14, 0xc2, 0x28, 0xfb, 0x2c, 0x05,
    0x2b, 0x67, 0x9a, 0x76, 0x2a, 0xbe, 0x04, 0xc3, 0xaa, 0x44, 0x13, 0x26, 0x49, 0x86, 0x06, 0x99,
    0x9c, 0x42, 0x50, 0xf4, 0x91, 0xef, 0x98, 0x7a, 0x33, 0x54, 0x0b, 0x43, 0xed, 0xcf, 0xac, 0x62,
    0xe4, 0xb3, 0x1c, 0xa9, 0xc9, 0x08, 0xe8, 0x95, 0x80, 0xdf, 0x94, 0xfa, 0x75, 0x8f, 0x3f, 0xa6,
    0x47, 0x07, 0xa7, 0xfc, 0xf3, 0x73, 0x17, 0xba, 0x83, 0x59, 0x3c, 0x19, 0xe6, 0x85, 0x4f, 0xa8,
    0x68, 0x6b, 0x81, 0xb2, 0x71, 0x64, 0xda, 0x8b, 0xf8, 0xeb, 0x0f, 0x4b, 0x70, 0x56, 0x9d, 0x35,
    0x1e, 0x24, 0x0e, 0x5e, 0x63, 0x58, 0xd1, 0xa2, 0x25, 0x22, 0x7c, 0x3b, 0x01, 0x21, 0x78, 0x87,
    0xd4, 0x00, 0x46, 0x57, 0x9f, 0xd3, 0x27, 0x52, 0x4c, 0x36, 0x02, 0xe7, 0xa0, 0xc4, 0xc8, 0x9e,
    0xea, 0xbf, 0x8a, 0xd2, 0x40, 0xc7, 0x38, 0xb5, 0xa3, 0xf7, 0xf2, 0xce, 0xf9, 0x61, 0x15, 0xa1,
    0xe0, 0xae, 0x5d, 0xa4, 0x9b, 0x34, 0x1a, 0x55, 0xad, 0x93, 0x32, 0x30, 0xf5, 0x8c, 0xb1, 0xe3,
    0x1d, 0xf6, 0xe2, 0x2e, 0x82, 0x66, 0xca, 0x60, 0xc0, 0x29, 0x23, 0xab, 0x0d, 0x53, 0x4e, 0x6f,
    0xd5, 0xdb, 0x37, 0x45, 0xde, 0xfd, 0x8e, 0x2f, 0x03, 0xff, 0x6a, 0x72, 0x6d, 0x6c, 0x5b, 0x51,
    0x8d, 0x1b, 0xaf, 0x92, 0xbb, 0xdd, 0xbc, 0x7f, 0x11, 0xd9, 0x5c, 0x41, 0x1f, 0x10, 0x5a, 0xd8,
    0x0a, 0xc1, 0x31, 0x88, 0xa5, 0xcd, 0x7b, 0xbd, 0x2d, 0x74, 0xd0, 0x12, 0xb8, 0xe5, 0xb4, 0xb0,
    0x89, 0x69, 0x97, 0x4a, 0x0c, 0x96, 0x77, 0x7e, 0x65, 0xb9, 0xf1, 0x09, 0xc5, 0x6e, 0xc6, 0x84,
    0x18, 0xf0, 0x7d, 0xec, 0x3a, 0xdc, 0x4d, 0x20, 0x79, 0xee, 0x5f, 0x3e, 0xd7, 0xcb, 0x39, 0x48};

    #endif
  • sm4.c:

    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
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    #include "sm4.h"

    // 4字节无符号数组转无符号long型
    void four_uCh2uLong(u8 *in, u32 *out)
    {
    int i = 0;
    *out = 0;
    for (i = 0; i < 4; i++)
    *out = ((u32)in[i] << (24 - i * 8)) ^ *out;
    }

    // 无符号long型转4字节无符号数组
    void uLong2four_uCh(u32 in, u8 *out)
    {
    int i = 0;
    // 从32位unsigned long的高位开始取
    for (i = 0; i < 4; i++)
    *(out + i) = (u32)(in >> (24 - i * 8));
    }

    // 左移,保留丢弃位放置尾部
    u32 move(u32 data, int length)
    {
    u32 result = 0;
    result = (data << length) ^ (data >> (32 - length));

    return result;
    }

    // 秘钥处理函数,先使用Sbox进行非线性变化,再将线性变换L置换为L'
    u32 func_key(u32 input)
    {
    int i = 0;
    u32 ulTmp = 0;
    u8 ucIndexList[4] = {0};
    u8 ucSboxValueList[4] = {0};
    uLong2four_uCh(input, ucIndexList);
    for (i = 0; i < 4; i++)
    {
    ucSboxValueList[i] = TBL_SBOX[ucIndexList[i]];
    }
    four_uCh2uLong(ucSboxValueList, &ulTmp);
    ulTmp = ulTmp ^ move(ulTmp, 13) ^ move(ulTmp, 23);

    return ulTmp;
    }

    // 加解密数据处理函数,先使用Sbox进行非线性变化,再进行线性变换L
    u32 func_data(u32 input)
    {
    int i = 0;
    u32 ulTmp = 0;
    u8 ucIndexList[4] = {0};
    u8 ucSboxValueList[4] = {0};
    uLong2four_uCh(input, ucIndexList);
    for (i = 0; i < 4; i++)
    {
    ucSboxValueList[i] = TBL_SBOX[ucIndexList[i]];
    }
    four_uCh2uLong(ucSboxValueList, &ulTmp);
    ulTmp = ulTmp ^ move(ulTmp, 2) ^ move(ulTmp, 10) ^ move(ulTmp, 16) ^ move(ulTmp, 24);

    return ulTmp;
    }

    // 加密函数(可以加密任意长度数据,16字节为一次循环,不足部分补0凑齐16字节的整数倍)
    // len:数据长度(任意长度数据) key:密钥(16字节) input:输入的原始数据 output:加密后输出数据
    void encode_fun(u8 len, u8 *key, u8 *input, u8 *output)
    {
    int i = 0, j = 0;
    u8 *p = (u8 *)malloc(50); // 定义一个50字节缓存区
    u32 ulKeyTmpList[4] = {0}; // 存储密钥的u32数据
    u32 ulKeyList[36] = {0}; // 用于密钥扩展算法与系统参数FK运算后的结果存储
    u32 ulDataList[36] = {0}; // 用于存放加密数据

    /***************************开始生成子秘钥********************************************/
    four_uCh2uLong(key, &(ulKeyTmpList[0]));
    four_uCh2uLong(key + 4, &(ulKeyTmpList[1]));
    four_uCh2uLong(key + 8, &(ulKeyTmpList[2]));
    four_uCh2uLong(key + 12, &(ulKeyTmpList[3]));

    ulKeyList[0] = ulKeyTmpList[0] ^ TBL_SYS_PARAMS[0];
    ulKeyList[1] = ulKeyTmpList[1] ^ TBL_SYS_PARAMS[1];
    ulKeyList[2] = ulKeyTmpList[2] ^ TBL_SYS_PARAMS[2];
    ulKeyList[3] = ulKeyTmpList[3] ^ TBL_SYS_PARAMS[3];

    for (i = 0; i < 32; i++) // 32次循环迭代运算
    {
    // 5-36为32个子秘钥
    ulKeyList[i + 4] = ulKeyList[i] ^ func_key(ulKeyList[i + 1] ^ ulKeyList[i + 2] ^ ulKeyList[i + 3] ^ TBL_FIX_PARAMS[i]);
    }
    /***********************************生成32轮32位长子秘钥结束**********************************/

    for (i = 0; i < len; i++) // 将输入数据存放在p缓存区
    *(p + i) = *(input + i);
    for (i = 0; i < 16 - len % 16; i++) // 将不足16位补0凑齐16的整数倍
    *(p + len + i) = 0;

    for (j = 0; j < len / 16 + ((len % 16) ? 1 : 0); j++) // 进行循环加密,并将加密后数据保存(可以看出此处是以16字节为一次加密,进行循环,即若16字节则进行一次,17字节补0至32字节后进行加密两次,以此类推)
    {
    /*开始处理加密数据*/
    four_uCh2uLong(p + 16 * j, &(ulDataList[0]));
    four_uCh2uLong(p + 16 * j + 4, &(ulDataList[1]));
    four_uCh2uLong(p + 16 * j + 8, &(ulDataList[2]));
    four_uCh2uLong(p + 16 * j + 12, &(ulDataList[3]));
    // 加密
    for (i = 0; i < 32; i++)
    {
    ulDataList[i + 4] = ulDataList[i] ^ func_data(ulDataList[i + 1] ^ ulDataList[i + 2] ^ ulDataList[i + 3] ^ ulKeyList[i + 4]);
    }
    /*将加密后数据输出*/
    uLong2four_uCh(ulDataList[35], output + 16 * j);
    uLong2four_uCh(ulDataList[34], output + 16 * j + 4);
    uLong2four_uCh(ulDataList[33], output + 16 * j + 8);
    uLong2four_uCh(ulDataList[32], output + 16 * j + 12);
    }
    free(p);
    }

    // 解密函数(与加密函数基本一致,只是秘钥使用的顺序不同,即把钥匙反着用就是解密)
    // len:数据长度 key:密钥 input:输入的加密后数据 output:输出的解密后数据
    void decode_fun(u8 len, u8 *key, u8 *input, u8 *output)
    {
    int i = 0, j = 0;
    u32 ulKeyTmpList[4] = {0}; // 存储密钥的u32数据
    u32 ulKeyList[36] = {0}; // 用于密钥扩展算法与系统参数FK运算后的结果存储
    u32 ulDataList[36] = {0}; // 用于存放加密数据

    /*开始生成子秘钥*/
    four_uCh2uLong(key, &(ulKeyTmpList[0]));
    four_uCh2uLong(key + 4, &(ulKeyTmpList[1]));
    four_uCh2uLong(key + 8, &(ulKeyTmpList[2]));
    four_uCh2uLong(key + 12, &(ulKeyTmpList[3]));

    ulKeyList[0] = ulKeyTmpList[0] ^ TBL_SYS_PARAMS[0];
    ulKeyList[1] = ulKeyTmpList[1] ^ TBL_SYS_PARAMS[1];
    ulKeyList[2] = ulKeyTmpList[2] ^ TBL_SYS_PARAMS[2];
    ulKeyList[3] = ulKeyTmpList[3] ^ TBL_SYS_PARAMS[3];

    for (i = 0; i < 32; i++) // 32次循环迭代运算
    {
    // 5-36为32个子秘钥
    ulKeyList[i + 4] = ulKeyList[i] ^ func_key(ulKeyList[i + 1] ^ ulKeyList[i + 2] ^ ulKeyList[i + 3] ^ TBL_FIX_PARAMS[i]);
    }
    /*生成32轮32位长子秘钥结束*/

    for (j = 0; j < len / 16; j++) // 进行循环加密,并将加密后数据保存
    {
    /*开始处理解密数据*/
    four_uCh2uLong(input + 16 * j, &(ulDataList[0]));
    four_uCh2uLong(input + 16 * j + 4, &(ulDataList[1]));
    four_uCh2uLong(input + 16 * j + 8, &(ulDataList[2]));
    four_uCh2uLong(input + 16 * j + 12, &(ulDataList[3]));

    // 解密
    for (i = 0; i < 32; i++)
    {
    ulDataList[i + 4] = ulDataList[i] ^ func_data(ulDataList[i + 1] ^ ulDataList[i + 2] ^ ulDataList[i + 3] ^ ulKeyList[35 - i]); // 与加密唯一不同的就是轮密钥的使用顺序
    }
    /*将解密后数据输出*/
    uLong2four_uCh(ulDataList[35], output + 16 * j);
    uLong2four_uCh(ulDataList[34], output + 16 * j + 4);
    uLong2four_uCh(ulDataList[33], output + 16 * j + 8);
    uLong2four_uCh(ulDataList[32], output + 16 * j + 12);
    }
    }

    // 无符号字符数组转16进制打印
    void print_hex(u8 *data, int len)
    {
    int i = 0;
    char alTmp[16] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
    for (i = 0; i < len; i++)
    {
    printf("%c", alTmp[data[i] / 16]);
    printf("%c", alTmp[data[i] % 16]);
    putchar(' ');
    }
    putchar('\n');
    }
    /*在主函数中实现任意字节加密与解密,并且结果正确*/
    int main(void)
    {
    u8 i, len;
    u8 encode_Result[50] = {0}; // 定义加密输出缓存区
    u8 decode_Result[50] = {0}; // 定义解密输出缓存区
    u8 key[16] = {0x74, 0x68, 0x69, 0x73, 0x5f, 0x69, 0x73, 0x73, 0x73, 0x5f, 0x6b, 0x65, 0x65, 0x79, 0x21, 0x21}; // 定义16字节的密钥
    u8 Data_plain[64] = {0x78, 0x2D, 0x69, 0xC1, 0x23, 0xCF, 0x20, 0xD7, 0xA6, 0x99,
    0xB2, 0x01, 0x1F, 0x01, 0x04, 0x0F, 0xEB, 0x22, 0x4B, 0x4F,
    0x9B, 0x62, 0x64, 0x86, 0xD9, 0x97, 0xBD, 0x12, 0xF6, 0x27,
    0xE7, 0xBD, 0x53, 0x89, 0x89, 0xC4, 0x95, 0x2B, 0xBB, 0xE2,
    0x0B, 0xA3, 0x98, 0x5E, 0x54, 0x6B, 0xBD, 0xFC}; // 定义16字节的原始输入数据(测试用)
    len = 16 * (sizeof(Data_plain) / 16) + 16 * ((sizeof(Data_plain) % 16) ? 1 : 0); // 得到扩充后的字节数(解密函数会用到)

    // encode_fun(sizeof(Data_plain), key, Data_plain, encode_Result); // 数据加密
    // printf("加密后数据是:\n");
    // for (i = 0; i < len; i++)
    // printf("%x ", *(encode_Result + i));
    /*注意:此处解密函数的输入数据长度应为扩展后的数据长度,即必为16的倍数*/
    decode_fun(len, key, Data_plain, decode_Result); // 数据解密
    printf("解密后数据是:\n");
    for (i = 0; i < len; i++)
    printf("%c", *(decode_Result + i));

    system("pause");
    return 0;
    }

ezrust

拿到题目,看见是rust,两眼一黑

64位,无壳,用IDA打开

定位到主函数,首先是一堆被去了符号的函数:

下面是一堆不明数据,共有42个字节,猜测是对我们的输入进行加密,然后用这部分数据进行验证:

再下面就是判断输入是否正确的逻辑:

那我们的目标就很显然,那就是找出对输入进行加密的函数

学长说过,高级语言看不懂,那就可以直接开始动调了,在动调的过程中验证自己静态分析时的猜想,并定位关键逻辑

动调之后,将while(1)之前的逻辑理了个大差不差(

大概就是输入flag,然后将flag在几个变量之间来回倒腾

继续看下面的代码,大概率就是加密逻辑了:

这里可以从上往下和从下往上两头分析

if ( v7 == 1114112 )处下断,F9跳转,发现又跳了回来,结合变量值的变化,得知这个循环是在遍历输入的每个字符,v26和v25分别是当前字符和其对应的索引,v15貌似是当前字符和密钥(其实就是thisiskey)的的某一位异或后的结果(说起来密钥和plz input放一起还挺具有迷惑性的,其实当时看到[v25%9]就应该意识到密钥只取前9位的,还是不够敏感)

经过不懈地F9,观察数据的变化,终于把代码分析懂了,这里直接给出修改后的变量名和注释:

  • res_list在内存里的样子:

接下来又对res_list进行了加密……

用到了下面三个函数:

点进去看,第一个和第三个函数有点像RC4的KSA和PRGA……然后第二个函数应该只是将res_list的值拷贝到了v7

经过验证,确实是这样的

接下来我们只需将上述两步加密操作逆过来,即可得到flag,给出EXP:

  • RC4:

    代码参考:RC4加密算法 - shelmean - 博客园

    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
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    #include <stdio.h>
    #include <string.h>

    #define SBOX_LEN 256

    #define rc4_encrypt rc4_crypt
    #define rc4_decrypt rc4_crypt

    static inline void swap_uchar(unsigned char *puc_x, unsigned char *puc_y)
    {
    *puc_x = *puc_x ^ *puc_y;
    *puc_y = *puc_x ^ *puc_y;
    *puc_x = *puc_x ^ *puc_y;
    }

    void hexdump(unsigned char *puc_data, int length)
    {
    int i = 0;

    for (i = 0; i < length; i++)
    {
    printf("%02X", puc_data[i]);
    if (i && (i + 1) % 16 == 0)
    {
    putchar('\n');
    }
    }
    printf("\n");
    }

    /**
    * 利用Key生成S盒
    * the Key-Scheduling Algorithm
    */
    static void rc4_ksa(unsigned char *puc_sbox, unsigned char *puc_key, int key_length)
    {
    int i = 0;
    int j = 0;
    char tmp[SBOX_LEN] = {0};

    for (i = 0; i < SBOX_LEN; i++)
    {
    puc_sbox[i] = i;
    tmp[i] = puc_key[i % key_length];
    }

    for (i = 0; i < SBOX_LEN; i++)
    {
    j = (j + puc_sbox[i] + tmp[i]) % SBOX_LEN;
    swap_uchar(&puc_sbox[i], &puc_sbox[j]); // 交换puc_sbox[i]和puc_sbox[j]
    }
    }

    /**
    * 利用S盒生成密钥流
    * The pseudo-random generation algorithm(PRGA)
    */
    static void rc4_prga(unsigned char *puc_sbox, unsigned char *puc_key_stream, unsigned long ul_data_length)
    {
    int i = 0;
    int j = 0;
    int t = 0;
    unsigned long k = 0;

    for (k = 0; k < ul_data_length; k++)
    {
    i = (i + 1) % SBOX_LEN;
    j = (j + puc_sbox[i]) % SBOX_LEN;
    swap_uchar(&puc_sbox[i], &puc_sbox[j]);
    t = (puc_sbox[i] + puc_sbox[j]) % SBOX_LEN;
    /* 为了更清晰理解rc4算法流程,此处保存keystream,不直接进行XOR运算 */
    puc_key_stream[k] = puc_sbox[t];
    }
    }

    /* 加解密 */
    void rc4_crypt(unsigned char *puc_data, unsigned char *puc_key_stream, unsigned long ul_data_length)
    {
    unsigned long i = 0;

    /* 把PRGA算法放在加解密函数中可以不需要保存keystream */
    for (i = 0; i < ul_data_length; i++)
    {
    puc_data[i] ^= puc_key_stream[i];
    }
    }

    int main(int argc, char *argv[])
    {
    unsigned char sbox[SBOX_LEN] = {0x74, 0x25, 0x7D, 0x3E, 0x2B, 0xA3, 0x14, 0x79, 0x01, 0x7E,
    0x0A, 0x64, 0x7C, 0x6B, 0xB2, 0x54, 0x43, 0x53, 0x1E, 0x0F,
    0x35, 0xC5, 0xF3, 0x13, 0x05, 0x55, 0xE8, 0x77, 0x15, 0x81,
    0xB8, 0x9A, 0x1C, 0x89, 0x40, 0x92, 0x00, 0x08, 0x90, 0x2A,
    0x0B, 0x57, 0x58, 0xEA, 0xF7, 0x8B, 0x9D, 0xC4, 0x41, 0xF6,
    0xBF, 0xEE, 0xD2, 0x07, 0xE9, 0x99, 0x6A, 0x16, 0xB3, 0xE6,
    0xDD, 0x0C, 0x4D, 0x87, 0x9E, 0x02, 0xFD, 0xA9, 0x86, 0xD0,
    0x28, 0x56, 0x30, 0xE1, 0x93, 0xFB, 0xD1, 0x11, 0x73, 0xC6,
    0xC7, 0x04, 0xF8, 0x1D, 0x4A, 0x09, 0x8E, 0xDB, 0x98, 0x97,
    0xEC, 0x6E, 0x95, 0x96, 0xDF, 0xB1, 0xF1, 0xBE, 0xDE, 0x8D,
    0xAD, 0xC0, 0xCF, 0x9F, 0x7A, 0x94, 0xEB, 0xA5, 0x0E, 0x84,
    0xAF, 0xAA, 0x34, 0xFA, 0xF4, 0x69, 0x38, 0xD4, 0x7F, 0x75,
    0xBC, 0x68, 0xA6, 0xD8, 0x5A, 0x48, 0x49, 0x91, 0x3F, 0xA4,
    0x4F, 0x6C, 0xD7, 0x33, 0x60, 0xC3, 0x21, 0x2E, 0x0D, 0x71,
    0x67, 0xE7, 0x7B, 0xB6, 0x24, 0xA0, 0x32, 0xA1, 0xE4, 0xEF,
    0x6D, 0xD9, 0x5E, 0x10, 0x03, 0x2F, 0xC8, 0xAB, 0xBA, 0x66,
    0xAE, 0x4E, 0x44, 0x78, 0x72, 0x51, 0x22, 0x59, 0x2D, 0x9C,
    0x8F, 0x17, 0x5F, 0x50, 0xCC, 0xD6, 0xE0, 0x2C, 0x46, 0xC1,
    0x63, 0xF9, 0xD3, 0xD5, 0x8A, 0x26, 0x37, 0xDA, 0xCD, 0x85,
    0xF0, 0x9B, 0x31, 0xB0, 0x6F, 0x1A, 0x18, 0x65, 0xFE, 0xBB,
    0xE2, 0x1B, 0xB7, 0x4C, 0x06, 0xB4, 0xB9, 0x20, 0x83, 0x80,
    0xCA, 0x8C, 0xCB, 0x62, 0xF2, 0x88, 0x52, 0xF5, 0xAC, 0x3A,
    0xED, 0x29, 0x42, 0x36, 0x3C, 0x3D, 0xE5, 0xC2, 0x23, 0x45,
    0x61, 0xB5, 0x3B, 0x5D, 0x47, 0xDC, 0xE3, 0x12, 0x39, 0x5C,
    0x1F, 0xA2, 0xBD, 0x70, 0xFF, 0xFC, 0x4B, 0x5B, 0x82, 0x27,
    0xA7, 0xA8, 0xCE, 0xC9, 0x19, 0x76};
    char key[SBOX_LEN] = {"thisiskey"};
    unsigned char data[512] = {0x12, 0x2f, 0x3b, 0x17, 0x3, 0xce, 0x47, 0xa8, 0x5d, 0xde, 0x37, 0x67, 0x8, 0x57, 0xd1, 0xc3, 0xc8, 0x5b, 0x34, 0x83, 0x8d, 0x3d, 0xb2, 0x9e, 0xfb, 0xcd, 0x36, 0xf, 0x9e, 0x42, 0xd, 0xcb, 0xaa, 0xf5, 0x83, 0x94, 0xb9, 0x9, 0xde, 0x6b, 0xb6, 0xcb};
    unsigned char puc_keystream[512] = {0};
    unsigned long ul_data_length = strlen(data);

    printf("key=%s, length=%d\n\n", key, strlen(key));
    printf("Raw data string:%s\n", data);
    printf("Raw data hex:\n");
    hexdump(data, ul_data_length);

    // /* 生成S-box */
    // rc4_ksa(sbox, (unsigned char *)key, strlen(key));

    /* 生成keystream并保存,S-box也会被更改 */
    rc4_prga(sbox, puc_keystream, ul_data_length);

    printf("S-box final status:\n");
    hexdump(sbox, sizeof(sbox));

    printf("key stream:\n");
    hexdump(puc_keystream, ul_data_length);

    /* 解密 */
    rc4_decrypt((unsigned char *)data, puc_keystream, ul_data_length);
    printf("decypt hexdump:\n");
    hexdump(data, ul_data_length);
    printf("decypt data:%s\n", data);

    return 0;
    }
  • 异或:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    input=[0x99, 0x9F, 0x9D, 0xA1, 0xA9, 0xA2, 0xF6, 0xEA, 
    0xA4, 0x9E, 0xF4, 0xEE, 0x9E, 0xDB, 0xD3, 0xEA,
    0x9E, 0xA6, 0xE0, 0xF7, 0xEF, 0xD1, 0xF2, 0xEB,
    0xA1, 0xE8, 0xDB, 0xCA, 0xE0, 0xEE, 0xA3, 0xF0,
    0xCE, 0xA0, 0xEF, 0xA6, 0xCE, 0xA7, 0xF2, 0xD1,
    0xE7, 0x9B]
    key = 'thisiskey'

    for i in range(0,len(input)):
    print(chr(((input[i] + ord(key[(8 - i) % 9])) ^ ord(key[i % 9])) & 0xFF), end='')

天堂之门

32位,无壳,拖入IDA,定位main函数,把函数和变量重命名一下

感觉看不出来啥,于是在16行处下断,开始动调

由于有一段内联汇编代码,果断放弃看伪代码,直接看汇编:

这里有个奇怪的远眺转,我单步步过的时候会跑飞,上网查了一下,原来天堂之门是一种技术,通过在程序内修改代码段寄存器来切换32位模式和64位模式,从而达到反调试的目的(调试器不能跨架构)

这里涉及一个我之前折腾自制操作系统时接触到的一个知识点——在Windows中,程序可以通过修改代码段寄存器切换32位模式和64位模式,当CS为0x33时,CPU按64位模式执行指令,当CS为0x23,时,CPU按32位模式执行指令。执行完这个远跳转后,程序跳转到2E5310这个地址(也就是下一条指令),CPU切换到64位模式执行,所以接下来的代码都要按64位模式解析。

[原创]羊城杯OddCode题解(unicorn模拟调试+求解)-CTF对抗-看雪-安全社区|安全招聘|kanxue.com

也就是说loc_401125处的函数会在64位模式下运行,那么好,我们跟进(

尝试读了一下,感觉怪怪的(因为是在32位模式下解析64位的代码),不过也大体能知道在干啥

传了3个参数进去,然后调用[ebp+var_88]处的函数,然后再将程序切换回32位模式,调用loc_401158处的函数

这里先点进[ebp+var_88]处的函数看看,发现是个标准的XTEA加密(大概能看懂

看不懂的话也可以选择写个脚本将64位模式下执行的代码dump出来,我这里懒得搞了,反正看出来是XTEA加密了,而且大概率没魔改

继续分析汇编的控制流程图,发现后面就是个与目标数据逐字节比对的过程,循环次数为0x50,目标数据在byte_422140数组中,正好80字节

直接写脚本解密,发现是乱码,后来学长告诉我就差一点,然后发现他把密钥的第一位改了(

修改密钥后解密,成功!

EXP如下:

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
#include <stdio.h>
void tea_decode(unsigned int ciphertext[2], unsigned int *key)
{
unsigned int cip0 = ciphertext[0];
unsigned int cip1 = ciphertext[1];
unsigned int sum = 32 * (-1640531527);
for (int i = 0; i <= 0x1F; ++i)
{
cip1 -= (cip0 + ((cip0 >> 5) ^ (16 * cip0))) ^ (sum + key[(sum >> 11) & 3]);
sum += 1640531527;
cip0 -= (cip1 + ((cip1 >> 5) ^ (16 * cip1))) ^ (sum + key[sum & 3]);
}
ciphertext[0] = cip0;
ciphertext[1] = cip1;
}

int main()
{
unsigned int key[] = {0x76616548, 0x745F6E65, 0x645F6568, 0x726F6F6F}; // 密钥
unsigned int v4[] = {
0xD915C57F,
0x5C1F5453,
0xE1A1A964,
0xE9DA3FA7,
0x0849BFAF,
0x1C1B1D10,
0xD2F4F10F,
0xAACBB1E2,
0xB437543B,
0xF91AD3AA,
0x19FE31CC,
0xC20FF606,
0x9095852F,
0xC7C93CC6,
0xE451A1A8,
0x298E7684,
0xD9B3FCF8,
0x3906D922,
0xC754C18F,
0xEE79F78D};
for (int i = 0; i < 10; i++)
{
tea_decode(v4 + 2 * i, (unsigned int *)key); // 以两个32位数据为单位来进行解密
}
for (int i = 0; i < 80; i++)
{
printf("%c", *((unsigned char *)v4 + i));
}
return 0;
}

ezNt

真ez吗……

  • 这道题比赛的时候没做出来,赛后学长推了源码,大概知道如何去做了,但从解题的逆向手的角度而言,这道题还是有很多待解决的地方(例如不知道如何绕过反调试)准备出官方WP之后再研究一下,这里只是记录一下目前的思考

64位exe,无壳,拖入IDA

shift+f12 捕捉到关键字符串”WHUCTF”,点击跟进

定位关键函数,反编译,然后天塌了……

浏览代码,捕捉到关键数据,有几段关键数据:

结合代码中检索到的为数不多的能看懂的信息,猜测程序功能如下:

  • 首先要求我们输入一个字符串,要求以 WHUCTF{ 开头,} 结尾,输入字符的ASCII值必须在33-125之间

  • 其次将我们输入的字符串通过'_'来进行分割,共7部分,每部分的长度第三张图已给出,分别是5 7 6 2 3 2 4

  • 最后对我们输入的结果进行验证,这里我们明显能看到的有CRC32校验和MD5校验,目标校验结果第三张图已给出(其实还有条路,那就是Base64,看源码的时候发现的,但是不能直接拿到编码后的结果,可能需要动调,但现在不知道如何绕过反调试,先放弃)

如果你以为这就完了,然后直接开始爆破MD5或CRC32,那就寄了(别问我怎么知道的),因为你漏了一条关键信息:

RtlCreateUnicodeStringFromAsciiz 是 Windows 内核模式提供的一个函数,属于 NT 内核运行时库(NtDll.dll 或内核模块),主要用于将 ASCII 字符串(以 \0 结尾) 转换为 Unicode 字符串(UTF-16)。

来自微软官网的截图:

具体实现搞不懂,但大体意思就是,我们输入的字符串会被转换为UTF-16编码的Unicode字符串!!!

如果你直接将字符串按照utf-8编码解释并爆破,恭喜你白干了

知道了这些,下面就可以愉快的写MD5的爆破脚本了(CRC32不会

给出我的EXP:

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
import hashlib
from itertools import product
table = [chr(item) for item in range(33,125)]

def get_str_list(length):
for item in product(table,repeat=length):
yield "".join(item) # 用yield按需生成,而不是储存在数组里,避免内存爆炸


def get_md5(target_md5, length, max_tries=None):
count = 0
for item in get_str_list(length):
# 计算MD5
md5_hash = hashlib.md5(item.encode('utf-16le')).hexdigest()

if md5_hash == target_md5:
print(f'Found match! String: "{item}"')
return item

count += 1
if count % 1000000 == 0: # 每100万次打印一次进度
print(f"Tried {count} combinations... Current: '{item}'")

if max_tries and count >= max_tries:
print(f"Reached maximum tries ({max_tries}) without finding a match.")
return None

print(f"Exhausted all {count} possibilities without finding a match.")
return None

if __name__ == "__main__":
# target_md5 = "57f650568eb39c9b1f5e60ca9e583eab" #
# target_md5 = "eb65eec0cd7a09f1a2cd8988916806c0" #
# target_md5 = "a4d5fa0f29b424492a8e2302f80f179d" #
# target_md5 = "a3f11de53829d5978ef28f599f0ba825" # 1s
# target_md5 = "739df9a3b39609d6a4e50516bac3d19d" # N0t
# target_md5 = "2a78fea533b26a7d58c3c02cfad822a9" # SO
target_md5 = "dfd7572d3b4a5b786de1328be0057577" # Ha3d


string_length = 4 # 根据需求调整

# 开始爆破(可以设置max_tries限制尝试次数)
result =get_md5(target_md5, string_length, max_tries=100000000000)

if not result:
print("No match found.")

只可惜这个脚本爆长度为4及以下的字符串速度还可以,5及以上直接寄……这就是宝宝python,可能换成C写会好一点

比赛的时候听学长提示说可以hashcat,等有空研究一下

先这样,等出官方WP再复现

总结

学逆向时间不算长,有很多知识都是第一次见(如TLS回调和天堂之门),在比赛中边做题边学,并和学长积极交流,感觉收获蛮多

题目难度适中,但是比较综合,有很多细节,需要沉下心来耐心分析。动调、汇编以及对常见密码算法的识别都是解题的关键

整体做下来感觉体验挺不错的,就是差一道ezNT没做出来,有点遗憾。跟前面的两个AK了的大佬师傅还是有不小差距,但其实已经很满意了hhh,接下来也会好好努力,向大佬们看齐

最后希望All1n战队越来越好,加油!


WHUCTF2025-Re复现
http://lingchen0408.github.io/2025/04/07/WHUCTF2025-Re复现/
作者
七秒钟的记忆
发布于
2025年4月7日
许可协议