免杀学习-认识shellcode
免杀学习-认识shellcode
接着暑假的学习继续更,这学期比较忙,学校里要过逆向考试,加上学生会加上新生班助,还要忙着研究小车做比赛作品,杂七杂八的事一大堆就没更。
这个学期正好学了点逆向的基础知识,那就借此机会学习一下免杀,也是内网渗透比较重要的一个东西,争取这个寒假搓个免杀马,玩一玩兄弟的电脑
什么是shellcode
这一大堆反斜杠加数字是一串msfvenom生成的shellcode,例如\x75代表两个十六进制数,它会被翻译成汇编语言,
例如:
这里55被翻译成了push ebp
55这2个16进制转换成8位二进制,然后告诉CPU执行,CPU识别发现是代码就会执行push ebp
无文件(内存马)和有文件
上面利用msfvenom生成的shellcode,被翻译成汇编语言后,可以直接在内存中运行
可以直接在内存中运行,不依赖硬盘文件运行,我们称之为无文件运行
比如上面生成的shellcode在成功运行后,我们在后续利用过程中可以做进程迁移,磁盘文件只是起到了一个触发作用,我们可以把这一段shellcode迁移到其他进程上,此时触发文件可以直接删除,不会对木马进程造成影响,除非关机,关机会清理内存(无文件!=进程迁移)
例如通过利用java漏洞或者apache,tomcat的漏洞写入的内存马
如果木马程序与磁盘中的文件关联我们需要依托文件运行,我们称之为有文件(程序运行时我们会无法删除文件,只有关闭进程才能删除文件)
当然这段shellcode只有在data段的时候,才会被CPU识别成是代码,才有机会运行
(图为未做免杀的木马编写)
shellcode在msf和cs中扮演了一个什么样的角色?
staged和stageless
shellcode--小马--只负责连接
meterprete--大马--真正包含功能模块,实现各项功能
小马第一次把大马拉上的时候,只加载了Core Commands中的功能
实现其他功能就需要meterpreter再拉其他文件,反射DLL注入(什么是反射DLL注入?)
上面这种阶段化的过程我们称之为staged
如果我们的shellcode直接就是meterpreter,不需要小马拉大马
那么在实际使用中如何区分大马和小马?
我们以meterpreter中的windows相关的payload举例
通过对比可以发现,第一张中名字和第二张的区别在于一个是'/'一个是'_'
通过这一区别我们可以区分shell_reverse_tcp和shell/reverse_tcp分别是stageless和staged类型
在Cobalt Strike中,如果选择生成的是payload generator,那么默认是staged的,分阶段
如果直接选择生成可执行文件,windows executable如果不带s,就是分阶段的,带就是不分阶段的
再聊聊msf中payload名称的分类
windows代表使用的操作系统平台,x64代表系统位数,vncinject代表监听端要做的具体操作(拿到会话后的操作工具),reverse代表反向连接,对方主动来连我们(bind代表正向,我们主动去连对方,不常用)tcp代表了连接方式,采用的什么通信协议,rc4代表加密方式,其他在rc4这个位置的通常代表一些加密方法或者其他特殊手段
shellcode实现的细节
可以在下面的链接中去研究下msf源码
https://github.com/rapid7/metasploit-framework/tree/master/external/source/shellcode/windows/x86/src/block
1.hash寻找api函数
windows api函数都会写在dll文件中
为什么要hash寻找api函数?
can can汇编
上面的代码做了什么?
这里就和前面谈到无文件和有文件有关了,
Windows平台下的可执行文件的格式,我们称之为PE(Portable Executable)文件结构(学PE看这个)
PE表中有一个导入表,操作系统可以知道用文件调用了哪些windows api函数,函数地址就在其中
而无文件shellcode是没有PE结构的,也就无法去调用各种api
所以汇编代码中如果为了实现一些功能需要调用windows api,那么只能自行去寻找,这也是第一段shellcode实现的方法
任何一个exe文件都会调用kernal32.dll,如果我们要调用的函数不在这个dll中,就需要利用kernal32.dll中的LoadLibrary函数去加载其他DLL文件
例如LoadLibrary(user32.dll)
这里首先把需要的函数名字转换成一个hash值,
然后通过一种方式去找函数,接下来是一个复杂的过程
需要通过遍历进程模块的方式去查找user32.dll的地址
具体就是做了下图所示的过程,teb和peb是两个结构体,teb中包含进程中运行线程的各种信息,每个线程都对应一个TEB结构体。
TEB
typedef struct _NT_TEB { NT_TIB Tib; // 00h PVOID EnvironmentPointer; // 1Ch CLIENT_ID Cid; // 20h PVOID ActiveRpcInfo; // 28h PVOID ThreadLocalStoragePointer; // 2Ch PPEB Peb; // 30h <--注意这里 ULONG LastErrorValue; // 34h ULONG CountOfOwnedCriticalSections; // 38h PVOID CsrClientThread; // 3Ch PVOID Win32ThreadInfo; // 40h ULONG Win32ClientInfo[0x1F]; // 44h PVOID WOW32Reserved; // C0h ULONG CurrentLocale; // C4h ULONG FpSoftwareStatusRegister; // C8h PVOID SystemReserved1[0x36]; // CCh PVOID Spare1; // 1A4h LONG ExceptionCode; // 1A8h ULONG SpareBytes1[0x28]; // 1ACh PVOID SystemReserved2[0xA]; // 1D4h GDI_TEB_BATCH GdiTebBatch; // 1FCh ULONG gdiRgn; // 6DCh ULONG gdiPen; // 6E0h ULONG gdiBrush; // 6E4h CLIENT_ID RealClientId; // 6E8h PVOID GdiCachedProcessHandle; // 6F0h ULONG GdiClientPID; // 6F4h ULONG GdiClientTID; // 6F8h PVOID GdiThreadLocaleInfo; // 6FCh PVOID UserReserved[5]; // 700h PVOID glDispatchTable[0x118]; // 714h ULONG glReserved1[0x1A]; // B74h PVOID glReserved2; // BDCh PVOID glSectionInfo; // BE0h PVOID glSection; // BE4h PVOID glTable; // BE8h PVOID glCurrentRC; // BECh PVOID glContext; // BF0h NTSTATUS LastStatusValue; // BF4h UNICODE_STRING StaticUnicodeString; // BF8h WCHAR StaticUnicodeBuffer[0x105]; // C00h PVOID DeallocationStack; // E0Ch PVOID TlsSlots[0x40]; // E10h LIST_ENTRY TlsLinks; // F10h PVOID Vdm; // F18h PVOID ReservedForNtRpc; // F1Ch PVOID DbgSsReserved[0x2]; // F20h ULONG HardErrorDisabled; // F28h PVOID Instrumentation[0x10]; // F2Ch PVOID WinSockData; // F6Ch ULONG GdiBatchCount; // F70h ULONG Spare2; // F74h ULONG Spare3; // F78h ULONG Spare4; // F7Ch PVOID ReservedForOle; // F80h ULONG WaitingOnLoaderLock; // F84h PVOID StackCommit; // F88h PVOID StackCommitMax; // F8Ch PVOID StackReserve; // F90h PVOID MessageQueue; // ??? }
PEB
typedef struct _PEB { UCHAR InheritedAddressSpace; // 00h UCHAR ReadImageFileExecOptions; // 01h UCHAR BeingDebugged; // 02h 这里QAQ UCHAR Spare; // 03h PVOID Mutant; // 04h PVOID ImageBaseAddress; // 08h PPEB_LDR_DATA Ldr; // 0Ch PRTL_USER_PROCESS_PARAMETERS ProcessParameters; // 10h PVOID SubSystemData; // 14h PVOID ProcessHeap; // 18h PVOID FastPebLock; // 1Ch PPEBLOCKROUTINE FastPebLockRoutine; // 20h PPEBLOCKROUTINE FastPebUnlockRoutine; // 24h ULONG EnvironmentUpdateCount; // 28h PVOID* KernelCallbackTable; // 2Ch PVOID EventLogSection; // 30h PVOID EventLog; // 34h PPEB_FREE_BLOCK FreeList; // 38h ULONG TlsExpansionCounter; // 3Ch PVOID TlsBitmap; // 40h ULONG TlsBitmapBits[0x2]; // 44h PVOID ReadOnlySharedMemoryBase; // 4Ch PVOID ReadOnlySharedMemoryHeap; // 50h PVOID* ReadOnlyStaticServerData; // 54h PVOID AnsiCodePageData; // 58h PVOID OemCodePageData; // 5Ch PVOID UnicodeCaseTableData; // 60h ULONG NumberOfProcessors; // 64h ULONG NtGlobalFlag; // 68h 还有这里!_(:зゝ∠)_ UCHAR Spare2[0x4]; // 6Ch LARGE_INTEGER CriticalSectionTimeout; // 70h ULONG HeapSegmentReserve; // 78h ULONG HeapSegmentCommit; // 7Ch ULONG HeapDeCommitTotalFreeThreshold; // 80h ULONG HeapDeCommitFreeBlockThreshold; // 84h ULONG NumberOfHeaps; // 88h ULONG MaximumNumberOfHeaps; // 8Ch PVOID** ProcessHeaps; // 90h PVOID GdiSharedHandleTable; // 94h PVOID ProcessStarterHelper; // 98h PVOID GdiDCAttributeList; // 9Ch PVOID LoaderLock; // A0h ULONG OSMajorVersion; // A4h ULONG OSMinorVersion; // A8h ULONG OSBuildNumber; // ACh ULONG OSPlatformId; // B0h ULONG ImageSubSystem; // B4h ULONG ImageSubSystemMajorVersion; // B8h ULONG ImageSubSystemMinorVersion; // C0h ULONG GdiHandleBuffer[0x22]; // C4h PVOID ProcessWindowStation; // ??? }
在找到dll文件之后,就要读dll文件的PE结构(EXE文件与DLL文件的区别完全是语义上的。它们使用的是相同的PE格式。惟一的不同在于一个位,这个位用来指示文件应该作为EXE还是DLL。)
前面找导入函数,这里通过dll的导出表找需要导出的函数
这里和前面一样采用hash的方法将函数名字变数字,然后遍历,匹配相同数字就能找到需要的函数
上面整串代码相当于定义了一个函数
def hanshu(api函数名字)
2.建立一个tcp连接
上汇编
这里汇编上来做了一个call ebp
其实就是将前面第一部分的所做的事调用过来
所以上面的内容可以这么理解
def hanshu(api函数名字)---->def ebp(0x0726774c)
最后加载了一个ws2_32.dll文件
接下来是差不多的操作
找到ws2_32.dll中的WSAStartup函数,然后调用了这个函数WSAStartup()
这个函数会直接通过操作系统的驱动告诉网卡准备网络连接
接下来是设置基本参数开始连接
这里WSASocketA中AF_INET代表IPV4,push进去的eax值是2(如果eax是23那就是IPV6)
SOCKET_STREAM代表TCP连接
IP地址和端口号
其中IP地址转16进制是小端序读取
(内存中展示是这样的)
这里可以ollydbg调试一下看看返回的IP地址,分析一下
然后就是连接
connect()中s就是前面WSASocketA(),&sockaddr是前面set_address一段中的参数,包括ip地址和端口号信息的整个结构体的指针,16是sockaddr这个结构体的长度
3.将连接接收到的东西放在内存中
前面的看完到这里也不是很难理解了
在连接上后,就需要调用meterpreter的功能,所以需要接收
因为meterpreter的数据不是一个文件,而是一串二进制数据,所以需要申请一块空间地址存放数据然后运行
接收以后,就需要不断接收不断读取,然后return的方式直接对程序进行执行
此时如果在ret出打断点,也可以看到meterpreter的代码
以上是msf的shellcode功能实现剖析,因为msf的裸马很容易被杀,所以可以参照上面的过程用C++自行编写,一样可以达到免杀的目的(代码就不沾了,试了试可以过火绒)
这一章学习了一些基本的知识,也学到了一种静态免杀的方法,通过修改hash值的方式,感兴趣的师傅可以研究下,这里不做赘述了,我自己尝试了下使用这个方法,然后二分法定位被查杀段再结合花指令是可以绕过杀软检测的
https://www.huntress.com/blog/hackers-no-hashing-randomizing-api-hashes-to-evade-cobalt-strike-shellcode-detection
工具:
https://github.com/embee-research/APIHashReplace