PWN是什么

PWN是一个俗语,每次攻破某个服务或者系统就是“pwn进去了”,也就是:

破解、利用成功(二进制漏洞)

攻破(设备、服务器)

控制(设备、服务器)

流程如下:

Exploit:用于攻击的脚本和方案

Payload:攻击载荷,也就是构造的恶意数据

Shellcode:调用攻击目标shell的代码

二进制基础

C语言编译流程

c/c++在整个世界里面就和超级搅屎棍一样,一方面因为他真的很优秀,一方面因为时代局限性,导致有很多含有漏洞的程序在世界各处到处都有。

当我拥有一个c语言程序,例如:

1
2
3
4
5
#include<stdio.h>
int main(){
puts("Helloworld");
return 0;
}

我们对他进行编译:

1
gcc helloworld.c

就可以得到一个二进制程序a.out

a.out的内容如下:

1
2
3
4
5
6
7
8
9
10
11
@ @@@���uu   ���-�=�=X`�-�=�=�8880hhh��S�td8880P�td   44Q�tdR�td�-�=�=HH/lib64/ld-linux-x86-64.so.2 GNU���GNU
����c�e���w��U+``~���FDO{"type":"deb","os":"ubuntu","name":"glibc","version":"2.40-1ubuntu3.1","architecture":"amd64"}GNU��e�mH d s "puts__libc_start_main__cxa_finalizelibc.so.6GLIBC_2.2.5GLIBC_2.34_ITM_deregisterTMCloneTable__gmon_start___ITM_registerTMCloneTable'u�i 1���=�@�?�?�?�?�?�?��H�H��/H��t��H���5�/�%�/@��h�����f����%�/fD���%v/fD��1�I��^H��H���PTE1�1�H�=��S/�f.�H�=y/H�r/H9�tH�6/H��t �����H�=I/H�5B/H)�H��H��?H��H�H��tH�/H����fD�����=/u+UH�=�.H��t
H�=�.�����d�����.]������w�����UH��H��H��������]���H�H��helloworld4���h0����@����P���P9����zRx
����&D$4���� FJ
�?�9*3$"\���Ut�����q���E�C
@'
h������o �H

�?�� ������o����o���o~���o�=@GCC: (Ubuntu 14.2.0-4ubuntu2) 14.2.0�� � ��� �3IU�=|@��=������� ��Scrt1.o__abi_tagcrtstuff.cderegister_tm_clones__do_global_dtors_auxcompleted.0__do_global_dtors_aux_fini_array_entryframe_dummy__frame_dummy_init_array_entryhelloworld.c__FRAME_END___DYNAMIC__GNU_EH_FRAME_HDR_GLOBAL_OFFSET_TABLE___libc_start_main@GLIBC_2.34_ITM_deregisterTMCloneTableputs@GLIBC_2.2.5_edata_fini__data_start__gmon_start____dso_handle_IO_stdin_used_end__bss_startmain__TMC_END___ITM_registerTMCloneTable__cxa_finalize@GLIBC_2.2.5_init.symtab.strtab.shstrtab.interp.note.gnu.property.note.gnu.build-id.note.package.note.ABI-tag.gnu.hash.dynsym.dynstr.gnu.version.gnu.version_r.rela.dyn.rela.plt.init.plt.got.plt.sec.text.fini.rodata.eh_frame_hdr.eh_frame.init_array.fini_array.dynamic.data.bss.comment#886hh$I��pW�� e���o o
� � 4�H H ����=�- Hw������o~~����o�����B�� �@@�PP�``�hh
�=���?�@@00%80` �3�x5(

我们可以从中看到一些看得懂的和大量看不懂的内容,其中绝大部分都是无法查看的二进制数据(机器码),而那些可以看出来的二进制数据,可以看到许多信息。这到底是什么文件呢,我们可以file一下。

我们可以发现他是一个ELF(linux下可执行文件格式),64位的文件。

如图所示,C语言到可执行程序的流程也就是_编译(compilier),汇编(assembler),链接(linker)_

可执行文件

什么是可执行文件

广义上,可执行文件就是可以执行的文件,比如说python文件在python环境下就可以直接执行(因为他是一个脚本)。

狭义上,可执行文件必须是一个经历过编译,汇编和链接后得到的二进制文件,并且在环境合适的情况下可以直接打开运行。也就是说CPU可以直接认识,比如.out.exe.dll.so文件

所谓的二进制漏洞,都是在狭义上的程序中的漏洞。

可执行文件的分类

Windows为**PE(portable Executable)**

可执行程序:.exe

动态链接库:.dll

静态链接库:.lib

Linux为**ELF(Executable and Linkable Format)**

可执行程序:.out

动态链接库:.so

静态链接库:.a

ELF文件、内存加载和虚拟内存

ELF文件结构

段视图(Segment View:用于进程在内存区域中读、写、执行(rwx)权限划分

节视图(Section View:一个ELF文件编译链接时候,在磁盘上存储时的文件结构组织

一个段可包含多个节,节是段的细分单元。(不准确,映射关系中,进入内存后看起来段是高于节的)

不过大部分时候,不是严格区分成两个部分的,而是要根据不同的需求做不同的分析的。

比如说在程序从普通存储在硬盘状态到内存中运行的状态,就要从一种段视图的角度转移到节视图。如图所示,一个存储在Disk的程序变为进程进入Memory的时候,其中_**可读可写的.data.bss.got.plt段 **_就变成了Memory中的 Data节 ,很显然,Code节也是一样的由多个段组成的。而程序运行后又产生了新的节,例如stack(栈)和heap(堆)部分…

虚拟内存

程序在调试的时候需要查看内存里面的内容,但是很可惜真实的内存因为一些机制难以查看。

在硬件层面,cpu和memory之间互相沟通没有问题,但是作为程序员是没有办法和硬件沟通的,于是通过OS,OS将物理内存(memory)给抽象出来。

OS通过软件机制将物理内存、外存(硬盘)等资源整合后,向程序员呈现的 “逻辑内存”。

地址空间的隔离与映射

_**虚拟内存用户空间每个进程各一份,**_每个程序各自占用独立的空间,例如在32位系统中,默认会划分给2^32byte,也就是4GB空间的大小。

将CPU中的内存管理单元(MMU)和OS维护的页表(Page Table)实现映射。

因为所有的数据都需要内核,因此_虚拟内存内核空间所有进程共享一份_

最后,虚拟内存mmap段中的动态链接库(glibc)仅在物理内存中装载一份。虽然在每个程序的虚拟内存中都存在一份。

内存空间

如图所示,32位和64位的系统分别创造出来的空间大小各不相同。依次介绍一下分出来的各个部分分别是干嘛的。

首先是Kernel Space,就是映射出来的内核空间。

stack和heap都是动态存储各种数据的地方。

下面都是静态存储区。

RW段(可读可写,一般是.bss.data等)

ReadOnly段(只读,例如.init_array、.fini_array等)

RE段(可读可执行,一般是.init、.rodata、.text等)

Reserved是保留部分。

段(segment)与节(section)概述

1. 代码段(Text segment)

功能:存储代码与只读数据(ReadOnly)

包含节:

  `<font style="background-color:rgb(187,191,196);">.text</font>` 节(核心代码存储) `<font style="background-color:rgb(187,191,196);">.rodata</font>` 节(只读常量,如字符串字面量 ) `<font style="background-color:rgb(187,191,196);">.hash</font>` 节(符号哈希表,辅助符号查找 ) `<font style="background-color:rgb(187,191,196);">.dynsym</font>` 节(动态链接符号表 ) `<font style="background-color:rgb(187,191,196);">.dynstr</font>` 节(动态链接符号名字符串 ) `<font style="background-color:rgb(187,191,196);">.plt</font>` 节(过程链接表,用于延迟绑定 ) `<font style="background-color:rgb(187,191,196);">.rel.got</font>` 节(全局偏移表重定位信息 )

2. 数据段(Data segment)

功能:存储可读可写数据(Read Write)

包含节:

  `<font style="background-color:rgb(187,191,196);">.data</font>` 节(已初始化全局 / 静态变量 ) `<font style="background-color:rgb(187,191,196);">.dynamic</font>` 节(动态链接相关信息,如依赖库 ) `<font style="background-color:rgb(187,191,196);">.got</font>` 节(全局偏移表,加速地址访问 ) `<font style="background-color:rgb(187,191,196);">.got.plt</font>` 节(针对 plt 的全局偏移表 ) `<font style="background-color:rgb(187,191,196);">.bss</font>` 节(未初始化 / 初始化为 0 的全局 / 静态变量,运行时分配内存 )

3. 栈段(Stack segment)

通常用于函数调用上下文、局部变量存储,由系统自动管理栈帧分配与释放

程序数据如何在内存中组织

一开始,有两个全局变量。其中glb存放在内存中的.Bss节部分(因为他没有被初始化)。Bss的其中之一的作用就是存储未初始化的变量。在运行时候分配内存(静态变量也是如此)。

而在char* str中,其中str放在了Data节部分,也就是已经被初始化的变量存储部分。然后”Helloworld”因为是一个字符串,一个只读数据,所以就被放在了Text段中(一般是Text段里面的.rodata节)

然后所有的函数存放在了Text段的text节里面,例如sum、main等等。在程序运行中,所有在函数中的变量(函数的内存开销)都被放在了Stack,也就是栈里面。

在程序中有void* ptr=malloc(0x100);其中这个0x100的空间就通过malloc申请到Heap里面了。如此这般,read函数读取到的”deadbeef”就被读取到Heap里面存储着了,也就是堆里。

大端序和小端序

小端序 低地址存放数据低位,高地址存放数据高位。主流格式

大端序 低地址存放数据高位,高地址存放数据低位

比如说我现在

1
char* str = "ABC";

就可以得到【ABC\0】这个字符串,分别的ASCII码的十六进制是41,42,43,00,合在一起数据就变成了”00434241”。

其中数据最低位为41,数据最高位为00。数据低位放在内存低位,数据高位内存高位(如小端序解释图所示)。

程序的装载与进程的执行

越靠近CPU的存储器,速度越快,价格越高昂,而且存储空间越小。而Register(寄存器)就是一个速度极快的存储器。

寄存器

寄存器一共就四个任务:

第一是把数据存到寄存器

第二是把寄存器中的东西存放到另一个寄存器

第三是把内存中地址上数据获取后存放到寄存器

第四是把寄存器的数据存到地址上

一、amd64 寄存器结构(以 RAX 为例的分级访问)

amd64 架构中,<font style="background-color:rgb(187,191,196);">RAX</font> 是 8 字节(64 位)通用寄存器,支持向下兼容的分级访问:

  • <font style="background-color:rgb(187,191,196);">rax</font>:完整 8 字节(64 位)寄存器,可存 64 位数据。
  • <font style="background-color:rgb(187,191,196);">eax</font>:低 4 字节(32 位),兼容 32 位程序,存 <font style="background-color:rgb(187,191,196);">rax</font> 的低 32 位数据。
  • <font style="background-color:rgb(187,191,196);">ax</font>:低 2 字节(16 位),兼容 16 位程序,存 <font style="background-color:rgb(187,191,196);">rax</font> 的低 16 位数据。
  • <font style="background-color:rgb(187,191,196);">ah</font><font style="background-color:rgb(187,191,196);">ax</font> 的高 1 字节(8 位),存 <font style="background-color:rgb(187,191,196);">ax</font> 的高 8 位。
  • <font style="background-color:rgb(187,191,196);">al</font><font style="background-color:rgb(187,191,196);">ax</font> 的低 1 字节(8 位),存 <font style="background-color:rgb(187,191,196);">ax</font> 的低 8 位。

这种设计让程序能灵活处理不同位数的数据(64/32/16/8 位),适配旧代码兼容需求。

二、部分寄存器的功能

  1. RIP(程序计数器指针寄存器)(Program Counter)
    1. 存当前下一条指令的偏移地址。
  2. RSP(栈指针寄存器)
    1. 存当前栈帧的栈顶偏移地址,栈是 “后进先出” 的内存区域(如函数调用时局部变量、返回地址的存储 ),<font style="background-color:rgb(187,191,196);">RSP</font> 随栈操作(<font style="background-color:rgb(187,191,196);">push</font>/<font style="background-color:rgb(187,191,196);">pop</font> )动态变化,始终指向栈顶位置。
  3. RBP(基址指针寄存器)
    1. 存当前栈帧的栈底偏移地址,用于定位栈内数据(如函数局部变量相对于栈底的偏移 ),辅助访问栈帧中的内容,常配合 <font style="background-color:rgb(187,191,196);">RSP</font> 管理函数调用栈。
  4. RAX(通用寄存器)
    1. 作为通用寄存器,可临时存各种数据;特殊功能是存放函数返回值(如 C 语言中 <font style="background-color:rgb(187,191,196);">int</font>/<font style="background-color:rgb(187,191,196);">long long</font> 等类型的返回值,会通过 <font style="background-color:rgb(187,191,196);">RAX</font> 传递 )。

静态链接的程序的执行过程

fork函数

当我们开始运行程序的时候,随后运行fork()函数

fork()在这张图的意思大概如下:我运行一个helloworld程序,需要用到一些内核kernel的东西。于是乎在我运行,也就是 $ ./binary的时候,shell使用fork函数复制自己,他会在父进程(也就是我的系统)中分裂出一个子进程(也就是./binary程序)。然后当子进程需要用到和修改父进程(kernel中的内容)的时候,fork函数就会复制一份数据(在初始阶段共享所有的数据(全局、 栈区、 堆区、 代码))到子进程中。所以在程序运行的时候,fork函数就是做这个的。

再单独解释一下fork函数,它的作用是从主进程中分裂出一个进程,他们之间是父子进程的关系。然后fork函数会返回一个int类型,这个int类型就是进程号。

fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

在父进程中,fork返回新创建子进程的进程ID; 在子进程中,fork返回0; 如果出现错误,fork返回一个负值;

因此我们可以通过fork返回的值来判断当前进程是子进程还是父进程。(注: fork 调用生成的新进程与其父进程谁先执行不一定,哪个进程先执行要看系统的进程调度策略)

execve函数

当然上述过程只是创建了一个进程,而进程中没有需要运行的内容,execve函数就是用来把./binary里面的内容(也就是需要运行的程序本身)放到子进程中的

子进程调用 execve("./binary", argv[], envp[]) ,目的是用新程序(./binary )替换当前子进程的内存空间:

argv[]:传递程序运行参数(比如 ./binary arg1 arg2 里的参数 )。 envp[]:传递环境变量(比如 PATH``HOME 等 )。

内核态切换与处理

execve() 会触发系统调用,进入内核态(Kernel mode):

先调用 sys_execve() (内核层的系统调用处理函数 ),再调用 do_execve()

do_execve() 会执行 search_binary_handler() ,作用是查找能处理该可执行文件的加载器(因为是静态链接的 ELF 文件,会找到 ELF 加载器 )。

找到后调用 load_elf_binary() ,负责加载 ELF 文件到内存(解析文件头、分配内存、加载代码段 / 数据段等 )。

sys_execve()和do_execve()

简单来说,<font style="background-color:rgb(187,191,196);">sys_execve()</font> 是系统调用处理函数,是用户态到内核态的接口之一。在 Linux 内核里,每个系统调用都有对应的处理函数,<font style="background-color:rgb(187,191,196);">sys_execve()</font> 就是处理 <font style="background-color:rgb(187,191,196);">execve()</font> 系统调用的函数,对用户请求进行初步审核和预处理;而 <font style="background-color:rgb(187,191,196);">do_execve()</font> 则是是内核中真正执行程序加载和执行关键操作的函数,属于 <font style="background-color:rgb(187,191,196);">sys_execve()</font> 处理流程的下一级,真正去操办程序加载运行的各项事务。

它们共同协作,完成从用户请求运行程序到程序成功加载运行的整个过程。

search_binary_handler()和load_elf_binary()

<font style="background-color:rgb(187,191,196);">search_binary_handler()</font>:负责识别文件格式,通过遍历格式处理器链表找到能处理当前文件的加载器。

<font style="background-color:rgb(187,191,196);">load_elf_binary()</font>:负责 ELF 文件的具体加载,包括内存映射、环境设置、动态链接等,最终让程序得以执行。也就是让程序可以静态调试的主要函数。

_start和main()

没什么好说,就是启动后让用户可以看到程序的最后一步。

动态连接的程序的执行过程

相比于静态储存,多了一个ld.so__libc_start_main()_init。这些是额外处理共享库加载、初始化和运行时支持。

分别依赖库是ld.so解决函数库地址映射关系之类的,加载是__libc_start_main()把这些东西放到,初始化是_init