首页
社区
课程
招聘
[原创]在ShellCode里面使用异常处理(Win64位平台)
2023-6-12 23:46 18049

[原创]在ShellCode里面使用异常处理(Win64位平台)

2023-6-12 23:46
18049

      网上关于ShellCode编写的文章很多,但介绍如何在ShellCode里面使用异常处理的却很少。笔者前段时间写了一个ShellCode,其中有一个功能是内存加载多个别人写的DLL插件,然后调用里面的函数,结果因为某个DLL函数里面发生了异常,导致ShellCode进程直接闪退,所以学习了一下在ShellCode里面如何使用异常处理的方法。


      Windows程序的异常处理,其实是三者结合的:操作系统、编译器和程序代码。因为x86下异常处理的文章太多,所以本文只介绍Win64下的。X86的异常(本文仅谈论SEH异常)一般的流程为:进入 try...语句之前先注册到异常链表,执行完代码后,再摘除,不管有没有异常发生,这个步骤都是必不可少的,所以多多少少会影响程序的性能。而Win64的异常处理,是基于表的。也就是说,编译器在编译代码的时候,会同时对每个函数(还分非叶,不深究)生成一个异常表,最后链接到PE的异常表里。当程序发生异常的时候,操作系统跟根据当前地址,枚举所有异常表,如果地址位于某个表的开始和结束地址之间,则使用这个表处理。相对来说,这个比X86更加安全和高效。


    这个异常表,称为RUNTIME_FUNCTION,MSDN里面定义如下:

typedef struct _IMAGE_RUNTIME_FUNCTION_ENTRY {
  DWORD BeginAddress;
  DWORD EndAddress;
  union {
    DWORD UnwindInfoAddress;
    DWORD UnwindData;
  } DUMMYUNIONNAME;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION, _IMAGE_RUNTIME_FUNCTION_ENTRY, *_PIMAGE_RUNTIME_FUNCTION_ENTRY;

    这些表连续存放在PE的异常目录中,每个表对应一个函数,所有成员都是相当于ImageBase的开始相对地址。其中BeginAddress表示这个函数的开始地址,EndAddress则对应结束地址,最重要的,是UnwindInfoAddress,它对应另外一个结构UNWIND_INFO


typedef struct _UNWIND_INFO {
    UBYTE Version       : 3;
    UBYTE Flags         : 5;
    UBYTE SizeOfProlog;
    UBYTE CountOfCodes;
    UBYTE FrameRegister : 4;
    UBYTE FrameOffset   : 4;
    UNWIND_CODE UnwindCode[1];/*  UNWIND_CODE MoreUnwindCode[((CountOfCodes + 1) & ~1) - 1];
*   union {
*       OPTIONAL ULONG ExceptionHandler;
*       OPTIONAL ULONG FunctionEntry;
*   };
*   OPTIONAL ULONG ExceptionData[]; */} UNWIND_INFO, *PUNWIND_INFO;

    成员如下:


    Version:版本号,一般为1,最高目前是2。


    Flags:取值如下

            #define UNW_FLAG_EHANDLER  0x01 ---表示有except块

           #define UNW_FLAG_UHANDLER  0x02 ---表示有finally块

           #define UNW_FLAG_CHAININFO 0x04 ---表示后面是另外一个Runtime_Function


 SizeOfProlog:函数头到try块的位置。


 CountOfCodes:表示后面有多少个UNWIND_CODE 结构。注意:这个数值是偶数对齐的,如果为奇数,说明最后一个为空值的UNWIND_CODE。


 UNWIND_CODE数组:实际上就是回滚表,保存了进入异常代码前的寄存器状态,用于异常处理后回滚到原来的状态。


    ExceptionHandler(可选):如果Flags包含了UNW_FLAG_EHANDLER 或UNW_FLAG_UHANDLER,则ExceptionHandler指向异常处理函数。其中Delphi语言是指向system.pas里面的函数_DelphiExceptionHandler;VS相对来说复杂一些,如果是SEH异常,一般是指向__C_specific_handler函数。


ExceptionData(可选):这个具体语言是不同的,所以windbg解释异常表的时候也是只解释到上一个成员。这个一般(Delphi语言则肯定也只会)指向一个SCOPE_TABLE结构:


 typedef struct _SCOPE_TABLE 
 {
  ULONG Count;     
  struct     
    {         
     ULONG BeginAddress;         
     ULONG EndAddress;         
     ULONG HandlerAddress;         
     ULONG JumpTarget;     
     } ScopeRecord[1]; 
 } SCOPE_TABLE, *PSCOPE_TABLE;


    简单一点来说,Runtime_function是给操作系统判断异常发生在哪个函数里面,SCOPE_TABLE则是在异常处理函数里面使用的(触发异常处理函数的时候,会传递过去),主要记录了更精确的异常发生位置区间和需要跳转处理的地址。


    如果想在ShellCode里面使用异常,则请进行如下步骤:



1、提取语言的异常处理函数,让它称为ShellCode的一个函数(Delphi语言是system.pas里面的_DelphiExceptionHandler函数,VS则应该提取C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\crt\src\amd64\chandler.c里面的__C_specific_handler函数)。注意提取后的ShellCode化(修正API调用等)。当然,你也可以完全自己编写处理函数。


2、在需要使用异常处理的函数像往常一样使用try catch...try except...,在保存ShellCode的时候,使用API函数RtlLookupFunctionEntry查找这个函数的Runtime_Function,然后查找成员UNWIND_INFO,修正所有的相对地址后,跟ShellCode放在一起。


3、ShellCode运行后,调用API函数RtlAddFunctionTable将第二步修正的表添加到系统。



附件说明:

1、PE64异常表枚举工具.exe:一个用于枚举异常表的小工具。对于Delphi编译的PE,还会枚举SCOPE_TABLE。

2、ShellCode64.bin:一个在ShellCode里面使用异常的DEMO。

3、LoadTest.exe:加载ShellCode64.bin进行测试的程序,代码见LoadTest.dpr。

4、Dll.dll:测试调用Dll里面有异常发生的函数。

5、Dll.dpr、LoadTest.dpr。Delphi代码。



其中ShellCode64.bin的部分代码如下:


“程序内置单个异常测试”按钮:

procedure OnButton1Click(hForm1: HWND);
var
  bExcept: Boolean;
  i, j, nRet: Integer;
  szMaker: array[0..3] of AnsiChar;
  szBuffer: array[0..127] of AnsiChar;
begin
  i := 10;
  j := 0;
  bExcept := False;
  try
    nRet := i div j;
  except
    WindowsAPI^.fnMessageBoxA(hForm1, '进入except节', FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
    nRet := 0;
    bExcept := True;
  end;

  if bExcept then
  begin
    WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('发生异常!nRet=%d'), nRet);
  end
  else
  begin
    WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('没有异常!nRet=%d'), nRet);
  end;

  WindowsAPI^.fnMessageBoxA(hForm1, szBuffer, FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
end;


“程序内置镶套异常测试”按钮:这个实际上是先触发除0异常,再在异常里面通过给一个空指针赋值产生第二个异常:

procedure OnButton2Click(hForm1: HWND);
var
  bExcept: Boolean;
  i, j, nRet: Integer;
  szMaker: array[0..3] of AnsiChar;
  szBuffer: array[0..127] of AnsiChar;
  p: Pointer;
begin
  i := 10;
  j := 0;
  p := nil;//空指针
  bExcept := False;

  try
    nRet := i div j;
  except
    WindowsAPI^.fnMessageBoxA(hForm1, '进入except节1', FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);

    try
      PInteger(p)^ := 999;
    except
      bExcept := True;
      WindowsAPI^.fnMessageBoxA(hForm1, '进入except节2', FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
    end;

    nRet := 0;
    bExcept := True;
  end;

  if bExcept then
  begin
    WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('发生异常!nRet=%d'), nRet);
  end
  else
  begin
    WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('没有异常!nRet=%d'), nRet);
  end;

  WindowsAPI^.fnMessageBoxA(hForm1, szBuffer, FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
end;



“Dll函数1"按钮和“Dll函数2"按钮的代码其实是一样的,只不过是Dll导出的函数名称不一样而已

procedure OnButton4Click(hForm1, hEdit: HWND);
type
  TGetInteger = function: Integer; stdcall;
var
  szDllFileName: array[0..MAX_PATH - 1] of AnsiChar;
  hDll: HMODULE;
  MyGetIntege: TGetInteger;
var
  bExcept: Boolean;
  nRet: Integer;
  szMaker: array[0..3] of AnsiChar;
  szBuffer: array[0..127] of AnsiChar;
begin
  WindowsAPI^.fnGetWindowTextA(hEdit, szDllFileName, MAX_PATH);

  hDll := WindowsAPI^.fnLoadLibraryA(szDllFileName);
  if hDll = 0 then
  begin
    WindowsAPI^.fnMessageBoxA(hForm1, FixPAnsiChar('Dll文件加载失败!'), FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
    Exit;
  end;

  @MyGetIntege := WindowsAPI^.fnGetProcAddress(hDll, FixPAnsiChar('GetInteger1'));
  if @MyGetIntege = nil then
  begin
    WindowsAPI^.fnMessageBoxA(hForm1, FixPAnsiChar('函数GetInteger1获取失败!'), FixPAnsiChar('MyTest'), MB_ICONWARNING + MB_TOPMOST);
    WindowsAPI^.fnFreeLibrary(hDll);
    Exit;
  end;

  bExcept := False;
  try
    nRet := MyGetIntege;
  except
    WindowsAPI^.fnMessageBoxA(hForm1, '进入except节', FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
    nRet := 0;
    bExcept := True;
  end;

  if bExcept then
  begin
    WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('发生异常!nRet=%d'), nRet);
  end
  else
  begin
    WindowsAPI^.fnwsprintfA(szBuffer, FixPAnsiChar('没有异常!nRet=%d'), nRet);
  end;

  WindowsAPI^.fnMessageBoxA(hForm1, szBuffer, FixPAnsiChar('MyTest'), MB_ICONINFORMATION + MB_TOPMOST);
end;



Dll主要导出两个函数,一个有异常,一个没有异常(GetInteger2实际上也是可能存在异常的---如果随机数为0),你也可以使用VS之类编写一个DLL来测试:

function GetInteger1:Integer;stdcall;
var
i,j:Integer;
begin
  i:=0;
  j:=Random(100);
  Result:=j div i;
end;

function GetInteger2:Integer;stdcall;
var
i,j:Integer;
begin
  i:=Random(100);
  j:=Random(10000);
  Result:=j div i;
end;



ShellCode的入口函数:

procedure _Start;
var
  p: PAnsiChar;
  nCount: Integer;
  SDKForm: TSDKForm; //SDK窗口类
begin
  if not InitWindowsAPI then Exit; //初始化全局API函数表

  p := FixPAnsiChar(fnA); //获取异常表信息位置
  nCount := PInteger(p)^;
  Inc(p, sizeof(Integer));
  if not WindowsAPI^.fnRtlAddFunctionTable(p, nCount, DWORD64(@_Start)) then
  begin
    WindowsAPI^.fnMessageBoxA(0, FixPAnsiChar('fnRtlAddFunctionTable Error'), FixPAnsiChar('Caption'), MB_ICONWARNING + MB_TOPMOST);
    Exit;
  end;

  SDKForm.CreateWindow;
  SDKForm.MessageLoop;

  DoneWindowsAPI;//释放全局API函数表
end;


里面的函数FixPAnsiChar其实是在X86下使用的,Win64下Delphi使用的都是相对地址了,可以直接跟平时一样使用字符串的。


这是我写的第五个shellcode程序,错误在所难免,见谅!



[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2023-6-14 14:16 被bestbird编辑 ,原因:
上传的附件:
收藏
点赞8
打赏
分享
最新回复 (19)
雪    币: 19734
活跃值: (29369)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-6-13 09:52
2
1
mark
雪    币: 884
活跃值: (4095)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
kagayaki 2023-6-13 15:09
3
0
谢谢!
雪    币: 248
活跃值: (3789)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
luskyc 2023-6-13 23:33
4
0
谢谢分享,留个mark
雪    币: 8236
活跃值: (4946)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
大道在我 2023-6-13 23:47
5
0
mark
雪    币: 30090
活跃值: (2102)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-6-15 23:01
6
0

更新了一下附件,主要是针对多线程的异常做了演示,同时为"PE64异常表枚举工具"关闭了文件重定向,添加了一个注入到记事本的加载程序和代码。

最后于 2023-6-15 23:35 被bestbird编辑 ,原因:
上传的附件:
雪    币: 462
活跃值: (713)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
_emmm_ 2023-6-16 22:40
7
0
32位的程序里面会出现shellcode 与 loader的编译器版本不一样,而导致shellcode里面的异常无法正常运行。大佬直到这个咋解决吗
雪    币: 483
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
出其东门Nn 2023-6-17 08:35
8
0
感谢分享!
雪    币: 30090
活跃值: (2102)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-6-17 14:13
9
0
_emmm_ 32位的程序里面会出现shellcode 与 loader的编译器版本不一样,而导致shellcode里面的异常无法正常运行。大佬直到这个咋解决吗
32位的我还没有弄过,所以没办法回答,不好意思。
雪    币: 30090
活跃值: (2102)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-6-17 16:41
10
0
_emmm_ 32位的程序里面会出现shellcode 与 loader的编译器版本不一样,而导致shellcode里面的异常无法正常运行。大佬直到这个咋解决吗
花时间看了一下win32的(delphi语言),感觉跟x64的处理是大同小异的,所以我有点怀疑你的情况是生成shellcode后,没有对异常处理函数的地址作修正---有可能你没有把异常处理函数(Delphi下是system.pas里面的_HandleAnyException)扣出来跟shellcode放在一起,所以我大胆猜测:你是直接保存了shellcode,里面的异常处理函数地址指向原来的(以Delphi为例,_HandleAnyException位于低地址),你的Loader加载Shellcode后,当ShellCode里面有异常发生,就跳转到地址比如说0x5678,而这个地址恰好是Loader里面的异常处理函数的地址,所以一切看起来都没有问题,但是如果你更换了不同的编译器,导致这个地址改变了,所以肯定无效而且估计会闪崩。ShellCode的异常处理是不应该依赖Loader的,比如说,你将shellcode注入到其它进程执行,也不应该有问题才是。
雪    币: 10153
活跃值: (1789)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wzmooo 2023-6-20 14:44
11
0
期待完善方案,并配套基本代码跟着学学
雪    币: 1533
活跃值: (3355)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
小希希 2023-6-20 21:47
12
0
mark
雪    币: 462
活跃值: (713)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
_emmm_ 2023-6-24 18:16
13
0
bestbird 花时间看了一下win32的(delphi语言),感觉跟x64的处理是大同小异的,所以我有点怀疑你的情况是生成shellcode后,没有对异常处理函数的地址作修正---有可能你没有把异常处理函数(Del ...
好像不太是,不同版本的编译器生成的异常有点区别,比如说VC6.0生成的程序,和VS 2013生成的。我如果去用
vs2013生成的loader去加载vs2013生成的 程序,就能正常运行。但是去加载 vc6.0生成的程序就无法正常运行。
雪    币: 282
活跃值: (732)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
方圆弈事 2023-6-27 11:26
14
0
mark
雪    币: 0
活跃值: (364)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
wx_pretty^ 2023-6-27 20:41
15
0
大佬,可否给个C++的版本以供学习,谢谢
雪    币: 30090
活跃值: (2102)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-7-10 14:21
16
0
_emmm_ 好像不太是,不同版本的编译器生成的异常有点区别,比如说VC6.0生成的程序,和VS 2013生成的。我如果去用 vs2013生成的loader去加载vs2013生成的 程序,就能正常运行。但是去加载 ...
没有用C,所以不清楚了。
雪    币: 30090
活跃值: (2102)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-7-10 14:22
17
0
wx_pretty^ 大佬,可否给个C++的版本以供学习,谢谢

C++的没有玩过,大同小异的。

最后于 2023-7-10 22:02 被bestbird编辑 ,原因:
雪    币: 30090
活跃值: (2102)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-7-11 15:52
18
0

.

最后于 2023-7-12 17:18 被bestbird编辑 ,原因:
雪    币: 4340
活跃值: (1977)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
GeaC 2023-7-14 15:07
19
0
好滴,支持
雪    币: 30090
活跃值: (2102)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-7-14 16:49
20
0
_emmm_ 好像不太是,不同版本的编译器生成的异常有点区别,比如说VC6.0生成的程序,和VS 2013生成的。我如果去用 vs2013生成的loader去加载vs2013生成的 程序,就能正常运行。但是去加载 ...
我还是很疑惑。因为不管是32位还是64位的,如果你想在ShellCode里面使用异常处理,理论上一个是对需要处理的代码段注册,异常触发后操作系统就会去执行注册的异常回调函数。如果你的异常回调函数本身是在ShellCode里面的,理论上是跟你的loader毫无关系的。
顺便说一句,32位的我也写了一篇:https://bbs.kanxue.com/thread-278009.htm,感兴趣的话可以去看看。
游客
登录 | 注册 方可回帖
返回