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

VSole2023-06-21 11:19:17

网上关于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程序,如有错漏之处,敬请指正!

异常处理delphi
本作品采用《CC 协议》,转载必须注明作者和本文链接
网上关于ShellCode编写的文章很多,但介绍如何在ShellCode里面使用异常处理的却很少。Windows程序的异常处理,其实是三者结合的:操作系统、编译器和程序代码。因为x86下异常处理的文章太多,所以本文只介绍Win64下的。而Win64的异常处理,是基于表的。也就是说,编译器在编译代码的时候,会同时对每个函数生成一个异常表,最后链接到PE的异常表里。相对来说,这个比X86更加安全和高效。
近日,奇安信威胁情报中心红雨滴团队在日常的威胁狩猎中捕获了该组织多个Crimson RAT攻击样本。值得注意的是Transparent Tribe组织为了降低攻击样本的查杀率,对相关攻击样本进行了加壳处理。在本次攻击活动中,MuddyWater还利用金丝雀令牌来跟踪目标的成功感染,这是该组织新利用的TTP。
六方云工业卫士:OT与IT融合下的工控主机“守护神”
Zeppelin勒索软件的开发者在经历了一段相对沉寂的时期后,已经恢复了活动,并开始宣传该恶意软件的新版本。上个月末,该恶意软件的一种最新变种出现在一个黑客论坛上。
0x00 日常查壳无壳64位0x01 CFGGETC在讲这题ollvm与异常处理之前,有必要先搞懂我们到底是怎么输入的。一共有三处getc处理我们第一段输入的地方。程序最先开始运行的是 407629,这里我们可以输入上下左右箭头与特定的数字。随后到 40553A 读取为5B。
内核学习-异常处理
2021-12-31 16:22:12
异常产生后,首先是要记录异常信息,然后要寻找异常的处理函数,称为异常的分发,最后找到异常处理函数并调用,称为异常处理异常处理异常分发,异常处理 展开。
Windows中主要的异常处理机制:VEH、SEH、C++EH。 SEH中文全称:结构化异常处理。就是平时用的__try __finally __try __except,是对c的扩展。 VEH中文全称:向量异常处理。一般来说用AddVectoredExceptionHandler去添加一个异常处理函数,可以通过第一个参数决定是否将VEH函数插入到VEH链表头,插入到链表头的函数先执行,如果为
但是,这样一来,就必须在每一个Controller类都定义一套这样的异常处理方法,因为异常可以是各种各样。所以注解@ControllerAdvice出现了,简单的说,该注解可以把异常处理器应用到所有控制器,而不是单个控制器。这就是统一异常处理的原理。统一异常处理实战在定义统一异常处理类之前,先来介绍一下如何优雅的判定异常情况并抛异常
Kafka消息积压的典型场景:1.实时/消费任务挂掉比如,我们写的实时应用因为某种原因挂掉了,并且这个任务没有被监控程序监控发现通知相关负责人,负责人又没有写自动拉起任务的脚本进行重启。此外,Kafka分区数是Kafka并行度调优的最小单元,如果Kafka分区数设置的太少,会影响Kafka consumer消费的吞吐量。
VSole
网络安全专家