获取入口点
如果没有有用的符号,首先需要找到可执行文件的入口点。有几种方法可以做到(取决于您拥有的工具或您最喜欢的工具):
使用 readelf
$> readelf -h /bin/ls
ELF 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: EXEC (Executable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x40489c
Start of program headers: 64 (bytes into file)
Start of section headers: 108264 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 27
Section header string table index: 26
所以,入口点地址是0x40489c
.
使用 objdump
$> objdump -f /bin/ls
/bin/ls: file format elf64-x86-64
architecture: i386:x86-64, flags 0x00000112:
EXEC_P, HAS_SYMS, D_PAGED
start address 0x000000000040489c
同样,入口点是0x000000000040489c
.
使用 gdb
$> gdb /bin/ls
GNU gdb (GDB) 7.6.2 (Debian 7.6.2-1)
...
Reading symbols from /bin/ls...(no debugging symbols found)...done.
(gdb) info files
Symbols from "/bin/ls".
Local exec file:
`/bin/ls', file type elf64-x86-64.
Entry point: 0x40489c
0x0000000000400238 - 0x0000000000400254 is .interp
0x0000000000400254 - 0x0000000000400274 is .note.ABI-tag
0x0000000000400274 - 0x0000000000400298 is .note.gnu.build-id
0x0000000000400298 - 0x0000000000400300 is .gnu.hash
0x0000000000400300 - 0x0000000000400f18 is .dynsym
0x0000000000400f18 - 0x00000000004014ab is .dynstr
0x00000000004014ac - 0x00000000004015ae is .gnu.version
0x00000000004015b0 - 0x0000000000401640 is .gnu.version_r
0x0000000000401640 - 0x00000000004016e8 is .rela.dyn
0x00000000004016e8 - 0x0000000000402168 is .rela.plt
0x0000000000402168 - 0x0000000000402182 is .init
0x0000000000402190 - 0x00000000004028a0 is .plt
0x00000000004028a0 - 0x0000000000411f0a is .text
0x0000000000411f0c - 0x0000000000411f15 is .fini
0x0000000000411f20 - 0x000000000041701c is .rodata
0x000000000041701c - 0x0000000000417748 is .eh_frame_hdr
...
入口点仍然是0x40489c
。
定位main
程序
知道入口点后,您可以在其上设置断点并开始查找main
过程。因为,你必须知道所有的程序都会由一个_start()
负责初始化进程内存和加载动态库的过程开始。事实上,这第一个过程是 Unix 世界的惯例。
这个初始化过程到底是做什么的,遵循起来非常乏味,而且在大多数情况下,根本没有兴趣理解你的程序。main()
只有在所有内存设置完毕并准备就绪后,程序才会启动。
让我们看看如何做到这一点(我假设可执行文件已经用 编译gcc
):
(gdb) break *0x40489c
Breakpoint 1 at 0x40489c
(gdb) run
Starting program: /bin/ls
warning: Could not load shared library symbols for linux-vdso.so.1.
Breakpoint 1, 0x000000000040489c in ?? ()
好的,所以我们在可执行文件的最开始处停了下来。此时,什么都没有准备好,一切都需要设置。让我们看看可执行文件的第一步是什么:
(gdb) disas 0x40489c,+50
Dump of assembler code from 0x40489c to 0x4048ce:
=> 0x000000000040489c: xor %ebp,%ebp
0x000000000040489e: mov %rdx,%r9
0x00000000004048a1: pop %rsi
0x00000000004048a2: mov %rsp,%rdx
0x00000000004048a5: and $0xfffffffffffffff0,%rsp
0x00000000004048a9: push %rax
0x00000000004048aa: push %rsp
0x00000000004048ab: mov $0x411ee0,%r8
0x00000000004048b2: mov $0x411e50,%rcx
0x00000000004048b9: mov $0x4028c0,%rdi
0x00000000004048c0: callq 0x4024f0 <__libc_start_main@plt>
0x00000000004048c5: hlt
0x00000000004048c6: nopw %cs:0x0(%rax,%rax,1)
End of assembler dump.
后面的hlt
只是由于 执行的线性扫描而获得的垃圾gdb
。所以,忽略它。相关的是我们正在调用的事实__libc_start_main()
(我不会对此发表评论,@plt
因为它会将我们拖出问题的范围)。
实际上,该过程__libc_start_main()
为使用libc
动态库运行的进程初始化内存。并且,一旦完成,跳转到位于%rdi
(通常是main()
程序)中的程序。看下图有一个全局视图是什么__libc_start_main()
程序[ 1 ]
因此,确实,该main()
过程的地址位于0x4028c0
。让我们在这个地址反汇编一些指令:
(gdb) x /10i 0x4028c0
0x4028c0: push %r15
0x4028c2: push %r14
0x4028c4: push %r13
0x4028c6: push %r12
0x4028c8: push %rbp
0x4028c9: mov %rsi,%rbp
0x4028cc: push %rbx
0x4028cd: mov %edi,%ebx
0x4028cf: sub $0x388,%rsp
0x4028d6: mov (%rsi),%rdi
...
而且,如果你看看它,这确实是main()
程序。所以,这是真正开始分析的地方。
警告的话
即使这种查找main()
程序的方式在大多数情况下都有效。您必须知道我们强烈依赖以下假设:
用纯汇编语言编写并用gcc -nostdlib
(或直接用gas
或nasm
)编译的程序不会有第一次调用,而是__libc_start_main()
直接从入口点开始。因此,对于这些程序,_start()
程序就是main()
程序。事实上,重要的是要理解,main()
过程只是 C 语言引入的约定,作为程序中要运行的第一个函数(由程序员编写)。当然,您可以发现该约定在许多其他语言中也得到了复制,例如 Java、C++ 和其他语言。但是,所有这些语言都源自 C。
我们还强烈依赖对__libc_start_main()
工作方式的了解。并且,这个程序是如何由gcc
团队设计的。因此,如果您正在分析的程序是用另一个编译器编译的,您可能需要进一步研究这个编译器以及它如何在运行该main()
过程之前执行内存设置。
无论如何,如果您仔细阅读此答案,您现在应该能够找到一个完全没有符号的程序。
最后,您可以通过阅读Patrick Horgan 的“ Linux x86 程序启动或 - 我们如何到达main()
? ”找到关于启动可执行文件的出色总结。