作者:TheSeven
0x00 引子
昨天頭老師扔給我一個php的加密擴展,讓我看看能不能解密,我從垃圾桶里拖出我14年版的ida打開簡單瞅了一眼,php擴展庫的加載流程不是特別熟悉,遂想放棄剛正面用一些雜項手段來搞,看了眼被加密的php文件,文件頭部是個標志,google了一發,發現某個解密站點在15年曾經支持過這種加密:

但是目前該站已經無法解密這個方法了。
然而神奇的是,盡管知道了這種方法叫做 AtStar/VoiceStar ,但是用了各種方法都沒在網上搜到其他任何關于這種加密的信息。。。。于是,再次老老實實打開ida,正面剛。
0x01 定位入口及整體流程
一般來說,php代碼加密有三種形式:
-
第一種類似代碼混淆,這種方法可以不依賴擴展庫,例如phpjiami,一般這種加密需要將解碼程序也打包進代碼中,也就是我們說的殼,然后殼會被代碼混淆,原本的代碼會被加密,最終由殼進行解密后執行。這種不依賴擴展庫的加密方法有個非常簡單的破解方法,因為原代碼執行前一定會去調用底層的
zend_compile_string函數,而且這種加密方法是可以直接運行的,所以我們在運行時把zend_compile_stringhook住就可以得到源碼了。 -
第二種是使用擴展庫,如果使用php擴展庫那么可以玩兒的加密手段就更多了,比如hook住
zend_compile_*的一系列函數,在編譯流程上動手腳,并且一般我們在線上環境中(比如一個shell上)得到的so庫,可能在本地運行不起來,這個時候可能就比較難通過動態調試的方法拿到源碼。但是一般來說,只要我們拿到了這個加密庫最終輸出到zend虛擬機的數據,不管是源碼還是opcode,我們一般都能做到代碼還原,因為他最終逃不過zend engine(ze)。 -
還有一種方法是第二種方法的子集,比如
Swoole Compile,牛逼之處在于他部分脫離了zend虛擬機,對opcode做了混淆,這就比較像是vmp,所以破解難度就會變的很大。
但是比較幸運,這次遇到的 voicestar 是第二種方法里比較簡單的一種加密方式。
查了下php擴展開發的手冊,在載入擴展庫時,會首先調用get_module來獲得模塊接口,ida里看下發現返回了一個入口指針,但是我沒找到賦值在哪,然后我就把so里的所有function按照起始位置排序了下,然后就看到了get_module附近定義的幾個函數,特別是 zm_startup_php_voice 和 zm_shutdown_php_voice 這倆函數從函數名上看上去比較像是入口:

查看zm_startup_php_voice:
__int64 zm_startup_php_voice()
{
*((_DWORD *)&compiler_globals + 135) |= 1u;
lr = gl((char *)&ll);
if ( lr )
php_error_docref0(0LL, 2LL, "No License: %d");
org_compile_file = (int (__fastcall *)(_QWORD, _QWORD))zend_compile_file;
zend_compile_file = pcompile_file;
return 0LL;
}
很明顯,其關鍵部分是把 zend_compile_file 放到 org_compile_file 這個指針中,然后用 pcompile_file hook 住 zend_compile_file。
zm_shutdown_php_voice 主要就是把 zend_compile_file 還原。
__int64 zm_shutdown_php_voice()
{
*((_DWORD *)&compiler_globals + 135) |= 1u;
zend_compile_file = org_compile_file;
return 0LL;
}
0x02 分析 & 解碼
zend_compile_file 是ze中負責將讀入的源代碼文件編譯為opcode然后執行的函數,那么解密過程應該就是在pcompile_file這個hook函數中了。
int __fastcall pcompile_file(__int64 a1, unsigned int a2)
{
__int64 v2; // rbx@1
unsigned int v3; // er13@1
FILE *v4; // rax@3
FILE *v5; // rbp@3
bool v6; // zf@4
__int64 v7; // rdi@4
signed __int64 v8; // rcx@4
char *v9; // rsi@4
int v10; // er12@9
int v11; // eax@10
FILE *v12; // rax@12
__int64 v13; // rdi@12
__int64 v14; // rax@12
int result; // eax@12
__int64 v16; // rax@14
const char *v17; // rax@15
__int64 v18; // [sp+0h] [bp-58h]@1
__int64 v19; // [sp+8h] [bp-50h]@1
__int64 v20; // [sp+10h] [bp-48h]@1
__int64 v21; // [sp+18h] [bp-40h]@1
char ptr; // [sp+20h] [bp-38h]@4
v2 = a1;
v3 = a2;
v18 = 0LL;
v19 = 0LL;
v20 = 0LL;
v21 = 0LL;
if ( (unsigned __int8)zend_is_executing() && (LODWORD(v16) = get_active_function_name(), v16) )
{
LODWORD(v17) = get_active_function_name();
strncpy((char *)&v18, v17, 0x1EuLL);
if ( !(_BYTE)v18 )
goto LABEL_3;
}
else if ( !(_BYTE)v18 )
{
goto LABEL_3;
}
if ( !strcasecmp((const char *)&v18, "show_source") )
return 0;
if ( !strcasecmp((const char *)&v18, "highlight_file") )
return 0;
LABEL_3:
v4 = fopen(*(const char **)(a1 + 8), "r");
v5 = v4;
if ( !v4 )
return org_compile_file(v2, v3);
fread(&ptr, 8uLL, 1uLL, v4);
v7 = (__int64)"\tATSTAR\t";
v8 = 8LL;
v9 = &ptr;
do
{
if ( !v8 )
break;
v6 = *v9++ == *(_BYTE *)v7++;
--v8;
}
while ( v6 );
if ( !v6 )
{
fclose(v5);
return org_compile_file(v2, v3);
}
if ( lr )
{
php_error_docref0(0LL, 2LL, "No License:");
result = 0;
}
else
{
v10 = cle(&ll, v9);
if ( v10 )
{
php_error_docref0(0LL, 2LL, "No License: %d");
printf("No License:%d\n", (unsigned int)v10, v18);
result = 0;
}
else
{
v11 = *(_DWORD *)v2;
if ( *(_DWORD *)v2 == 2 )
{
fclose(*(FILE **)(v2 + 24));
v11 = *(_DWORD *)v2;
}
if ( v11 == 1 )
close(*(_DWORD *)(v2 + 24));
v12 = ext_fopen(v5);
v13 = *(_QWORD *)(v2 + 8);
*(_QWORD *)(v2 + 24) = v12;
*(_DWORD *)v2 = 2;
LODWORD(v14) = expand_filepath(v13, 0LL);
*(_QWORD *)(v2 + 16) = v14;
result = org_compile_file(v2, v3);
}
}
return result;
}
簡單看一下邏輯,首先是判斷 show_source 和 highlight_file 倆函數有沒有被禁用,然后判斷當前解析的文件有沒有”\tATSTAR\t” 這個文件頭,如果有,則進入到解密流程,解密流程前面一段都是在判斷有沒有授權,我們直接跳到83行之后的這個最后的分支上去。
zend_compile_file 定義如下:
extern ZEND_API zend_op_array *(*zend_compile_file)(zend_file_handle *file_handle, int type);
其第一個參數是zend_file_handle結構,其偏移量24的位置放的是打開的源碼文件指針,此分支代碼的主要邏輯就是關閉原來打開的代碼文件,然后使用ext_fopen函數重新打開該php代碼文件,然后替換掉 zend_file_handle結構體中原來的文件句柄。然后調用 zend_compile_file(hook之前的函數,現在為org_compile_file)。那么很顯然,當函數調用到zend_compile_file的時候,v2里裝的應該就是還原后的代碼了,那么還原操作應該是在ext_fopen函數內,跟進去看。
FILE *__fastcall ext_fopen(FILE *stream)
{
int v1; // eax@1
signed int v2; // ebx@1
unsigned int v3; // er13@1
void *v4; // rbp@1
void *v5; // rcx@2
__int64 v6; // rax@3
const void *v7; // r12@4
FILE *v8; // rbx@4
__int64 v10; // [sp+0h] [bp-C8h]@1
__int64 v11; // [sp+30h] [bp-98h]@1
int v12; // [sp+9Ch] [bp-2Ch]@4
v1 = fileno(stream);
__fxstat(1, v1, (struct stat *)&v10);
v2 = v11 - 8;
v3 = v11 - 8;
v4 = malloc((signed int)v11 - 8);
fread(v4, v2, 1uLL, stream);
fclose(stream);
if ( v2 > 0 )
{
v5 = v4;
do
{
v6 = (signed int)((((_BYTE)v2 + ((unsigned int)(v2 >> 31) >> 28)) & 0xF) - ((unsigned int)(v2 >> 31) >> 28));
*(_BYTE *)v5 = ~(*(_BYTE *)v5 ^ (d[v6] + p[2 * v6] + 5));
v5 = (char *)v5 + 1;
--v2;
}
while ( v2 );
}
v7 = zdecode((__int64)v4, v3, (__int64)&v12);
v8 = tmpfile();
fwrite(v7, v12, 1uLL, v8);
free(v4);
free((void *)v7);
rewind(v8);
return v8;
}
該部分代碼的關鍵部分應該是在 22-33行,這段代碼明顯是在做一個對稱加密(易得,證略~~2333終于可以說這句話了),雖然沒看到密鑰,但是我們發現這里面d和p這倆盒子是放在data段的:

然后這個對稱加(解)密做完之后,解密后的內容又進入到了 zdecode 這個函數中,跟進去看一眼,
zdecode其實就是zcodecom又封裝了一層,直接看zcodecom:
void *__fastcall zcodecom(int a1, __int64 a2, int a3, __int64 a4)
{
int v4; // er13@1
__int64 v5; // r12@1
int v6; // er14@3
void *v7; // r13@3
int v8; // eax@4
int v10; // ST08_4@21
v4 = a3;
v5 = a4;
*((_QWORD *)&z + 8) = 0LL;
*((_QWORD *)&z + 9) = 0LL;
*((_QWORD *)&z + 10) = 0LL;
z = 0LL;
*((_DWORD *)&z + 2) = 0;
if ( a1 )
inflateInit_(&z, "1.2.8", 112LL);
else
deflateInit_(&z, 1LL, "1.2.8", 112LL);
z = a2;
*((_DWORD *)&z + 2) = v4;
*((_DWORD *)&z + 8) = 100000;
v6 = 0;
*((_QWORD *)&z + 3) = &outbuf;
v7 = malloc(0x186A0uLL);
while ( 1 )
{
if ( a1 )
{
v8 = inflate(&z, 0LL);
if ( v8 == 1 )
{
LABEL_9:
if ( 100000 == *((_DWORD *)&z + 8) )
{
if ( a1 )
goto LABEL_11;
LABEL_16:
deflateEnd(&z);
}
else
{
v10 = 100000 - *((_DWORD *)&z + 8);
v7 = realloc(v7, v6 + 100000);
memcpy((char *)v7 + v6, &outbuf, v10);
v6 += v10;
if ( !a1 )
goto LABEL_16;
LABEL_11:
inflateEnd((__int64)&z);
}
*(_DWORD *)v5 = v6;
return v7;
}
}
else
{
v8 = deflate(&z, 4LL);
if ( v8 == 1 )
goto LABEL_9;
}
if ( v8 )
break;
if ( !*((_DWORD *)&z + 8) )
{
v7 = realloc(v7, v6 + 100000);
memcpy((char *)v7 + v6, &outbuf, 0x186A0uLL);
*((_QWORD *)&z + 3) = &outbuf;
*((_DWORD *)&z + 8) = 100000;
v6 += 100000;
}
}
if ( a1 )
inflateEnd((__int64)&z);
else
deflateEnd(&z);
*(_DWORD *)v5 = 0;
return v7;
}
發現這個函數有點復雜,看了半天沒看懂在干啥,然后我就把代碼復制下來本地編譯了一發,但是這段代碼調了半晚上都沒跑起來,我想了一下,感覺問題應該是出在 z 和 outbuf 這倆全局變量上,這段代碼中對z上的偏移頻繁操作,z應該是某個比較復雜的結構體,并且inflate、inflateEnd等等這幾個函數在編譯的時候必須link zlib,gcc 1.c -o a -lz -g3 ,我猜測 z 應該是zlib中某個結構體,我就去查了zlib的手冊,看個Example(為啥這個zlib的logo看上去這么像某lv文。。。。):
https://www.zlib.net/zlib_how.html
此處的z應該是 z_stream結構,這個過程應該就是使用zlib解壓。我按照zlib手冊中的結構和宏試圖復原zcodecom函數,但是每當執行到這句解壓代碼時v8 = inflate(&z, 0LL); 總會返回一個0xfffffffb,查手冊發現是Z_BUF_ERROR,沒轍,c語言太菜搞不定,只能想另外的方法。
此時我已經基本確定這里是一個解壓流程,盡管不知道有沒有啥另外的操作,索性把數據導出來用python解壓下。
此時我已經把從pcompile_file到解壓流程前的代碼都調通了,編譯好之后在zdecode前下個斷點,文件長度存在v3中,此處直接print出來,為0x4f7,查看下v4的地址,然后直接從該地址dump binary memory 0x4f7個字節:

file 瞅一眼:
root@penGun:/tmp# file aaa
aaa: zlib compressed data
感覺沒啥毛病,上python直接解:
Python 2.7.12 (default, Nov 19 2016, 06:48:10)
[GCC 5.4.0 20160609] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from zlib import *
>>> data = open('aaa','rb').read()
>>> data = decompress(data)
>>> file = open('ddd.php','wb')
>>> file.write(data)
>>> file.close()
搞定:

0x03 關于抄ida中c代碼
注意導入ida的定義頭文件,因為某些類型比較蛋疼,其實手動typedef也是可以的:
typedef unsigned long long __int64;
typedef unsigned long _DWORD;
typedef unsigned long long _QWORD;
typedef unsigned short _WORD;
typedef unsigned char _BYTE;
typedef int bool;
直接include plugins目錄下的defs.h就好啦。
還要注意某些結構體ida不能識別,所以可能f5出來的代碼存在這種形式:
__int64 v10; // [sp+0h] [bp-C8h]@1
__int64 v11; // [sp+30h] [bp-98h]@1
int v12; // [sp+9Ch] [bp-2Ch]@4
這幾個變量應該本在一個結構體中的,但是這樣看下面的代碼似乎他們沒經過賦值就使用了,此時可以重新寫一個結構體,也可以手動布局下堆棧,把變量放在指定的偏移上,其實這兩種方法原理完全一樣,就是操作起來可能形式不同。
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/478/
暫無評論