0x0 前言

今天看到了一个某手游的注入类外挂样本,目标是脱壳和加密流程的分析,以及分析出外挂实现功能修改的地方,此篇仅作为学习记录。

0x1 GZIP加密

首先,这个样本本体是一个500kb的sh文件,拖到ida里提示是二进制文件。于是用everedit打开这个文件看一眼,发现开头有这个自解压脚本

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
#!/data/data/com.termux/files/usr/bin/bash

skip=50

set -e

tab=' '

nl='

'

IFS=" $tab$nl"

umask=`umask`

umask 77

gztmpdir=

trap 'res=$?

test -n "$gztmpdir" && rm -fr "$gztmpdir"

(exit $res); exit $res

' 0 1 2 3 5 10 13 15

case $TMPDIR in

/ | /*/) ;;

/*) TMPDIR=$TMPDIR/;;

*) TMPDIR=/data/data/com.termux/files/usr/tmp/;;

esac

if type mktemp >/dev/null 2>&1; then

gztmpdir=`mktemp -d "${TMPDIR}gztmpXXXXXXXXX"`

else

gztmpdir=${TMPDIR}gztmp$$; mkdir $gztmpdir

fi || { (exit 127); exit 127; }

gztmp=$gztmpdir/$0

case $0 in

-* | */*'

') mkdir -p "$gztmp" && rm -r "$gztmp";;

*/*) gztmp=$gztmpdir/`basename "$0"`;;

esac || { (exit 127); exit 127; }

case `printf 'X\n' | tail -n +1 2>/dev/null` in

X) tail_n=-n;;

*) tail_n=;;

esac

if tail $tail_n +$skip <"$0" | gzip -cd > "$gztmp"; then

umask $umask

chmod 700 "$gztmp"

(sleep 5; rm -fr "$gztmpdir") 2>/dev/null &

"$gztmp" ${1+"$@"}; res=$?

else

printf >&2 '%s\n' "Cannot decompress $0"

(exit 127); res=127

fi; exit $res

解这个方式很简单,把shell脚本的部分删掉,然后用gzip解压就行了,解压出来后把文件拖进ida看,发现有壳和混淆

0x2 upx+xor+ollvm加密

然后本来以为接下来就是unidbg还原ollvm环节,但是开始还原之后发现这玩意的解密流程很像之前看到过的一个加密样本。

那个加密样本的加密流程是:

1.读取需要加密的文件,先用upx压缩

2.破坏segment信息:修改(RW_)Loadable Segment:p_addr_VIRTUAL_address(0x000f8)的字节,先取低四位,然后将高四位写为1001,修改(RW_)Loadable Segment:p_memsz_SEGMENT_RAM_LENGTH(0x110) 位置的三个字节为 FF FF FF,修改(R_X)Loadable Segment(0x000B0 ~ 0x000E8)的所有字节为0x0A

3.找到这段字串

1
2
3
4
5
"UPX!", "$Info: This file is packed with the UPX executable packer http://upx.sf.net $",

"$Id: UPX 4.22 Copyright (C) 1996-2024 the UPX Team. All Rights Reserved. $",

"PROT_EXEC|PROT_WRITE failed."};

将这段字串的字符替换成\x0A

4.将整个文件xor加密,加密后的数据头部接一个字串特征,把这段数据拼接到一个已经编译好的壳的后边

5.写回文件完成加密

在运行的时候那个壳文件会通过字串特征定位到xor加密数据,然后通过key把加密的数据段解密,解密之后通过魔数判断文件类型用对应方法执行。

这一段就是它的解密过程(无混淆):

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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
 void __noreturn sub_202DC()

{

v49 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);

memset(v46, 0, sizeof(v46));

filebuffer = fopen((const char *)v46 + 1, "rb");

if ( !filebuffer )

((void (__fastcall __noreturn *)(__int64 *, const char *, __int64))loc_22200)(

&qword_63B78,

"Failed to open file.",

20LL);

v1 = filebuffer;

fseek(filebuffer, 0LL, 2);

filesize = ftell(v1);

rewind(v1);

filebufferTMP = (void *)sub_404E0(filesize);

v4 = fread(filebufferTMP, 1uLL, filesize, v1);

fclose(v1);

if ( v4 != filesize )

((void (__fastcall __noreturn *)(__int64 *, const char *, __int64))loc_22200)(

&qword_63B78,

"Failed to read file.",

20LL);

if ( filesize >= 0xFFFFFFFFFFFFFFF0LL )

sub_40684(&v43);

if ( filesize >= 0x17 )

{

v5 = (char *)sub_4047C((filesize + 16) & 0xFFFFFFFFFFFFFFF0LL);

v44 = filesize;

ptr = v5;

v43 = (filesize + 16) & 0xFFFFFFFFFFFFFFF0LL | 1;

}

else

{

v5 = (char *)&v43 + 1;

LOBYTE(v43) = 2 * filesize;

if ( !filesize )

{

LABEL_11:

v5[filesize] = 0;

j_j__free(filebufferTMP);

v6 = sub_4047C(32LL);

v7 = (unsigned __int8)v43;

*(_QWORD *)(v6 + 16) = 0xA3A7E8B4A0E7BF8BLL;

v8 = (_QWORD *)v6;

if ( (v7 & 1) != 0 )

v9 = (char *)ptr;

else

v9 = (char *)&v43 + 1;

v10 = v7 >> 1;

if ( (v7 & 1) != 0 )

v10 = v44;

*(_OWORD *)v6 = xmmword_DC73;

*(_BYTE *)(v6 + 24) = 0;

if ( v10 >= 24 )

{

v11 = &v9[v10];

v12 = v9;

while ( 1 )

{

if ( v10 == 23 )

goto LABEL_17;

v13 = (char *)memchr(v12, 232, v10 - 23);

if ( !v13 )

goto LABEL_17;

if ( !(*(_QWORD *)v13 ^ *v8 | *((_QWORD *)v13 + 1) ^ v8[1] | *((_QWORD *)v13 + 2) ^ v8[2]) )

break;

v12 = v13 + 1;

v10 = v11 - (_BYTE *)v12;

if ( v11 - (_BYTE *)v12 < 24 )

goto LABEL_17;

}

if ( v13 != v11 && v13 - v9 != -1 )

{

sub_40C0C(&v40, &v43, v13 - v9 + 24, -1LL, &v43);

v47 = 6;

v48 = 6518904;

sub_40830(&v37, &v40);

for ( i = 0LL; ; ++i )

{

if ( (v40 & 1) != 0 )

{

if ( i >= *(_QWORD *)&v41[7] )

{

LABEL_35:

generateRandomString_1FB78(64LL);

generateRandomString_1FB78(64LL);

sub_41620("mkdir -p /data/data/", v36);

if ( (v30 & 1) != 0 )

mkdirCommand = (const char *)v32;

else

mkdirCommand = v31;

system(mkdirCommand);

sub_41620("/data/data/", v36);

v19 = sub_40F1C((int)&v25, "/");

v20 = *(_OWORD *)v19;

v27 = *(void **)(v19 + 16);

v26 = v20;

*(_QWORD *)(v19 + 8) = 0LL;

*(_QWORD *)(v19 + 16) = 0LL;

*(_QWORD *)v19 = 0LL;

if ( (v33 & 1) != 0 )

v21 = v35;

else

v21 = v34;

if ( (v33 & 1) != 0 )

v22 = *(_QWORD *)&v34[7];

else

v22 = (unsigned __int64)v33 >> 1;

v23 = sub_40B48((int)&v26, v21, v22);

v24 = *(_OWORD *)v23;

v29 = *(void **)(v23 + 16);

v28 = v24;

*(_QWORD *)(v23 + 8) = 0LL;

*(_QWORD *)(v23 + 16) = 0LL;

*(_QWORD *)v23 = 0LL;

sub_21180(&v47, &v28, 16LL);

}

v15 = v42;

}

else

{

v15 = v41;

if ( i >= (unsigned __int64)v40 >> 1 )

goto LABEL_35;

}

v16 = v15[i];

if ( (v37 & 1) != 0 )

v17 = v39;

else

v17 = v38;

v17[i] = *((_BYTE *)&v48 + i + -3 * (i / 3)) ^ v16;

}

}

}

LABEL_17:

((void (__fastcall __noreturn *)(__int64 *, void *, __int64))loc_22200)(&qword_63B78, &unk_D808, 27LL);

}

}

memcpy(v5, filebufferTMP, filesize);

goto LABEL_11;}

于是可以直接在壳文件中找到xor的key,把加密数据取出来,按照壳的解密流程去还原一次就得到了原elf文件,打开看了之后发现这只是一个下载器,并不是外挂文件,他的功能是用curl下载注入器和动态库,并把这两个东西放到/data/app/~~xxxxxxxxxxxxxxx==/com.xxxx.xxxx.xxxx-xxxxxxxxxxxxxxxxx==/lib/arm64/这个文件夹下执行注入,直接在这个文件夹就能获取到文件。

后面想了想,感觉有挺多可以偷懒不用分析这个下载器就能获取注入器和动态库的方法,例如去ebpf监控他的写入文件的系统调用、或者通过charles抓包定位到他的文件下载网址,他注入的动态库也没有隐藏,可以直接去看/proc/maps/下加载的动态库文件路径等等。

0x4 小结

至此获取到了外挂的动态库文件,接下来就是分析这个动态库里的功能。