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 小结
至此获取到了外挂的动态库文件,接下来就是分析这个动态库里的功能。