avatar

CTF-Pwn-dl_resolve详解

CTF-Pwn-dl_resolve详解


近期对于dl-resolve不大熟练,所以准备整理一份比较完整资料来学习一波,以源码和实践相互结合分析,逐渐深入。

首先我编译了一份简单的elf文件,然后通过ida在load段看到了如下的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
LOAD:0804820C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00                 Elf32_Sym <0>
LOAD:0804821C 49 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 Elf32_Sym <offset aSetbuf - offset unk_80482CC, 0, 0, 12h, 0, 0> ; "setbuf"
LOAD:0804822C 36 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 Elf32_Sym <offset aRead - offset unk_80482CC, 0, 0, 12h, 0, 0> ; "read"
LOAD:0804823C 27 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 Elf32_Sym <offset aPrintf - offset unk_80482CC, 0, 0, 12h, 0, 0> ; "printf"
LOAD:0804824C 42 00 00 00 00 00 00 00 00 00 00 00 11 00 00 00 Elf32_Sym <offset aStderr - offset unk_80482CC, 0, 0, 11h, 0, 0> ; "stderr"
LOAD:0804825C 2E 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 Elf32_Sym <offset aGetchar - offset unk_80482CC, 0, 0, 12h, 0, 0> ; "getchar"
LOAD:0804826C 20 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 Elf32_Sym <offset aGetpid - offset unk_80482CC, 0, 0, 12h, 0, 0> ; "getpid"
LOAD:0804827C 6C 00 00 00 00 00 00 00 00 00 00 00 20 00 00 00 Elf32_Sym <offset aGmonStart - offset unk_80482CC, 0, 0, 20h, 0, 0> ; "__gmon_start__"
LOAD:0804828C 50 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00 Elf32_Sym <offset aLibcStartMain - offset unk_80482CC, 0, 0, 12h, 0, \ ; "__libc_start_main"
LOAD:0804828C 0>
LOAD:0804829C 1A 00 00 00 00 00 00 00 00 00 00 00 11 00 00 00 Elf32_Sym <offset aStdin - offset unk_80482CC, 0, 0, 11h, 0, 0> ; "stdin"
LOAD:080482AC 3B 00 00 00 00 00 00 00 00 00 00 00 11 00 00 00 Elf32_Sym <offset aStdout - offset unk_80482CC, 0, 0, 11h, 0, 0> ; "stdout"
LOAD:080482BC 0B 00 00 00 04 A0 04 08 04 00 00 00 11 00 0F 00 Elf32_Sym <offset aIoStdinUsed - offset unk_80482CC, \ ; "_IO_stdin_used"
LOAD:080482BC offset _IO_stdin_used, 4, 11h, 0, 0Fh>
LOAD:080482CC ; ELF String Table
LOAD:080482CC 00 unk_80482CC db 0 ; DATA XREF: LOAD:0804821C↑o
LOAD:080482CC ; LOAD:0804822C↑o ...
LOAD:080482CD 6C 69 62 63 2E 73 6F 2E 36 00 aLibcSo6 db 'libc.so.6',0
LOAD:080482D7 5F 49 4F 5F 73 74 64 69 6E 5F 75 73 65 64 00 aIoStdinUsed db '_IO_stdin_used',0 ; DATA XREF: LOAD:080482BC↑o
LOAD:080482E6 73 74 64 69 6E 00 aStdin db 'stdin',0 ; DATA XREF: LOAD:0804829C↑o
LOAD:080482EC 67 65 74 70 69 64 00 aGetpid db 'getpid',0 ; DATA XREF: LOAD:0804826C↑o
LOAD:080482F3 70 72 69 6E 74 66 00 aPrintf db 'printf',0 ; DATA XREF: LOAD:0804823C↑o
LOAD:080482FA 67 65 74 63 68 61 72 00 aGetchar db 'getchar',0 ; DATA XREF: LOAD:0804825C↑o
LOAD:08048302 72 65 61 64 00 aRead db 'read',0 ; DATA XREF: LOAD:0804822C↑o
LOAD:08048307 73 74 64 6F 75 74 00 aStdout db 'stdout',0 ; DATA XREF: LOAD:080482AC↑o
LOAD:0804830E 73 74 64 65 72 72 00 aStderr db 'stderr',0 ; DATA XREF: LOAD:0804824C↑o
LOAD:08048315 73 65 74 62 75 66 00 aSetbuf db 'setbuf',0 ; DATA XREF: LOAD:0804821C↑o
LOAD:0804831C 5F 5F 6C 69 62 63 5F 73 74 61 72 74 5F 6D 61 69+aLibcStartMain db '__libc_start_main',0
LOAD:0804831C 6E 00 ; DATA XREF: LOAD:0804828C↑o
LOAD:0804832E 47 4C 49 42 43 5F 32 2E 30 00 aGlibc20 db 'GLIBC_2.0',0
LOAD:08048338 5F 5F 67 6D 6F 6E 5F 73 74 61 72 74 5F 5F 00 aGmonStart db '__gmon_start__',0 ; DATA XREF:

从上面的数据中我们不难看出Elf32_Sym的结构体如下

1
2
3
4
5
6
7
8
9
typedef struct
{
Elf32_Word st_name; /4字节* Symbol name (string tbl index) */
Elf32_Addr st_value; /4字节* Symbol value */
Elf32_Word st_size; /4字节* Symbol size */
unsigned char st_info; /1字节* Symbol type and binding */
unsigned char st_other; /1字节* Symbol visibility */
Elf32_Section st_shndx; /2字节* Section index */
} Elf32_Sym;

先对于 Elf32_Word st_name 如何算出来的?

程序编译的时候设置了一个被调用函数名字符串的基址,就是下面这个地址,下面的数据可以在上面呈现的代码中找到

1
LOAD:080482CC                                                 ; ELF String Table

所以 st_name = 被调用函数名字符串地址 - 基址

比如上述代码的printf的 st_name 应该等于[0x080482F3 - 080482CC = 0x27],刚好第一位数字就是0x27

1
LOAD:0804823C 27 00 00 00 00 00 00 00 00 00 00 00 12 00 00 00                 Elf32_Sym <offset aPrintf - offset unk_80482CC, 0, 0, 12h, 0, 0> ; "printf"

1、st_name表示符号名,这里的值是它在相应字符串表中的索引号。如.dynsym section中所有表项的符号名要从.dynstr section中获取,而.symtab是从.strtab中获取。而.dynstr和.strtab都是字符串表。

2、st_value表示符号的值。一般为虚拟地址。

3、st_size表示符号的大小。比如对于变量,它的值就是变量的数据类型(如int则它的值可能为4个字节)的大小。如果为0则表示该符号无需大小或大小未知。

4、st_info表示符号的类型和绑定特征。成员st_info的大小为一个字节,低4位表示符号类型,高4位表示符号的绑定特征。在/usr/include/elf.h文件中,有定义以下三个宏来获取类型或绑定特征的值或将它俩合成st_info的值:

1
2
3
#define ELF32_ST_BIND(val)      (((unsigned char) (val)) >> 4)
#define ELF32_ST_TYPE(val) ((val) & 0xf)
#define ELF32_ST_INFO(bind, type) (((bind) << 4) + ((type) & 0xf))

(1)、绑定特征的可能取值如下所示:

1
2
3
4
5
6
7
8
9
#define STB_LOCAL   0       /* Local symbol */
#define STB_GLOBAL 1 /* Global symbol */
#define STB_WEAK 2 /* Weak symbol */
#define STB_NUM 3 /* Number of defined types. */
#define STB_LOOS 10 /* Start of OS-specific */
#define STB_GNU_UNIQUE 10 /* Unique symbol. */
#define STB_HIOS 12 /* End of OS-specific */
#define STB_LOPROC 13 /* Start of processor-specific */
#define STB_HIPROC 15 /* End of processor-specific */

STB_LOCAL表示局部符号,STB_GLOBAL表示全局符号,STB_WEAK表示弱符号。

弱符号是全局符号,但它们的优先级相对全局符号低。

局部符号,顾名思义,它只被局限在定义它的目标文件之内,多个目标文件定义的同名局部符号互不影响,全局符号则与之相反。

相对弱符号,其它的就是强符号。两个同名的强符号会导致重复定义的错误,但如果同名的是一个强符号和多个弱符号,则不报错,链接器会选择其中的强符号。

(2)、符号类型的可能取值如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
#define STT_NOTYPE  0       /* Symbol type is unspecified */
#define STT_OBJECT 1 /* Symbol is a data object */
#define STT_FUNC 2 /* Symbol is a code object */
#define STT_SECTION 3 /* Symbol associated with a section */
#define STT_FILE 4 /* Symbol's name is file name */
#define STT_COMMON 5 /* Symbol is a common data object */
#define STT_TLS 6 /* Symbol is thread-local data object*/
#define STT_NUM 7 /* Number of defined types. */
#define STT_LOOS 10 /* Start of OS-specific */
#define STT_GNU_IFUNC 10 /* Symbol is indirect code object */
#define STT_HIOS 12 /* End of OS-specific */
#define STT_LOPROC 13 /* Start of processor-specific */
#define STT_HIPROC 15 /* End of processor-specific */

STT_NOTYPE表示未指定类型。每个符号表开头都有一个该类型的表项。

STT_OBJECT表示数据,如变量、数组等。

STT_FUNC表示函数或可执行代码。

STT_SECTION表示一个section,该类型符号的绑定特征是STB_LOCAL。

STT_FILE表示文件名。该类型符号的绑定特征是STB_LOCAL,st_shndx的值为SHN_ABS。

5、st_other表示符号的可见性,只使用了该成员的低2位。它的可能取值如下所示:

1
2
3
4
#define STV_DEFAULT 	0       /* Default symbol visibility rules */
#define STV_INTERNAL 1 /* Processor specific hidden class */
#define STV_HIDDEN 2 /* Sym unavailable in other modules */
#define STV_PROTECTED 3 /* Not preemptible, not exported */

6、st_shndx表示符号所在的section对应的section header的索引号。它有以下三个特殊值:

1
2
3
#define SHN_UNDEF   0       	/* Undefined section */
#define SHN_ABS 0xfff1 /* Associated symbol is absolute */
#define SHN_COMMON 0xfff2 /* Associated symbol is common */

SHN_UNDEF表示该符号是未定义的,也就是说它可能定义在其他目标文件中,只有在程序执行时才能实际确定。对于索引值为STN_UNDEF的表项,st_shndx的值也是SHN_UNDEF。

SHN_ABS表示该符号的值在重定位时不会改变。

SHN_COMMON表示该符号尚未被分配空间,它的值st_value为地址对齐字节数。

对于pwn来说,总之我们这里只要考虑st_name, st_info

我们抽取最一般的规律,如下

1
2
3
4
5
st_name = 被调用函数名字符串地址的偏移
st_info = (0x1 << 4) + 0x2 // (STB_GLOBAL << 4) + STT_FUNC
// 如果要绑定函数 st_info = 0x12 例如:__libc_start_main
// 如果要绑定一般的指针 st_info = 0x11 例如:stdin
// 如果要绑定变量 st_info = 0x20 例如:__gmon_start__

对于其他的数据统统为0,如下

1
被调用函数名字符串地址的偏移(4字节) 00 00 00 00 00 00 00 00 st_info(1字节) 00 00 00

这样我们学好了.dynstr和.dynsym结构体的知识

接着我们学习.rel.plt,接着通过ida找到下面的数据,如下

1
2
3
4
5
6
7
8
9
10
11
12
LOAD:08048380                                                 ; ELF REL Relocation Table
LOAD:08048380 F0 BF 04 08 06 04 00 00 Elf32_Rel <804BFF0h, 406h> ; R_386_GLOB_DAT stderr
LOAD:08048388 F4 BF 04 08 06 07 00 00 Elf32_Rel <804BFF4h, 706h> ; R_386_GLOB_DAT __gmon_start__
LOAD:08048390 F8 BF 04 08 06 09 00 00 Elf32_Rel <804BFF8h, 906h> ; R_386_GLOB_DAT stdin
LOAD:08048398 FC BF 04 08 06 0A 00 00 Elf32_Rel <804BFFCh, 0A06h> ; R_386_GLOB_DAT stdout
LOAD:080483A0 ; ELF JMPREL Relocation Table
LOAD:080483A0 0C C0 04 08 07 01 00 00 Elf32_Rel <804C00Ch, 107h> ; R_386_JMP_SLOT setbuf
LOAD:080483A8 10 C0 04 08 07 02 00 00 Elf32_Rel <804C010h, 207h> ; R_386_JMP_SLOT read
LOAD:080483B0 14 C0 04 08 07 03 00 00 Elf32_Rel <804C014h, 307h> ; R_386_JMP_SLOT printf
LOAD:080483B8 18 C0 04 08 07 05 00 00 Elf32_Rel <804C018h, 507h> ; R_386_JMP_SLOT getchar
LOAD:080483C0 1C C0 04 08 07 06 00 00 Elf32_Rel <804C01Ch, 607h> ; R_386_JMP_SLOT getpid
LOAD:080483C8 20 C0 04 08 07 08 00 00 Elf32_Rel <804C020h, 807h> ; R_386_JMP_SLOT __libc_start_main

以上数据就是.rel.plt,从中我们可以提取出一个结构体,如下

1
2
3
4
5
typedef struct
{
Elf32_Addr r_offset; /4字节* Address * (这个就是got的地址[当然你可以自定义一个地址])/
Elf32_Word r_info; /4字节* Relocation type and symbol index */
} Elf32_Rel;

简单说r_offset就是got地址

接下来稍微简单说一下r_info

r_info最低位为6则是变量,为7则是函数

r_info第二位开始为.dynsym的位置,这个位置是以下代码的位置开始的,其实本质上就是.dynsym的头部,然后相对于这个头部取偏移,然后每个偏移的长度为0x10,因为一个.dynsym的长度为0x10

LOAD:0804820C 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00                 Elf32_Sym <0>

所以总结一下

如果要绑定变量 r_info = (((fake_dynsym - dynsym_head) / 0x10) << 8) | 0x6;

如果要绑定函数 r_info = (((fake_dynsym - dynsym_head) / 0x10) << 8) | 0x7

接下来就是reloc_offset的取值问题

这里直接给出规律 reloc_offset = fake_rel_plt - rel_plt_head

总结一下结构体伪造的步骤

1、在bss段构造dynsym顺便把函数名也放到后面,接着继续在函数名后面继续构造.rel.plt

2、通过栈迁移,将reloc_offset和参数放到栈的位置里,然后跳到plt_jmp绑定函数的plt上就可以触发dl_runtime_resolve然后通过_fix_up来任意函数调用

参考了部分原文链接:https://blog.csdn.net/npy_lp/article/details/102704732

文章作者: 咲夜南梦
文章链接: http://yoursite.com/2020/02/07/CTF-Pwn-dl_resolve%E8%AF%A6%E8%A7%A3/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 咲夜南梦's 博客
打赏
  • 微信
    微信
  • 支付宝
    支付宝

评论