自动生成系统关键api的hash函数


layout: post
title: 自动生成系统关键api的hash函数
categories: Algorithm
description: 自动生成系统关键api的hash函数
keywords: Windows
url: https://lichao890427.github.io/ https://github.com/lichao890427/


背景

  《0day安全 软件漏洞分析技术》第二版一书中,有一节动态定位API地址的shellcode,一年前看到该篇文章时,第一次发现hash函数的妙用,不过该文章的缺憾是需要对用到的api,自己分析出不产生碰撞的hash函数,如果api数比较多,用文中使用的hash函数就可能碰撞,因此有必要设计出一种自动适应算法自动生成散列函数。
  文中是这么说的:shellcode最重要放到缓冲区,为了让shellcode更加通用能被大多数缓冲区容纳,总希望shellcode尽可能短。因此在函数名导出表中搜索函数名时,一般不会用"MessageBoxA"这么长的字符串直接比较。通常情况下我们会对所需的api函数进行hash运算,在搜索导出表是对当前遇到的函数进行同样的hash,这样只要比对hash所得的摘要就能判定是不是我们所需的api了,虽然这种搜索算法需要引入额外的hash算法但是可以节省出存储函数名字符串的代码。
  现在考虑,如果想构造出hash表达式可以对任意api都生成不碰撞摘要,应该怎么做呢?基于算法复杂度和计算时间的考虑,应该采用多元一次表达式,设函数名字符串数组为name[64],则hash应为k0name[0]+k1name[1]+k2name[2]+....+k63name[63];现在就是选择一种算法计算出k0~k63。巧妇难为无米之炊,所以我们先要生成api列表,如何获取这个列表呢?列表中应该有哪些api呢?我们仅考虑系统关键函数kernel32.dll user32.dll gdi32.dll ntdll.dll中的导出函数(如需其它函数,做法类似)。参照我之前的一篇文章:http://www.0xaa55.com/thread-312-1-1.html

探索

  • 1.首先保证dumpbin.exe在搜索路径中
  • 2.cd c:\windows\system32
  • 3.(for %i in (dir /s /b kernel32.dll gdi32.dll user32.dll ntdll.dll) do dumpbin -exports %i) >out.txt
  • 4.findstr /X ".[0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F][0-9A-F].[a-zA-Z_]$" out.txt > out1.txt
  • 5.findstr /V "@ $ . : characteristics" out1.txt > out2.txt
  • 6.notepad++正则表达式 将out2.txt用^.*[0-9A-F]{8} ([a-zA-Z_0-9]+)$替换为($1)
  • 7.用perl脚本排序,结果发现out2.txt共4700+个win7 api函数
open RESULT,'out1.txt';
my $hash=();
foreach $key (<RESULT>)
{
    chomp $key;
    $hash{$key}=length $key;
}
close RESULT;
open RESULT,'>out5.txt';
select RESULT;

#for $i (sort keys %hash)按字母排序
for $i (sort sort_by_len keys %hash)
{
    print $i,"\n";
}
close RESULT;
sub sort_by_len
{
    if(length($a)<length($b)){-1}
    elsif(length($a)>length($b)){1}
    else{0}
}

  统计出来的api最大长度为65,最小为3,win7有不到5000个API。下面开始编程,由于hash要消耗很大内存空间所以先将编译器栈区大小设置为0x400000以上的,我们会生成0x100000元素的大数组,利用率=5000/0x100000=4.7‰。算稀疏矩阵了,碰撞率相对较低(PS:很多hash算法都是0x100000000的,自然碰撞率更低,我曾尝试过0xFFFF的数组,利用率=5000/65536=76.3‰,但是计算太慢了,不知道会不会有解)
  设coef为hash系数矩阵,apiname为函数名,hash=apiname[0]coef[0]+apiname[1]coef[1]+....

  我在解决这个问题时遇到的问题有:

  • 1.如何设计算法在短时间内(<1min)计算出系数矩阵,且生成的hash摘要尽可能短
  • 2.遇到2个api名碰撞以后,如何调整系数矩阵coef使这2个api不碰撞
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <stdlib.h>
#include <time.h>
using namespace std;
typedef unsigned char ubyte;
typedef unsigned short uword;
int main(int argc, char* argv[])
{
    string filename;//存储api字符串的txt
    vector<string> apinamearray;//存储所有api字符串
    bool find=false;//是否找到正确hash函数
    int maxsize=0;//获取最长api长度
    bool flag[0x100000];//hash表
    int index[0x100000];//用于存储已经占用hash表的api在apinamearray的索引
    //0x100000意味着生成的hash值在0~0x100000,占用20位,比"AA"的长度还小,很节省空间
    //下面2各变量用于定位当前搜索进程
    char curchar='A';
    int curindx=0;
    cout<<"input filename"<<endl;
    cin>>filename;
    ifstream file(filename.c_str());
    string temp;
    while(getline(file,temp))
    {
        if(temp.size() > maxsize)
            maxsize=temp.size();//寻找最长api以设置hash表达式系数数组长度
        apinamearray.push_back(temp);//添加所有api
    }
    time_t begin=time(NULL);
    //hash=apiname[0]*coef[0]+apiname[1]*coef[1]+....
    ubyte* coef=new ubyte[maxsize];//使coef元素不会超过65535
    for(int i=0;i<maxsize;i++)
    {
        coef[i]=0;//初始化系数
    }
    while(!find)
    {
        memset(flag,0,sizeof(flag));
        bool duplicate=false;
        vector<string>::const_iterator itor=apinamearray.begin();
        int size=0;
        int indx=0;
        while(itor != apinamearray.end())
        {
            temp=*itor;
            int sum=1;
            //核心算法
            for(int i=0;i<temp.size();i++)
            {
                sum += temp.at(i)*coef[i];
                sum &= 0xFFFFF;
            }
            if(flag[sum])//如果已存在,则调整hash表达式coef系数
            {
                string other=apinamearray.at(index[sum]);
                printf("%s duplicate with %s\n",temp.c_str(),other.c_str());//输出碰撞函数,便于调整算法
                int i;
                {//显示当前系数矩阵
                    for(i=0;i<maxsize;i++)
                    {
                        if(i%16 == 0)
                            printf("\n");
                        printf("%d ",coef[i]);
                    }
                }*/
                            //遇到碰撞调整系数
                int size1=temp.size(),size2=other.size();
                //核心算法:对这2个字符串选最后一位不同字符进行修改,这样就可以使下次计算出的结果不同
                //PS:我先前的方法是吧所有不同位存储起来,随机取一位进行设置,不过效果没有这个好
                if(size1<size2)
                {
                    coef[size2-1]++;
                }
                else if(size1>size2)
                {
                    coef[size1-1]++;
                }
                else
                {
                    for(i=size1-1;i>=0;i--)
                    {
                        if(temp.at(i) != other.at(i))
                        {
                            coef[i]++;
                            break;
                        }
                    }
                }
                duplicate=true;
                if(indx>curindx)
                {//显示进度
                    curindx=indx;
                    printf("%d:%d\n",indx,apinamearray.size());
                }
                if(temp.at(0) > curchar)
                {//显示进度
                    curchar=temp.at(0);
                    printf("%c %d:%d\n",curchar,indx,apinamearray.size());
                }
                break;//已经碰撞了,所以修正系数后进行下次检测
            }
            else
            {
                flag[sum]=true;
                index[sum]=indx;
            }
            itor++;
            indx++;
        }
        if(duplicate)
        {
        }
        else
        {
            printf("最佳hash系数:");
            for(int i=0;i<maxsize;i++)
            {
                if(i%30 == 0)
                    printf("\n");
                printf("%d ",coef[i]);
            }
            printf("\n");
            find=true;//找到hash表达式解
        }
    }
    printf("\ntime used:%d\n",time(NULL)-begin);
    delete []coef;
    return 0;
}

确定最佳系数

  最佳hash系数:byte hash[50]=
1 1 1 1 2 30 94 117 223 144 78 181 229 235 183 179 112 129 210 209 233 153 137 112 125 149 196 197 187 252
115 40 168 252 58 189 253 201 228 250 192 202 164 101 137 0 44 0 0 119
time used:22

  确定了hash表达式系数以后,用hash表达式计算出需要的api的hash值存于shellcode中即可,可以发现这样做很节省空间
  对任意api生成的摘要为0~0x100000的不重复数,如果你有n个api,则shellcode占用空间为:20n+400 位,而直接存储api字符串,即使假设平均长度10字节(如果统计一下,会发现平均为18),也需要80n 位,当然节省了空间是用时间换取的,计算hash会稍微损耗一点时间,不过影响不大。

优化

  对任意api生成的摘要为0~0x40000的不重复数,如果你有n个api,则shellcode占用空间为:18n+800 位,而直接存储api字符串,假设平均长度10字节,同样需要80n 位,如果n较大,这种方式更合适,下面是这个问题的回溯解法,这种方式适合文本按长度排列的情况:

#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <stdlib.h>
#include <time.h>
using namespace std;
typedef unsigned char ubyte;
vector<string> apinamearray;//存储所有api字符串
bool flag[0x10000];//hash表
int index[0x10000];//用于存储已经占用hash表的api在apinamearray的索引
bool valid(ubyte* coef,int curdepth)
{
    memset(flag,0,sizeof(flag));
    vector<string>::const_iterator itor=apinamearray.begin();
    int indx=0;
    while(itor != apinamearray.end() && curdepth+1 >= (*itor).size())
    {
        int sum=0;
        //核心算法
        int size=(*itor).size();
        for(int i=0;i<size;i++)
        {
            sum += (*itor).at(i)*coef[i];
            sum &= 0xFFFF;
        }   
        if(flag[sum])
        {
            string other=apinamearray.at(index[sum]);
            printf("%s duplicate with %s\n",(*itor).c_str(),other.c_str());
            return false;
        }
        else
        {
            flag[sum]=true;
            index[sum]=indx;
        }
        itor++;
        indx++;
    }
    return true;
}
void showhash(ubyte* coef,int maxsize)
{
    vector<string>::const_iterator itor=apinamearray.begin();
    while(itor != apinamearray.end())
    {
        int sum=0;
        //核心算法
        int size=(*itor).size();
        for(int i=0;i<size;i++)
        {
            sum += (*itor).at(i)*coef[i];
            sum &= 0xFFFF;
        }
        printf("%s->%d ",(*itor).c_str(),sum);
        itor++;
    }
}
int main(int argc, char* argv[])
{
    int maxcoef=64;//参数界限
    string filename;//存储api字符串的txt
    int minsize=INT_MAX;//获取最短api长度
    int maxsize=0;//获取最长api长度
    //下面变量用于定位当前搜索进程
    int curmaxdepth=0;//搜索到的最大深度
    time_t begin=time(NULL);
    cout<<"input filename"<<endl;
    cin>>filename;
    ifstream file(filename.c_str());
    string temp;
    while(getline(file,temp))
    {
        if(temp.size() > maxsize)
            maxsize=temp.size();//寻找最长api以设置hash表达式系数数组长度
        if(temp.size() < minsize)
            minsize=temp.size();//寻找最短api长度
        apinamearray.push_back(temp);
    }
    ubyte* coef=new ubyte[maxsize+1];//使coef元素不会超过65535
    for(int i=0;i<maxsize;i++)
    {
        coef[i]=0;//初始化系数
    }
    int layer=0;
    while(layer >= 0)
    {
        if(curmaxdepth<layer)
        {
            curmaxdepth=layer;
            printf("max:%d:%d\n",layer,maxsize);
        }
        else if(layer < 2)
        {
            printf("%d:%d/%d\n",layer,coef[layer],maxcoef);
        }
        while(coef[layer]<maxcoef && !valid(coef,layer))
        {
            coef[layer]++;
        }
        if(coef[layer]<maxcoef)
        {
            if(layer == maxsize)
            {
                //显示当前系数矩阵
                for(i=0;i<maxsize;i++)
                {
                    if(i%16 == 0)
                        printf("\n");
                    printf("%d ",coef[i]);
                    showhash(coef,maxsize);
                }
                printf("\n");
                layer--;
                coef[layer]++;
            }
            else
            {
                layer++;
                coef[layer]=0;
            }
        }
        else
        {
            layer--;
            if(layer>=0)
                coef[layer]++;
        }
    }
    printf("\ntime used:%d\n",time(NULL)-begin);
    delete []coef;
    return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 219,490评论 6 508
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,581评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 165,830评论 0 356
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,957评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,974评论 6 393
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,754评论 1 307
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,464评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,357评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,847评论 1 317
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,995评论 3 338
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,137评论 1 351
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,819评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,482评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,023评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,149评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,409评论 3 373
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,086评论 2 355

推荐阅读更多精彩内容

  • 一、温故而知新 1. 内存不够怎么办 内存简单分配策略的问题地址空间不隔离内存使用效率低程序运行的地址不确定 关于...
    SeanCST阅读 7,815评论 0 27
  • sqlmap用户手册 说明:本文为转载,对原文中一些明显的拼写错误进行修正,并标注对自己有用的信息。 ======...
    wind_飘阅读 2,051评论 0 5
  • http://192.168.136.131/sqlmap/mysql/get_int.php?id=1 当给sq...
    xuningbo阅读 10,322评论 2 22
  • 挂经幡祈祷世界和平 愿众生离苦得乐 一直看着月亮走 很快就到山顶了 撒隆达 敬畏山神 路上还发现了动物的骷髅
    牦牛出街阅读 246评论 0 0
  • 今天哥风尘仆仆地带着侄子从淮北开车到亳州,到人才招聘市场给侄子找工作。我上午有课,没时间陪他们一块儿去。十点多我刚...
    柏郎阅读 199评论 1 1