本帖最后由 jbczzz 于 2024-8-6 16:58 编辑
0x0 前言
今天看到了一个某手游的注入类外挂样本,目标是脱壳和加密流程的分析,以及分析出外挂实现功能修改的地方,此篇仅作为学习记录。
0x1 GZIP加密
首先,这个样本本体是一个500kb的sh文件,拖到IDA里提示是二进制文件。于是用everedit打开这个文件看一眼,发现开头有这个自解压脚本
[Asm] 纯文本查看 复制代码 #!/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.找到这段字串
[Asm] 纯文本查看 复制代码 "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把加密的数据段解密,解密之后通过魔数判断文件类型用对应方法执行。
这一段就是它的解密过程(无混淆):
[Asm] 纯文本查看 复制代码 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 小结
至此获取到了外挂的动态库文件,接下来就是分析这个动态库里的功能。 |