首页
社区
课程
招聘
[原创]一键生成windows x86 shellcode的python脚本
2023-4-15 18:36 9008

[原创]一键生成windows x86 shellcode的python脚本

2023-4-15 18:36
9008

Shellcode_Generator

一键生成windows x86 shellcode的python脚本

目录

项目演示

演示生成的shellcode代码功能为创建regedit进程,向regedit进程远程线程注入添加自启动注册表项完成持久化功能。

介绍

​ 本项目主要想解决手动编写shellcode的过程中痛点问题(全局变量重定位Windows API调用等),帮助用户使用C语言实现的方法一键转换成shellcode。

优点和不足之处


 

优点:

  • 大胆使用全局变量和系统API,再也不用担心地址重定位问题

  • 不局限于MSF和CS提供shellcode模板、随意发挥。

  • 支持嵌套使用shellcode

不足:

  • 暂不支持x64的shellcode生成
  • 对于有些C标准库函数处理不了,比如strtok函数。

使用环境

  • 项目目前仅支持生成x86的shellcode
  • 需要安装IDA软件并支持运行python脚本
  • 使用VS编译项目时需要禁用优化并把代码生成设置为多线程/MT
  • 能使用Windows APIC标准库函数完成功能时,优先使用Windows API,可以减少大量系统依赖。比如exitExitProcess可以实现结束进程,优先使用ExitProcess方法。

用法 Usage

  1. 打开VS项目,在Shellcode_Generator_Demo.c文件中的strat函数处添加需要生成的shellcode代码,编译项目

  2. 使用IDA打开编译好的程序,一路默认选项,可参考演示实例

  3. 在IDA中找到main函数,光标选中main函数内任意地址

  4. 按下快捷键ALT+F7,选择项目中ida_shellcode_generator.py脚本

  5. 运行结束后会生成一个shellcode文件路径和大小,可以生成raw或txt格式(需要修改脚本中的outType)

  6. 可以使用Shellcode_Generator_Demo.c中的testShellcodeRun方法测试shellcode是否可用

设计思路

python脚本:

  1. 输入启动函数的地址,递归遍历所有被调用的函数写入shellcode
  2. 修复shellcode中函数体的调用指令的偏移操作数
  3. 在shellcode去除security_check相关内容(替换为nop)
  4. 拷贝全局变量和已初始化变量到shellcode末尾
  5. 构造IAT的字符串数据到shellcode末尾,替换C代码中iatInfoOffset变量值
  6. 构造IAT表到shellcode末尾,修复IAT表中的函数调用偏移,替换C代码中iatBeginOffset变量值
  7. 构造重定位表到shellcode末尾,修复重定位数据的引用偏移,替换C代码中relocBeginOffset和relocEndOffset变量值
  8. 输出shellcode到指定文件格式,打印输出路径和shellcode长度

C文件:

  1. 获取shellcode所在的基址
  2. 根据relocBeginOffset变量判断是否有重定位信息需要修复,有的话就根据基址和重定位表修复
  3. 根据iatInfoOffset变量判断是否有IAT表需要修复,不需要修复时直接跳转到用户定义的start函数
  4. 需要修复IAT表时,需要先通过汇编代码获取到GetProcAddress、GetModuleHandle等方法
  5. 根据iatInfoOffset存放的IAT的字符串信息依次修复IAT表
  6. 跳转到用户定义的start函数执行

项目地址

github地址:https://github.com/jxust-xiaoxiong/Shellcode_Generator,希望大家使用愉快,多多Star。


[培训]二进制漏洞攻防(第3期);满10人开班;模糊测试与工具使用二次开发;网络协议漏洞挖掘;Linux内核漏洞挖掘与利用;AOSP漏洞挖掘与利用;代码审计。

最后于 2023-4-15 20:23 被jxust_xx编辑 ,原因: 补充内容
收藏
点赞3
打赏
分享
最新回复 (5)
雪    币: 19299
活跃值: (28933)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
秋狝 2023-4-15 22:38
2
1
感谢分享
雪    币: 30090
活跃值: (2037)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-4-16 17:25
3
1

老实说,写shellcode还是Delphi最舒服。比如说,写一个SDK窗口显示:

procedure _Start;
var
  pSDKForms: array[0..5] of LPSDKForm;
begin

 for i := Low(pSDKForms) to High(pSDKForms) do
  begin
    pSDKForms[i] := LPSDKForm(fnLocalAlloc(LMEM_ZEROINIT, sizeof(TSDKForm)));
    if pSDKForms[i] = nil then Exit;
    fnRtlMoveMemory(pSDKForms[i], @SDKForm, sizeof(TSDKForm));
    pSDKForms[i]^.InitAPI;
    pSDKForms[i]^.CreateWindow;
  end;
  
  MessageLoop;
 end;
 
 { TSDKForm }

procedure TSDKForm.CreateWindow;
var
  Inst: HMODULE;
  tmpWndClass: WNDCLASSA;
  p: Pointer;
begin

  Inst := fnGetModuleHandleA(nil);
  if Inst = 0 then Exit;

  if not fnGetClassInfoA(Inst, FixPAnsiChar('TFormx'), tmpWndClass) then
  begin
    with WinClass do
    begin
      style := CS_CLASSDC or CS_PARENTDC;
      lpfnWndProc := FixPAnsiChar(@WindowProc);
      cbClsExtra := 0;
      cbWndExtra := sizeof(Pointer); 
      hInstance := Inst;
      hIcon := fnLoadIconA(Inst, FixPAnsiChar('MAINICON'));
      hCursor := fnLoadCursorA(0, (MakeIntResourceA(32512)));
      lpszMenuName := nil;
      hbrBackground := COLOR_BTNFACE + 1;
      lpszClassname := FixPAnsiChar('TFormx');
    end; { with }

    fnRegisterClassA(WinClass);
  end;
 
 .....
 end;
 
 
 function TSDKForm.InitAPI: Boolean;
 begin
  ...
  {$IFDEF WIN64}
  @fnGetWindowLongA := GetWindowAPI(FixPAnsiChar('user32.dll'), FixPAnsiChar('GetWindowLongPtrA'));
{$ELSE}
  @fnGetWindowLongA := GetWindowAPI(FixPAnsiChar('user32.dll'), FixPAnsiChar('GetWindowLongA'));
{$ENDIF}
...
 end;
 
procedure _End;
begin
//空函数,只是用来计算Shellcode的体积
end;

就是说,跟平时写程序没什么区别的。需要保存的时候:


procedure TForm1.Button1Click(Sender: TObject);
var
  ShellCodeSize: Integer;
  ShellCodeAddr: Pointer;
  StartRunAddr: procedure;
  MS: TMemoryStream;
begin
  ShellCodeSize := PAnsiChar(@_End) - PAnsiChar(@_Start); //计算大小
  ShellCodeAddr := VirtualAlloc(nil, ShellCodeSize, MEM_COMMIT, PAGE_EXECUTE_READWRITE); //申请运行内存
  MoveMemory(ShellCodeAddr, Pointer(@_Start), ShellCodeSize);
  MS := TMemoryStream.Create;
  MS.Write(Pointer(@_Start)^, ShellCodeSize);
{$IFDEF WIN64}
  MS.SaveToFile(ExtractFilePath(Application.ExeName) + 'ShellCode64.bin');
{$ELSE}
  MS.SaveToFile(ExtractFilePath(Application.ExeName) + 'ShellCode32.bin');
{$ENDIF}
  MS.Free;
  @StartRunAddr := ShellCodeAddr; //计算拷贝后函数的运行地址
  StartRunAddr;//直接测试shellcode
end;

当然,也可以直接这样测试:

program Test;

{$R *.res}

procedure ShellCode;
asm
.....................................
DB $00,$00,$48,$89,$C6,$48,$85,$C0,$0F,$84,$A1,$00,$00,$00,$41,$BE
DB $06,$00,$00,$00,$4C,$8D,$6D,$38,$B9,$40,$00,$00,$00,$BA,$98,$01
DB $00,$00,$48,$8B,$45,$28,$FF,$D0,$49,$89,$45,$00,$48,$85,$C0,$74
..................................
DB $8D,$75,$38,$48,$8B,$06,$48,$8B,$48,$50,$BA,$02,$00,$00,$00,$4D
DB $33,$C0,$4D,$33,$C9,$FF,$D7,$48,$83,$C6,$08,$41,$83,$ED,$01,$45
DB $85,$ED,$75,$DF,$90,$BF,$06,$00,$00,$00,$48,$8D,$75,$38,$48,$8B
..............................
end;
 
begin
  ShellCode;
end.



总的来说,优点就是:


1、不用第三方工具。


2、不需要编译后再后期处理。所见即所得。

     a、开始函数

     b、其它函数

     c、结束函数(空函数)


编译后直接保存a-c的内容即可,编译器的特点决定的。当然,有些地址需要运行时调整,比如说字符串:


@fnGetWindowLongA := GetWindowAPI(FixPAnsiChar('user32.dll'), FixPAnsiChar('GetWindowLongPtrA'));

不过也就是32位的需要调用FixPAnsiChar,64位可以直接写成:


@fnGetWindowLongA := GetWindowAPI('user32.dll', 'GetWindowLongPtrA');


3、32位和64位通杀(64位也可以直接內镶汇编)。


缺点:Delphi不流行了。

最后于 2023-4-16 17:31 被bestbird编辑 ,原因:
上传的附件:
雪    币: 227
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
jxust_xx 2023-4-16 19:37
4
0
bestbird 老实说,写shellcode还是Delphi最舒服。比如说,写一个SDK窗口显示:procedure _Start; var   pSDKFo ...

确实是一个编写shellcode不错的选择,有时间的时候我去试试。可能Delphi生成的汇编代码阅读起来没有C/C++来得轻松。还有方便提供那两个shellcode文件给我研究下嘛

最后于 2023-4-16 19:38 被jxust_xx编辑 ,原因: 文字漏打
雪    币: 30090
活跃值: (2037)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
bestbird 2023-4-16 22:25
5
0
jxust_xx bestbird 老实说,写shellcode还是Delphi最舒服。比如说,写一个SDK窗口显示:procedure  ...

汇编层面差不多的吧,本身也可以直接在IDE里面打开CPU窗口对照代码看的。 shellcode文件不放了,就是随便写的,验证一下如何创建对象(一次性创建6个SDK窗口对象:pSDKForms: array[0..5of LPSDKForm;)。只是获取kernel32基地址的函数(其实也是网上随便找的)可能会有特征,所以还是算了网上很多的,我随便复制一个:


function Kernel32Handle(): HMODULE;
{$IFDEF CPUX64}
asm
  mov rbx,$60
  mov rax,[gs:rbx]   // peb
  mov rax,[rax+$18]  // LDR
  mov rax,[rax+$30]  // InLoadOrderModuleList.Blink,
  mov rax,[rax]  // [_LDR_MODULE.InLoadOrderModuleList].Blink kernelbase.dll
  mov rax,[rax]  // [_LDR_MODULE.InLoadOrderModuleList].Blink kernel32.dll
  mov rax,[rax+$10]  //[_LDR_MODULE.InLoadOrderModuleList]. BaseAddress
end;
{$ELSE}
asm
  mov     eax,[fs:$30]  // Peb
  mov     eax,[eax+$C]  // LDR
  mov     eax,[eax+$C]  // InLoadOrderModuleList
  mov     eax,[eax]   // [_LDR_MODULE.InLoadOrderModuleList].Blink kernelbase.dll
  mov     eax,[eax]    //[_LDR_MODULE.InLoadOrderModuleList].Blink kernel32.dll
  mov     eax,[eax+$18] //[_LDR_MODULE.InLoadOrderModuleList]. BaseAddress
end;
{$ENDIF}



//Delphi的64位编译器不再把字符串常量地址直接拿来用,而是用的相对地址.
//也就是说实际上X64的ShellCode,Delphi连字符串常量的地址都不用修正了,因为它用的是相对地址:

Function FixPChar(Value: PChar): PChar;
{$IFDEF CPUX64}
asm
  mov rax,rcx //Delphi X64字符串常量用的相对地址算出来的(X64中绝对地址比较少,一般都有32位数的相对地址偏移.例如跳转,造成Hook的时候要用一些技巧),这个函数纯属做样子,是为了统一32位代码
end;
{$ELSE}
asm
  call   @next                //
@next:    pop    ecx                  // ecx里面装的就是@next的相对地址,当前执行时
  mov    ebx, offset @next    // ebx里面装的就是@next的绝对地址,编译时生成的
  add    eax, ecx             // 返回Value的地址+(相对地址-绝对地址)
  sub    eax, ebx
end;
{$ENDIF}

有了这两个函数,其它就没什么了,平时该怎么写就这么写,写完直接运行、导出。


最后于 2023-4-16 22:28 被bestbird编辑 ,原因:
雪    币: 227
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
jxust_xx 2023-4-17 09:36
6
0
bestbird jxust_xx bestbird 老实说,写shellcode还是Delphi最舒服。比如 ...
感谢指教
游客
登录 | 注册 方可回帖
返回