首页
社区
课程
招聘
[原创]KCTF2020_Android题
发表于: 2020-4-10 20:43 7547

[原创]KCTF2020_Android题

2020-4-10 20:43
7547

2020/4 kctf

设计思路

假的AES静态白盒。AES以16字节为一组加解密,通过固定密钥,预先生成10轮加密的所有情况。

 

每个字节有256种可能,则生成一个"10x16x256"的表即可覆盖所有情况,从原来的计算转换成查表操作。

 

因为表未经过其他数学公式变换,所以有很明显的规律和特征,以此推出原始密钥。

解题

图片描述

 

把输入的值传入jni函数,根据返回值判断是否是flag,但是却只处理0和1。

 

注:
设置了测试模式,所以可以通过adb -t install 安装。
图片描述

图片描述
so中用了和上次的题差不多一样的花指令,可patch修复。一些函数地址被转成动态计算得到地址,ida无法直接识别,也可以patch。

 

例如这样patch、跳过垃圾代码(偏移地址可能不一致)
图片描述

 

修复后的伪代码:
图片描述

 

分析完JNI_OnLoad可以发现,该so加载后启动新的线程又把自身重新加载到内存并卸载首次加载的so,所以这个so自身实现了linker的功能。

 

但是重新加载的so和原始的so并没有不同,所以其实分析原so即可。

 

动态调试到注册jni函数或者hook得到地址(注意反调试改变的逻辑),注意首次注册的地址并不是最终的jni函数地址。(或者其他方式,因为很多时候逆向是可以跳跃式的,包括得到真实的jni地址)

 

jni函数内从data中得到一个so,加载到内存中。
图片描述

 

dump下来,发现文件头被抹去了,修复节表或者直接动态调试。

 

修复后如下,发现真实的逻辑,需要从字符串逆推出flag。
图片描述

 

注:
逆向先走一遍流程,梳理出大体的逻辑,不要陷入逆向壳的逻辑中。

算法

通过特征,16字节,9轮相同的操作加1轮,表大小等可以确定应该是AES算法
图片描述
图片描述

 

把表保存出来,发现最后用到的"4 16 256"数据前三个字节都是0,而代码中也只用到最后一个字节,所以其实最后"4 16 256"保存的就是16 * 256个字节。

 

图片描述

 

通过最后一轮猜测表就是扩展后的最后一组key和256个字节的所有情况,怎么验证呢?

 

AES最后一轮流程:(前边也是轮秘钥加)字节替换(sbox固定)、行移位、轮秘钥加。那么暴力三层循环,和最后一组256字节比对,如果相同则可以逆推出最后一轮扩展密钥,依次推出原始的密钥。

 

代码如下:

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
//
// Created by ZT on 2020/4/10.
//
 
#include <cstdio>
#include <cstdint>
 
 
static unsigned const char sbox[256] = {
        0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
        0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
        0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
        0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
        0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
        0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
        0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
        0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
        0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
        0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
        0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
        0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
        0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
        0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
        0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
        0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16
};
 
void reverse(const unsigned char dest[256], int index, unsigned char expKey[16]) {
    bool found = false;
    int key = 0;
    for (size_t lastKey = 0; lastKey < 256; ++lastKey) {
        for (size_t curKey = 0; curKey < 256; ++curKey) {
            for (size_t k = 0; k < 256; ++k) {
                unsigned char tmp = sbox[k ^ lastKey];
                size_t st = tmp ^ curKey;
                if (st != dest[k]) {
                    found = false;
                    break;
                }
                found = true;
                key = k;
            }
            if (found) {
                expKey[index] = curKey;
                expKey[index - 16] = lastKey;
                printf("found[%d] lastKey=0x%02x, x=%d, curKey=0x%02x !\n", index, lastKey, key, curKey);
                return;
            }
        }
    }
}
 
// uint8_t y[4] -> uint32_t x
#define LOAD32H(x, y) \
  do { (x) = ((uint32_t)((y)[0] & 0xff)<<24) | ((uint32_t)((y)[1] & 0xff)<<16) | \
             ((uint32_t)((y)[2] & 0xff)<<8)  | ((uint32_t)((y)[3] & 0xff));} while(0)
 
// uint32_t x -> uint8_t y[4]
#define STORE32H(x, y) \
  do { (y)[0] = (uint8_t)(((x)>>24) & 0xff); (y)[1] = (uint8_t)(((x)>>16) & 0xff);   \
       (y)[2] = (uint8_t)(((x)>>8) & 0xff); (y)[3] = (uint8_t)((x) & 0xff); } while(0)
 
// 从uint32_t x中提取从低位开始的第n个字节
#define BYTE(x, n) (((x) >> (8 * (n))) & 0xff)
 
// uint32_t x循环左移n位
#define ROF32(x, n)  (((x) << (n)) | ((x) >> (32-(n))))
// uint32_t x循环右移n位
#define ROR32(x, n)  (((x) >> (n)) | ((x) << (32-(n))))
 
#define MIX(x) (((sbox[BYTE(x, 2)] << 24) & 0xff000000) ^ ((sbox[BYTE(x, 1)] << 16) & 0xff0000) ^ \
                ((sbox[BYTE(x, 0)] << 8) & 0xff00) ^ (sbox[BYTE(x, 3)] & 0xff))
 
//逆行移位, 第一行不变,第二行循环右移8/一个字节,第三行循环右移16/2个字节,第四行循环右移24/3个字节。
int invShiftRows(unsigned char (*state)[4]) {
    uint32_t block[4] = {0};
//    printf("逆行移位[%d]:\n", ++shiftRowsCount);
 
    /* i: row */
    for (int i = 0; i < 4; ++i) {
        LOAD32H(block[i], state[i]);
        block[i] = ROR32(block[i], 8*i);
        STORE32H(block[i], state[i]);
        printf("0x%02x, 0x%02x, 0x%02x, 0x%02x, \n", state[i][0], state[i][1], state[i][2], state[i][3]);
    }
 
    return 0;
}
 
void rightShift(unsigned char expKey[16]) {
    unsigned char tmp[4][4];
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            tmp[j][i] = expKey[i*4 + j];
        }
    }
 
    invShiftRows(tmp);
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            expKey[i*4 + j] = tmp[j][i];
        }
    }
 
}
 
 
constexpr uint32_t rcon[] = {
        0x01000000, 0x02000000, 0x04000000,
        0x08000000, 0x10000000, 0x20000000,
        0x40000000, 0x80000000, 0x1b000000,
        0x36000000, 0x6c000000, 0xd8000000,
        0xab000000, 0x4d000000, 0x9a000000
};
 
void reverse(unsigned char expKey[16], int round) {
    uint32_t w[4];
    uint32_t top[4];
 
    for (int i = 0; i < 4; ++i) {
        LOAD32H(w[i], expKey + 4*i);
    }
    top[3] = w[3] ^w[2];
    top[2] = w[2] ^w[1];
    top[1] = w[1] ^w[0];
    printf("0x%02x ^ 0x%02x ^ 0x%02x\n", w[0], rcon[round -1], MIX(top[3]));
    top[0] = w[0] ^rcon[round -1] ^ MIX(top[3]);
 
    unsigned char *tmp = expKey - 16;
    for (int j = 0; j < 4; ++j) {
        tmp[j*4] = top[j] >> 24;
        tmp[j*4 + 1] = (top[j] >> 16) ;
        tmp[j*4 + 2] = top[j] >> 8;
        tmp[j*4 + 3] = top[j];
    }
 
}
 
void check(const unsigned char expKey[16]) {
    uint32_t top[8];
    uint32_t *w = top;
 
    for (int i = 0; i < 8; ++i) {
        LOAD32H(top[i], expKey + 4*i);
    }
 
    for (int i = 0; i < 1; ++i) {
        if (w[4] != (w[0] ^ MIX(w[3]) ^ rcon[9])) {
            printf("error expkey!\n");
            break;
        }
        if (w[5] != (w[1] ^ w[4])){
            printf("error expkey!\n");
            break;
        }
        if (w[6] != (w[2] ^ w[5])) {
            printf("error expkey!\n");
            break;
        }
        if(w[7] != (w[3] ^ w[6])){
            printf("error expkey!\n");
            break;
        }
 
        printf("right expkey!\n");
        w += 4;
    }
 
}
 
int main() {
    unsigned char expKey[11][16] = {0};
 
    //把最后的一组int数组提取出来转成字节数组,放入
    const unsigned char Tboxes[16][256] = {...};
 
    //暴破最后两组16字节的扩展key
    for (int i = 0; i < 16; ++i) {
        reverse(Tboxes[i], i, expKey[10]);
    }
 
    rightShift(expKey[9]);
 
    check(expKey[9]);
 
    //倒着推出上一组key
    for (int j = 9; j > 0; --j) {
        reverse(expKey[j], j);
    }
 
    printf("expKey:\n");
    for (int k = 0*16; k < 11*16; k++ ) {
        if (k && k % 4 == 0) {
            printf("\n");
            if (k % 16 == 0) {
                printf("\n");
            }
        }
        printf("0x%02x, ", expKey[0][k]);
    }
    printf("\nend expKey\n");
 
    return 0;
 
 
}

得到初始key,"you get the key!",之后解密0b6f04a3427089858041945e70082508得到flag。

 

只是给出一种通过分析逻辑和AES算法特征,猜测验证的方式,其他攻击方式DFA等也可以。

补充

因为时间和能力有限,逆向的某壳厂商的动态白盒未能实现加入,把动态白盒的加密算法逆出来了,但是数学和密码学知识欠缺,没推理还原出动态白盒的二次密钥(就这么叫吧,因为它这个动态白盒下发的更像二次密钥,不是表)生成算法。

 

关于壳,引入了一个某APP的壳,原来是打算按照该APP的壳的方式实现一遍的,但是时间和兼容性等问题未能完成,自己写的这个壳其实还是很弱的,很多都还没实现。最后折衷了一下把这个APP的壳引用进来,但是其实只是虚晃一枪并没起作用,只是展示下还有这样一种壳的实现方式。

 

因为jni函数返回值只处理了0和1的情况,但是第一次注册的jni函数每次都是随机生成的密钥,无法逆推、无法触发返回1的情况(也不可能达到题目的要求:CrackMe在没有被附加调试的情况下运行时,第一次运行时输入正确注册码,必须显示成功提示信息,若是重启验证的,在重启后必须显示。)。小陷阱,对于经验不足的可能会陷入逆推假的jni函数?其实有很多逻辑都暴漏了真实的jni函数,可以一步到位直接获取到。

 

写完之后ctf群听大佬们吐槽逆向题的代码保护,认为只是浪费时间。默默的把算法所在的so撤掉了代码保护,壳只加了低级的保护,去掉了函数运行时解密、自定义elf结构等。自己和朋友做了下,如果直接逆(包括壳),一天足够逆出来,而如果直接hook、系统插桩直接绕过壳找到jni函数定位到算法,那应该秒破。所以就算是个白盒加密的入门题吧,同时展示下Android elf壳的两种实现方式,我实现的无限嵌套壳等(可以再加一层upx壳,以及递归加载自己等,壳中壳,不过既然大佬们觉得浪费时间,就去掉吧)。

 

最后能力有限、水平一般,只能写个半成品的壳和假的白盒加密,抱歉了。

flag

cf02c524c20e8711


[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课

最后于 2020-5-8 14:02 被kanxue编辑 ,原因:
上传的附件:
收藏
免费
支持
分享
最新回复 (2)
雪    币: 195
能力值: ( LV4,RANK:57 )
在线值:
发帖
回帖
粉丝
2
你好 为什么我在app里点击确定的时候就异常退出 查看log提示找不到eq?
2020-5-9 00:45
0
雪    币: 6435
活跃值: (441)
能力值: ( LV12,RANK:831 )
在线值:
发帖
回帖
粉丝
3
提供一点反馈:直接静态逆(包括壳),一个晚上左右差不多可以逆出来,作者在这点上的估计没问题。

我对加载了一个某 app 的 so 这一点有一些意见,原因是那个 so 里有一些反调试的逻辑,检测到调试之后会把程序搞崩,而这个行为是只要加载了就会有的,并不需要你去实际调用它。这导致你只要想好好调试这个程序,就得去稍微逆一下那个某 app 的 so(至少摸清它的反调试在干嘛),而我并不是很愿意干这件事(因为看雪比赛要求大家公开解法),只好纯静态逆你这个 so 了,比较麻烦。

照这么玩的话,下次出 win 上的 crackme 的时候是不是还能加载个某 P 进来反调试啊……
2020-5-9 07:59
0
游客
登录 | 注册 方可回帖
返回

账号登录
验证码登录

忘记密码?
没有账号?立即免费注册