继续开启全栈梦想之逆向之旅~
这题是攻防世界逆向高手题的handcrafted-pyc
.
.
下载附件,照例扔入exeinfope中查看信息:
.
.
(这里积累第一个经验)
额,这里写着py脚本文件,但是因为我下载的附件没有后缀名,以为是编译好的pyc
文件,于是用https://tool.lu/pyc/
在线反编译来反编译文件。编译的结果一言难尽,我以为是出题人和 https://tool.lu/pyc/
串通好了,结果发现是我错了:
.
.
正常流程应该是打开文件,发现python
代码:
#!/usr/bin/env python # -*- coding: utf-8 -*- import marshal, zlib, base64 exec(marshal.loads(zlib.decompress(base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ=='))))
.
.
(这里积累第二个经验)
代码量不多,开始查函数作用:
base64.b64decode()
函数的作用是将Base64加密的字符串解码;
zlib.decompress()
函数的作用是将压缩的字符串解压缩;
marshal.loads(bytes)
函数的作用是将pyc字节码反序列化为Python模块对象;
exec()
执行Python代码。
.
.
这里就涉及了虚拟机的概念了,回顾以前博客https://blog.csdn.net/xiao__1bai/article/details/120045624
(secret-string-400),虚拟机就是对字节码的操作以转换为代码。
.
.
所以这里Python反编译的正常做法应该是考虑将字节码
用工具反编译成代码
,然后逆向
算法。
所以我们要在marshal.loads(bytes)
之前把字节码打印出来到pyc
文件中去:
import zlib, base64 f=open('1.pyc','wb') pyc=zlib.decompress(base64.b64decode('eJyNVktv00AQXm/eL0igiaFA01IO4cIVCUGFBBJwqRAckLhEIQmtRfPwI0QIeio/hRO/hJ/CiStH2M/prj07diGRP43Hs9+MZ2fWMxbnP6mux+oK9xVMHPFViLdCTB0xkeKDFEFfTIU4E8KZq8dCvB4UlN3hGEsdddXU9QTLv1eFiGKGM4cKUgsFCNLFH7dFrS9poayFYmIZm1b0gyqxMOwJaU3r6xs9sW1ooakXuRv+un7Q0sIlLVzOCZq/XtsK2oTSYaZlStogXi1HV0iazoN2CV2HZeXqRQ54TlJRb7FUlKyUatISsdzo+P7UU1Gb1POdMruckepGwk9tIXQTftz2yBaT5JQovWvpSa6poJPuqgao+b9l5Aj/R+mLQIP4f6Q8Vb3g/5TB/TJxWGdZr9EQrmn99fwKtTvAZGU7wzS7GNpZpDm2JgCrr8wrmPoo54UqGampFIeS9ojXjc4E2yI06bq/4DRoUAc0nVnng4k6p7Ks0+j/S8z9V+NZ5dhmrJUM/y7JTJeRtnJ2TSYJvsFq3CQt/vnfqmQXt5KlpuRcIvDAmhnn2E0t9BJ3SvB/SfLWhuOWNiNVZ+h28g4wlwUp00w95si43rZ3r6+fUIEdgOZbQAsyFRRvBR6dla8KCzRdslar7WS+a5HFb39peIAmG7uZTHVm17Czxju4m6bayz8e7J40DzqM0jr0bmv9PmPvk6y5z57HU8wdTDHeiUJvBMAM4+0CpoAZ4BPgJeAYEAHmgAUgAHiAj4AVAGORtwd4AVgC3gEmgBBwCPgMWANOAQ8AbwBHgHuAp4D3gLuARwoGmNUizF/j4yDC5BWM1kNvvlxFA8xikRrBxHIUhutFMBlgQoshhPphGAXe/OggKqqb2cibxwuEXjUcQjccxi5eFRL1fDSbKrUhy2CMb2aLyepkegDWsBwPlrVC0/kLHmeCBQ==')) f.write(pyc) f.close()
.
.
生成的1.pyc
扔入工具EasyPythonDecompiler
中报错,说是魔术值不匹配:
.
.
(这里积累第三个经验)
查了查python2
的pyc
文件头部:
前4个字节:03f3 0d0a,表示python版本
5-8个字节:0e6b 905d,表示pyc文件修改时间
Python3的magic文件头则是12字节
.
.
所以找一个正常python2
生成的pyc
文件复制前8个字节
过去即可:
.
.
继续扔入工具EasyPythonDecompiler
中,虽然报错,但还是反编译出来了:(用uncompyle6
反编译结果也是一样的)
.
.
是一个一千多行的汇编类型的字节码,查了好些资料,学习了https://www.cnblogs.com/blili/p/11799398.html
的python逆向,也整理出了自己的博客笔记https://blog.csdn.net/xiao__1bai/article/details/120598841
后才大致看懂了。
.
.
这里顺便摘录博客https://blog.csdn.net/Onlyone_1314/article/details/120151769
的内容方便理解:
(这里积累第四个经验)
Python字节码结构如下:
源码行号 | 跳转注释符 | 指令在函数中的偏移 | 指令符号(助记符) | 指令参数 | 实际参数值
.
字节码操作的详细信息:
starts_line(源码行号):以此操作码开头的行(如果有),否则 None
is_jump_target(跳转注释符):True 如果其他代码跳转到这里,否则 False
Offset(指令在函数中的偏移):字节码序列中操作的起始索引
opcode:操作的数字代码,对应于下面列出的操作码值和操作码集合中的字节码值。
opname(指令符号(助记符)):人类可读的操作名称
arg(指令参数):操作的数字参数(如果有),否则 None
argval:解析的 arg 值(如果已知),否则与 arg 相同
Argrepr(实际参数值):操作参数的人类可读描述
.
例如:
1 0 LOAD_GLOBAL 0 ‘chr’
该字节码指令在源码中对应1行
此处不是跳转
0该字节指令的字节码偏移
操作指令对应的助记符为LOAD_GLOBAL
指令参数为0
操作参数对应的实际值为’chr’
.
LOAD_GLOBA:将全局变量co_names[namei]加载到堆栈上。这里是第0个变量
LOAD_FAST(var_num):将对本地co_varnames[var_num]的引用推入堆栈。一般加载局部变量的值,也就是读取值,用于计算或者函数调用传参等。
STORE_FAST(var_num):将TOS存储到本地co_varnames[var_num]中。一般用于保存值到局部变量。
LOAD_CONST:推入一个实整数值到计算栈的顶部。,比如数值、字符串等等,一般用于传给函数的参数。这里是108。
.
ROT_TWO:交换最顶部的两个堆栈项。
BINARY_ADD:二元运算从堆栈中删除堆栈顶部 (TOS) 和第二个最顶部堆栈项 (TOS1)。他们执行操作,并将结果放回堆栈中,实施.TOS = TOS1 + TOS。这里是两个字符的相加,而不是ASCII码数字的相加。
.
.
那么这里的字节码我们应该这样理解:
.
.
所以这里前面一小部分的转换应该是这样的:
以字符形式加载常量108(l)
、108(l)
、97(a)
、67(C)
,这个Python虚拟机入栈之后参数的顺序就是栈顶是‘l’、第二个值是‘l’、第三个值是‘a’、第四个值是‘C’,之后通过ROT_TWO
和BINARY_ADD
重新排列字符串:
36 ROT_TWO #交换栈顶的值‘l’和第二个值‘l’,变成‘l’和‘l’ 37 BINARY_ADD #‘l’和‘l’字符相加,变成‘ll’存储在栈顶 38 ROT_TWO #交换栈顶的值‘ll’和第二个值‘a’,变成‘a’和‘ll’ 39 BINARY_ADD #‘a’和‘ll’字符相加,变成‘all’存储在栈顶 40 ROT_TWO #交换栈顶的值‘all’和第二个值‘C’,变成‘C’和‘all’ 41 BINARY_ADD #‘C’和‘all’字符相加,变成‘Call’存储在栈顶
.
.
因为有1000多行代码,所以必须用脚本批量执行才行,获取字符ASCII
值和ROT_TWO
、BINARY_ADD
的操作位置,仿照逻辑输出即可:
(这里积累第五个经验)
这里在别人脚本学到的一个厉害的思路就是line = line.split()[-1] # 按空格切分取最后一个字符,就省去了复杂的正则表达式了
def BINARY_ADD(list1): # 模拟BINARY_ADD相加操作 top1=list1.pop() top2=list1.pop() list1.append(top2+top1) def ROT_TWO(list1): # 模拟ROT_TWO交换操作 top1=list1.pop() top2=list1.pop() list1.append(top1) list1.append(top2) with open('3.py','r') as fd: # 将代码中的内容读取进来 lines=fd.readlines() list1=[] for line in lines: # 遍历每一行 if "LOAD_CONST" in line: # 包含LOAD_CONST line = line.split()[-1] # 按空格切分取最后一个字符,就省去了复杂的正则表达式了 if line.isdigit(): # 如果是数字的话直接添加该字符 list1.append(chr(int(line))) else: list1.append(0) # 如果不是数字的话,栈中添0。代码333行中出现了None,即添加空字符常量 else: if "BINARY_ADD" in line: # 如果包含BINARY_ADD,就执行相加操作 BINARY_ADD(list1) elif "ROT_TWO" in line: # 如果包含ROT_TWO,就执行交换操作 ROT_TWO(list1) print(list1)
.
.
结果:
.
.
.
总结:
1:
(这里积累第一个经验)
额,这里写着py脚本文件,但是因为我下载的附件没有后缀名,以为是编译好的pyc
文件,于是用https://tool.lu/pyc/
在线反编译来反编译文件。编译的结果一言难尽,我以为是出题人和
https://tool.lu/pyc/
串通好了,结果发现是我错了:
2:
(这里积累第二个经验)
base64.b64decode()
函数的作用是将Base64加密的字符串解码;
zlib.decompress()
函数的作用是将压缩的字符串解压缩;
marshal.loads(bytes)
函数的作用是将pyc字节码反序列化为Python模块对象;
exec()
执行Python代码。.
.
这里就涉及了虚拟机的概念了,回顾以前博客https://blog.csdn.net/xiao__1bai/article/details/120045624
(secret-string-400),虚拟机就是对字节码的操作以转换为代码。
.
.
所以这里Python反编译的正常做法应该是考虑将字节码
用工具反编译成代码
,然后逆向
算法。所以我们要在
marshal.loads(bytes)
之前把字节码打印出来到pyc
文件中去:
3:
(这里积累第三个经验)
查了查python2
的pyc
文件头部:
前4个字节:03f3 0d0a,表示python版本
5-8个字节:0e6b 905d,表示pyc文件修改时间
Python3的magic文件头则是12字节
.
所以找一个正常python2
生成的pyc
文件复制前8个字节
过去即可。
4:
(这里积累第四个经验)Python字节码结构如下: 源码行号 | 跳转注释符 | 指令在函数中的偏移 | 指令符号(助记符) | 指令参数 | 实际参数值 . 字节码操作的详细信息: starts_line(源码行号):以此操作码开头的行(如果有),否则 None
is_jump_target(跳转注释符):True 如果其他代码跳转到这里,否则 False
Offset(指令在函数中的偏移):字节码序列中操作的起始索引
opcode:操作的数字代码,对应于下面列出的操作码值和操作码集合中的字节码值。 opname(指令符号(助记符)):人类可读的操作名称
arg(指令参数):操作的数字参数(如果有),否则 None argval:解析的 arg 值(如果已知),否则与 arg 相同
Argrepr(实际参数值):操作参数的人类可读描述 . 例如: 1 0 LOAD_GLOBAL 0 ‘chr’
该字节码指令在源码中对应1行 此处不是跳转 0该字节指令的字节码偏移 操作指令对应的助记符为LOAD_GLOBAL 指令参数为0
操作参数对应的实际值为’chr’ . LOAD_GLOBA:将全局变量co_names[namei]加载到堆栈上。这里是第0个变量
LOAD_FAST(var_num):将对本地co_varnames[var_num]的引用推入堆栈。一般加载局部变量的值,也就是读取值,用于计算或者函数调用传参等。 STORE_FAST(var_num):将TOS存储到本地co_varnames[var_num]中。一般用于保存值到局部变量。
LOAD_CONST:推入一个实整数值到计算栈的顶部。,比如数值、字符串等等,一般用于传给函数的参数。这里是108。 .
ROT_TWO:交换最顶部的两个堆栈项。 BINARY_ADD:二元运算从堆栈中删除堆栈顶部 (TOS) 和第二个最顶部堆栈项
(TOS1)。他们执行操作,并将结果放回堆栈中,实施.TOS = TOS1 + TOS。这里是两个字符的相加,而不是ASCII码数字的相加。
.
.
那么这里的字节码我们应该这样理解:
.
.
所以这里前面一小部分的转换应该是这样的:
以字符形式加载常量108(l)
、108(l)
、97(a)
、67(C)
,这个Python虚拟机入栈之后参数的顺序就是栈顶是‘l’、第二个值是‘l’、第三个值是‘a’、第四个值是‘C’,之后通过ROT_TWO
和BINARY_ADD
重新排列字符串:
36 ROT_TWO #交换栈顶的值‘l’和第二个值‘l’,变成‘l’和‘l’ 37 BINARY_ADD #‘l’和‘l’字符相加,变成‘ll’存储在栈顶 38 ROT_TWO #交换栈顶的值‘ll’和第二个值‘a’,变成‘a’和‘ll’ 39 BINARY_ADD #‘a’和‘ll’字符相加,变成‘all’存储在栈顶 40 ROT_TWO #交换栈顶的值‘all’和第二个值‘C’,变成‘C’和‘all’ 41 BINARY_ADD #‘C’和‘all’字符相加,变成‘Call’存储在栈顶
5:
(这里积累第五个经验)
这里在别人脚本学到的一个厉害的思路就是line = line.split()[-1] # 按空格切分取最后一个字符,就省去了复杂的正则表达式了
。
解毕!敬礼!