导入表与导出表

首先说几个基本问题:

1.导入表的作用是什么?没有它exe能运行么?
作用:记录了一个exe或者一个dll所用到的其他模块导出的函数;
所记录的信息有:用了哪些模块(用了哪些dll),用了dll的哪些函数

2.导出表的作用?没有它exe能运行么?
作用:记录了导出符号的地址,名称,与序号
(提示:exe文件中很少有导出表的,大多数dll都有导出表,某些存放资源文件的dll就没有导出表)

下面截取自笔记,便于理解导入表与导出表的关系
在这里插入图片描述

在这里插入图片描述

3.怎样才能知道一个exe用了哪些API?
通过遍历导入表就可以知道导入了哪些dll以及dll中的哪些API

4.已知一个dll名和dll导出函数的名字,如何得到这个函数名的地址
通过Loadlibrary(GetModelHandle)将dll模块映射进内存并返回一个可以被GetProcAddress函数使用的句柄,再利用GetProcAddress得到dll的加载地址,通过遍历导出表就可以得到该函数的地址.

5.如何判断导入函数是以序号导入还是以名称导入?
在IMAGE_THUNK_DATA32这个结构体(结构体里面就是一个联合体,大小为32位)里面,判断结构体字段中的最高位,
最高位为1:以序号导入
最高位为0:以名称导入

6.怎样才能知道导出函数是以序号导出还是以名称导出?
遍历序号表,判断地址表的下标有没有存在与序号表中,存在就说明是以名称导出,不存在就说明是以序号导出

双桥结构

导入表几个简图(笔记中的,将就看吧):

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

解析导入表:

#include "stdafx.h"
#include <windows.h>



DWORD RvaToOffset( IMAGE_NT_HEADERS* pNtHdr , DWORD dwRva ) {

    // 1. 找Rva所在的区段
    // 2. 用Rva减去所在区段的首Rva ,再用减出来的差,加上所在
    //    区段的首区段偏移
    IMAGE_SECTION_HEADER* pSechdr = NULL;
    pSechdr = IMAGE_FIRST_SECTION( pNtHdr );
    for( int i = 0 ; i < pNtHdr->FileHeader.NumberOfSections; ++i ) {
        if( dwRva >= pSechdr[ i ].VirtualAddress
            && dwRva <= pSechdr[ i ].VirtualAddress + pSechdr[ i ].SizeOfRawData ) {

            dwRva -= pSechdr[ i ].VirtualAddress;
            dwRva += pSechdr[ i ].PointerToRawData;
            return dwRva;
        }
    }
    return -1;
}



int main( ) {
    
    //typedef struct _IMAGE_IMPORT_DESCRIPTOR {
    //  union {
    //      DWORD   Characteristics;            
    //      DWORD   OriginalFirstThunk; /*保存导入函数名称表(INT)的地址(RVA)*/        
    //  } DUMMYUNIONNAME;
    //  DWORD   TimeDateStamp;
    //  DWORD   ForwarderChain;// 是否是dll转发
    //  DWORD   Name;/*导入的模块名(DLL的名字)*/
    //  DWORD   FirstThunk;/*导入函数地址表(IAT)(RVA)*/
    //} IMAGE_IMPORT_DESCRIPTOR;
    // 解析导入表
    // 1. 读取文件到内存
    printf( "请输入要解析的DLL的路径:" );
    char szPath[ MAX_PATH ];
    gets_s( szPath , MAX_PATH );

    HANDLE hFile = INVALID_HANDLE_VALUE;
    hFile = CreateFileA( szPath , GENERIC_READ , FILE_SHARE_READ ,
                         NULL , OPEN_EXISTING , FILE_ATTRIBUTE_NORMAL , NULL );
    if( hFile == INVALID_HANDLE_VALUE ) {
        printf( "文件不存在\n" );
        system( "pause" );
        return 0;
    }
    DWORD dwHeight = 0;
    DWORD dwFileSize = GetFileSize( hFile , &dwHeight );

    BYTE* pBuff = new BYTE[ dwFileSize ];

    ReadFile( hFile , pBuff , dwFileSize , &dwFileSize , NULL );

    // 2. 解析出Dos头,Nt头,扩展头
    IMAGE_DOS_HEADER* pDosHdr = NULL;
    IMAGE_NT_HEADERS* pNtHdr = NULL;
    IMAGE_OPTIONAL_HEADER* pOptHdr = NULL;
    IMAGE_DATA_DIRECTORY* pDataDir = NULL;
    IMAGE_EXPORT_DIRECTORY* pExortTable = NULL;


    pDosHdr = (IMAGE_DOS_HEADER*)pBuff;
    pNtHdr = (IMAGE_NT_HEADERS*)( (ULONG_PTR)pDosHdr + pDosHdr->e_lfanew );
    pOptHdr = &pNtHdr->OptionalHeader;

    // 3. 得到扩展头中的数据目录表
    pDataDir = pOptHdr->DataDirectory;

    // 4. 通过数据目录表中的第1个元素得到导入表的RVA
    DWORD dwImpRva = pDataDir[ 1 ].VirtualAddress;
    if( dwImpRva == 0 ) {
        printf( "没有导入表" );
        system( "pause" );
        return 0;
    }
    // 5. 将导入表的RVA转换成文件偏移
    DWORD dwImpOfs = RvaToOffset( pNtHdr , dwImpRva );

    // 6. 使用导入表结构体指针指向导入表所在的内存地址
    IMAGE_IMPORT_DESCRIPTOR* pImpTab = NULL;
    pImpTab = (IMAGE_IMPORT_DESCRIPTOR*)( dwImpOfs + (ULONG_PTR)pDosHdr );

    // 7. 遍历所有的导入表
    //    导入表是以一个全0元素作为结束的标志
    while( pImpTab->Name != 0 ) {

        // 7.1 获取这个导入所导入的DLL的名称,导入表里面的Name也是一个RVA
        ULONG_PTR dwNameOfs = RvaToOffset( pNtHdr , pImpTab->Name );
        char* pName = nullptr;
        pName = (char *)( dwNameOfs + (ULONG_PTR)pDosHdr );
        printf( "导入的DLL:%s\n" , pName );
        //IMAGE_THUNK_DATA32
        // 7.2 遍历从这个dll导入的函数名称
        DWORD dwINToffset = RvaToOffset(pNtHdr, pImpTab->OriginalFirstThunk);

        // INT : 导入名称表
        // 这个表记录了所有导入函数名称的地址(RVA)
        // 每个地址都是4/8字节(如果是32位PE文件,就是4字节,如果是64位的PE文件就是8字节)
        ULONG_PTR* pInt = (ULONG_PTR*)( dwINToffset + (ULONG_PTR)pDosHdr );

        while( *pInt != 0 ) {
            // 导入函数有两种方式:
            // 1. 以序号导入
            // 2. 以名称导入
            // 如果INT表中保存的值,最高位是0的时候,说明这个值是一个函数名称的RVA
            // 否则这个值的低16位就是一个导入的序号。
            if( IMAGE_SNAP_BY_ORDINAL( *pInt ) ) {

                // 以序号方式导入
                // 以序号方式导入, 保存的是序号,需要只有2字节
                printf( "\t0x%04X\n" , *pInt & 0xFFFF );
            }
            else {
                // 以名称方式导入,数组保存的RVA,并非是一个字符串的RVA
                // 而是一个结构体(IMAGE_IMPORT_BY_NAME)的RVA
                IMAGE_IMPORT_BY_NAME* pImpByName = NULL;
                DWORD dwNameRva = *pInt;
                dwNameOfs = RvaToOffset( pNtHdr , dwNameRva );
                pImpByName = (IMAGE_IMPORT_BY_NAME*)( dwNameOfs + (ULONG_PTR)pDosHdr );
                printf( "\t序号:%04X,名称:%s这是以名称方式导入的\n" ,
                        pImpByName->Hint ,
                        pImpByName->Name );
            }

            // 得到下一个导入函数的名称的地址
            ++pInt;
        }
        // 得到下一个导入表的地址
        ++pImpTab;
    }  

    system( "pause" );
    return 0;
}

另外一种方式:

遍历导入表中的INT修复IAT
while (exeimport->Name != NULL) //遍历模块
{
HMODULE h_dllModule = apis.pfnLoadLibraryA((char *)(exeimport->Name + ImageBase));
PIMAGE_THUNK_DATA  import_Int = (PIMAGE_THUNK_DATA)(exeimport->OriginalFirstThunk+ImageBase);
PIMAGE_THUNK_DATA  import_IAT = (PIMAGE_THUNK_DATA )(exeimport->FirstThunk+ ImageBase);

while (import_Int->u1.Ordinal != 0) //遍历函数
{
UCHAR *buf = (UCHAR *)apis.pfnHeapAlloc(heap, HEAP_ZERO_MEMORY, 10);
buf[0] = 0xb8;
buf[5] = 0xff;
buf[6] = 0xe0;
//new char[20]{ "\xB8\x00\x00\x00\0x00\0xff\0xe0" };
DWORD opl = 0;
apis.pfnVirtualProtect((LPVOID)buf, 20, PAGE_EXECUTE_READWRITE, &opl);
if (import_Int->u1.Ordinal &0x80000000) //这里是重点!!!序号导出, 最高位为1,这里是获取最高位,如果最高位为1,就执行下面里面的语句,即
//以序号导入 ,否则以名称导入,执行else中的语句
{


//获取序号函数
LPVOID apiaddr =
apis.pfnGetProcAddress(h_dllModule, (char *)(import_Int->u1.Ordinal & 0xFFFF));

*(DWORD*)&buf[1] = (DWORD)apiaddr;  //函数写入shellcode

//DWORD funaddr = ;


apis.pfnVirtualProtect((LPVOID)(import_IAT ), 4, PAGE_EXECUTE_READWRITE, &opl);
*(DWORD*)((DWORD)import_IAT ) = (DWORD)buf; //将函数写入到iat
}
else
{
//DWORD Faddr = *(DWORD*)(import_Int->u1.AddressOfData + ImageBase);
PIMAGE_IMPORT_BY_NAME funname = (PIMAGE_IMPORT_BY_NAME)(import_Int->u1.AddressOfData+ImageBase);
LPVOID apiaddr =
apis.pfnGetProcAddress(h_dllModule, funname->Name);

*(DWORD*)&buf[1] = (DWORD)apiaddr;  //函数写入shellcode

apis.pfnVirtualProtect((LPVOID)(import_IAT), 4, PAGE_EXECUTE_READWRITE, &opl);
*(DWORD*)((DWORD)import_IAT ) = (DWORD)buf; //将函数写入到iat
//              DWORD funaddr =import_IAT->u1.Function  ;  //获取iat地址
//
//              apis.pfnVirtualProtect((LPVOID)funaddr, 4, PAGE_EXECUTE_READWRITE, &opl);
//              *(DWORD*)(funaddr) = (DWORD)buf; //将函数写入到iat
}

import_Int++;
import_IAT++;

}
exeimport++;
}



导入表:

(exe例子为:内存管理-VirtualAlloc)
LordPE查看其导入表RVA:

在这里插入图片描述
010editor中验证:
在这里插入图片描述

导入表位置,落在了.idata段:
在这里插入图片描述

1A000 【7800】
1A1C8 【79C8】

79C8:(这个值为文件中的偏移,就是它在文件中的起始位置,利用79C8在010editor里面可以找到导入表)
在010editor中:

在这里插入图片描述

typedef struct _IMAGE_IMPORT_DESCRIPTOR{
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;//指向一个结构体数组的相对位移,RVA to original unbound IAT (PIMAGE_THUNK_DATA)
}DUMMYUNIONNAME;
DWORD TimeDataStamp;
DWORD ForwarderChain;
DWORD Name;    //导入的PE文件的名字的相对位移(RVA)
DWORD FirstThunk ;//指向一个结构体数组的相对位移(RVA  to IAT)
}

在这里插入图片描述

18 A2 01 00【OriginalFirstThunk:INT(Import Name Table)导入名称表地址RVA】
00 00 00 00
00 00 00 00
22 A4 01 00【DLL名称的RVA】
00 A0 01 00【IAT(Import Address Table)导入地址表地址RVA】

1.dll的名字【22 A4 01 00(小端)—>0001A422(因为1A000对应7800,故这里dll在文件中偏移为7C22)】
在这里插入图片描述

2.看下INT(OriginalFirstThunk):1A218【7A18】(以全0结尾)

在这里插入图片描述

函数名数组(对应IMAGE_THUNK_DATA32结构体数组,每一个结构体就是一个联合体)

E0 A3 01 00----0001A3E0【7BE0】最高位为0,说明是以名称导入的,不是序号导入的;
一共导入了25个函数,这里只写一个,其他依次类推!
注意:IAT和INT都指向下面的这个数据结构,4Byte

typedef struct  _IMAGE_THUNK_DATA32{
union{
DWORD ForwarderString;
DWORD Function;//导入函数的地址,在加载到内存之后,这里才起作用
DWORD Ordinal;//假如是序号导入的,会用到这里
DWORD AddressOfData;//假如是函数名导入,会用到这里,它指向另外一个结构体PIMAGE_IMPORT_BY_NAME
}u1;
}IMAGE_THUNK_DATA32;
//如果是函数名导入的,AddressOfData会指向下面这个结构体
typedef struct _IMAGE_IMPORT_BY_NAME{
WORD  Hint;//序号
CHAR Name[1];//不定长,字符串

}

由上可知:是按照函数名导入的(大多数都是按名称导入的),故上面的地址值,就会指向一个PIMAGE_IMPORT_BY_NAME的结构体
【7BE0】
在这里插入图片描述

在这里插入图片描述

看下IAT(00 A0 01 00->0001A000–>【7800】)

在这里插入图片描述

发现最高位也都是0,所以,也是名称导入的,另外,还可以发现,这个位置的值,和INT的值是一样的,因此,不再赘述了
这里的kernel32.dll里面有25个函数

在这里插入图片描述

  • 3
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值