《黑客攻防-系统实战》--shellcode
shellcode
shellcode 是一组可注入的指令,可以在被攻击得到程序内运行,因为shellcode要直接操作寄存器和程序函数,所以通常用汇编语言编写并被翻译为十六进制操作码,因此不能用高级语言编写shellcode, 即使细微的差别有可能导致shellcode无法准确执行,这些导致编写shellcode难度的原因
shellcode最初的作用是漏洞利用程序的特殊部分,破解漏洞就是预先把shellcode注入到缓冲区,然后欺骗目标程序执行它
理解系统调用
写shellcode的目的就是想让目标程序以不同于设计者预期的方式运行,而操纵程序的方式之一就是强制它产生系统调用,通过系统调用可以获取一些特权操作,访问系统内核,
调用系统调用方法:1)使用C库包装(libc) 2)使用汇编指令(把适当的参数加载到寄存器,然后调用软中断)执行系统调用
系统调用的过程
linux环境程序通过int 0x80软中断来执行系统调用,程序执行int 0x80时,CPU切换到内核模式并执行相应的系统调用,使用fastcall约定, 提高寄存器的使用率
__fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,
剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者均不同。
__fastcall方式的函数采用寄存器传递参数,VC将函数编译后会在函数名前面加上”@”前缀,在函数名后加上”@”和参数的字节数。 __cdecl (The C default calling convention)即C调用约定按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的。
另外,在函数名修饰约定方面也有所不同。 _cdecl是C和C++程序的缺省调用方式。每一个调用它的函数都包含清空堆栈的代码,所以产生的可执行文件大小
会比调用_stdcall函数的大 __stdcall是Pascal方式清理C方式压栈,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。VC将函数编译后会在函数名前面加上下划线前缀,
在函数名后加上”@”和参数的字节数。 int f(void *p) –>> _f@4(在外部汇编语言里可以用这个名字引用这个函数) 参考: https://blog.csdn.net/sunriver2000/article/details/84913380
调用过程:
1)系统调用编号载入EAX
2)把系统调用参数压入栈中(注意:最多支持6个参数,分别保存在:EBX,ECX,EDX,ESI,EDI,EBP, 超过六个,通过第一个指定数据结构传递)
3)执行int 0x80指令
4)CPU切换到内核模式
5)执行系统调用
举个例子:
1 #include <stdio.h> 2 3 int main() 4 { 5 exit(0); 6 return 0; 7 }
exit() -> _exit()
反汇编看一下:
call *%gs:0x10 ;系统调用从这里进入,会调用到int80
mov 0x4(%esp), %ebx; 退出把系统调用参数加载到ebx, 系统调用之前压栈
编写shellcode
注意:
shellcode大小:由于较小的shellcode 可以注入更多的缓冲区,可以用来攻击更多的程序,所以要使得shellcode尽量保持简单,紧凑
当攻击问题程序的时候,需要把shellcode复制到缓存区,另外还要加上调用shellcode的指令, 所以必须要小
分析程序:
前面exit系统调用分析主要完成三个动作:
1)把0放到EBX
2)把1放到EAX
3)执行int 0x80 指令来产生系统调用
1. hello.c
1 section .data ;section declaration 2 msg db "Hello, world!",0xA ;our dear string 3 len equ $ - msg ;length of our dear string 4 section .text ;section declaration 5 ;we must export the entry point to the ELF linker or 6 global _start ;loader. They conventionally recognize _start as their 7 ;entry point. Use ld -e foo to override the default. 8 _start: 9 ;write our string to stdout 10 mov eax,4 ;system call number (sys_write) 11 mov ebx,1 ;first argument: file handle (stdout) 12 mov ecx,msg ;second argument: pointer to message to write 13 mov edx,len ;third argument: message length 14 int 0x80 ;call kernel 15 ;and exit 16 mov eax,1 ;system call number (sys_exit) 17 xor ebx,ebx ;first syscall argument: exit code 18 int 0x80 ;call kernel
2. objdump -d hello
1 foobar: fiobjdump -d foobarle format elf32-i386 2 3 4 Disassembly of section .text: 5 6 08049000 <.text>: 7 8049000: b8 04 00 00 00 mov $0x4,%eax 8 8049005: bb 01 00 00 00 mov $0x1,%ebx 9 804900a: b9 00 a0 04 08 mov $0x804a000,%ecx 10 804900f: ba 0e 00 00 00 mov $0xe,%edx 11 8049014: cd 80 int $0x80 12 8049016: b8 01 00 00 00 mov $0x1,%eax 13 804901b: 31 db xor %ebx,%ebx 14 804901d: cd 80 int $0x80
3. 把操作码写到字符串数组中执行
char shellcode[] = "\xb8\x04\x00\x00\x00"
"\xbb\x01\x00\x00\x00"
"\xb9\x00\xa0\x04\x08"
"\xba\x0e\x00\x00\x00"
"\xcd\x80"
"\xb8\x01\x00\x00\x00"
"\x31\xdb"
"\xcd\x80"
gcc -o wack wack.c -m32 -z execstack
跳板技术(转https://zhuanlan.zhihu.com/p/88459547)
程序每次运行后在内存中的指令地址都是变化的,所以shellcode入口地址也是动态的,所以为了能够动态找到shellcode的位置,引入了跳板技术。
如图所示,左边表示存储返回地址的栈帧填充为shellcode入口地址,这种方式下次运行时入口地址将发生变化导致失败,右边表示跳板技术后,通过esp来定位shellcode,这种方式可保证下次运行exp依然有效。
跳板技术是用来动态跳转shellcode的,shellcode代码需要从函数返回后esp的栈顶位置开始,然后函数返回到JMP ESP指令处,指令执行后跳到esp位置进入shellcode入口。
注: 对于不同的返回指令的不同,函数返回后esp的指向也有所不同。一般执行ret指令后esp+4,此时shellcode放在存放返回地址的栈帧的下一位置。若是ret N指令,执行后esp+4+N,则shellcode需要放在计算出的对应位置处才行。JMP ESP指令的地址要已知,在xp中JMP ESP可以通过加载kernel32.dll、user32.dll、mfc32.dll等这些经常被加载到内存中的库中寻找,一般地址都是确定的。利用C实现查找代码如下:
1 # include <stdio.h> 2 #include <windows.h> 3 4 main() 5 { 6 HINSTANCE hLib; 7 hLib = LoadLibrary("user32.dll"); 8 if(!hLib) 9 { 10 printf("Load dll error!\n"); 11 exit(0); 12 } 13 14 byte* ptr = (byte*) hLib; 15 int address; 16 int position; 17 bool done_flag = false; 18 19 for(position=0; !done_flag; position++) 20 { 21 try 22 { 23 if(ptr[position] == 0xFF && ptr[position+1] == 0xE4) 24 { 25 // jmp esp 的机器码 为 0xFFE4 26 address = (int)ptr + position; 27 printf("Find OPcode at 0x%08lX\n", address); 28 } 29 } 30 catch(...) 31 { 32 address = (int)ptr + position; 33 printf("End of 0x%08lX\n", address); 34 done_flag = true; 35 } 36 } 37 }
抬高栈顶保护shellcode
如果shellcode放在返回地址栈帧之前,那么在函数返回后栈顶位置会在shellcode下方,虽然出栈后的数据不被清空,但是却会受入栈操作的影响,所以shellcode中若存在push操作,很有可能破坏shellcode结构:
所以要在shellcode开头适当先抬高栈顶让shellcode在栈顶下方,这样push就不会影响shellcode。
抬高栈顶可以用sub esp, N,N大于shellcode长度即可。