其他分享
首页 > 其他分享> > 攻防世界逆向高手题之handcrafted-pyc

攻防世界逆向高手题之handcrafted-pyc

作者:互联网

攻防世界逆向高手题之handcrafted-pyc

继续开启全栈梦想之逆向之旅~
这题是攻防世界逆向高手题的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中报错,说是魔术值不匹配:
在这里插入图片描述
.
.
(这里积累第三个经验)
查了查python2pyc文件头部:
在这里插入图片描述
前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_TWOBINARY_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_TWOBINARY_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:
(这里积累第三个经验)
查了查python2pyc文件头部:
在这里插入图片描述
前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_TWOBINARY_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] # 按空格切分取最后一个字符,就省去了复杂的正则表达式了

解毕!敬礼!

标签:攻防,字节,BINARY,TWO,ADD,pyc,ROT,handcrafted
来源: https://blog.csdn.net/xiao__1bai/article/details/120568154