-
-
[原创]病毒分析之shellcode的分析方法
-
发表于: 2020-10-27 12:06 4313
-
最近破事儿真多,一周七天有六天要实训,想出去实习都出不去,每天被迫营业。
前几天逆恶意代码遇到了shellcode,我一想蠕虫头不都是shellcode利用漏洞加载自身吗,今天稍微总结一下shellcode的分析方法
一般而言,shellcode(或者说蠕虫病毒)用三种方法获取被感染文件的执行控制权,
1 替换进入点(先将原来的地址保存)
2 进入点附近插入病毒码(就本人经验来讲,插入的地方大多都在第一个ret之前,很少有长跳转),用jmp或call跳到病毒的开头,如何识别那?与合法的函数调用相比,病毒不会用栈传递参数,用寄存器传递参数时,病毒仔细保存被修改的寄存器,当把控制权交给最初的函数时,病毒必须从栈顶移走返回地址,否则的话会有两个返回地址
3 修改导入表(节的stub插入,这里需要补充PE文件的相关知识,这里主要讲shellcode,读者可以参考《加密与解密》第四版关于PE文件的相关知识,另外可以看一下DLL注入部分的第一个方法,同样利用了stub插入,PE文件的静态修改实现,你猜对了,shellcode除了经常与漏洞一起使用以外,还可以用于进程注入)
从漏洞利用的角度:(就是从shellcode编写的角度,你要想更好的分析它可以先了解如何编写它,知己知彼,百战不殆嘛)
从漏洞利用的角度来讲编写shellcode要注意一些问题,在编写shellcode的时候有一些限制,在众多的限制中,最令人痛苦的是NULL字符,它在一个字符串中只能出现一次,也就是说,只能出现在字符串的末尾(对Unicode字符串无效),有效消除NULL的方法不止一种,这里说一个最实用的,即对shellcode进行编码,(就个人经验来看,一般都是用XOR,因为这样有一个好处,a xor b xor b ==a,只要选择一个合适的key,在选择这个key的时候要注意,一般用8bit长的key,这个key必须与被编码的shellcode的所有字节都不一样才行,如果8bit不行,就用16bit,32bit),把控制权传给编码的代码之前,必须先把他解码,这个任务由解码器处理,解码器要满足以下要求:
1 尽量简洁(一般堆栈溢出漏洞shellcode可利用的空间只有几个字节,必须节省空间)
2 解码器必须完全支持重定位
3 解码器本身不能包含NULL字符
从反汇编的角度:
将shellcode载入反汇编器,检查多种启动地址,在这些启动地址中,选择提供最有意义代码的那个,实现这个操作最好使用十六进制编辑器,IDA显得有点笨重,比如WinHEX,HIEW等等,前面讲过,shellcode大部分是加密后杂乱的数据,看起来有意义的其实是解密器。(有些蠕虫的作者可能会把解密器与蠕虫分开,然后故意在其中填充一些垃圾指令,让你误以为是解密器。
一:位置无关代码:
PIC指不使用硬编码地址来寻址指令或数据的代码,shellcode必须是位置无关代码,因为在运行的时候,不同版本的程序会把shellcode加载到不同的位置,所以它不能使用硬编码,看几个例子
mov edx,dword_407030 8B 15 30 70 40 00
jnz short loc_401044 75 0e
mov eax,[ebp-4] 8b 45 fc
call sub_401000 e8 c1 ff ff ff
在这个例子中,只有第一个不是PIC,其他的都是位置无关代码,jmp和call指令都是位置无关的,这些指令的目标地址是把EIP+保存在指令中的偏移地址,(这里可以参考王爽老师的《汇编语言》第三版,后面部分讲的很详细
二:识别执行位置
shell code在以位置无关的方式访问数据时,需要解引用一个基址指针,用这个基址指针加上或减去偏移值来访问shellcode中包含的数据,有两种方法解决这个问题
1 使用call/pop指令
当一个call指令被执行的时候,处理器会把call后面的指令的地址压到栈上,然后转到被请求的位置进行执行,这个函数执行完的时候会执行一个ret命令用来把返回地址弹到栈顶病将他载入指令指针寄存器中。这样做的结果就是执行刚好返回到call后面的指令。shellcode可以通过在一个call后面的立刻执行pop指令来把这个指令黑化,让他pop一个我们想要让他去的地方
call指令将控制转移到sub_17函数,call指令将下一个返回地址压栈,pop指令将保存在栈顶的地址载入EDI,这种混合代码和数据的方法对shellcode来说很普遍,但是它能很容易使那些试图将call指令后面的数据作为代码进行解析的反汇编器困惑,结果妖魔是解析出来的反汇编代码很乱,要么遇到无效的opcode组合停止反汇编
2 使用fnstenv指令
X87 FPU在普通的x86里面提供了一个隔离的环境,这个隔离环境里面包含了一个专用的单独的寄存器集合,当一个进程正在使用FPU执行浮点运算的时候,这些寄存器需要由操作系统上下文切换的时候保存,也就是fstenv指令和fnstenv指令。下面的结构体在32位保护模式中执行时用来保存FPU的状态到内存中。
这里唯一影响使用的域是在字节偏移量12处的fpu_instruction_point,这个变量会保留被FPU使用的最后一条CPU指令的地址,并为异常处理器标示哪条FPU指令可能导致错误的上下文信息。需要这个与是因为FPU和CPU是并行的,如果FPU抛出异常,异常处理器不能简单的通过参照中断返回地址来找到这个错误的指令,下面这个代码是使用fnstenv获取EIP中Hello FNSTENV程序的反汇编代码。
loc_1C处第一句fldz指令将浮点数0.0压到FPU栈上,fpu_instruction_pointer的值在FPU中被更新成指向fldz指令,接着执行fnstenv byte ptr[ esp-0Ch ],把FpuSaveState结构体保存到栈上的esp-0Ch处,这地方会允许shellcode在这一句执行一个pop指令把fpu_instruction_pointer的值载入ebx寄存器中,一旦这个pop执行,ebx会包含一个指向内存中fldz指令位置的值,之后shellcode会调用ebx作为一个基址寄存器访问嵌入到代码中的数据。
和之前call/pop一样,这个代码里面的7E4507EAh、7C81CAFAh也是需要找到相应的位置替换掉才可以取运行。
手动符号解析:
shellcode作为一个获得执行的二进制代码块存在,一旦它获得执行,必须做些有用的事,这通常意味者通过API与系统交互,shellcode不能使用WIndos的加载器来确保所有需要的库被加载使用,也不能确认所有的外部符号依赖都被解决,它必须自己找到这个符号。shellcode必须动态定位这些函数,以确保它们在不同的环境中都能工作,这里要用到两个函数,LoadLibraryA和GetProcAddress
LoadLibraryA是加载指定的库,并返回一个句柄。而GetProcAddress函数则在库导出表中查找指定的符号名和序号。这两个函数都是从kernel32.dll中导出的,所以shellcode必须做以下事情:
1 在内存中找到kernel32.dll
2 解析kernel32.dll的PE文件,并且搜索这两个函数
找到kernell32.dll有三种方法,最常用的是使用PEB结构体
原理:FS段选择器作为选择子指向当前的TEB结构,在TEB偏移0x30处是PEB指针,而在PEB偏移的0x0c处是指向PEB_LDR_DATA结构的指针,位于PEB_LDR_DATA结构偏移0x1c处,是一个叫 InInitialzationOrderModuleList的成员。它是指向LDR_MODULE链表结构中,相应的双向链表头部的指针,该链表加载的DLL的顺序是ntdll.dll,kernel32.dll,该成员所指的链表偏移0x08处是kernel32.dll的地址
PEB结构:
typedef struct _PEB {
// Size: 0x1D8
/*000*/ UCHAR InheritedAddressSpace;
/*001*/ UCHAR ReadImageFileExecOptions;
/*002*/ UCHAR BeingDebugged;
/*003*/ UCHAR SpareBool; // Allocation size/*004*/ HANDLE Mutant;
/*008*/ HINSTANCE ImageBaseAddress; // Instance
/*00C*/ VOID *DllList;/*010*/ PPROCESS_PARAMETERS *ProcessParameters;
/*014*/ ULONG SubSystemData;
/*018*/ HANDLE DefaultHeap;
/*01C*/ KSPIN_LOCK FastPebLock;
/*020*/ ULONG FastPebLockRoutine;
/*024*/ ULONG FastPebUnlockRoutine;
/*028*/ ULONG EnvironmentUpdateCount;
/*02C*/ ULONG KernelCallbackTable;
/*030*/ LARGE_INTEGER SystemReserved;
/*038*/ ULONG FreeList;/
*03C*/ ULONG TlsExpansionCounter;
/*040*/ ULONG TlsBitmap;
/*044*/ LARGE_INTEGER TlsBitmapBits;
/*04C*/ ULONG ReadOnlySharedMemoryBase;
/*050*/ ULONG ReadOnlySharedMemoryHeap;
/*054*/ ULONG ReadOnlyStaticServerData;
/*058*/ ULONG AnsiCodePageData;
/*05C*/ ULONG OemCodePageData;
/*060*/ ULONG UnicodeCaseTableData;
/*064*/ ULONG NumberOfProcessors;
/*068*/ LARGE_INTEGER NtGlobalFlag; // Address of a local copy
/*070*/ LARGE_INTEGER CriticalSectionTimeout;
/*078*/ ULONG HeapSegmentReserve;/
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!
赞赏
- 分页内存和非分页内存 3948
- IoGetTopLevelIrp 7549
- windbg命令行中的引号是什么意思 6826
- [求助]windbg dump 蓝屏 debug 7285