freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

PengCode:EXE转换ShellCode工具
2022-08-02 11:47:52

0x0 工具介绍

PengCode是Windows下PE文件转换成ShellCode格式的二进制生成工具,目前支持32位和64位的程序兼容大部分Windows版本,较老的系统如XP不支持,可以兼容Go和Vs生成的EXE文件,只要是正常PE文件均可(C#目前不兼容),使用PIC ShellCode作为引导后续进行ShellCode Loader内存还原运行EXE,下面将讲解如何使用PengCode以及如何开发。

0x1 使用场景

1)云端加载

将生成的ShellCode部署到OSS或者隐写到图片中传到云端等多种方式(只要能够下载到内存中),在加载器程序运行后从网络上下载ShellCode到内存中,使用传统ShellCode加载器加载即可运行。
  优点:如果样本被捕获时关闭云上ShellCode文件将无法对其继续分析,只能分析到是外壳程序,可以根据输入参数下载不同的ShellCode到内存中执行,实现一个程序可以当多种程序运行,也可以开启多线程充当多个程序。
  缺点:需要联网操作。

2)分离式加载

程序读取加密后的ShellCode文件,在内存中进行加载运行可以伪装成各类文件比如图片文件或者视频文件,拓展名随意,读取到内存后解密之后使用传统ShellCode加载器运行即可。
  优点:不需要联网操作,同时增加样本分析难度在只捕获加载器外壳的时候无法分析其实质内容,可以将多个ShellCode文件合并到一起,读取到内存后进行截取分段可以做到两个文件集合多种工具的效果,或者将ShellCode隐写到其他文件中进行读取,增加隐蔽性。
  缺点:制作过程较为复杂,实战中需要上传多个文件。

3)内置加载

将生成的ShellCode加密后添加到资源节中如果较小的话还可以放到函数内,如果比较大也可以放到全局变量中使用,加载器运行后可以解密ShellCode然后内存加载运行。
  优点:增强免杀效果或直接免杀,单个文件可以直接运行并且增加分析难度无法快速判断出是否为什么软件。
  缺点:分析难度较低,可以提取出ShellCode进行深入分析。

4)ShellCode快速开发

生成的ShellCode可以用作各种场景,比如注入到其他进程中极大增加隐蔽性与分析溯源难度,极大降低了ShellCode开发流程。

0x2 使用方式

软件截图:
1659361947206.jpg

使用方式比较简单,就跟截图中的例子一样,生成出来的code.bin就是制作好的ShellCode文件,可以直接拿来使用,比如用传统的C加载器

BYTE shellcode[]={xxxxx};
void* exec = VirtualAlloc(0, sizeof shellcode, MEM_COMMIT,``PAGE_EXECUTE_READWRITE);
memcpy(exec, shellcode, sizeof shellcode);
((void(*)())exec)();

0x3 注意事项

如何给ShellCode传递参数:在使用过程中可能会遇到原本的程序是需要参数才能运行起来的,现在转成ShellCode要如何传递参数呢,这里给出的方案是使用GetCommandLineA、GetCommandLineW进行参数传递,在程序正式执行ShellCode之前,使用这俩函数获取当前进程的命令行字符串的指针,在字符串指针后面进行参数的拼接就可以了。内置加载ShellCode编译很慢:如果将ShellCode提取转为C格式的数组编译很慢的话可以将char数组改为BYTE数组,这将大大加快编译速度。
ShellCode如何转为C格式数组文件:这里推荐使用010Editor编辑器进行提取,将生成好的文件拖到010Editor中单击文件栏下面的导出十六进制,可以选择导出类型为C代码或者Java代码。

1659362047070.jpg
1659362074150.jpg

0x4 下载

https://github.com/Mephostophiles/PengCode

0x5 PengCode如何实现

PengCode本质上就是一段PIC ShellCode,由PIC指令为基石实现的Pe文件的内存装载由此实现EXE转ShellCode功能,最开始控制权交由PengCode完成内存修复PE并将控制权转交给PE OEP,下面将简单讲述下PengCode相关知识点与开发技巧。

何为Opcode

汇编指令是由机器码构成的,不同的机器码组合可以组成不同的汇编指令,OpCode就是阐述机器码与汇编指令的对应,这里以X86 Opcode为例由六部分组成分别是:InstructionPrefixesOpcodeModR/MSIBDisplacementImmediate

1659362177184.jpg

前缀指令(InstructionPrefixes)
在x86指令格式中,前缀指令是可选的不是必须存在的,CPU判断前缀指令的原理很简单,是通过指令的内容进行判断的,因为前缀指令数量比较少很容易判断,在OD中前缀指令以:分开前面的就是前缀指令前缀指令最多占4个字节最少占0个字节。

1659362286947.jpg

前缀指令分组
前缀指令最多有四组每组最多一个,分别是LOCK和REPEAT、段前缀、操作数宽度前缀、地址宽度前缀。
LOCK和REPEAT前缀指令:LOCK(F0)REPNE/REPNZ(F2)REP/REPZ(F3)
段前缀指令:CS(2E)SS(36)DS(3E)ES(26)FS(64)GS(65)
操作数宽度前缀指:66操作数宽度前缀。
地址宽度前缀指令:67地址宽度前缀。

前缀指令不是必须的,通过增加前缀指令可以修改汇编指令的含义,比如默认为SS段可以通过前缀指令修改为FS段,使用x86操作数宽度前缀指令可以实现32位与16位操作数的互相转换等。
SS段前缀指令修改前
1659362305455.jpg
SS段前缀指令修改后
1659362315323.jpg

Opcode组成
Opcode是指令最核心的组成部分,Opcode最少1个字节最多3个字节,决定一条指令的长度主要是由三部分组成,分别是Opcode、ModR/M、SIB,前缀指令并不影响后面只会影响自己,Displacement、Immediate的长度已经由于Opcode、ModR/M、SIB组合决定好了,Opcode决定后面是否有ModR/M,ModR/M有决定是否有SIB是一环套一环的。
定长指令与变长指令是什么

1659362331391.jpg
定长指令:Opcode后面没有ModR/M就是定长指令只要Opcode的决定了也就确定了指令的长度。
变长指令:取决于Opcode后面是否有ModR/M,如何判断Opcode是否是变长指令可以根据One-byte Opcode Map表来判断,如果是Eb、Gb、Ev、Gv这种Zz标识那么一般就是变长指令。
下图是Intel白皮书卷2中的主表可以看到那些是定长与变长指令,以及E、G为什么是ModR/M的详细内容。
1659362349744.jpg1659362362880.jpg
1659362376695.jpg
1659362393280.jpg

ShellCode开发中指令的槽点

上面简单讲述了一下Opcode的相关内容,在ShellCode开发中很容出现槽点这里以定长指令JCC为例,明明两个字节就可以完成的事情,却白白浪费了六个字节这个在开发ShellCode中是需要注意的一点,比如在开发过程中JCC指令只需要向下跳转偏移6A的位置这里用JO指令为例应该使用Opcode 0x70的写法而不是使用0x0F 0x80 的JO Opcode,这样将会缩减4个字节的空间,当然槽点还有很多但这里不赘述了后续会专门撰稿写几篇关于ShellCode开发的系列文章,小伙伴们求关注。
1659362407915.jpg
1659362417631.jpg

PIC Opcode

在开发ShellCode中需要使用的都是位置无关指令,当然有些场景比较特殊可能是绝对地址,关于PIC Opcode有那些其实白皮书都有讲可以去查,我这里就简单讲解一下为什么需要使用PIC Opcode指令,以CALL指令为例可以分为E8和FF15两种形式,前者是相对位置,后者是绝对位置,在ShellCode开发中其实要避免绝对位置的出现,否则ShellCode无法找到堆栈位置将崩溃,当然像是其他指令也需要注意是不是PIC Opcode不然一不小心就崩溃了。
1659362430694.jpg

利用前缀指令优化ShellCode

在一些重复的逻辑地方可以使用前缀指令进行段、操作数、地址等变更使用得当能够降低大量字节空间,是性价比非常高的做法下面以段变更为例。
1659362439211.jpg
1659362448808.jpg

ShellCode开发

上面简单讲述了下何为Opcode,掌握Opcode之后就是正式开始开发PIC ShellCode,这里简单讲述下x86 shellcode开发过程,并实现弹窗效果。

通过FS段选择子找到当前TEB,TEB(线程环境块)偏移0x30的地方存放着PEB(进程环境块)的指针。
PEB偏移0x0C的位置存放着PEB_LDR_DATA结构体指针里面包含进程装载的动态库信息。
PEB_LDR_DATA结构体偏移位置0x1C的地址指向模块初始化链表头指针InInitializationOrderModuleList。
InInitializationOrderModuleList中存放着初始化模块信息,第一个链表是ntdll.dll,第二个链表是kernel32.dll。
找到kernel32.dll后在偏移0x08找到kernel32.dll在内存中的加载基地址。
kernel32.dll基地址起偏移0x3C找到PE头,在偏移0x78的地方指向导出表指针。
使用导出表API编写ShellCode
PEB、PEB_LDR_DATA、LIST_ENTRY 结构体

PEB:

typedef struct _PEB``{

BYTE Reserved1[2];
BYTE BeingDebugged;
BYTE Reserved2[1];
PVOID Reserved3[2];
PPEB_LDR_DATA Ldr;
PRTL_USER_PROCESS_PARAMETERS ProcessParameters;
PVOID Reserved4[3];
PVOID AtlThunkSListPtr;
PVOID Reserved5;
ULONG Reserved6;
PVOID Reserved7;
ULONG Reserved8;
ULONG AtlThunkSListPTR32;
PVOID Reserved9[45];
BYTE Reserved10[96];
PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;
BYTE Reserved11[128];
PVOID Reserved12[1];
ULONG SessionId;

}``PEB, *PPEB;

PEB_LDR_DATA:

typedef struct _PEB_LDR_DATA {

BYTE Reserved1[8];
PVOID Reserved2[3];
LIST_ENTRY InMemoryOrderModuleList;

} PEB_LDR_DATA, *PPEB_LDR_DATA;

typedef struct _LIST_ENTRY {

struct _LIST_ENTRY *Flink;
struct _LIST_ENTRY *Blink;

} LIST_ENTRY, *PLIST_ENTRY;

Kernle32.dll地址获取
读取PEB结构
mov eax,fs:[0x30]
读取_LDR_DATA结构
mov eax,[eax+0xc]
读取_PEB_LDR_DATA下InMemoryOrderModuleList
LISt_ENTRT是一个链表Flink
第三个是Kernel.dll
mov esi,[eax+0x14]
LIST_ENTRT指针是可以是_LDR_DATA_TABLE_ENTRY结构(该结构未被公开)该结构	DllBase 0x18位置是dll加载位置,现在需要将LIST_ENTRY链表Fink移动到Kernel.dll
1 Fink exe本身内存位置
2 Fink ntdll.dll内存位置
3 Fink Kernel.dll内存位置
lodsd;  eax <- esi , (esi +0x4  ntdll.dll) esi=exe内存位置
xchg eax,esi; esi eax互换    ntdll.dll下一个
lodsd; Kernle32.dll
mov ebx,[eax+0x10]; _LDR_DATA_TABLE_ENTRY 结构下DllBase属性即模块RVA地址
Kernel32.dll导出表
Kernel PE通过DOS头到NT映像头
mov edx,[ebx+0x3c]
找到Kernel NT映像头
add edx,ebx
在NT映像头中找到_IMAGE_DATA_DIRECTORY0x78,可选文件头偏移是0x60
mov edx,[edx+0x78]
add edx,ebx edx=export table
_IMAGE_DATA_DIRECTORY偏移0x20导出函数表RVA
mov esi,[edx+0x20]
add esi,ebx ebx Kernle内存地址+esi 偏移
xor ecx ecx
GetProcAddress函数名称与序号获取
Get_Function: 标志
inc ecx  ecx自增1用于记录当前函数地址的序号
lodsd eax <- esi  <- (esi=esi+0x4)
add eax,ebx  eax=第一个导出函数名称
cmp dword ptr [eax],0x50746547 ; GetP 判断开头是不是要找的
jnz Get_Function
cmp dword ptr[eax+0x4],0x41636f72; rocA
jnz Get_Function
cmp dword ptr [eax+0x8],0x65726464; ddre
jnz Get_Function
GetProcAddress函数地址获取
AddressOfNameOrdlnals
mov esi,[edx+0x24] 导出函数序号RVA 
add esi,ebx kernel 导出函数RVA
mov cx,[esi+ecx*2] 导出函数序号表RVA是一个 word大小的数组因此ecx*2 找到导出函数序号表RVA GetProcAddress函数地址
dec ecx 因为是数组所以减一
AddressOfFunction
mov esi,[edx+0x1c]  esi=导出函数地址表
add esi,ebx 内存kernel 中导出函数地址表RVA
mov edx,[esi+ecx*4]通过序号ecx找到GetProcAddress
add edx,ebx 找到kernel中GetProcAddress 地址
获得LoadLibrary函数地址
xor ecx,ecx
push ebx Kernel32 base Address
push edx GetProcAddress
push ecx 0
push 0x41797261 arryA
push 0x7262694c Libr
push 0x64616f4c Load
push esp
push ebx
call edx
加载USER32.dll动态库
add esp,0xc 落栈
pop ecx
push eax LoadLibraryA Address
push ecx
mov cx,0x6c6c6 ; ll
push ecx
push 0x642e3233; 32.d
push 0x72657375
push esp
call eax
获得MessageBoxA函数地址
add esp,0x10 落栈
mov edx,[esp+0x4] GetProcAddress
xor ecx ecx
push ecx
mov ecx,0x6141786F oxAa
push ecx 结束符
sub dword ptr[esp+0x3],0x61 将a填充删除
push 0x42656761 ageB
push 0x7373654D Mess
push esp MessageBoxA
push eax USER32.dll address
call edx GetPorcAddress(USER32.dll ,MessageBoxA)
add esp,0x10 落栈 _cdecl调用
使用MessageBox弹出PengCode
xor ecx, ecx; ECX = 0
push 0x61616161
sub dword ptr[esp], 0x61
sub dword ptr[esp + 0x1], 0x61
sub dword ptr[esp + 0x2], 0x61
sub dword ptr[esp + 0x3], 0x61
push 0x65646F43;     Code
push 0x676E6550;     Peng
push edi
lea edi, [esp + 0x4]
push ecx;
push ecx;
push edi;
push ecx;
 call eax; MessageBox!
pop edi;
结束ShellCode
add esp,0x10 _cdecl
pop edx
pop ebx
mov ecx,0x61737365 essa
push ecx
sub dowrd ptr [esp+0x3],0x61
push 0x636f7250
push 0x74697845
push esp
push ebx;kernle32.dll Address
call edx;GetProcAddress(ExitProcess)
xor ecx,ecx
push ecx
call eax 结束ShellCode
完整asm
__asm {

    nop

    nop

    nop

    nop

    nop

    nop

    nop

    nop

    xor ecx, ecx

    mov eax, fs: [ecx + 0x30] ; EAX = PEB

    mov eax, [eax + 0xc]; EAX = PEB->Ldr

    mov esi, [eax + 0x14]; ESI = PEB->Ldr.InMemOrder

    lodsd; EAX = Second module

    xchg eax, esi; EAX = ESI, ESI = EAX

    lodsd; EAX = Third(kernel32)

    mov ebx, [eax + 0x10]; EBX = Base address

    mov edx, [ebx + 0x3c]; EDX = DOS->e_lfanew

    add edx, ebx; EDX = PE Header

    mov edx, [edx + 0x78]; EDX = Offset export table

    add edx, ebx; EDX = Export table

    mov esi, [edx + 0x20]; ESI = Offset namestable

    add esi, ebx; ESI = Names table

    xor ecx, ecx; EXC = 0



Get_Function:



    inc ecx; Increment the ordinal

        lodsd; Get name offset

        add eax, ebx; Get function name

        cmp dword ptr[eax], 0x50746547; GetP

        jnz Get_Function

        cmp dword ptr[eax + 0x4], 0x41636f72; rocA

        jnz Get_Function

        cmp dword ptr[eax + 0x8], 0x65726464; ddre

        jnz Get_Function

        mov esi, [edx + 0x24]; ESI = Offset ordinals

        add esi, ebx; ESI = Ordinals table

        mov cx, [esi + ecx * 2]; Number of function

        dec ecx

        mov esi, [edx + 0x1c]; Offset address table

        add esi, ebx; ESI = Address table

        mov edx, [esi + ecx * 4]; EDX = Pointer(offset)

        add edx, ebx; EDX = GetProcAddress



        xor ecx, ecx; ECX = 0

        push ebx; Kernel32 base address

        push edx; GetProcAddress

        push ecx; 0

        push 0x41797261; aryA

        push 0x7262694c; Libr

        push 0x64616f4c; Load

        push esp; "LoadLibrary"

        push ebx; Kernel32 base address

        call edx; GetProcAddress(LL)



        add esp, 0xc; pop "LoadLibrary"

        pop ecx; ECX = 0

        push eax; EAX = LoadLibrary

        push ecx

        mov cx, 0x6c6c; ll

        push ecx

        push 0x642e3233; 32.d

        push 0x72657375; user

        push esp; "USER32.dll"

        call eax; LoadLibrary("USER32.dll")



        add esp, 0x10; Clean stack

        mov edx, [esp + 0x4]; EDX = GetProcAddress

        xor ecx, ecx; ECX = 0

        push ecx

        mov ecx, 0x6141786F; oxWa  MessageBoxA

        push ecx

        sub dword ptr[esp + 0x3], 0x61

        push 0x42656761; eBut   ageB

        push 0x7373654D; Mous   Mess



        push esp; 

        push eax; USER32.dll address

        call edx; GetProc(MessageBoxA)



        add esp, 0x10; Cleanup stack

        xor ecx, ecx; ECX = 0

        push 0x61616161

        sub dword ptr[esp], 0x61

        sub dword ptr[esp + 0x1], 0x61

        sub dword ptr[esp + 0x2], 0x61

        sub dword ptr[esp + 0x3], 0x61

        push 0x65646F43;     Code

        push 0x676E6550;     Peng

        push edi

        lea edi, [esp + 0x4]

        push ecx;

        push ecx;

        push edi;

        push ecx;

        call eax; Mess!

        pop edi;



        add esp, 0x10; Clean stack

        pop edx; GetProcAddress

        pop ebx; kernel32.dll base address

        mov ecx, 0x61737365; essa

        push ecx

        sub dword ptr[esp + 0x3], 0x61; Remove "a"

        push 0x636f7250; Proc

        push 0x74697845; Exit

        push esp

        push ebx; kernel32.dll base address

        call edx; GetProc(Exec)

        xor ecx, ecx; ECX = 0

        push ecx; Return code = 0

        call eax; ExitProcess

        nop

        nop

        nop

        nop

        nop

        nop

        nop

        nop

}

PengCode开发过程

上述的ShellCode功能比较简单仅仅是弹窗,PengCode便是基于ShellCode开发的,可以简单的认为是ShellCode版的PeLoader,ShellCode的开发需要极大的耐心,我很认可《0day软件安全漏洞》中所说的ShellCode开发是一件极其细致,难度极高的工作,当然在熟悉之后ShellCode的开发也并没有想象的多难。

0x6 结尾

最后可能有同学不懂什么是PeLoader技术,简单来说是自己实现Windows的PE装载过程,,如果对PeLoader感兴趣可以自行到网上搜索,如何要讲解的话篇幅可能有点多这里就不展开了,现在ShellCode开发其实骚操作挺多后面有时间的话我会出系列文章详细讲解ShellCode开发的相关技巧。

个人博客:https://blog.csdn.net/qq_18811919?type=blog

本文作者:, 转载请注明来自FreeBuf.COM

# 黑客 # 网络安全 # 系统安全 # 木马 # 网络安全技术
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
评论 按热度排序

登录/注册后在FreeBuf发布内容哦

相关推荐
\
  • 0 文章数
  • 0 评论数
  • 0 关注者
文章目录
登录 / 注册后在FreeBuf发布内容哦