Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程。

教程仅针对i386/amd64下的Linux Pwn常见的Pwn手法,如栈,堆,整数溢出,格式化字符串,条件竞争等进行介绍,所有环境都会封装在Docker镜像当中,并提供调试用的教学程序,来自历年赛事的原题和带有注释的python脚本。

课程回顾>>

Linux Pwn入门教程第一章:环境配置

Linux Pwn入门教程第二章:栈溢出基础

Linux Pwn入门教程第三章:ShellCode

Linux Pwn入门教程第四章:ROP技术

Linux Pwn入门教程第五章:调整栈帧的技巧

Linux Pwn入门教程第六章:利用漏洞获取libc

Linux Pwn入门教程第七章:格式化字符串漏洞

Linux Pwn入门教程第八章:PIE与bypass思路

Linux Pwn入门教程第九章:stack canary与绕过的思路

今天i春秋与大家分享的是Linux Pwn入门教程第十章:针对函数重定位流程的相关测试(上),阅读用时约15分钟。

got表、plt表与延迟绑定

在之前的章节中,我们无数次提到过got表和plt表这两个结构。这两个表有什么不同?为什么调用函数要经过这两个表?ret2dl-resolve与这些内容又有什么关系呢?本节我们将通过调试和“考古”来回答这些问题。

我们先选择程序~/XMAN 2016-level3/level3进行实验。这个程序在main函数中和vulnerable_function中都调用了write函数,我们分别在两个call _write和一个call _read上下断点,调试观察发生了什么。

调试启动后程序断在第一个call _write处:

此时我们按F7跟进函数,发现EIP跳到了.plt表上,从旁边的箭头我们可以看到这个jmp指向了后面的push 18h; jmp loc_8048300。

我们继续F7执行到jmp loc_8048300发生跳转,发现这边又是一个push和一个jmp,这段代码也在.plt上。

同样的,我们直接执行到jmp执行完,发现程序跳转到了ld_2.24.so上,这个地址是loc_F7F5D010。

到这里,有些人可能已经发现了不对劲。刚刚的指令明明是jmp ds:off_804a008,这个F7F5D010是从哪里冒出来的呢?其实这行jmp的意思并不是跳转到地址0x0804a008执行代码,而是跳转到地址0x0804a008中保存的地址处。同理,一开始的jmp ds:off_804a018也不是跳转到地址0x0804a018.OK,我们来看一下这两个地址里保存了什么。

回到call _write F7跟进后的那张图,跟进后的第一条指令是jmp ds:off_804a018,这个地址位于.got.plt中。我们看到其保存的内容是loc_8048346,后面还跟着一个DATA XREF:_write↑r. 说明这是一个跟write函数相关的代码引用的这个地址,上面的有一个同样的read也说明了这一点。而jmp ds:0ff_804a008也是跳到了0x0804a008保存的地址loc_F7F5D010处。

回到刚刚的eip,我们继续F8单步往下走,执行到retn 0Ch,继续往下执行就到了write函数的真正地址:

现在我们可以归纳出call write的执行流程如下图:

然后我们F9到断在call _read,发现其流程也和上图差不多,唯一的区别在于addr1和push num中的数字不一样,call _read时push的数字是0。

接下来我们让程序执行到第二个call _write,F7跟进后发现jmp ds:0ff_804a018旁边的箭头不再指向下面的push 18h。

我们查看.got.plt,发现其内容已经直接变成了write函数在内存中的真实地址。

由此我们可以得出一个结论,只有某个库函数第一次被调用时才会经历一系列繁琐的过程,之后的调用会直接跳转到其对应的地址。那么程序为什么要这么设计呢?

要想回答这个问题,首先我们得从动态链接说起。为了减少存储器浪费,现代操作系统支持动态链接特性。即不是在程序编译的时候就把外部的库函数编译进去,而是在运行时再把包含有对应函数的库加载到内存里。由于内存空间有限,选用函数库的组合无限,显然程序不可能在运行之前就知道自己用到的函数会在哪个地址上。比如说对于libc.so来说,我们要求把它加载到地址0x1000处,A程序只引用了libc.so,从理论上来说这个要求不难办到。但是对于用了liba,so, libb.so, libc.so……liby.so, libz.so的B程序来说,0x1000这个地址可能就被liba.so等库占据了。因此,程序在运行时碰到了外部符号,就需要去找到它们真正的内存地址,这个过程被称为重定位。为了安全,现代操作系统的设计要求代码所在的内存必须是不可修改的,那么诸如call read一类的指令即没办法在编译阶段直接指向read函数所在地址,又没办法在运行时修改成read函数所在地址,怎么保证CPU在运行到这行指令时能正确跳到read函数呢?这就需要got表(Global Offset Table,全局偏移表)和plt表(Procedure Linkage Table,过程链接表)进行辅助了。

正如我们刚刚分析过的流程,在延迟加载的情况下,每个外部函数的got表都会被初始化成plt表中对应项的地址。当call指令执行时,EIP直接跳转到plt表的一个jmp,这个jmp直接指向对应的got表地址,从这个地址取值。此时这个jmp会跳到保存好的,plt表中对应项的地址,在这里把每个函数重定位过程中唯一的不同点,即一个数字入栈(本例子中write是18h,read是0,对于单个程序来说,这个数字是不变的),然后push got[1]并跳转到got[2]保存的地址。在这个地址中对函数进行了重定位,并且修改got表为真正的函数地址。当第二次调用同一个函数的时候,call仍然使EIP跳转到plt表的同一个jmp,不同的是这回从got表取值取到的是真正的地址,从而避免重复进行重定位。

符号解析的过程

我们通过调试已经大概搞清楚got表,plt表和重定位的流程了,ret2dl-resolve的核心原理是攻击符号重定位流程,使其解析库中存在的任意函数地址,从而实现got表的劫持。为了完成这一目标,我们就必须得深入符号解析的细节,寻找整个解析流程中的潜在攻击点。我们可以在https://ftp.gnu.org/gnu/glibc/下载到glibc源码,这里我用了glibc-2.27版本的源码。

我们回到程序跳转到ld_2.24.so的部分,这一段的源码是用汇编实现的,源码路径为glibc/sysdeps/i386/dl-trampoline.S(64位把i386改为x86_64),其主要代码如下:

 .text.globl _dl_runtime_resolve.type _dl_runtime_resolve, @functioncfi_startproc.align 16_dl_runtime_resolve:cfi_adjust_cfa_offset (8)pushl %eax # Preserve registers otherwise clobbered.cfi_adjust_cfa_offset (4)pushl %ecxcfi_adjust_cfa_offset (4)pushl %edxcfi_adjust_cfa_offset (4)movl 16(%esp), %edx # Copy args pushed by PLT in register. Notemovl 12(%esp), %eax # that `fixup' takes its parameters in regs.call _dl_fixup # Call resolver.popl %edx # Get register content back.cfi_adjust_cfa_offset (-4)movl (%esp), %ecxmovl %eax, (%esp) # Store the function address.movl 4(%esp), %eaxret $12 # Jump to function address.cfi_endproc.size _dl_runtime_resolve, .-_dl_runtime_resolve

其采用了GNU风格的语法,可读性比较差,我们对应到IDA中的反汇编结果中修正符号如下:

_dl_fixup的实现位于glibc/elf/dl-runtime.c,我们首先来看一下函数的参数列表:

_dl_fixup (
# ifdef ELF_MACHINE_RUNTIME_FIXUP_ARGSELF_MACHINE_RUNTIME_FIXUP_ARGS,
# endifstruct link_map *__unbounded l, ElfW(Word) reloc_arg)

忽略掉宏定义部分,我们可以看到_dl_fixup接收两个参数,link_map类型的指针l对应了push进去的got[1],reloc_arg对应了push进去的数字。由于link_map *都是一样的,不同的函数差别只在于reloc_arg部分。我们继续追踪reloc_arg这个参数的流向。

如果你真的阅读了源码,你会发现这个函数里头找不到reloc_arg,那么这个参数是用不着了吗?不是的,我们往上面看,会看到一个宏定义。

#ifndef reloc_offset
# define reloc_offset reloc_arg
# define reloc_index reloc_arg / sizeof (PLTREL)
#endif
reloc_offset在函数开头声明变量时出现了。const ElfW(Sym) *const symtab= (const void *) D_PTR (l, l_info[DT_SYMTAB]);const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);const PLTREL *const reloc= (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];const ElfW(Sym) *refsym = sym;void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);lookup_t result;DL_FIXUP_VALUE_TYPE value;

D_PTR是一个宏定义,位于glibc/sysdeps/generic/ldsodefs.h中,用于通过link_map结构体寻址。这几行代码分别是寻找并保存symtab, strtab的首地址和利用参数reloc_offset寻找对应的PLTREL结构体项,然后会利用这个结构体项reloc寻找symtab中的项sym和一个rel_addr.我们先来看看这个结构体的定义。这个结构体定义在glibc/elf/elf.h中,32位下该结构体为:

typedef struct
{Elf32_Addr r_offset; /* Address */Elf32_Word r_info; /* Relocation type and symbol index */
} Elf32_Rel;

这个结构体中有两个成员变量,其中r_offset参与了初始化变量rel_addr,这个变量在_dl_fixup的最后return处作为函数elf_machine_fixup_plt的参数传入,r_offset实际上就是函数对应的got表项地址。另一个参数r_info参与了初始化变量sym和一些校验,而sym和其成员变量会作为参数传递给函数_dl_lookup_symbol_x和宏DL_FIXUP_MAKE_VALUE中,显然我们必须关注一下它。不过首先我们得看一下reloc->r_info参与的其他部分代码。

首先我们看到这么一行代码:

 assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT);

这行代码用了一大堆宏,ELFW宏用来拼接字符串,在这里实际上是为了自动兼容32和64位,R_TYPE和前面出现过的R_SYM定义如下:

#define ELF32_R_SYM(i) ((i)>>8)
#define ELF32_R_TYPE(i) ((unsigned char)(i))
#define ELF32_R_INFO(s, t) (((s)<<8) + (unsigned char)(t))
所以这一行代码取reloc->r_info的最后一个字节,判断是否为ELF_MACHINE_JMP_SLOT,即7.我们继续往下看if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL){const ElfW(Half) *vernum =(const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;version = &l->l_versions[ndx];if (version->hash == 0)version = NULL;}

这段代码使用reloc->r_info最终给version进行了赋值,这里我们可以看出reloc->r_info的高24位异常可能导致ndx数值异常,进而在version = &l->l_versions[ndx]时可能会引起数组越界从而使程序崩溃。

看完了这一段,我们回头看一下变量sym, sym同样使用了ELFW(R_SYM)(reloc->r_info)作为下标进行赋值。

const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

Elfw(Sym)会被处理成Elf32_Sym,定义在glibc/elf/elf.h,结构体如下:

typedef struct
{Elf32_Word st_name; /* Symbol name (string tbl index) */Elf32_Addr st_value; /* Symbol value */Elf32_Word st_size; /* Symbol size */unsigned char st_info; /* Symbol type and binding */unsigned char st_other; /* Symbol visibility */Elf32_Section st_shndx; /* Section index */
} Elf32_Sym;

这里面的成员变量st_other和st_name都被用到了。

 if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0){………………result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,version, ELF_RTYPE_CLASS_PLT, flags, NULL);………………
}

这里省略了部分代码,我们可以从函数名判断出,只有这个if成立,真正进行重定位的函数_dl_lookup_symbol_x才会被执行。ELFW(ST_VISIBILITY)会被解析成宏定义。

define ELF32_ST_VISIBILITY(o) ((o) & 0x03)位于glibc/elf/elf.h,所以我们得知这边的sym->st_other后两位必须为0。

我们可以看到传入_dl_lookup_symbol_x函数的参数中,第一个参数为strtab+sym->st_name,第三个参数是sym指针的引用。strtab在函数的开头已经赋值为strtab的首地址,查阅资料可知strtab是ELF文件中的一个字符串表,内容包括了.symtab和.debug节的符号表等等。

我们根据readelf给出的偏移来看一下这个表。

可以看到这里面是有read、write、__libc_start_main等函数的名字的。那么函数_dl_lookup_symbol_x为什么要接收这个名字呢?我们进入这个函数,发现这个函数的代码有点多。考虑到我们关心的是重定位过程中不同的reloc_arg是如何影响函数的重定位的,我们在此不分析其细节。

_dl_lookup_symbol_x (const char *undef_name, struct link_map *undef_map,const ElfW(Sym) **ref,struct r_scope_elem *symbol_scope[],const struct r_found_version *version,int type_class, int flags, struct link_map *skip_map)
{const uint_fast32_t new_hash = dl_new_hash (undef_name);unsigned long int old_hash = 0xffffffff;struct sym_val current_value = { NULL, NULL };............./* Search the relevant loaded objects for a definition. */for (size_t start = i; *scope != NULL; start = 0, ++scope){int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,¤t_value, *scope, start, version, flags,skip_map, type_class, undef_map);if (res > 0)break;if (__glibc_unlikely (res < 0) && skip_map == NULL){/* Oh, oh. The file named in the relocation entry does notcontain the needed symbol. This code is never reachedfor unversioned lookups. */assert (version != NULL);const char *reference_name = undef_map ? undef_map->l_name : "";struct dl_exception exception;/* XXX We cannot translate the message. */_dl_exception_create_format(&exception, DSO_FILENAME (reference_name),"symbol %s version %s not defined in file %s"" with link time reference%s",undef_name, version->name, version->filename,res == -2 ? " (no version symbols)" : "");_dl_signal_cexception (0, &exception, N_("relocation error"));_dl_exception_free (&exception);*ref = NULL;return 0;}...............
}

我们看到函数名字会被计算hash,这个hash会传递给do_lookup_x,从函数名和下面对分支的注释我们可以看出来do_lookup_x才是真正进行重定位的函数,而且其返回值res大于0说明寻找到了函数的地址。我们继续进入do_lookup_x,发现其主要是使用用strtab + sym->st_name计算出来的参数new_hash进行计算,与strtab + sym->st_name,sym等并没有什么关系。对比do_lookup_x的参数列表和传入的参数,我们可以发现其结果保存在current_value中。

do_lookup_x:
static int
__attribute_noinline__
do_lookup_x (const char *undef_name, uint_fast32_t new_hash,unsigned long int *old_hash, const ElfW(Sym) *ref,struct sym_val *result, struct r_scope_elem *scope, size_t i,const struct r_found_version *const version, int flags,struct link_map *skip, int type_class, struct link_map *undef_map)
_dl_lookup_symbol_x:
int res = do_lookup_x (undef_name, new_hash, &old_hash, *ref,¤t_value, *scope, start, version, flags,skip_map, type_class, undef_map);

至此,我们已经分析完了reloc_arg对函数重定位的影响,我们用下面这张图总结一下整个影响过程:

我们以write函数为例进行调试分析,write的reloc_arg是0x18。

使用readelf查看程序信息,找到JMPREL在0x080482b0。

事实上该信息存储在.rel.plt节里。

我们找到这块内存,按照结构体格式解析数据,可知r->offset = 0x0804a018 , r->info=407,与readelf显示的.rel.plt数据吻合。

所以是symtab的第四项,我们可以通过#include<elf.h>导入该结构体后使用sizeof算出Elf32_Sym大小为0x10,通过上面readelf显示的节头信息我们发现symtab并不会映射到内存中,可是重定位是在运行过程中进行的,显然在内存中会有相关数据,这就产生了矛盾。通过查阅资料我们可以得知其实symtab有个子集dymsym,在节头表中显示其位于080481cc。

对照结构体,st_name是0x31,接下来我们去strtab找,同样的,strtab也有个子集dynstr,地址在0804822c.加上0x31后为0804825d。

以上是今天的内容,大家看懂了吗?后面我们将持续更新Linux Pwn入门教程的相关章节,希望大家及时关注。

arg是什么函数_CTF必备技能丨Linux Pwn入门教程——针对函数重定位流程的相关测试(上)...相关推荐

  1. c# 定位内存快速增长_CTF丨Linux Pwn入门教程:针对函数重定位流程的相关测试(下)...

    Linux Pwn入门教程系列分享已到尾声,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  2. python系统提供构造函数传入参数_[ Python入门教程 ] Python函数定义和使用

    函数是一段可以重复多次调用的代码,通过输入的参数值,返回需要的结果.通过使用函数,可以提高代码的重复利用率.本文主要介绍Python函数的定义.调用和函数参数设置方法. 函数的定义 Python函数定 ...

  3. mysql数据库入门教程(14):函数

    函数 #函数 /* 含义:一组预先编译好的SQL语句的集合,理解成批处理语句 1.提高代码的重用性 2.简化操作 3.减少了编译次数并且减少了和数据库服务器的连接次数,提高了效率 区别: 存储过程:可 ...

  4. setwindowshookex回调函数不执行_ECMAScript 6 入门教程—Generator 函数的异步应用

    作者 | 阮一峰异步编程对 JavaScript 语言太重要.JavaScript 语言的执行环境是"单线程"的,如果没有异步编程,根本没法用,非卡死不可.本章主要介绍 Gener ...

  5. C语言 const 修饰函数返回值 - C语言零基础入门教程

    目录 一.const 简介 1.const 修饰变量 2.const 修饰指针 二.const 修饰在函数名前面 三.const 修饰在函数名后面 四.猜你喜欢 零基础 C/C++ 学习路线推荐 : ...

  6. Python 函数声明和调用 - Python零基础入门教程

    目录 一.前言 二.Python 函数定义 三.Python 函数的调用 四.Python 函数传参 1.Python 函数常规参数 2.Python 函数缺省参数 3.Python 函数不定长参数 ...

  7. go 协程回调函数 传入参数_ECMAScript 6 入门教程—Generator 函数的异步应用

    作者 | 阮一峰异步编程对 JavaScript 语言太重要.JavaScript 语言的执行环境是"单线程"的,如果没有异步编程,根本没法用,非卡死不可.本章主要介绍 Gener ...

  8. python def函数报错详解_JSer 快速入门 Python 之函数详解

    前一篇文章,用一天的时间,通过与 JavaScript 做对比的方式,快速领略了 Python 全貌. 梳理了那么多,若忽略细节差异,两门语言只有两个重要差异: 1.书写风格上大相同 2.功能覆盖上, ...

  9. python编程入门教程第6讲_Python开发的入门教程(六)-函数

    介绍 本文主要介绍Python中函数的基本知识和使用 Python之什么是函数 我们知道圆的面积计算公式为: S = πr² 当我们知道半径r的值时,就可以根据公式计算出面积.假设我们需要计算3个不同 ...

  10. linux中c语言kbhit函数用法,检测按键(Linux中kbhit()函数的实现)

    编写过MS-DOS程序的人通常都会查找Linux下等同于kbhit的函数,这个函数会检测一个按键是否被按下而并不实际的读取.不幸的是他们并没有找到这样的函数,因为并没有直接等同的函数.Unix程序员并 ...

最新文章

  1. jquery自定义对话框alert、confirm和prompt
  2. PAT甲级1114 Family Property:[C++题解]结构体、并查集、测试点3、4、5有问题的进来!!
  3. utorrent设置上传速度_utorrent下载速度慢怎样设置 utorrent常用设置图文教程
  4. CentOS中安装git
  5. boost::thread相关的测试程序
  6. php设置accept,PHP或htaccess通过Accept-Language重写URL?
  7. CCD和CMOS摄像头成像原理以及其他区别
  8. JSP内置对象之WEB安全性及config对象
  9. easyui启用行号错位解决方案
  10. 编写高质量代码:改善Java程序的151个建议(第1章:JAVA开发中通用的方法和准则___建议6~10)...
  11. bzoj 3209 花神的数论题 —— 数位DP
  12. IPTV在线服务器地址,德芯IPTV网关服务器,德芯直播点播服务器
  13. i.mx536 linux视频录制,linux mint 18.3下录制网站上的视频与声音的软件 vokoscreen 安装与使用...
  14. EasyCamera中海康摄像头语音对讲和云台控制转发实现
  15. python 弹窗选择文件并获取文件路径
  16. word文件太大如何压缩到最小?
  17. 上海Apple面试php,面试Apple苹果APO的MQE经验
  18. 电子商务里的P2P、O2O、P2C、B2C、B2B、C2C是什么?
  19. 框架协议、合同的下达
  20. bandizip修改压缩文件内容_Bandizip: 压缩和解压缩

热门文章

  1. RIP简易配置第二篇
  2. EL属性范围用法sessionScope等(转)
  3. SpringCloud之Ribbon源码分析(一)
  4. ElasticSearch全文搜索引擎之Aggregation聚合查询(基于RestHighLevelClient)
  5. 设计模式 ( 六 ) 建造者模式
  6. OpenCV-Python教程8-图像混合
  7. 中国工业机器人市场正在迎来爆发式增长
  8. 【apache】phpstudy中apache 隐藏入口文件index.php (解决no input file specified错误)
  9. redis重做从库时报Connection with master lost错误
  10. 揭开CSS的绝对定位真实的面纱(二)