C 语言,关于 GCC, GDB, x86 组语和呼叫惯例 (1)

Outline

Source Code to Executable FileWhat is ELFX86 Assembly and Calling Convention静态分析与动态分析动态分析: 使用 GDB静态分析: 使用 IDA

前言

本篇章我们将回顾一个 C 语言原始码到执行档的整个流程,从中了解执行档 ELF 格式,配合 cemu 回顾 x86 组合语言,通过 machine code 了解 Shellcode 概念,最后,通过分析二进位档案来了解静态分析工具 IDA 与动态分析工具 GDB 的使用。

前置工具

Linux 虚拟机 (任意发行板),或是 WSL 也可以。

安装 make

$ sudo apt install make -y

安装 32 位元函式库

$ sudo apt-get install gcc-multilib

安装 cemu

$ python3 -m pip install cemu$ mkdir -p ~/.local/bin$ sudo ln -s ~/.local/bin/cemu  /usr/bin/cemu

安装 gcc

$ sudo apt-get install gcc

安装 IDA

$ wget https://out7.hex-rays.com/files/idafree82_linux.run$ chmod +x idafree82_linux.run$ ./idafree82_linux.run

安装 NASM

$ sudo apt-get install nasm

概观:从 hello.chello.out

由GCC了解C语言,学习笔记

这里我们将通过 hello.c 来理解整个程式是如何被电脑执行的,C 语言为设计从事 系统程式 开发的语言,从 C 语言我们可以较为直觉的观察到背后机器的行为,更準确的说,我们可以更精确的预期到 C 语言编译后所产生的组合语言为何,以下将通过一系列的实验来理解 hello.out 是如何被执行的。

首先我们先看到 GCC 的编译过程,GCC 全名为 GNU Compiler Collection,从这个英文名称,我们可以知道 GCC 是包含编译时所需工具的集合,以下先看到 hello.c 的原始码

#include <stdio.h>int main(void) {    printf("Hello, World!\n");    return 0;}

在通常情况,我们会使用以下指令产生出可执行档

$ gcc hello.c -o hello

使用 GCC 将 hello.c 编译成一个可执行档,并将可执行档命名成 hello.out (-o <name> 表示将可执行档命名成 name)。

而将 GCC 加上 -save-temps--verbose 可以让我们获得 GCC 编译过程中所产生的中间档案,以及详细的编译资讯

$ gcc hello.c -o hello -save-temps --verbose

以下为指令输出

Using built-in specs.COLLECT_GCC=gccCOLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapperOFFLOAD_TARGET_NAMES=nvptx-none:amdgcn-amdhsaOFFLOAD_TARGET_DEFAULT=1Target: x86_64-linux-gnuConfigured with: ../src/configure -v --with-pkgversion='Ubuntu 11.3.0-1ubuntu1~22.04' --with-bugurl=file:///usr/share/doc/gcc-11/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++,m2 --prefix=/usr --with-gcc-major-version-only --program-suffix=-11 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --enable-bootstrap --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-plugin --enable-default-pie --with-system-zlib --enable-libphobos-checking=release --with-target-system-zlib=auto --enable-objc-gc=auto --enable-multiarch --disable-werror --enable-cet --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none=/build/gcc-11-xKiWfi/gcc-11-11.3.0/debian/tmp-nvptx/usr,amdgcn-amdhsa=/build/gcc-11-xKiWfi/gcc-11-11.3.0/debian/tmp-gcn/usr --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu --with-build-config=bootstrap-lto-lean --enable-link-serialization=2Thread model: posixSupported LTO compression algorithms: zlib zstdgcc version 11.3.0 (Ubuntu 11.3.0-1ubuntu1~22.04) COLLECT_GCC_OPTIONS='-o' 'hello' '-save-temps' '-v' '-mtune=generic' '-march=x86-64'/usr/lib/gcc/x86_64-linux-gnu/11/cc1 -E -quiet -v -imultiarch x86_64-linux-gnu hello.c -mtune=generic -march=x86-64 -fpch-preprocess -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o hello.iignoring nonexistent directory "/usr/local/include/x86_64-linux-gnu"ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/include-fixed"ignoring nonexistent directory "/usr/lib/gcc/x86_64-linux-gnu/11/../../../../x86_64-linux-gnu/include"+#include "..." search starts here:+#include <...> search starts here:+ /usr/lib/gcc/x86_64-linux-gnu/11/include+ /usr/local/include+ /usr/include/x86_64-linux-gnu+ /usr/include+End of search list.COLLECT_GCC_OPTIONS='-o' 'hello' '-save-temps' '-v' '-mtune=generic' '-march=x86-64'+ /usr/lib/gcc/x86_64-linux-gnu/11/cc1 -fpreprocessed hello.i -quiet -dumpbase hello.c -dumpbase-ext .c -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o hello.sGNU C17 (Ubuntu 11.3.0-1ubuntu1~22.04) version 11.3.0 (x86_64-linux-gnu)        compiled by GNU C version 11.3.0, GMP version 6.2.1, MPFR version 4.1.0, MPC version 1.2.1, isl version isl-0.24-GMPGGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072GNU C17 (Ubuntu 11.3.0-1ubuntu1~22.04) version 11.3.0 (x86_64-linux-gnu)        compiled by GNU C version 11.3.0, GMP version 6.2.1, MPFR version 4.1.0, MPC version 1.2.1, isl version isl-0.24-GMPGGC heuristics: --param ggc-min-expand=100 --param ggc-min-heapsize=131072Compiler executable checksum: 3f6cb05d963ad324b8f9442822c95179COLLECT_GCC_OPTIONS='-o' 'hello' '-save-temps' '-v' '-mtune=generic' '-march=x86-64'+ as -v --64 -o hello.o hello.sGNU assembler version 2.38 (x86_64-linux-gnu) using BFD version (GNU Binutils for Ubuntu) 2.38COMPILER_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/LIBRARY_PATH=/usr/lib/gcc/x86_64-linux-gnu/11/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib/:/lib/x86_64-linux-gnu/:/lib/../lib/:/usr/lib/x86_64-linux-gnu/:/usr/lib/../lib/:/usr/lib/gcc/x86_64-linux-gnu/11/../../../:/lib/:/usr/lib/COLLECT_GCC_OPTIONS='-o' 'hello' '-save-temps' '-v' '-mtune=generic' '-march=x86-64' '-dumpdir' 'hello.'+ /usr/lib/gcc/x86_64-linux-gnu/11/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/11/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper -plugin-opt=-fresolution=hello.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/11 -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/11/../../.. hello.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/11/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crtn.oCOLLECT_GCC_OPTIONS='-o' 'hello' '-save-temps' '-v' '-mtune=generic' '-march=x86-64' '-dumpdir' 'hello.'

我们特别关注绿色标记的部份,我们可以看到 GCC 在编译一个 .c 档案时,会拆分成四个部份

前处理 (Preprocess):
#include "..." search starts here:#include <...> search starts here: /usr/lib/gcc/x86_64-linux-gnu/11/include /usr/local/include /usr/include/x86_64-linux-gnu /usr/includeEnd of search list.
从这一个部份可以看到前处理会到特定目录底下搜寻对应到的函式库,前处理器会将 #include 部份展开,加入到 .c 档中,产生出 hello.i 档案编译 (Compile):
/usr/lib/gcc/x86_64-linux-gnu/11/cc1 -fpreprocessed hello.i -quiet -dumpbase hello.c -dumpbase-ext .c -mtune=generic -march=x86-64 -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o hello.s
看到我们使用在 /usr/lib/gcc/x86_64-linux-gnu/11 目录底下,名称为 cc1 的工具,cc1 为编译器,编译器会将 hello.c 编译成 hello.shello.s 的内容为组合语言组译 (Assmble):
as -v --64 -o hello.o hello.s
as 为组译器,目的为将 hello.s 变成 hello.o,将组合语言变成机器指令,hello.o 也称为目的档 (Object file)链结 (Link):
/usr/lib/gcc/x86_64-linux-gnu/11/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/11/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/11/lto-wrapper -plugin-opt=-fresolution=hello.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o hello /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/11/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/11 -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/11/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/11/../../.. hello.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/11/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/11/../../../x86_64-linux-gnu/crtn.o
我们使用目录 /usr/lib/gcc/x86_64-linux-gnu/11 底下 collect2 为一个指令,封装了 ld 这个指令,ld 为链结器 (linker),collect2 作用为将 C 语言执行时需要的函式库 (C Run Time, CRT) 中的目的档 (crt1.o, crti.o, crtbegin.o, crtend.o, crtn.o),以及需要的动态链结函式库 (libgcc.so, libgcc_s.so, libc.so),和 hello.o 链结到一起,形成一个可执行档案,hello.out

我们可以观察我们在编译过程中,产生的所有中间档案

$ ls                                                                                                                                                                                     ─╯hello.out  hello.c  hello.i  hello.o  hello.s

前处理阶段

前处理阶段会处理程式码中以 # 开头的指令,例如 #include, #define,转换之后加入到程式码原始档中,得到另外一个 C 语言,副档名 .i

我们可以使用以下指令产生出 .i 档,也就是完成前处理之后的档案,以下为 hello.c

#include <stdio.h>#define ONE 1int main(void) {    printf("Hello, world!\n");    printf("%d\n", ONE);    return 0;}

使用以下指令得到 hello.i

$ gcc -E hello.c -o hello.i

以下为 hello.i 的内容

...extern int fprintf (FILE *__restrict __stream,      const char *__restrict __format, ...);extern int printf (const char *__restrict __format, ...);extern int sprintf (char *__restrict __s,      const char *__restrict __format, ...) __attribute__ ((__nothrow__));...int main(void) {    printf("Hello, world!\n");    printf("%d\n", 1);    return 0;}

可以看到巨集定义的 ONE 完成了替换
整个预处理完整的处理方式为以下

递迴处理 #include 预处理指令,将对应的档案内容複製到该指令的位置,如上方的 printf删除所有 #define 的指令,并将其被引用的地方递迴进行展开处理前处理指令,如 #if, #ifdef...删除所有注解添加行号,档案名称注解

编译阶段

编译阶段会将 hello.i 进行语法分析,字词分析,语意分析,优化等等,最终产生出组合语言程式码。

使用 -S 选项,可以对 hello.c 或是 hello.i 产生出 hello.s,我们使用以下指令进行编译

$ gcc -S hello.i -o hello.s -masm=intel -fno-asynchronous-unwind-tables

其中 masm = intel 表示将产生出组合语言的格式设置为 intel 格式,hello.s 的内容如下

        .file   "hello.c"        .intel_syntax noprefix        .text        .section        .rodata.LC0:        .string "Hello, world!".LC1:        .string "%d\n"        .text        .globl  main        .type   main, @functionmain:        endbr64        push    rbp        mov     rbp, rsp        lea     rax, .LC0[rip]        mov     rdi, rax        call    puts@PLT        mov     esi, 1        lea     rax, .LC1[rip]        mov     rdi, rax        mov     eax, 0        call    printf@PLT        mov     eax, 0        pop     rbp        ret        .size   main, .-main        .ident  "GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0"        .section        .note.GNU-stack,"",@progbits        .section        .note.gnu.property,"a"        .align 8        .long   1f - 0f        .long   4f - 1f        .long   50:        .string "GNU"1:        .align 8        .long   0xc0000002        .long   3f - 2f2:        .long   0x33:        .align 84:

这边可以看到编译器进行了一些优化,printf 被优化成了 puts (可以加上 -O0 优化选项禁止所有优化)

组译阶段

组译阶段会将组合语言依照对照表 (又称为 optable) 将组合语言翻译成机器语言,产生出 hello.o

使用以下指令可以产生出目的档

$ gcc -c hello.s -o hello.o

我们可以试着查看 hello.o

��UH��H�H����]�Hello, world!GCC: (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0GNU�zRxU                                                                           E�C��      hello.cmainputs

可以看到里面有许多无法使用 ASCII 表示的字元,我们可以使用 objdump 这个工具查看其内容

$ objdump -sd hello.o -M intel
hello.o:     file format elf64-x86-64Contents of section .text: 0000 f30f1efa 554889e5 488d0500 00000048  ....UH..H......H 0010 89c7e800 000000b8 00000000 5dc3      ............].  Contents of section .rodata: 0000 48656c6c 6f2c2077 6f726c64 2100      Hello, world!.  Contents of section .comment: 0000 00474343 3a202855 62756e74 75203131  .GCC: (Ubuntu 11 0010 2e332e30 2d317562 756e7475 317e3232  .3.0-1ubuntu1~22 0020 2e303429 2031312e 332e3000           .04) 11.3.0.    Contents of section .note.gnu.property: 0000 04000000 10000000 05000000 474e5500  ............GNU. 0010 020000c0 04000000 03000000 00000000  ................Contents of section .eh_frame: 0000 14000000 00000000 017a5200 01781001  .........zR..x.. 0010 1b0c0708 90010000 1c000000 1c000000  ................ 0020 00000000 1e000000 00450e10 8602430d  .........E....C. 0030 06550c07 08000000                    .U......        Disassembly of section .text:0000000000000000 <main>:   0:   f3 0f 1e fa             endbr64    4:   55                      push   rbp   5:   48 89 e5                mov    rbp,rsp   8:   48 8d 05 00 00 00 00    lea    rax,[rip+0x0]        # f <main+0xf>   f:   48 89 c7                mov    rdi,rax  12:   e8 00 00 00 00          call   17 <main+0x17>  17:   b8 00 00 00 00          mov    eax,0x0  1c:   5d                      pop    rbp  1d:   c3                      ret

从上面的结果,我们可以看到除了组合语言外,还有 .text, .rodata 等等资讯,这一些资讯我们将会在后面 ELF 档案时说明。

链结阶段

链结可以分成静态链结和动态链结。GCC 预设使用动态链结,如果要使用静态链结,加上 -static 的选项即可使用。

链结会将目的档和其他函式库或是相依的档案进行链结,产生出一个可执行档,在这个过程中,会分配记忆体空间,符号绑定,重新定位等等,以下解释,首先,我们先产生出可执行档,并指定使用静态链结的方式。

$ gcc hello.o -o hello -static

内容如下 (仅列出部份)

0000000000001050 <puts@plt>:    1050:       f3 0f 1e fa             endbr64     1054:       f2 ff 25 75 2f 00 00    bnd jmp QWORD PTR [rip+0x2f75]        # 3fd0 <puts@GLIBC_2.2.5>    105b:       0f 1f 44 00 00          nop    DWORD PTR [rax+rax*1+0x0]    0000000000001149 <main>:    1149:       f3 0f 1e fa             endbr64     114d:       55                      push   rbp    114e:       48 89 e5                mov    rbp,rsp    1151:       48 8d 05 ac 0e 00 00    lea    rax,[rip+0xeac]        # 2004 <_IO_stdin_used+0x4>    1158:       48 89 c7                mov    rdi,rax    115b:       e8 f0 fe ff ff          call   1050 <puts@plt>    1160:       b8 00 00 00 00          mov    eax,0x0    1165:       5d                      pop    rbp    1166:       c3                      ret

下面我们将目的档和执行档进行比较

0000000000000000 <main>:                                 │0000000000001149 <main>:   0:   f3 0f 1e fa             endbr64                  │    1149:       f3 0f 1e fa             endbr64         4:   55                      push   rbp               │    114d:       55                      push   rbp    5:   48 89 e5                mov    rbp,rsp           │    114e:       48 89 e5                mov    rbp,rsp        8:   48 8d 05 00 00 00 00    lea    rax,[rip+0x0]     │    1151:       48 8d 05 ac 0e 00 00    lea    rax,[rip+0xeac]        # 2004 <_IO_stdin_used + 0x4>                                  f:   48 89 c7                mov    rdi,rax           │    1158:       48 89 c7                mov    rdi,rax                                                                   12:   e8 00 00 00 00          call   17 <main+0x17>    │    115b:       e8 f0 fe ff ff          call   1050 <puts@plt>        17:   b8 00 00 00 00          mov    eax,0x0           │    1160:       b8 00 00 00 00          mov    eax,0x0                                  1c:   5d                      pop    rbp               │    1165:       5d                      pop    rbp  1d:   c3                      ret                      │    1166:       c3                      ret 

比较两边的 main,可以发现到记忆体地址改变了,且 function 的部份 puts 被填入了正确的记忆体地址。

思考: 为什么编译器会将 printf 优化成 puts() ?
原因为 printf 需要处理许多的格式,包含 %d, %c, %f, %s 等等,而 puts() 只需要判断 \0 出现在何处即可,做的判断以及处理更少,因此执行时间也来的更好。

延伸阅读: 你所不知道的 C 语言:编译器和最佳化原理篇

main 改变了记忆体地址,意义上为重新定位,也就是 main 会被放置在记忆体中某一处。

经过了重新定位以及填入了 function 的记忆体地址,整个程式就可以正常的载入到记忆体中执行了。

接着,我们可以尝试使用 file 检视档案资讯
我们目前知道了程式是如何从原始码变成一个执行档,而执行档载入从硬碟载入到记忆体中,我们需要在记忆体中放置程式的一些资讯,诸如 ... 以下先介绍段的概念

Lab1: Hello

观察 hello.c 的所有中间档案

关于 ELF 格式

ELF (Executable and Linkable Format),可执行与可链结格式,最初为 UNIX 实验室为了 ABI (Application Binary Interface) 的一部分制定的,为 COFF (Commmon file format) 格式的变种,ELF 定义可以在 /usr/include/elf.h 中看到。

以下为 /usr/include/elf.h 中部份内容

typedef Elf32_Half Elf32_Versym;                                                              │    1104:       80 3d 05 2f 00 00 00    cmp    BYTE PTR [rip+0x2f05],0x0        # 4010 <__TMCtypedef Elf64_Half Elf64_Versym;                                                              │_END__>                                                                                                                                                  │    110b:       75 2b                   jne    1138 <__do_global_dtors_aux+0x38>                                                                                              │    110d:       55                      push   rbp/* The ELF file header.  This appears at the start of every ELF file.  */                     │    110e:       48 83 3d e2 2e 00 00    cmp    QWORD PTR [rip+0x2ee2],0x0        # 3ff8 <__cx                                                                                              │a_finalize@GLIBC_2.2.5>                                                 #define EI_NIDENT (16)                                                                        │    1115:       00                                                                                                                                       │    1116:       48 89 e5                mov    rbp,rsptypedef struct                                                                                │    1119:       74 0c                   je     1127 <__do_global_dtors_aux+0x27>{                                                                                             │    111b:       48 8b 3d e6 2e 00 00    mov    rdi,QWORD PTR [rip+0x2ee6]        # 4008 <__ds  unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */                     │o_handle>                                        Elf32_Half    e_type;                 /* Object file type */                                │    1122:       e8 19 ff ff ff          call   1040 <__cxa_finalize@plt>  Elf32_Half    e_machine;              /* Architecture */                                    │    1127:       e8 64 ff ff ff          call   1090 <deregister_tm_clones>  Elf32_Word    e_version;              /* Object file version */                             │    112c:       c6 05 dd 2e 00 00 01    mov    BYTE PTR [rip+0x2edd],0x1        # 4010 <__TMC  Elf32_Addr    e_entry;                /* Entry point virtual address */                     │_END__>                                                      Elf32_Off     e_phoff;                /* Program header table file offset */                │    1133:       5d                      pop    rbp                     Elf32_Off     e_shoff;                /* Section header table file offset */                │    1134:       c3                      ret         Elf32_Word    e_flags;                /* Processor-specific flags */                        │    1135:       0f 1f 00                nop    DWORD PTR [rax]  Elf32_Half    e_ehsize;               /* ELF header size in bytes */                        │    1138:       c3                      ret             Elf32_Half    e_phentsize;            /* Program header table entry size */                 │    1139:       0f 1f 80 00 00 00 00    nop    DWORD PTR [rax+0x0]  Elf32_Half    e_phnum;                /* Program header table entry count */                │                                                                                    Elf32_Half    e_shentsize;            /* Section header table entry size */                 │0000000000001140 <frame_dummy>:  Elf32_Half    e_shnum;                /* Section header table entry count */                │    1140:       f3 0f 1e fa             endbr64              Elf32_Half    e_shstrndx;             /* Section header string table index */               │    1144:       e9 77 ff ff ff          jmp    10c0 <register_tm_clones>} Elf32_Ehdr;                                                                                 │                                                                                                                                                                     │0000000000001149 <main>:                                   typedef struct                                                                                │    1149:       f3 0f 1e fa             endbr64            {                                                                                             │    114d:       55                      push   rbp           unsigned char e_ident[EI_NIDENT];     /* Magic number and other info */                     │    114e:       48 89 e5                mov    rbp,rsp                                       Elf64_Half    e_type;                 /* Object file type */                                │    1151:       48 8d 05 ac 0e 00 00    lea    rax,[rip+0xeac]        # 2004 <_IO_stdin_used+  Elf64_Half    e_machine;              /* Architecture */                                    │0x4>                                                         Elf64_Word    e_version;              /* Object file version */                             │    1158:       48 89 c7                mov    rdi,rax                           Elf64_Addr    e_entry;                /* Entry point virtual address */                     │    115b:       e8 f0 fe ff ff          call   1050 <puts@plt>  Elf64_Off     e_phoff;                /* Program header table file offset */                │    1160:       b8 00 00 00 00          mov    eax,0x0       Elf64_Off     e_shoff;                /* Section header table file offset */                │    1165:       5d                      pop    rbp           Elf64_Word    e_flags;                /* Processor-specific flags */                        │    1166:       c3                      ret                                      Elf64_Half    e_ehsize;               /* ELF header size in bytes */                        │                                                             Elf64_Half    e_phentsize;            /* Program header table entry size */                 │Disassembly of section .fini:                                       Elf64_Half    e_phnum;                /* Program header table entry count */                │                                             Elf64_Half    e_shentsize;            /* Section header table entry size */                 │0000000000001168 <_fini>:                                           Elf64_Half    e_shnum;                /* Section header table entry count */                │    1168:       f3 0f 1e fa             endbr64   Elf64_Half    e_shstrndx;             /* Section header string table index */               │    116c:       48 83 ec 08             sub    rsp,0x8} Elf64_Ehdr;

ELF 本质上就是一个二进位档案的格式,在 Linux 中使用 ELF,Windows 中为 PE,Binary 档案开头会有一个 magic number 的栏位,可以让作业系统确认这是什么类型的档案,我们可以在上方 ELF 格式中看到 magic number 栏位的存在。

也可以使用 readelf 这个工具去读取 magic number,ELF 格式的档案 magic number 为 7f 45 4c 46 使用 ASCII 表示为 \177ELF,我们此用以下指令验证

$ readelf -h hello.oELF Header:  Magic:   7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00   Class:                             ELF64  Data:                              2's complement, little endian  Version:                           1 (current)  OS/ABI:                            UNIX - System V  ABI Version:                       0  Type:                              REL (Relocatable file)  Machine:                           Advanced Micro Devices X86-64  Version:                           0x1  Entry point address:               0x0  Start of program headers:          0 (bytes into file)  Start of section headers:          2256 (bytes into file)  Flags:                             0x0  Size of this header:               64 (bytes)  Size of program headers:           0 (bytes)  Number of program headers:         0  Size of section headers:           64 (bytes)  Number of section headers:         23  Section header string table index: 22

在 Linux 中许多档案都是以 ELF 格式存在,ELF 格式的档案可以分成三种类型,分别为以下

可执行档 (executable file): 经过链结后,可以执行的档案,也被称为程式 (program),包含二进位程式码与资料,可以直接被複製到记忆体中执行可重新定位档 (relocatable file): 原始码档案经过编译后尚未鍊结的目的档,以 .o 作为副档名。用来和其他目的档链结产生出可执行档或是动态鍊结函式库,通常是一段 PIC (Position Independent Code)共享目的档 (shared object file): 动态鍊结档案,用来在鍊结过程的时候,和其他动态鍊结或是可重新定位档案一同组成新的目的档。或是鍊结到一个 Process 中。

ELF 大致上我们可以分成以下几个部分

ELF Header: 技术基本资料Program Header Table: 记录程式该如何载入到记忆体中Section Header Table: 记录档案内的段 (Section)Section: 数个段

ELF 格式的档案可以用于鍊结,通常一个 ELF 档案我们可以把他分成多个区域,也就是上面提及段,Section 的概念,包含 .text, .data, .bss

.text: 放置程式码.data: 放置资料.bss: 放置未初始化资料

分成多个区域,在安全性方面有一些好处,当程式被载入到记忆体中,资料和程式码分别被映射到虚拟记忆体中不同的区域,而我们会为这一些不同的区域划分不同的权限,如资料的区域我们就定义成可读写,程式码区域定义成只可读取,避免程式遭到修改,我们可以根据不同的权限,将这一些区域进行划分

data: rw-code: r-xstack: rw-heap: rw-

值得注意的地方,我们这边分割的概念和上方解读 ELF 结构时的概念不同,这边读写与执行的概念,是当程式被载入到记忆体时,才有的概念。

以下我们通过简单的程式码来更加了解 ELF Section 的概念,考虑以下 C 语言程式码

Lab2: ELF_Section

#include <stdio.h>#include <stdlib.h>int a;int b = 100;int main(void) {    int c = 5;    puts("Welcome to Before PWN/Reverse");    return 0;}

按照上面提及的 ELF Section 的概念,我们预期上面程式经过编译后的 ELF Section 概念如下

以 b 这个全域变数,我们知道他初始化的值会放置在 .data 这个 Section 中,我们试着实验,为了方便实验,我们将关闭 PIE 保护机制,并使用 readelf -S ./welcome 查看所有 Section

$ gcc -g -no-pie welcome.c -o welcome_NO_NX
[Nr] Name              Type             Address           Offset       Size              EntSize          Flags  Link  Info  Align  [ 0]                   NULL             0000000000000000  00000000       0000000000000000  0000000000000000           0     0     0  [ 1] .interp           PROGBITS         0000000000400318  00000318       000000000000001c  0000000000000000   A       0     0     1  [ 2] .note.gnu.pr[...] NOTE             0000000000400338  00000338       0000000000000030  0000000000000000   A       0     0     8  [ 3] .note.gnu.bu[...] NOTE             0000000000400368  00000368       0000000000000024  0000000000000000   A       0     0     4  .  .  .  [25] .data             PROGBITS         0000000000404020  00003020       0000000000000014  0000000000000000  WA       0     0     8

可以看到 .data Section 会被映射到虚拟记忆体中 0x404020 的位置,而 .data Section 位于 ELF 档案中的位置为档案开头的地方,加上 0x3020 的位置。

接着我们使用 gdb 执行 welcome,在 main 下一个中断点,接着使用 gdb-peda 插件的 vmmap 查看 ELF 执行后,各各 Section 映射到虚拟记忆体空间上的情况

Start              End                Perm      Name0x00400000         0x00401000         r--p      /home/ubuntu/Desktop/workspace/Reverse_1/ELF_Section/welcome_NO_NX0x00401000         0x00402000         r-xp      /home/ubuntu/Desktop/workspace/Reverse_1/ELF_Section/welcome_NO_NX0x00402000         0x00403000         r--p      /home/ubuntu/Desktop/workspace/Reverse_1/ELF_Section/welcome_NO_NX0x00403000         0x00404000         r--p      /home/ubuntu/Desktop/workspace/Reverse_1/ELF_Section/welcome_NO_NX0x00404000         0x00405000         rw-p      /home/ubuntu/Desktop/workspace/Reverse_1/ELF_Section/welcome_NO_NX0x00007ffff7c00000 0x00007ffff7c28000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.60x00007ffff7c28000 0x00007ffff7dbd000 r-xp      /usr/lib/x86_64-linux-gnu/libc.so.60x00007ffff7dbd000 0x00007ffff7e15000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.60x00007ffff7e15000 0x00007ffff7e19000 r--p      /usr/lib/x86_64-linux-gnu/libc.so.60x00007ffff7e19000 0x00007ffff7e1b000 rw-p      /usr/lib/x86_64-linux-gnu/libc.so.60x00007ffff7e1b000 0x00007ffff7e28000 rw-p      mapped0x00007ffff7f99000 0x00007ffff7f9c000 rw-p      mapped0x00007ffff7fbb000 0x00007ffff7fbd000 rw-p      mappe...

可以看到总共有 5 个 Section,而前面我们在 ELF 档案中看到 .data Section 会被映射到 0x404000 (.data 位于 ELF 档案开头加上 0x3020 的位置,这个位置的 Section 映射到了虚拟记忆体空间中 0x404000 的位置,大小为 0x14),而我们又知道,.data Section 的权限为只可读取与写入,和上面我们对 ELF 归纳的结论相同。

这边还可以看到,在关闭 PIE 时,整个程式码的基底记忆体地址都是从 0x4000000 开始的,且 ELF 在虚拟记忆体中映射的状况,就如同 ELF 档案中所描述一般。

如果有打开 PIE 保护,则不会如此。

我们在 gdb 中执行 x/30x 0x404000 将虚拟记忆体地址 0x404000 之后 30 个 word 的内容,并以 16 进位的方式进行输出

gdb-peda$ x/30x 0x4040000x404000:       0x0000000000403e20      0x00007ffff7ffe2e00x404010:       0x00007ffff7fd8d30      0x00000000004010300x404020:       0x0000000000000000      0x00000000000000000x404030 <b>:   0x0000000000000064      0x00000000000000000x404040:       0x0000000000000000      0x00000000000000000x404050:       0x0000000000000000      0x00000000000000000x404060:       0x0000000000000000      0x00000000000000000x404070:       0x0000000000000000      0x00000000000000000x404080:       0x0000000000000000      0x00000000000000000x404090:       0x0000000000000000      0x00000000000000000x4040a0:       0x0000000000000000      0x00000000000000000x4040b0:       0x0000000000000000      0x00000000000000000x4040c0:       0x0000000000000000      0x00000000000000000x4040d0:       0x0000000000000000      0x00000000000000000x4040e0:       0x0000000000000000      0x0000000000000000

这边我们看到了,全域变数 b 的初始化数值 100 (0x64) 确实出现在 .data Section 中,这边的意义为 ELF 档案在执行后,将数值映射到虚拟记忆体空间中的情况,下面我们也可以试着寻找 100 这个数值在 ELF 档案中具体的位置,回顾我们 readelf 输出的内容,我们知道 .data Section 位于档案开头加上 0x3020 的位置,我们使用 16 进位检视的方式,检视 welcome 二进位档案的内容

$ xxd welcome

我们检视 0x3020 偏移的位置

00003020: 0000 0000 0000 0000 0000 0000 0000 0000  ................00003030: [64]00 0000 4743 433a 2028 5562 756e 7475  d...GCC: (Ubuntu

可以看到 0x64 确实出现在 ELF 档中。

关于 ELF 格式

下面为一个 ELF 格式表示图

ELF Header: 为一个 16 进位的序列,表示了产生这个 ELF 档案的系统的 word 大小,little endian 还是 big endian 等等,剩余的部份为帮助鍊结器进行语法分析以及解析目的档的资讯,具体如下ELF Header 大小目的档类型: 可执行档,可重新定位档,共享目的档机器类型: 如 x86-64section header table 的偏移section header table 中 section 的数目以及大小.text: 已经过编译的机器码.rodata: Read only data,像是 printf 中的格式化字串 (char *).data: 已经完成初始化的全域和静态变数。区域变数在执行时储存在 stack 中,也就是说不在 .data 中,也不在 .bss.bss: 未初始化的全域和静态变数,以及所有被初始化为 0 的全域或是静态变数。在目的档中这个 section 不佔据实际空间。目的档为了空间效率考量,未初始化变数不需要佔据实际硬碟空间,在程式执行时,记忆体分配空间给这一些变数,并初始化为 0.symtab: 符号表,存在在程式中函数的引用和全数变数的资讯,如果我们尝试用 gdb 对一个执行档进行除错,我们会需要加上 -g 的选项,要不在 gdb 执行程式时,会产生找不到符号表的讯息。但是实际上,每一个可重新定位目的档在 .symtab 都有一张符号表,和编译器中的符号表不同之处在于,.symtab 不包含区域变数的 section.rel.text: 当鍊结器把目的档和其他目的档进行鍊结时,会需要修改目的档中标示的记忆体地址,包含外部函数呼叫或是对全域变数的引用都需要修正,但是在本身目的档内的函数呼叫不需要修正,在可动态重新定位的档案中,没有统一的定址空间,所有地址都是和特定部份相对的,可能与函式相对等等,因此,每个重新定位的档案都需要自己的重新定位表,.rel.text 为用于重新定位的.rel.data: 包含全域变数以及其他引用的重新定位资讯.debug: 用于除错的符号表 (如果没有添加,则会在使用 gdb 对执行档进行除错时看见找不到符号表的讯息),包含程式中区域变数和型别的定义,全域变数的定义和引用,以及原始 C 语言程式码,可以在 gcc 编译加上 -g 选项得到这张表.line: C 语言程式码中行号和 .text 中机器指令对应的映射关係,gcc 加上 -g 选项得到.strtab: 字串表,包含 .symtab.debug 中的符号表,以及 section 名称

基础 x86 组合语言

在上面我们看到 C 语言原始码到可执行档的完整过程,在许多情况下,我们并没有程式的原始码,也就是 C 语言档案,而是只有一个可执行档,在分析可执行档时,我们时常会使用到反组议的技术,也就是将二进位可执行档中机器码转换成组合语言,进而去分析程式的行为。

组合语言实际上是一类语言的集合,每一种组合语言都对应到相对应的微处理器架构,诸如 x86, x64, RISC-V, ARAM 等等。在恶意程式中,大多数是以 x86 微处理器架构编译而成的,也就是目标机器为 x86 微处理器架构的机器,以下将会简单的介绍 x86 组合语言。

x86 组合语言格式问题, ATT vs Intel

ATT 风格

暂存器前面加上 %立即数前面加上 $16 进位数使用 0x 作为前缀==来源操作数在前面,目标操作数在后面==针对操作的位数,加上 l, w, b 修饰,如 movl, movw

mov %eax, %edx 等效于 edx = eax

Intel 风格

暂存器前面没有符号表示立即数前面没有符号表示16 进位使用 h 最为后缀==来源操作数在后面,目标操作数在前面==针对操作的位数,加上 +dword ptr 修饰,如 mov DWORD PTR[ebp-12], eax

mov eax, edx 等效于 eax = edx

以下组合语言,使用 Intel 风格表示。

mov 将资料从一个地方移动到另外一个地方

助忆符    目标操作数      来源操作数  mov         ecx    ,     0x42

上面这是 mov 指令的使用,前面我们看到,组合语言会经过组译器翻译成机器语言,也就是一连串二进位数,我们可以想像得到, mov 这个指令会翻译成一个二进位数,称为 opcode,实际上 CPU 看到的是 opcode,但是组译器是如何知道 mov 该翻译成怎样的 opcode?我们有一张表,称为 instruction table (optable),可以用来查询 opcode 和指令对应的转换关係,诸如 AAD 这个指令对应到的 opcode 为 0x37,而为了方便程式设计师开发,我们不需要记住 0x37 对应到的是 AAD,只需要知道 AAD 是做什么的,剩下交给组译器完成翻译,这也就是为什么我们会称 mov, AAD 为助忆符 aka 帮助记忆的符号。

操作数的一部分,常见的有以下三种类行

立即数 (immediate): 为一个固定的值,在上面的例子中 0x42 就是一个立即数暂存器 (register): 指向暂存器,在上面的力中为 ecx记忆体地址 (memory address): 指向到记忆体地址,一般来说,可以是由一个方括号包住一个值,如 [0x0002],或是将暂存器中储存的值当作记忆体地址使用,如 [eax],也可以是由一条运算式组成,像是 [eax + 0x10]

以下为一个典型的指令表 (instruction table)

我们也可以使用工具 Cemu 帮助我们看到组合语言与机器语言之间的对应关係


从上面可以看到 mov ecx 对应到的机器码为 0xb90x43 对应到的 machine code 为 43 00 00 00,特别注意到 0x43 在记忆体中摆放的方式,这和 big endian 以及 little endian 有关,假设现在有一个变数 x,型别为 int,存在于记忆体地址 0x100 中,也就是 &x = 0x100,而假设一个 int 为 4 bytes (32 bit),则变数 x 的 4 个位元组将被储存在记忆体中 0x100, 0x101, 0x102, 0x103 的位置。

x 的值为 0x01234567,则 big endian 以及 little endian 表示法为以下表示

回到上面的例子,我们观察到 0x00000043 放置到记忆体表示为 43 00 00 00,由此我们得到 x86 为 little endian。

More x86 指令(搭配 cemu 进行测试)

sub eax 0x1eax 的值减去 0x1,并放回 eax 暂存器中,eax = eax - 1add eax 0x1eax 的值加上 0x1,并放回 eax 暂存器中,eax = eax + 1call <some_address> 呼叫某一个 functioninc edxedx 的值增加 1

关于 x86 暂存器

通用暂存器 (general purpose register): eax, ebx, ecx, edx, ebp, esp, esi程式计数器 (program counter register): eip旗标暂存器 (flag register): eflags记忆体区段暂存器 (segment register): cs, ss, ds, es, fs, gs


从上面的图中,我们可以看到前缀字有所不同,如 rax, eax, ax 等等,这表示在不同的环境下,如在 64 位元下,使用的为 rax,32 位元使用 eax

而在后面的说明,我们可以看到暂存器有一些特定的用途 (但不一定遵守)

ax: 用于算术运算cx: 用于处理迴圈的计数bx: 指向一个资料,或是记忆体区块的基底记忆体地址sp: 指向 stack 的顶端,sp 为 stack pointer 简写dx: 用来算术或是 I/O 操作

为什么需要暂存器?

以下为记忆体阶乘图

越往上,表示存取速度越快,但容量也越小,一般为了加速存取速度,会将变数从记忆体中载入到暂存器中进行操作。

条件与分支 (condition and branch)

常见的条件指令有 testcmp,以下列举

cmp dst, src 意义为将 dst 和 src 相减,将结果记录到 EFLAGStest dst, src 意义为将 dst 和 src 做 bitwise AND 运算,并将结果记录到 EFLAGS

上面有提及到 EFLAGS 这个旗标暂存器,这个暂存器中会储存以下状态

CF (Carry Flag) 如果产生进位 (carry bit),则设为 1ZF (Zero Flag) 如果结果为 0,则设为 1SF (Sign Flag) 如果结果小于 0,则设为 1OF (Overflow Flag) 如果出现 overflow, underflow,则设为 1

接着是 jump 系列的指令,常常会搭配 cmp, test 使用

jmp dst 无条件跳到 dst 执行jz dst 如果 ZF == 1,则跳到 dst 执行jnz dst 如果 ZF == 0,则跳到 dst 执行...

关于 x86 呼叫惯例

所谓呼叫惯例,指的是我们在呼叫函式时,函式的参数该如何处理,回传值如何处理,函式如何进行呼叫。

函式呼叫 (function call): 定义 call 需要将 return address 推入 stack 中接着通过 jump 指令进行跳转函式回传 (function return): 定义 ret 需要将 return address 从 stack 中弹出常见在 32 位元的程式以及 64 位元的程式处理会有所不同32 位元程式: 参数放置于 stack 中,回传值放置在 rax 中 (cdecl)64 位元程式: 参数会放置在 rdi, rsi ,rdx ,rcx, r8, r9 其他部份放在 stack,回传值放置在 rax 中 (System V)

呼叫惯例会根据架构,作业系统,编译器等等而有所不同。

Lab3: Calling Convention

我们可以通过实验进行验证,给定以下 C 语言原始码档案

#include <stdlib.h>#include <stdio.h>void func(int a, int b){a + b;}int main(void) {int a = 2;int b = 3;func(a,b);}

对应的 Makefile

all:    gcc -g -m32 func_call.c -o func_call_32    gcc -g -m64 func_call.c -o func_call_64

以下为 32 位元程式经过反组译后产生的组合语言 (可以使用 gdb 执行执行档后,使用 disassemble main 即可得到 main 函式反组译的结果)

   0x000011c1 <+0>:     endbr32   0x000011c5 <+4>:     push   ebp   0x000011c6 <+5>:     mov    ebp,esp   0x000011c8 <+7>:     sub    esp,0x10   0x000011cb <+10>:    call   0x11f8 <__x86.get_pc_thunk.ax>   0x000011d0 <+15>:    add    eax,0x2e0c   0x000011d5 <+20>:    mov    DWORD PTR [ebp-0x8],0x2   0x000011dc <+27>:    mov    DWORD PTR [ebp-0x4],0x3   0x000011e3 <+34>:    push   DWORD PTR [ebp-0x4]   0x000011e6 <+37>:    push   DWORD PTR [ebp-0x8]   0x000011e9 <+40>:    call   0x11ad <func>   0x000011ee <+45>:    add    esp,0x8   0x000011f1 <+48>:    mov    eax,0x0   0x000011f6 <+53>:    leave   0x000011f7 <+54>:    ret

以下为 64 位元程式经过反组译后产生的组合语言

   0x000000000000113a <+0>:     endbr64   0x000000000000113e <+4>:     push   rbp   0x000000000000113f <+5>:     mov    rbp,rsp   0x0000000000001142 <+8>:     sub    rsp,0x10   0x0000000000001146 <+12>:    mov    DWORD PTR [rbp-0x8],0x2   0x000000000000114d <+19>:    mov    DWORD PTR [rbp-0x4],0x3   0x0000000000001154 <+26>:    mov    edx,DWORD PTR [rbp-0x4]   0x0000000000001157 <+29>:    mov    eax,DWORD PTR [rbp-0x8]   0x000000000000115a <+32>:    mov    esi,edx   0x000000000000115c <+34>:    mov    edi,eax   0x000000000000115e <+36>:    call   0x1129 <func>   0x0000000000001163 <+41>:    mov    eax,0x0   0x0000000000001168 <+46>:    leave   0x0000000000001169 <+47>:    ret

可以验证到,32 位元程式确实是通过 stack 进行参数传递,而 64 位元的程式,在上面只有两个参数进行传递的範例下,分别使用了 esiedi 暂存器。

关于 stack 与函式呼叫

在上面我们看到了一个函式的呼叫与其呼叫惯例,在一些情况中我们会使用到 stack 去管理函式的记忆体空间 (非 malloc),区域变数,流程控制等等。stack 是一种资料结构,支援两种操作,分别为 pushpop,且都是针对 stack 顶端进行操作,先进入的元素最后被弹出 stack,品课洋芋片一般。


source

stack 的生长方向为记忆体高位向低位生长,sp 暂存器指向到 stack 的顶端,使用 push, pop 指令对 stack 进行操作

如果我们要把资料放到 stack 上,我们需要使用 push 指令,举例,假设我们想要把 rax 中的资料 push 到 stack 上,则图像化为以下,搭配 cemu 进行实验

Lab4: Cemu: Stack

mov rax, 0x1234567push raxpop rbx


执行 push rax 时,首先 rsp 会 -8

接着把 rax 中的资料放入到 stack 中

将 stack 顶端的值给 rbx,接着 rsp + 8

执行结束后,暂存器为以下状态

在 x86 组合语言中,我们可以看到许多与 stack 操作有关的指令,像是 push, pop, call, ret, enter, leave 等等,在后续的组合语言中我们会时常看到这一些指令的存在。

Stack Frame

定义: esp 到函式参数的範围称为某一个函式的 Stack Frame,具体为 espebp 的範围

我们可以通过上方反组译出来的 64 位元程式看到 Stack Frame 的运作机制

   0x000000000000113a <+0>:     endbr64   0x000000000000113e <+4>:     push   rbp   0x000000000000113f <+5>:     mov    rbp,rsp   0x0000000000001142 <+8>:     sub    rsp,0x10   0x0000000000001146 <+12>:    mov    DWORD PTR [rbp-0x8],0x2   0x000000000000114d <+19>:    mov    DWORD PTR [rbp-0x4],0x3   0x0000000000001154 <+26>:    mov    edx,DWORD PTR [rbp-0x4]   0x0000000000001157 <+29>:    mov    eax,DWORD PTR [rbp-0x8]   0x000000000000115a <+32>:    mov    esi,edx   0x000000000000115c <+34>:    mov    edi,eax   0x000000000000115e <+36>:    call   0x1129 <func>   0x0000000000001163 <+41>:    mov    eax,0x0   0x0000000000001168 <+46>:    leave   0x0000000000001169 <+47>:    ret

在函式的开始,有一段程式码我们称为 prologue,作用为储存函式中需要使用到的 stack 空间以及暂存器,而在结束的地方,我们需要将 stack 和暂存器回覆到函式呼叫前的状态,而这段程式码我们称为 epilogue,我们可以试着找到上方组合语言中 prologue 以及 epliogue 的部份。

接着我们通过实际案例观察 prologue 和 epliogue 是如何分配 Stack Frame

在 prologue 部份,我们将 rbp 的值推入到 stack 中,接着 rsp 的值 -8。


再将 rsp 的值设为 rbp 的值,因此 rbp 的值更新成新的 rsp 的值,旧的 rbp 的值位于 stack 中,如下图所示


接着通过调整 rsp,开闢出一段给 main 函式使用的 stack 空间,可以在接下来函式主体中使用


经过中间 mov 操作,将函式需要的资料移入 stack,参数放置到暂存器后,接着我们看到 call 0x1129 <func>call 会执行两件事情,分别为 push return address,接着 jmp func

上面完成了 push return address 操作后,我们便 jmp 进入 func


接着我们看到 func 的组合语言程式码


rbp 储存到 stack 中,这个 rbp 原先指向到 main 的 base


接着将 rsp 的值複製到 rbp


接着通过调整 rsp 的值,在 stack 中开闢一段空间给 func 使用


接着我们要执行 leaveleavemov rsp, rbp,接着 pop rbp


接着是 ret,作用为 pop rip,这边对应的意义为将 return address 放置到储存 instruction pointer 的暂存器,对应到图上的意义,就是改变红色框框的位置,执行完毕后,我们便跳回到 main 函式中,红色框框的位置在 ret 时设定完毕。


接着我们跳回了 main 函式,依序执行 leave, ret

到这里整个函式呼叫流程结束。

关于 Exploitation

在上面的 Stack Frame 分析中我们看到函式跳转是通过 return address 进行控制,如果我们在逆向的过程中,发现到程式码中的漏洞,让我们可以改变程式的执行流程,像是修改程式的 return address,那么我们就称之为 Exploitation,又称作为 PWN。

Exploitation: 利用漏洞取得程式的控制权
Binary Exploitation: 针对二进位档案的相关漏洞利用

关于 Shellcode

所谓 Shellcode,就是将机器码全部串在一起,组成一连串 16 进位的程式码,而通常对于一个程式注入程式码主要目的是要得到 shell,因此称作为 Shellcode,以下为一个 x86 组合语言写成的 Hello.s

Lab5. Hello, Shellcode

section .text      global _start_start:                        ;程式起始点jmp msg                ;跳转到 msg 标籤所在位置write:mov eax, 4             ;把 System call 的编号存到 eax 暂存器中,编号 4 表示 write()mov ebx, 1             ;把档案描述子 (file descriptor) 储存到 rbx 暂存器中,1 表示 stdoutpop ecx                ;弹出 Stack 顶端的记忆体地址,该记忆体地址指向到 "Hello, World" 字串mov edx, 14            ;把要输出的字串长度储存到 edx 暂存器中int 0x80               ;呼叫 System callmov eax, 1             ;把 System call 的编号存到 eax 暂存器中,编号 1 表示 exit()int 0x80               ;呼叫 System callmsg:call write             ;呼叫 write,印出字串db 'Hello, World', 0xa ;使用 db 把每一个字元转换成 ASCII 并除存在记忆体中, 0xa 表示换行

参考: System call table

我们现在有了组合语言所撰写出的档案,如果我们要让他变成可执行档,首先我们需要先进行组译,产生出目的档 (object file)

$ nasm -felf32 hello.s -o hello.o           

有了目的档后,接着我们需要过链结器 (linker) 将目的档与其他资讯进行链结,产生出可执行档

$ ld hello.o -melf_i386 -o hello

产生可执行档后,我们可以执行看看测试结果

$ ./helloHello, World

接着我们试着得到 Hello 的 shellcode,前面说到 shellcode 本质上就是机器码组合而成,我们可以使用 objdump 这个工具进行检视

$ objdump -d -M intel hello
hello:     file format elf32-i386Disassembly of section .text:08049000 <_start>: 8049000:       eb 19                   jmp    804901b <msg>08049002 <write>: 8049002:       b8 04 00 00 00          mov    eax,0x4 8049007:       bb 01 00 00 00          mov    ebx,0x1 804900c:       59                      pop    ecx 804900d:       ba 0e 00 00 00          mov    edx,0xe 8049012:       cd 80                   int    0x80 8049014:       b8 01 00 00 00          mov    eax,0x1 8049019:       cd 80                   int    0x800804901b <msg>: 804901b:       e8 e2 ff ff ff          call   8049002 <write> 8049020:       48                      dec    eax 8049021:       65 6c                   gs ins BYTE PTR es:[edi],dx 8049023:       6c                      ins    BYTE PTR es:[edi],dx 8049024:       6f                      outs   dx,DWORD PTR ds:[esi] 8049025:       2c 20                   sub    al,0x20 8049027:       57                      push   edi 8049028:       6f                      outs   dx,DWORD PTR ds:[esi] 8049029:       72 6c                   jb     8049097 <msg+0x7c> 804902b:       64                      fs 804902c:       0a                      .byte 0xa

接着使用 objcopy 这个工具,将机器码储存到 shellcode.bin

$ objcopy -O binary hello shellcode.bin

机器码是由二进位所组成,我们无法通过文字检视器直接进行检视,这时候我们可以使用 16 进位检视工具,如 xxd

$ xxd -i shellcode.bin
unsigned char shellcode_bin[] = {  0xeb, 0x19, 0xb8, 0x04, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0x00,  0x59, 0xba, 0x0c, 0x00, 0x00, 0x00, 0xcd, 0x80, 0xb8, 0x01, 0x00, 0x00,  0x00, 0xcd, 0x80, 0xe8, 0xe2, 0xff, 0xff, 0xff, 0x48, 0x65, 0x6c, 0x6c,  0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a};

如此我们便得到了 hello 的 shellcode,仔细观察可以发现到,shellcode 确实就是由 hello 的机器码所组成,接着我们试着执行这一段 shellcode

#include <stdio.h>int main(void){    unsigned char shellcode_bin[] = {        0xeb, 0x19, 0xb8, 0x04, 0x00, 0x00, 0x00, 0xbb, 0x01, 0x00, 0x00, 0x00,        0x59, 0xba, 0x0e, 0x00, 0x00, 0x00, 0xcd, 0x80, 0xb8, 0x01, 0x00, 0x00,        0x00, 0xcd, 0x80, 0xe8, 0xe2, 0xff, 0xff, 0xff, 0x48, 0x65, 0x6c, 0x6c,        0x6f, 0x2c, 0x20, 0x57, 0x6f, 0x72, 0x6c, 0x64, 0x0a    };    void (*func)(void) = shellcode_bin;    func();}

使用以下指令进行编译

$ gcc -m32 -z execstack call_shellcode.c -o call_shellcode

在通常情况下,我们无法在 stack 上执行程式码,这与 C 语言中硬体保护机制有关 (如 NX, ASLR 等等,后续将介绍),因此,我们使用 -z execstack 禁用 NX 保护 (NX 为 Not Execute on stack 缩写)。

可以使用 gdb-peda 中 checksec 功能确认已经禁用 NX 保护

Warning: 'set logging off', an alias for the command 'set logging enabled', is deprecated.Use 'set logging enabled off'.Warning: 'set logging on', an alias for the command 'set logging enabled', is deprecated.Use 'set logging enabled on'.CANARY    : ENABLEDFORTIFY   : disabledNX        : disabledPIE       : ENABLEDRELRO     : FULL

执行程式

$ ./call_shellcodeHello, World

参考资讯

Binary Exploitation (Pwn) - Basic
PWN1
深入理解计算机系统, 3/e (Computer Systems: A Programmer's Perspective, 3/e)
程序员的自我修养--链接、装载与库
LiveOverflow Binary Exploitation


关于作者: 网站小编

码农网专注IT技术教程资源分享平台,学习资源下载网站,58码农网包含计算机技术、网站程序源码下载、编程技术论坛、互联网资源下载等产品服务,提供原创、优质、完整内容的专业码农交流分享平台。

热门文章