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.c
到 hello.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
档案时,会拆分成四个部份
#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.s
,hello.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
分成多个区域,在安全性方面有一些好处,当程式被载入到记忆体中,资料和程式码分别被映射到虚拟记忆体中不同的区域,而我们会为这一些不同的区域划分不同的权限,如资料的区域我们就定义成可读写,程式码区域定义成只可读取,避免程式遭到修改,我们可以根据不同的权限,将这一些区域进行划分
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 格式表示图
.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
对应到的机器码为 0xb9
,0x43
对应到的 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 0x1
将 eax
的值减去 0x1
,并放回 eax
暂存器中,eax = eax - 1
add eax 0x1
将 eax
的值加上 0x1
,并放回 eax
暂存器中,eax = eax + 1
call <some_address>
呼叫某一个 functioninc edx
将 edx
的值增加 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)
常见的条件指令有 test
和 cmp
,以下列举
cmp dst, src
意义为将 dst 和 src 相减,将结果记录到 EFLAGS
中test 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 位元的程式,在上面只有两个参数进行传递的範例下,分别使用了 esi
和 edi
暂存器。
关于 stack 与函式呼叫
在上面我们看到了一个函式的呼叫与其呼叫惯例,在一些情况中我们会使用到 stack 去管理函式的记忆体空间 (非 malloc
),区域变数,流程控制等等。stack 是一种资料结构,支援两种操作,分别为 push
和 pop
,且都是针对 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,具体为 esp
到 ebp
的範围
我们可以通过上方反组译出来的 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
使用
接着我们要执行 leave
,leave
会 mov 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