在linux經常使用的腳本不少,例如shell幾乎是linux必備的腳本。腳本也是一種可執行文件,所以,它也面臨這安全類的問題。腳本從它產生的狀況來看,能夠分爲兩類:node
一類是靜態的,就是腳本是之前寫好的,並且運行時只須要執行該腳本。之後不會去修改它。這是很常見的。不少linux服務器在部署的時候,這些腳本就會被部署。linux
另外一類是動態的,也就是腳本本省並非部署好的,而是其餘程序在運行時動態生成的,而後保存成臨時腳本的形式,生成程序或其餘程序來執行它。這種技術也是廣泛存在的,可能編程的人認爲這種方式實現某些問題比較簡單,從而採起了這種設計。事實上,這種腳本會帶來嚴重的安全問題,並且難以解決。算法
對於第一類腳本,解決方法比較多。例如加密,簽名等方法均可以增長安全性。例如,使用非對稱加密RSA私鑰給靜態的腳本進行簽名,服務器上放置公鑰,腳本在運行時,使用公鑰解密簽名,進行驗證。這種方式是足夠安全的,並且也不復雜。只須要修改一下腳本解釋器,在執行腳本的時候添加驗證邏輯就行。固然,這種狀況只限於使用了靜態腳本的服務器上。這種方式下,任何腳本都會被要求強制進行簽名校驗。(前提固然是腳本將解釋器不會被替換,這也可使用簽名驗證的方式在二進制可執行文件上,這裏就不討論這方面的問題,配合簽名和lsm模塊,能夠實現二進制可執行文件的簽名校驗)。shell
然而若是服務器上的程序使用了動態腳本技術,很顯然程序生成的腳本沒有被簽名,從而執行也會失敗。所以,動態腳本須要從新考慮。編程
動態腳本本質上的問題就是動態生成腳自己份的確認問題,也就是如何確認這個腳本是由程序動態生成的。關於身份確認的問題,確定離不開簽名,第一個解決方案也是基於此的:若是程序生成動態腳本的時候,同時生成一個數字簽名,這樣子腳本解釋器執行的時候驗證數字簽名就好了。爲了防止入侵者本身寫一個腳本,而後一樣生成一個數字簽名而來欺騙腳本解釋器,所以這裏的數字簽名算法必須被保護,也就是入侵者不能使用該方法一樣給其餘的腳本進行簽名。想到這裏,你們確定都想,我改寫一下某個數字簽名算法,好比sha1,而後將改寫的部分進行保護。這樣安全性就依賴改寫的sha1算法被保護的程度了。考慮這樣一個場景,某cgi程序a和bash使用了一樣的改寫的sha1算法計算數字簽名,這樣它們就能夠配合工做。所以a和bash中都有着改寫的sha1算法的實現。這問題就來了,逆向工程使得它們是極其的不安全。a或者bash被逆向後,就能夠分析出改寫的sha1算法的實現。所以,必需要增長逆向難度,例如使用模糊技術,加殼技術使得逆向過程難度增大。但並不意味着逆向不可能。或許大家公司使用先進的加殼技術以及對代碼進行復雜的模糊處理使得逆向實際上變得不可能也是能夠的。筆者爲了測試一下模糊代碼對反編譯確實能形成多大的困難,便作了一個很簡單的測試。也就是對sha1算法作了一個簡單的處理,按照一個簡單的規則改了下緩衝區中的數據,而後在計算sha1.主要是看代碼反編譯後的樣子。程序源代碼以下:api
#include <openssl/sha.h> #include <assert.h> #include <stdio.h> #include <sys/stat.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "variant_sha1.h" #define SHA_SWAP_BUFF_LEN 3 //sha1計算過程當中的臨時變量的長度 #define VARIANT_CONST 10 //變異算法中使用的常量 /** * 獲取文件大小 * @param filename 是輸入文件 * @return -1表示異常,>0表示文件大小 */ static int get_file_size(const char* filename) { struct stat buf; if (filename == NULL) return -1; if (stat(filename, &buf)<0) return -1; return buf.st_size; } /* * 用於對data_buff數據進行變異處理,爲了增長反編譯難度,該函數內大量 * 使用了goto來增長流程模糊的效果.該函數不符合checklist,目的是保護代碼 * @data_buff 緩衝區指針 * @length 緩衝區長度 由調用這保證前置條件 data_buff != NULL 以及 length >=0 */ void static variant_buff(char* data_buff, int length) { assert(data_buff); assert(length >= 0); int gotoflag = length & (~length) + 1; int location = gotoflag - (gotoflag & (~gotoflag)); int i = 0; int j = 0; int k =0; begin: if (length < 0) { goto length_error; }else{ srand((unsigned)time(NULL)); gotoflag = rand() % (VARIANT_CONST * VARIANT_CONST + length % (rand() % VARIANT_CONST + 1) + 1); if (gotoflag > 0) { gotoflag = 0; goto handle; }else{ goto begin; } } handle: if ((length & 1) == 0) { srand((unsigned)time(NULL)); gotoflag = rand() % (VARIANT_CONST * VARIANT_CONST + length % (rand() % VARIANT_CONST + 1) + 1); if (gotoflag > 0) { gotoflag = 0; goto even_handle; }else{ goto begin; } }else{ srand((unsigned)time(NULL)); gotoflag = (rand() + SHA_SWAP_BUFF_LEN) % (VARIANT_CONST * VARIANT_CONST + length % (rand() % VARIANT_CONST + 1) + 1); if (gotoflag > 0) { gotoflag = 0; goto odd_handle; }else{ goto begin; } } //實際變異處理的代碼 even_handle: if (length == 0) { goto length_error; } i = (length -1) >> 1; j = i / VARIANT_CONST; k = i - j * VARIANT_CONST; i = j; while (k > 0) { *(data_buff + k) = (*(data_buff + k) + k); j = i / VARIANT_CONST; k = i - j * VARIANT_CONST; i = j; } if (k == 0) { *(data_buff + k) = (*(data_buff + k) + VARIANT_CONST); } length = length >> 1; if ((length & 1) == 0) { length += 1; } goto begin; odd_handle: i = (length >> 1) - 1; j = i / (VARIANT_CONST >> 1); k = i - j * (VARIANT_CONST >> 1); i = j; while (k > 0) { //printf("k=%d\n",k); *(data_buff + k) = (*(data_buff + k) + k); j = i / (VARIANT_CONST >> 1); k = i - j * (VARIANT_CONST >> 1); i = j; } if (k == 0) { *(data_buff + k) = (*(data_buff + k) + VARIANT_CONST); } length_error: if (gotoflag >0) { goto begin; }else{ return; } }
/* * 生成變異後的sha1摘要 * @filename 待計算sha1的文件名稱 * @sha1buff 存放結果緩衝區 長度是41 * @return 0表示成功,<0表示失敗 */ int variant_sha1(const char* filename, char* sha1buff) { assert(sha1buff); SHA_CTX sha_ctx; int file_len = 0; unsigned char* data_buff = NULL; unsigned char sha[SHA_DIGEST_LENGTH+1] = {0}; char tmp[SHA_SWAP_BUFF_LEN] = {'\0'}; int i = 0; int retval = 0; FILE* fd = NULL; if (filename == NULL) { retval = FILE_NAME_ERROR; goto file_name_error; } fd = fopen(filename, "r"); if (fd == NULL) { retval = FILE_OPEN_ERROR; goto file_name_error; } file_len = get_file_size(filename); if (file_len <= 0) { retval = FILE_LEN_ERROR; goto file_len_error; } if ((data_buff = (unsigned char *)calloc(file_len, sizeof(unsigned char))) == NULL) { retval = MALLOC_ERROR; goto file_len_error; } if (SHA1_Init(&sha_ctx) != 1) { retval = SHA1_ERROR; goto sha1_error; } if (file_len == fread(data_buff, sizeof(unsigned char), file_len,fd)) { //printf("-========>data_buff:%s\n",data_buff); variant_buff(data_buff, file_len); if (SHA1_Update(&sha_ctx, data_buff, file_len) != 1) { retval = SHA1_ERROR; goto sha1_error; } }else { retval = FREAD_ERROR; goto sha1_error; } if (SHA1_Final(sha, &sha_ctx) != 1) { retval = SHA1_ERROR; goto sha1_error; } //將sha轉換成字符到sha1buff for (i = 0;i < SHA_DIGEST_LENGTH ; i++ ) { sprintf(tmp, "%2.2x", sha[i]); strcat(sha1buff, tmp);//sha1buff長度是固定的41,該循環不會致使緩衝區溢出 } sha1_error: free(data_buff); data_buff = 0; file_len_error: fclose(fd); fd = 0; file_name_error: return retval; } int main() { int v = 0; char sha1buff[41] = {0}; v = variant_sha1("123.txt", sha1buff); printf("%s", sha1buff); }
這段代碼很簡單,就是在SHA1_Init以後改變一下data_buff,在函數varinant_buff中有許多goto,將一個很簡單的流程寫的亂起八糟,目的就是想看反彙編和反編譯的結果。從反彙編結果來看,代碼流程確實顯得亂,但也不是不可分析。不過能夠明顯的感受到源代碼中代碼模糊處理後在彙編代碼裏看起來,更加的混亂。在這裏就不貼彙編結果了,有興趣的能夠本身使用ida反彙編一下。再看一下反編譯的代碼,這個比較有用,雖然反編譯後的程序大部分幾乎都是不能編譯運行的,可是能夠看出程序原來的部分面貌。這裏只給出variant_buff反編譯後的代碼,能夠看出反編譯質量仍是很高的。安全
//----- (080487B9) -------------------------------------------------------- int __cdecl variant_buff(int a1, signed int a2) { unsigned int v2; // eax@6 int v3; // ebx@6 unsigned int v4; // eax@8 int v5; // ebx@8 unsigned int v6; // eax@11 signed int v7; // ebx@11 int result; // eax@24 int v9; // [sp+2Ch] [bp-1Ch]@0 int v10; // [sp+34h] [bp-14h]@12 signed int v11; // [sp+34h] [bp-14h]@13 int v12; // [sp+3Ch] [bp-Ch]@12 int v13; // [sp+3Ch] [bp-Ch]@13 if ( !a1 ) __assert_fail("data_buff", "variant_sha1.c", 0x35u, "variant_buff"); if ( a2 < 0 ) __assert_fail("length >= 0", "variant_sha1.c", 0x36u, "variant_buff"); while ( 1 ) { while ( 1 ) { do { if ( a2 < 0 ) goto LABEL_24; v2 = time(0); srand(v2); v3 = rand(); v9 = v3 % (a2 % (rand() % 10 + 1) + 101); } while ( v9 <= 0 ); if ( a2 & 1 ) break; v4 = time(0); srand(v4); v5 = rand(); v9 = v5 % (a2 % (rand() % 10 + 1) + 101); if ( v9 > 0 ) { v9 = 0; if ( !a2 ) goto LABEL_24; v13 = ((a2 - 1) >> 1) + -10 * (((signed int)((unsigned __int64)(1717986919LL * ((a2 - 1) >> 1)) >> 32) >> 2) - ((a2 - 1) >> 32)); v11 = ((signed int)((unsigned __int64)(1717986919LL *((a2 - 1) >> 1)) >> 32) >> 2) - ((a2 - 1) >> 32); while ( v13 > 0 ) { *(_BYTE *)(a1 + v13) += v13; v13 = v11 % 10; v11 /= 10; } if ( !v13 ) *(_BYTE *)a1 += 10; a2 >>= 1; if ( !(a2 & 1) ) ++a2; } } v6 = time(0); srand(v6); v7 = rand() + 3; v9 = v7 % (a2 % (rand() % 10 + 1) + 101); if ( v9 > 0 ) { v9 = 0; v12 = ((a2 >> 1) - 1) % 5; v10 = ((a2 >> 1) - 1) / 5; while ( v12 > 0 ) { printf("k=%d\n", v12); *(_BYTE *)(a1 + v12) += v12; v12 = v10 % 5; v10 /= 5; } if ( !v12 ) *(_BYTE *)a1 += 10; LABEL_24: result = v9; if ( v9 <= 0 ) return result; } } }
雖然反編譯的函數的參數原型是錯誤的,可是,反編譯的質量很高,提供了大量參考信息。並且不少都是有效信息。不過實際模糊處理要複雜的多,使用複雜的變量,名稱模糊和流程模糊技術使得反彙編和反編譯代碼難以理解是能夠作到的。配合加殼技術,使得程序變得安全。雖然這一切都很完美,彷佛能達到要求,可是,其實第一種方案是不安全的,也是不能採用的。緣由是破解並不必定須要理解你代碼。bash
入侵者拷貝走cgi程序a或者bash以後,在本身的機器上調試執行(指令級別),只須要尋找到函數調用的入口,而後在入口處傳入本身的參數就能夠了,他無需理解你內部複雜的過程,程序返回的結果將會是他指望的簽名。這個簽名就是它入侵腳本合法的憑據了,上傳到服務器後,便不會被發現。沒想到,破解如此容易,第一個方案是不可行的。服務器
看到這裏,不要灰心,咱們須要思考其餘的辦法。第一種方案失敗的緣由在於計算簽名的過程綁定在可執行文件中,一旦可執行文件被拷貝走,入侵者就開始分析,動態調試即可以幫助他達到目的,找到計算改變的sha1的入口,而後就是咱們的機器被入侵了,還不能被發現。所以咱們須要將須要保護的代碼段(算法)和可執行文件進行分離。其實這個問題相似於不能將加解密的密鑰放置在程序內部同樣。第二種方法即是從這個結論出發的。函數
將保護的代碼部分和程序進行分離,所以不須要將保護的代碼進行模糊等複雜處理,所以可使用des或者aes加密標準的sha1來看成簽名。所以,問題在於須要提供加密和解密服務,並且密鑰要安全,與可執行文件分離。提供加解密服務可使用內核模塊來提供,使用通訊的方式爲可執行文件提供服務。問題的關鍵便轉換到密鑰的保護問題上。
只要阻止來自用戶層的對密鑰的訪問,那麼密鑰就安全了。所以密鑰須要放在一個特別的目錄下,用戶不能訪問該文件夾,而只有內核能夠訪問它。實現這點能夠在lsm的inode訪問的鉤子上進行阻止,來自用戶的訪問統統給拒絕掉。固然若是用戶拆下了硬盤,而後在別的設備上去讀取,那確定沒有問題,這種方式仍是能夠獲取密鑰的。 若是競爭對手或者入侵者買下大家設備,拆下硬盤,獲取密鑰,進行其餘的操做...爲防止這種行爲,每臺設備使用的密鑰應該隨機生成,隨機生成的密鑰不影響程序的執行,這便能防止上面的攻擊方法。
小結一下,這種方式保護動態腳本有兩個前提條件,首先是保證腳本解釋器程序的正確性,第二點是須要確認全部的產生動態腳本的代碼。動態腳本生成後,將本身的數字簽名發給內核模塊,內核使用密鑰加密後返回給應用程序,應用程序將數字簽名保存。所以,須要修改使用動態腳本的應用程序的部分代碼。
看到這裏,精明的你可能發現了,這種方法不行,由於你須要暴露api給應用程序來調用內核的加密服務。若是可能你暴露的api很簡單,若是攻擊者發現了你這個接口,那麼咱們的這麼多努力不就所有浪費了嘛。確實,所以,在調用內核的加解密服務的api裏,須要白名單機制來保證請求的合法性。將會生成動態腳本的程序(包含二進制可執行程序以及腳本)加入到白名單,只有白名單裏的程序才能成功請求加解密服務。
爲解決動態腳本的安全問題,真是很費事。所以建議代碼中應該少用這種技術。這個需求都是用來解決遺留代碼的安全性的,新代碼強烈建議不要使用動態腳本技術。動態腳本在本質上難以和攻擊腳本進行區分,特別是你的動態腳本執行的動做相似於惡意腳本的時候,會使得系統安全能力急劇退化。
本文簡要分析了動態腳本的安全性問題,給出了兩個方案,並分析了第一種方案的漏洞。並逐步完善了第二種方案。僅供你們參考。