menu 牢记自己是菜
HSCTF--python 字节码
47 浏览 | 2020-06-07 | 阅读时间: 约 5 分钟 | 分类: 乱七八糟的小比赛 | 标签:

0x1 前言

开门见山的说,最近博客拖更严重,但是这并不代表我啥也没干哈!这周除了在研究子洋师傅XCTF的题解外还参加了一个外国的高大上比赛。做了几道题,但是没有啥好记录的地方,主要就是一些简单的java逆向。甚至都不需要任何动态调试,只需要静态理解代码逆推即可,所以在这里就不过多的赘述了。第五道题目是一道python字节码的题目,这已经不是我们第一次见到python字节码了,之前在虎符ctf中,re第一道题目就是一道python字节码的题目,当时子洋师傅就已经在比赛上把题目做出来了,并且写了很详细的题解,赛后我也是学习这个题解的。这次这个字节码也是师傅先完成的,然后我才在师傅的提示下摸出来的。由于师傅已经写过一篇python字节码的wp了,应该不会在复现这道题目了,所以这个wp就交给我代劳吧。这道python字节码的题目比虎符的要简单(毕竟这个比赛的年龄段是6-12岁),没有见过这类题目的小伙伴可以先学习这道题目,在去子洋师傅那里把虎符的那道题目搞定。要是想要挑战一下的话,可以直接去子洋师傅找python字节码的题目。这里给出师傅博客的传送门!==>>哈,召唤python字节码传送门


0x2 dis模块

这类题目其实很好识别,一般就是一个txt文件,打开txt文件就会看见一堆与汇编很类似语言,这就是python字节码。我之前有写过一篇关于python与字节码之间的联系,这里就不多提了。

dis模块,是一个很神奇的模块,我反正在没有接触这种题目前是没有见过的这个模块的。这个模块可以将对应的源码还原成字节码的形式。由于没有很好的办法,所以这类题目我多半都是读一读字节码,然后根据猜测写一点python的源码,之后使用dis模块还原成字节码,与题目进行对比,最终达到读懂代码还原逻辑的目的。这里拿这道题目的主函数举一个小例子。

def f():
    s = input("Guess?")
    o=b'\xae\xc0\xa1\xab\xef\x15\xd8\xca\x18\xc6\xab\x17\x93\xa8\x11\xd7\x18\x15\xd7\x17\xbd\x9a\xc0\xe9\x93\x11\xa7\x04\xa1\x1c\x1c\xed'
    if e(s)==o:
        print('Correct!')
    else:
        print('Wrong...')
import dis
print(dis.dis(f))

脚本输出字节码:

  2           0 LOAD_GLOBAL              0 (input)
              2 LOAD_CONST               1 ('Guess?')
              4 CALL_FUNCTION            1
              6 STORE_FAST               0 (s)

  3           8 LOAD_CONST               2 (b'\xae\xc0\xa1\xab\xef\x15\xd8\xca\x18\xc6\xab\x17\x93\xa8\x11\xd7\x18\x15\xd7\x17\xbd\x9a\xc0\xe9\x93\x11\xa7\x04\xa1\x1c\x1c\xed')
             10 STORE_FAST               1 (o)

  4          12 LOAD_GLOBAL              1 (e)
             14 LOAD_FAST                0 (s)
             16 CALL_FUNCTION            1
             18 LOAD_FAST                1 (o)
             20 COMPARE_OP               2 (==)
             22 POP_JUMP_IF_FALSE       34

  5          24 LOAD_GLOBAL              2 (print)
             26 LOAD_CONST               3 ('Correct!')
             28 CALL_FUNCTION            1
             30 POP_TOP
             32 JUMP_FORWARD             8 (to 42)

  7     >>   34 LOAD_GLOBAL              2 (print)
             36 LOAD_CONST               4 ('Wrong...')
             38 CALL_FUNCTION            1
             40 POP_TOP
        >>   42 LOAD_CONST               0 (None)

对比一下题目给出的txt,嗯完全一致。

这里帅气的小伙伴可能要问了,你咋知道这些字节码是啥的呢!不着急由于主函数比较简单,所以我们直接给出,后面的abce函数我们一点一点的分析。观察一下,在主函数里面就只调用的e函数,在e函数里面有调用了很多其他函数,所以我们判断e函数是一个加密函数,由于这个函数有点特殊,我们就先从a函数开始分析。配合注释理解一下。

0x3 a,b,c函数的分析

在分析代码之前首先讲几个基本的东西,我这里简单讲讲,要是觉得不够详细的话,可以在这里深度学习一下
首先就是:
LOAD_CONST:加载const 变量,比如数值,字符串等等, 一般用于传递给函数作为参数,这个指令很多情况下都是加载的常量比如:LOAD_CONST 1 (50)就是加载常量50
与之相对应的就是LOAD_GLOBAL与LOAD_FAST,LOAD_GLOBAL后面一般会跟一个函数名,一般来说是用来加载系统函数的语句。LOAD_FAST一般是加载函数的参数。FOR_ITER,是一个循环的语句。STORE_FAST则是对一个函数命名。其余的字节码很好懂(根据你多年的re汇编经验猜猜就可以了),下面我们进入a函数。

Disassembly of a:
  3           0 LOAD_CONST               1 (0)
              2 BUILD_LIST               1
              4 LOAD_GLOBAL              0 (len)
              6 LOAD_FAST                0 (s)
              8 CALL_FUNCTION            1
             10 BINARY_MULTIPLY
             12 STORE_FAST               1 (o)
            #创建一个列表o,初始值为零,*len(s)的长度

  4          14 LOAD_GLOBAL              1 (enumerate)
             16 LOAD_FAST                0 (s)
             18 CALL_FUNCTION            1
             20 GET_ITER
        >>   22 FOR_ITER                24 (to 48)
             24 UNPACK_SEQUENCE          2
             26 STORE_FAST               2 (i)
             28 STORE_FAST               3 (c)

  5          30 LOAD_FAST                3 (c)
             32 LOAD_CONST               2 (2)
             34 BINARY_MULTIPLY
             36 LOAD_CONST               3 (60)
             38 BINARY_SUBTRACT
             40 LOAD_FAST                1 (o)
             42 LOAD_FAST                2 (i)
             44 STORE_SUBSCR
             46 JUMP_ABSOLUTE           22
             #主要加密,循环c*2-60
  6     >>   48 LOAD_FAST                1 (o)
             50 RETURN_VALUE

a函数的逆向:忽略那几个print,纯属python不熟练,想看看中间发生了什么不得了的事。

def a(s):#简单的提取保存                #完全一至
    o=[0]*len(s)
    for i,c in enumerate(s):
        #print(o[i],end="")
        o[i]=c*2-60
        #print(o[i])
    #print(o)
    #len(s) 

b函数:

Disassembly of b:
  9           0 LOAD_GLOBAL              0 (zip)
              2 LOAD_FAST                0 (s)
              4 LOAD_FAST                1 (t)
              6 CALL_FUNCTION            2 #注意看这个地方,这个就是一个系统函数的调用,python字节码的列方式很
              8 GET_ITER                    #像一个栈,先压函数名,压参数,然后call弹栈调用。         
        >>   10 FOR_ITER                22 (to 34)#CALL_FUNCTION    2指的是有两个参数
             12 UNPACK_SEQUENCE          2
             14 STORE_FAST               2 (x)
             16 STORE_FAST               3 (y)

 10          18 LOAD_FAST                2 (x)   #上面的是for循环的初始化,下方函数为主体函数
             20 LOAD_FAST                3 (y)
             22 BINARY_ADD
             24 LOAD_CONST               1 (50)
             26 BINARY_SUBTRACT
             28 YIELD_VALUE
             30 POP_TOP
             32 JUMP_ABSOLUTE           10
        >>   34 LOAD_CONST               0 (None)
             36 RETURN_VALUE
            #主体函数干了一件事,就是x+y—50

b函数的逆向,可能略有不同给,但是大体意思没有问题:

def b(s,t):#                          #完全一至
    for x,y in zip(s,t):
        yield x+y-50

c函数:

Disassembly of c:
 13           0 LOAD_CONST               1 (<code object <listcomp> at 0x7ff31a16f0e0, file "vuln.py", line 13>)
              2 LOAD_CONST               2 ('c.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_FAST                0 (s)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 RETURN_VALUE
             #c函数到这里就已经结束了,但是一开始c函数调用了地址0x7ff31a16f0e0的函数(没有名字,师傅讲可能名字丢了)
Disassembly of <code object <listcomp> at 0x7ff31a16f0e0, file "vuln.py", line 13>:
 13           0 BUILD_LIST               0         
              2 LOAD_FAST                0 (.0)    #这个地方没有列表名称,说明列表被直接返回了
        >>    4 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (c)
              8 LOAD_FAST                1 (c)
             10 LOAD_CONST               0 (5)
             12 BINARY_ADD                         #这个循环其实有一些难想(因为python太烂了)
             14 LIST_APPEND              2         #这其实是一个用for循环创建列表的方式
             16 JUMP_ABSOLUTE            4
        >>   18 RETURN_VALUE
             #这个函数属于c的内部函数,我把它理解成了没有外部构造的匿名函数。

c函数的逆向,可能略有不同给,但是大体意思没有问题:

def c(s):                                #完全一至
     return [c+5 for c in s]

最后我们再看看除了e函数以外的几个没有名字的函数:
地址0x7ff31a16f240的函数

Disassembly of <code object <listcomp> at 0x7ff31a16f240, file "vuln.py", line 16>:
 16           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)
        >>    4 FOR_ITER                12 (to 18)
              6 STORE_FAST               1 (c)
              8 LOAD_GLOBAL              0 (ord)
             10 LOAD_FAST                1 (c)
             12 CALL_FUNCTION            1
             14 LIST_APPEND              2
             16 JUMP_ABSOLUTE            4
        >>   18 RETURN_VALUE
             有一说一,这个地方看见循环和ord,我觉得所有逆向手都能反应过来这个是个循环转码的函数

函数逆向:

def weishi(c):
    return [ord(c) for c in s]

由于没有函数名,与字节码不同的地方可能就只有命名哪里,但是大体上函数作用应该没问题。
地址为0x7ff31a16f2f0函数:有一说一这个函数太容易被忽略了,我第一次做题的时候就漏掉了这个函数,而且这还是一个蛮关键的加密函数。

Disassembly of <code object <listcomp> at 0x7ff31a16f2f0, file "vuln.py", line 17>:
 17           0 BUILD_LIST               0
              2 LOAD_FAST                0 (.0)      #创建一个返回的列表
        >>    4 FOR_ITER                16 (to 22)   #循环
              6 STORE_FAST               1 (c)       
              8 LOAD_FAST                1 (c)
             10 LOAD_CONST               0 (5)       #c+5
             12 BINARY_XOR
             14 LOAD_CONST               1 (30)      #^30
             16 BINARY_SUBTRACT
             18 LIST_APPEND              2
             20 JUMP_ABSOLUTE            4           #循环
        >>   22 RETURN_VALUE

逆向函数:

def weishi2(s):
    return [5^c-30 for c in s]

0x3 主调用函数e

前面的函数零零散散的,但都是加密的函数,而e函数应该是一个整合函数,因为在e函数里面几乎调用了所有函数。所以读懂e函数调用其余函数的方法就是我们的目的。第一次接触有点懵很正常,因为你会感觉这些破玩意是堆在一起的垃圾,根本看不明白,这里我引用子洋师傅给我的小提示-----“栈”。带着这个提示我们开始分析e函数。

Disassembly of e:
 16           0 LOAD_CONST               1 (<code object <listcomp> at 0x7ff31a16f240, file "vuln.py", line 16>)
              2 LOAD_CONST               2 ('e.<locals>.<listcomp>')
              4 MAKE_FUNCTION            0
              6 LOAD_FAST                0 (s)
              8 GET_ITER
             10 CALL_FUNCTION            1
             12 STORE_FAST               0 (s)
              #e函数真正难懂的地方是从17开始的
 17          14 LOAD_CONST               3 (<code object <listcomp> at 0x7ff31a16f2f0, file "vuln.py", line 17>)                                  #加载0x7ff31a16f2f0处代码
             16 LOAD_CONST               2 ('e.<locals>.<listcomp>')
             18 MAKE_FUNCTION            0 #将 0x7ff31a16f2f0处代码封装成一个函数,我们这里为了方便叫他H函数吧。这里将H函数压栈
             20 LOAD_GLOBAL              0 (b) #加载b函数压栈
             22 LOAD_GLOBAL              1 (a)#加载a函数压栈
             24 LOAD_FAST                0 (s)#加载变量s(输入字符串)
             26 CALL_FUNCTION            1    #调用一个参数的函数,此时弹栈s与函数a,所以第一个执行函数为a(s)执行完毕后,将结果作为常量压栈。
             28 LOAD_GLOBAL              2 (c) #加载c压栈
             30 LOAD_FAST                0 (s)#加载s压栈
             32 CALL_FUNCTION            1#调用c(s)
             34 CALL_FUNCTION            2#调用两个参数的函数,此时a(s),c(s)为函数b的参数
             36 GET_ITER
             38 CALL_FUNCTION            1#最后将返回结果作为函数H的参数调用。
             40 STORE_FAST               1 (o)

 18          42 LOAD_GLOBAL              3 (bytes)
             44 LOAD_FAST                1 (o)
             46 CALL_FUNCTION            1 #最后将处理完的字符串,转换为bytes
             48 RETURN_VALUE

e函数逆向:

def e(s):
    weishi(s)
    o=b(a(s),c(s))
    return bytes(o)

到这里我们就将全部文件读完了,代码逻辑也很清楚了,如果不给字节码,直接给python,这就是一道签到题的难度。后面的逆向步骤我就不多说了,有兴趣的自己试试吧。(附上一个我当时做题的脚本直接跑就可以看见flag的那种,就是有点乱)

def f():
    s = input("Guess?")
    o=b'\xae\xc0\xa1\xab\xef\x15\xd8\xca\x18\xc6\xab\x17\x93\xa8\x11\xd7\x18\x15\xd7\x17\xbd\x9a\xc0\xe9\x93\x11\xa7\x04\xa1\x1c\x1c\xed'
    if e(s)==o:
        print('Correct!')
    else:
        print('Wrong...')
def e(s):
    weishi(s)
    o=b(a(s),c(s))
    return bytes(o)
def weishi(c):
    return [ord(c) for c in s]
def weishi2(s):
    return [5^c-30 for c in s]
def a(s):#简单的提取保存                #完全一至
    o=[0]*len(s)
    for i,c in enumerate(s):
        #print(o[i],end="")
        o[i]=c*2-60
        #print(o[i])
    #print(o)
    #len(s)
def text(s,t):#                          #完全一至
    for x,y in zip(s,t):
        yield x+y-50
    #print()
def c(s):                                #完全一至
     return [c+5 for c in s]
import chardet
import dis

import hashlib
print(dis.dis(f))
#a([1,2,3,4,5,6,7,8])
o=b'\xae\xc0\xa1\xab\xef\x15\xd8\xca\x18\xc6\xab\x17\x93\xa8\x11\xd7\x18\x15\xd7\x17\xbd\x9a\xc0\xe9\x93\x11\xa7\x04\xa1\x1c\x1c\xed'
#print(str(o,encoding = "utf-8"))
b = b"example"
A=[]
A1=[]
B=[]
GG=[]
for i in o:
    A.append(i)
    #B.append(i+50)
print(A)
for W in A:
    A1.append(((W+30)^5))
print(A1)
for i in A1:
    B.append(i+50)
#print(B)
#C=[1,2,3,4,5]
#W=[6,7,8,9,10]
#for i in text(C,W):
    #print(i)
for j in B:
    for i in range(0,130):
        if  ((i*2-60)+(i+5))==j:
            GG.append(i)
            print(chr(i),end="")
D=c([1,2,3,4,5,6,7])
#print(D)


0x4 附录

这里给出字节码附件(点可直接看)
还有师傅博客传送门
zip下载字节码附件

发表评论

email
web

全部评论 (暂无评论)

info 还没有任何评论,你来说两句呐!