在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里,须要白名单机制来保证请求的合法性。将会生成动态脚本的程序(包含二进制可执行程序以及脚本)加入到白名单,只有白名单里的程序才能成功请求加解密服务。
为解决动态脚本的安全问题,真是很费事。所以建议代码中应该少用这种技术。这个需求都是用来解决遗留代码的安全性的,新代码强烈建议不要使用动态脚本技术。动态脚本在本质上难以和攻击脚本进行区分,特别是你的动态脚本执行的动做相似于恶意脚本的时候,会使得系统安全能力急剧退化。
本文简要分析了动态脚本的安全性问题,给出了两个方案,并分析了第一种方案的漏洞。并逐步完善了第二种方案。仅供你们参考。