请注意,本文编写于 1419 天前,最后修改于 1419 天前,其中某些信息可能已经过时。
开门见山的说,最近博客拖更严重,但是这并不代表我啥也没干哈!这周除了在研究子洋师傅XCTF的题解外还参加了一个外国的高大上比赛。做了几道题,但是没有啥好记录的地方,主要就是一些简单的java逆向。甚至都不需要任何动态调试,只需要静态理解代码逆推即可,所以在这里就不过多的赘述了。第五道题目是一道python字节码的题目,这已经不是我们第一次见到python字节码了,之前在虎符ctf中,re第一道题目就是一道python字节码的题目,当时子洋师傅就已经在比赛上把题目做出来了,并且写了很详细的题解,赛后我也是学习这个题解的。这次这个字节码也是师傅先完成的,然后我才在师傅的提示下摸出来的。由于师傅已经写过一篇python字节码的wp了,应该不会在复现这道题目了,所以这个wp就交给我代劳吧。这道python字节码的题目比虎符的要简单(毕竟这个比赛的年龄段是6-12岁),没有见过这类题目的小伙伴可以先学习这道题目,在去子洋师傅那里把虎符的那道题目搞定。要是想要挑战一下的话,可以直接去子洋师傅找python字节码的题目。这里给出师傅博客的传送门!==>>哈,召唤python字节码传送门。
这类题目其实很好识别,一般就是一个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函数开始分析。配合注释理解一下。
在分析代码之前首先讲几个基本的东西,我这里简单讲讲,要是觉得不够详细的话,可以在这里深度学习一下。
首先就是:
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]
前面的函数零零散散的,但都是加密的函数,而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)
这里给出字节码附件(点可直接看)
还有师傅博客传送门
zip下载字节码附件
全部评论 (暂无评论)
info 还没有任何评论,你来说两句呐!