关于python的逆向主要是针对Python字节码和Python编译后的.pyc文件进行的。在一般情况下拿到一个pyc文件可以直接使用工具uncompyle6将pyc文件直接转换成py文件。

关于uncompyle6的下载可以直接使用pip进行安装:

pip install uncompyle6

使用方法也很简单:

uncompyle6 -o OUTPUT_DIR INPUT_FILE.pyc

下面通过一个实例对基本的pyc反编译的操作进行介绍:

实例一:基本的反汇编操作

tips:不知道csdn怎么上传附件。需要附件的话可以去我自己博客的每个实例标题下的toggle中下载:

NotionNext BLOG | loading这是一个由NotionNext生成的站点icon-default.png?t=N7T8https://www.reveone.cn/article/c69aa456-9190-4b2d-b373-785886e616cc使用命令行输入如下命令:

uncompyle6 -o findkey.py findkey.pyc

如上图键入命令后,ubcompyle6就反编译出了一个py文件,打开这个文件查看一下反编译后的源代码:

可以看到反编译的效果非常好,顺便解出这个题的flag,难度并不大。

lookup = [196, 153, 149, 206, 17, 221, 10, 217, 167, 18, 36, 135, 103, 61, 111, 31, 92, 152, 21, 228, 105, 191, 173, 41, 2, 245, 23, 144, 1, 246, 89, 178, 182, 119, 38, 85, 48, 226, 165, 241, 166, 214, 71, 90, 151, 3, 109, 169, 150, 224, 69, 156, 158, 57, 181, 29, 200, 37, 51, 252, 227, 93, 65, 82, 66, 80, 170, 77, 49, 177, 81, 94, 202, 107, 25, 73, 148, 98, 129, 231, 212, 14, 84, 121, 174, 171, 64, 180, 233, 74, 140, 242, 75, 104, 253, 44, 39, 87, 86, 27, 68, 22, 55, 76, 35, 248, 96, 5, 56, 20, 161, 213, 238, 220, 72, 100, 247, 8, 63, 249, 145, 243, 155, 222, 122, 32, 43, 186, 0, 102, 216, 126, 15, 42, 115, 138, 240, 147, 229, 204, 117, 223, 141, 159, 131, 232, 124, 254, 60, 116, 46, 113, 79, 16, 128, 6, 251, 40, 205, 137, 199, 83, 54, 188, 19, 184, 201, 110, 255, 26, 91, 211, 132, 160, 168, 154, 185, 183, 244, 78, 33, 123, 28, 59, 12, 210, 218, 47, 163, 215, 209, 108, 235, 237, 118, 101, 24, 234, 106, 143, 88, 9, 136, 95, 30, 193, 176, 225, 198, 197, 194, 239, 134, 162, 192, 11, 70, 58, 187, 50, 67, 236, 230, 13, 99, 190, 208, 207, 7, 53, 219, 203, 62, 114, 127, 125, 164, 179, 175, 112, 172, 250, 133, 130, 52, 189, 97, 146, 34, 157, 120, 195, 45, 4, 142, 139]
pwda = [188, 155, 11, 58, 251, 208, 204, 202, 150, 120, 206, 237, 114, 92, 126, 6, 42]
pwdb = [53, 222, 230, 35, 67, 248, 226, 216, 17, 209, 32, 2, 181, 200, 171, 60, 108]
flag=''
for i in range(0,17):
    flag+=chr(lookup[i + pwdb[i]]-pwda[i]&255)
print(flag[::-1])

flag为:PCTF{PyC_Cr4ck3r}

看到这里是不是感觉python逆向就这?别急,下面慢慢上难度。

实例二:被打包成可执行程序的py逆向

为了在没有安装 Python 解释器的计算机上运行 Python 程序,诞生了一些工具将python文件打包成可执行程序。经过打包后的可执行程序如果直接使用ida进行分析的话是非常困难的Python本身是一种高级语言,其生成的二进制代码通常比C或C++更复杂,而且打包的程序包含了 Python 解释器和所有必要的库,这使得反编译的结果更加复杂,因为不仅需要理解python脚本中的原代码,还需要理解 Python 解释器的工作原理。所以碰到打包成exe的python程序首先是考虑提取还原出pyc文件再反汇编成py文件。

关于将Python程序打包成可执行文件的最常用工具通常是 PyInstaller、cx_Freeze、Py2exe。其中最常见的打包工具为PyInstaller。下面简单介绍一下PyInstaller的使用。

1.PyInstaller使用:

1.下载Pyinstaller:

pip install pyinstaller

2.使用pyinstaller打包程序:

基本命令如下:

pyinstaller 选项 Python 源文件
-h,--help查看该模块的帮助信息
-F,-onefile产生单个的可执行文件
-D,--onedir产生一个目录(包含多个文件)作为可执行程序
-a,--ascii不包含 Unicode 字符集支持
-d,--debug产生 debug 版本的可执行文件
-w,--windowed,--noconsolc指定程序运行时不显示命令行窗口(仅对 Windows 有效)
-c,--nowindowed,--console指定使用命令行窗口运行程序(仅对 Windows 有效)
-o DIR,--out=DIR指定 spec 文件的生成目录。如果没有指定,则默认使用当前目录来生成 spec 文件
-p DIR,--path=DIR设置 Python 导入模块的路径(和设置 PYTHONPATH 环境变量的作用相似)。也可使用路径分隔符(Windows 使用分号,Linux 使用冒号)来分隔多个路径
-n NAME,--name=NAME指定项目(产生的 spec)名字。如果省略该选项,那么第一个脚本的主文件名将作为 spec 的名字
-i ICON.ico, -icon=ICON.ico指定生成后程序的图标

介绍完了工具接下来就产生了一个问题:如何判断分析的可执行程序是不是经过打包的python程序?

2.判断可执行程序是否是经过打包的py程序

首先前面有说过经过打包的程序包含了 Python 解释器和所有必要的库,程序的体积必然不小基本都是几兆大小。其二是直接看图标,在打包时如果没有指定打包后exe的图标的话,默认打包后的exe图标长下面这个样子:

如果看到这个图标基本就可以确定这其实是打包后的python程序,当然如果指定了生成后的程序图标这个办法就白瞎了。

最后还可以将exe丢到ida中然后shift+f12查看程序的字符串,到包的程序会有很多python相关的字符串。

根据这三点基本就可以判断出是否是打包的python程序了。

下面还是通过实例来讲解打包后的exe如何逆向。

3.相关逆向办法:

看到实例的图标就可以确定这是由PyInstaller打包的python程序了,为了验证这个说法可以拖进ida查看其字符串可以看到确实有很多python相关的字符串:

下面介绍一下如何从exe中提取出pyc文件,这里需要用到的工具为pyinstxtractor,github链接为:

GitHub - extremecoders-re/pyinstxtractor: PyInstaller Extractoricon-default.png?t=N7T8https://github.com/extremecoders-re/pyinstxtractor/tree/master

将其中的pyinstxtractor.py下载下来,键入命令:

python pyinstxtractor.py login.exe

运行后显示提取成功,cmd内容如下:

D:\\Desktop\\keshan\\tmp>python pyinstxtractor.py login.exe
[+] Processing login.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 2.7
[+] Length of package: 3701450 bytes
[+] Found 18 files in CArchive
[+] Beginning extraction...please standby
[+] Possible entry point: pyiboot01_bootstrap.pyc
[+] Possible entry point: login.pyc
[!] Warning: This script is running in a different Python version than the one used to build the executable.
[!] Please run this script in Python 2.7 to prevent extraction errors during unmarshalling
[!] Skipping pyz extraction
[+] Successfully extracted pyinstaller archive: login.exe

You can now use a python decompiler on the pyc files within the extracted directory

上面的回显显示提取出来的东西放在了文件名夹加exetracted的目录下

进入login.exe_extracted文件夹

在这个文件夹中可以看到其中一个pyc文件就是提取出来的login.pyc,得到pyc文件后继续使用uncompyle6反编译成py文件:

D:\\Desktop\\keshan\\tmp\\login.exe_extracted>uncompyle6 -o login.py ./login.pyc
.\\login.pyc --
# Successfully decompiled file

打开反汇编后的代码:

可以看到到这里代码就成功反编译出来了,这里顺便贴一下exp:

import base64
rflag=base64.b64decode('YWtmYHxgaGhjWHRzcmN+eg==')
flag=''
for i in range(len(rflag)):
    flag+=chr(rflag[i]^7)
print(flag)

OK,又搞定一个下面继续加大难度。

实例三:

在前面的两个实例中我们都是使用工具将pyc直接反汇编成py,但设想一下有没有什么办法可以让uncompyle6在pyc→pc的过程中反编译失败呢?

在这个实例中就会出现这个问题,使用uncompyle6先尝试对其进行反编译:

D:\\Desktop\\keshan\\tmp>uncompyle6 -o BabyMaze.py BabyMaze.pyc

-- Stacks of completed symbols:
START ::= |- stmts .
_come_froms ::= \\e__come_froms . COME_FROM
_come_froms ::= \\e__come_froms . COME_FROM_LOOP
while1stmt ::= \\e__come_froms . l_stmts COME_FROM JUMP_BACK COME_FROM_LOOP
															.
															.  //省略
															.
whilestmt38 ::= \\e__come_froms . testexpr returns POP_BLOCK

# file BabyMaze.pyc
# --- This code section failed: ---

 L.   1         0  JUMP_ABSOLUTE         4  'to 4'
                2  JUMP_ABSOLUTE         6  'to 6'
                4  JUMP_BACK             2  'to 2'
              6_0  COLLECTION_START      0  'CONST_LIST'
                6  ADD_VALUE             1  1
                8  ADD_VALUE             1  1
               10  ADD_VALUE             1  1
               12  ADD_VALUE             1  1
															.
															.  //省略
															.
L.  33      2014  COMPARE_OP               ==
         2016_2018  POP_JUMP_IF_FALSE  2020  'to 2020'
           2020_0  COME_FROM          2016  '2016'
             2020  LOAD_NAME                main
             2022  CALL_FUNCTION_0       0  ''
             2024  POP_TOP

Parse error at or near `None' instruction at offset -1

BabyMaze.pyc --
# decompile failed
报错信息的部分截图

​看到命令行的回显最后一行显示反编译失败,然后上面报出了一大堆数据。根据现有的手段到了这里基本就是束手无策了。所以我们需要get一些新知识。

1.dis模块介绍:

在原理篇中有介绍到:Python 代码先被编译为字节码后,再由Python虚拟机来执行字节码, Python的字节码是一种类似汇编指令的中间语言, 一个Python语句会对应若干字节码指令,虚拟机一条一条执行字节码指令, 从而完成程序执行。

而dis模块可以帮助我们查看Python代码的字节码,它是python中内置的一个模块。下面看一下关于这个模块的简单示例:

import dis

def add(a,b):
    return a+b

dis.dis(add)

上面这段代码首先是导入了dis模块,然后随便定义了一个函数,最后的输出dis.dis(函数名)表示输出对应函数的字节码,然后看一下输出:

这里我们可以手动将输出分成三列来看:

  • 第一列表示当前字节码在源代码中的行号为第四行
  • 第二列是字节码的偏移量和对应的字节码,0表示当前字节码,LOAD_FAST是 Python 虚拟机要执行的操作,(LOAD_FAST表示将一个局部变量加载到栈顶)
  • 第三列是字节码指令的参数。这是字节码指令的操作数。第一个 LOAD_FAST 指令的参数是 0,表示它将第 0 个局部变量(即 a)加载到栈顶。

接着我们换个视角加深对字节码的理解,在原来的代码上加一行代码:

import dis

def add(a,b):
    return a+b

dis.dis(add)
print(list(add.__code__.co_code))     

最后这段代码将打印出 add 函数的字节码指令的二进制表示形式的整数列表。每个整数对应一个字节。查看一下输出情况:

下面这个列表的[124,0]就是上面第一行的0 LOAD_FAST 0 (a),所以124也就是字节码指令(124对应的字节码为LOAD_FAST),该字节码指令所在的列表偏移为0。第二个元素0就是字节码指令的参数。上一篇写的原理分析有讲过PyCodeObject对象中的co_varnames保存了在当前作用域的变量,以字符串的形式保存了变量名,而现在看到的这个0其实就表示co_varnames下标0处的变量。这样就相对好理解字节码相关的知识点了。最后还有一个问题就是如何得知字节码指令对应的数字是多少?

python源码中的opcode.h定义了 Python 的字节码指令集,可以去官网直接查看定义:

  • tips:版本之间关于字节码的定义会有不同,我这里给的是v3.8.10版本的定义

    #define POP_TOP                   1
    #define ROT_TWO                   2
    #define ROT_THREE                 3
    #define DUP_TOP                   4
    #define DUP_TOP_TWO               5
    #define ROT_FOUR                  6
    #define NOP                       9
    #define UNARY_POSITIVE           10
    #define UNARY_NEGATIVE           11
    #define UNARY_NOT                12
    #define UNARY_INVERT             15
    #define BINARY_MATRIX_MULTIPLY   16
    #define INPLACE_MATRIX_MULTIPLY  17
    #define BINARY_POWER             19
    #define BINARY_MULTIPLY          20
    #define BINARY_MODULO            22
    #define BINARY_ADD               23
    #define BINARY_SUBTRACT          24
    #define BINARY_SUBSCR            25
    #define BINARY_FLOOR_DIVIDE      26
    #define BINARY_TRUE_DIVIDE       27
    #define INPLACE_FLOOR_DIVIDE     28
    #define INPLACE_TRUE_DIVIDE      29
    #define GET_AITER                50
    #define GET_ANEXT                51
    #define BEFORE_ASYNC_WITH        52
    #define BEGIN_FINALLY            53
    #define END_ASYNC_FOR            54
    #define INPLACE_ADD              55
    #define INPLACE_SUBTRACT         56
    #define INPLACE_MULTIPLY         57
    #define INPLACE_MODULO           59
    #define STORE_SUBSCR             60
    #define DELETE_SUBSCR            61
    #define BINARY_LSHIFT            62
    #define BINARY_RSHIFT            63
    #define BINARY_AND               64
    #define BINARY_XOR               65
    #define BINARY_OR                66
    #define INPLACE_POWER            67
    #define GET_ITER                 68
    #define GET_YIELD_FROM_ITER      69
    #define PRINT_EXPR               70
    #define LOAD_BUILD_CLASS         71
    #define YIELD_FROM               72
    #define GET_AWAITABLE            73
    #define INPLACE_LSHIFT           75
    #define INPLACE_RSHIFT           76
    #define INPLACE_AND              77
    #define INPLACE_XOR              78
    #define INPLACE_OR               79
    #define WITH_CLEANUP_START       81
    #define WITH_CLEANUP_FINISH      82
    #define RETURN_VALUE             83
    #define IMPORT_STAR              84
    #define SETUP_ANNOTATIONS        85
    #define YIELD_VALUE              86
    #define POP_BLOCK                87
    #define END_FINALLY              88
    #define POP_EXCEPT               89
    #define HAVE_ARGUMENT            90
    #define STORE_NAME               90
    #define DELETE_NAME              91
    #define UNPACK_SEQUENCE          92
    #define FOR_ITER                 93
    #define UNPACK_EX                94
    #define STORE_ATTR               95
    #define DELETE_ATTR              96
    #define STORE_GLOBAL             97
    #define DELETE_GLOBAL            98
    #define LOAD_CONST              100
    #define LOAD_NAME               101
    #define BUILD_TUPLE             102
    #define BUILD_LIST              103
    #define BUILD_SET               104
    #define BUILD_MAP               105
    #define LOAD_ATTR               106
    #define COMPARE_OP              107
    #define IMPORT_NAME             108
    #define IMPORT_FROM             109
    #define JUMP_FORWARD            110
    #define JUMP_IF_FALSE_OR_POP    111
    #define JUMP_IF_TRUE_OR_POP     112
    #define JUMP_ABSOLUTE           113
    #define POP_JUMP_IF_FALSE       114
    #define POP_JUMP_IF_TRUE        115
    #define LOAD_GLOBAL             116
    #define SETUP_FINALLY           122
    #define LOAD_FAST               124
    #define STORE_FAST              125
    #define DELETE_FAST             126
    #define RAISE_VARARGS           130
    #define CALL_FUNCTION           131
    #define MAKE_FUNCTION           132
    #define BUILD_SLICE             133
    #define LOAD_CLOSURE            135
    #define LOAD_DEREF              136
    #define STORE_DEREF             137
    #define DELETE_DEREF            138
    #define CALL_FUNCTION_KW        141
    #define CALL_FUNCTION_EX        142
    #define SETUP_WITH              143
    #define EXTENDED_ARG            144
    #define LIST_APPEND             145
    #define SET_ADD                 146
    #define MAP_ADD                 147
    #define LOAD_CLASSDEREF         148
    #define BUILD_LIST_UNPACK       149
    #define BUILD_MAP_UNPACK        150
    #define BUILD_MAP_UNPACK_WITH_CALL 151
    #define BUILD_TUPLE_UNPACK      152
    #define BUILD_SET_UNPACK        153
    #define SETUP_ASYNC_WITH        154
    #define FORMAT_VALUE            155
    #define BUILD_CONST_KEY_MAP     156
    #define BUILD_STRING            157
    #define BUILD_TUPLE_UNPACK_WITH_CALL 158
    #define LOAD_METHOD             160
    #define CALL_METHOD             161
    #define CALL_FINALLY            162
    #define POP_FINALLY             163
    

了解完dis模块,我们就可以发现前面在反编译实例时出现的报错好像有点眼熟,这些返回的信息不就是字节码嘛:

报错信息的部分截图

到这里起码不是完全云里雾里了,接下来介绍一下python的花指令。

2.python的花指令:

花指令指的是插入到Python字节码中的额外、无意义或误导性的指令,用于干扰或误导反编译工具和分析者。还是用前面第例子:

def add(x, y):
    return x + y

前面有说到使用dis模块查看这个函数的字节码为:

4             0 LOAD_FAST                0 (x)
              2 LOAD_FAST                1 (y)
              4 BINARY_ADD
              6 RETURN_VALUE

根据上面这些字节码我们尝试插入一些花指令,这里可以考虑插入一些无效的跳转和无意义的操作。在Python字节码中,一个常见的花指令是使用**POP_TOP来移除栈顶项(这不会影响函数的实际行为)和JUMP_FORWARD**来进行无效的跳转。

4             0 LOAD_FAST                0 (x)
              2 LOAD_FAST                1 (y)
              4 JUMP_FORWARD             2 (to 8)
              6 POP_TOP
        >>    8 BINARY_ADD
             10 RETURN_VALUE

在这里,**JUMP_FORWARD指令实际上跳过了POP_TOP**指令,这使得实际上没有执行。但是,当尝试反编译这段字节码时,这些额外的指令可能会导致反编译工具产生更为复杂或难以理解的代码。

补充知识:

POP_TOP:

  • 功能:从堆栈顶部移除一个项并丢弃

举个例子,考虑以下Python代码:

del x

对应的字节码大致如下:

0 LOAD_NAME            0 (x)
2 POP_TOP

JUMP_FORWARD:

  • 功能:向前跳过指定数量的字节码。

举个例子,考虑以下Python代码:

if x == 0:
    pass
print("Hello")

对应的字节码大致如下:

scssCopy code
0 LOAD_NAME              0 (x)
2 LOAD_CONST             0 (0)
4 COMPARE_OP             2 (==)
6 POP_JUMP_IF_FALSE      12
8 JUMP_FORWARD           2 (to 12)
10 LOAD_CONST            1 ('Hello')
12 PRINT_ITEM
14 PRINT_NEWLINE

在上面的例子中,POP_JUMP_IF_FALSE是根据x == 0的结果进行跳转的另一个指令。如果条件为False,它会跳到标签12。JUMP_FORWARD指令确保,如果条件为True,解释器会跳过接下来的指令并直接转到标签12。

接着再看下一个花指令的例子:

def example(x):
    if x > 10:
        return True
    return False

正常的字节码如下:

  2           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (10)
              4 COMPARE_OP               4 (>)
              6 POP_JUMP_IF_FALSE       12
  3           8 LOAD_CONST               2 (True)
             10 RETURN_VALUE
  4     >>   12 LOAD_CONST               3 (False)
             14 RETURN_VALUE

现在,我们插入一些花指令来干扰反编译:

假设我们在**COMPARE_OP后面插入一些额外的指令,包括一个无效的JUMP_FORWARD和一个不会被执行的LOAD_CONST**,如下:

  2           0 LOAD_FAST                0 (x)
              2 LOAD_CONST               1 (10)
              4 COMPARE_OP               4 (>)
              6 JUMP_FORWARD             4 (to 10)
              8 LOAD_CONST               4 (42)   # 花指令,不会被执行
             10 POP_JUMP_IF_FALSE       16
  3          12 LOAD_CONST               2 (True)
             14 RETURN_VALUE
  4     >>   16 LOAD_CONST               3 (False)
             18 RETURN_VALUE

在这里,我们插入了一个**JUMP_FORWARD指令来跳过一个LOAD_CONST指令,该指令尝试加载一个常数值42**(这只是一个随便给的一个数)。

因为这个**LOAD_CONST指令实际上被JUMP_FORWARD跳过了,所以它从未被执行。然而,对于某些反编译工具来说,这可能会造成困惑,因为它们可能会期望每个LOAD_CONST后面都有一个相关的操作(例如,一个STORE_FASTBINARY_ADD)。当工具看到这个“悬挂”的LOAD_CONST**时,它可能不知道如何正确地处理,于是就会返回报错。

3.实例三反编译报错原因解决:

了解了这些知识点之后再头看实例三的报错:

第一条到第三条很明显就是花指令:

  1. 0 JUMP_ABSOLUTE 4 'to 4' - 这条指令跳转到标号 4 的位置。
  2. 2 JUMP_ABSOLUTE 6 'to 6' - 如果上述跳转不执行,这条指令将跳转到标号 6 的位置。
  3. 4 JUMP_BACK 2 'to 2' - 这条指令跳回标号 2 的位置。

结合以上三条指令,代码会在 2 和 4 之间无限循环后面的代码就永远都不会被执行到。接下里就是将这三条指令删除掉,具体步骤如下:

上面的指令码中有贴到#define JUMP_ABSOLUTE 113 113转换为十六进制为0x71,也就是说这三条指令转换为二进制就是71 04 71 06 71 02 ,将实例文件拖到010editor中。直接搜索二进制数据:

然后将这段数据直接删掉。删完之后还没完,co_code中有一个**ob_size** 成员里面保存了co_code的长度,如果co_code的实际长度与ob_size里记录的长度不匹配的话反编译时依然会报错。接下来就是找到ob_size所在的位置将其进行修改,在python3.8版本里ob_size会以**s** 或 t 的类型标志开始接下来的几个字节会是一个整数,代表co_code的长度。

如上图在这个实例中ob_size的标志为s,后面的EE 07就是代码长度,还有不要忘记这是小端存储,所以最终的代码长度为7EE。除了这个办法还可以利用marshal模块输出co_code的长度。

marshal 模块提供了读写 Python 的内部值到字节流的能力。该模块主要用于支持 .pyc 文件的读写。marshal常用的方法为:

  • marshal.dumps(value):

    将值序列化为一个字节字符串。

    data = marshal.dumps([1, 2, 3, 4])
    
  • marshal.load(file):

    从一个已打开的文件对象中读取一个值。

    with open('data.mar', 'rb') as f:
        mylist = marshal.load(f)
    print(mylist)  # Output: [1, 2, 3, 4]
    

我们可以利用marshal模块编写一个简单的脚本来输出实例pyc的co_code的长度:

import marshal

f = open('BabyMaze.pyc','rb')
f.read(16)
code = marshal.load(f)
print(len(code.co_code))

简单解析一下这个脚本首先是导入marshal模块,然后加载目标实例pyc到f中,read函数实际作用是跳过前16个字节因为marshal读取的是字节码,所以要跳过前面的魔数等。之后使用marshal的load函数读取实例pyc的co_code,再对其长度以十六进制的形式输出。

tips:为什么跳过16字节:

上一篇原理部分有讲过从python3.7版本后魔数部分增长到了16个字节,我们又是如何知道这个pyc文件的版本的呢,其实很简单可以利用010editor查看pyc的版本号:

版本号为3413,还是去看上一篇里面有每个版本与数字的对应关系就可以了,3413对应的版本为*Python 3.8b4。*因此知道了我们需要跳过16个字节的魔数定义。

运行脚本后输出的长度为:7EE

知道了长度注意改成小端存储形式,还是再010editor直接搜:

这样也能找到co_code的长度,删完了三条花指令(6个字节),将原长度修改为7ee-6=7e8,将修改保存后再次尝试使用uncompyle6反编译,就可以看到反汇编已经成功了。

D:\\Desktop\\keshan\\tmp>uncompyle6 -o BabyMaze.py BabyMaze.pyc
BabyMaze.pyc --
# Successfully decompiled file

这个题目是一个谜宫题,关于迷宫的题目还要介绍BFS和DFS,需要介绍的东西不少,所以包括这个题的解法以及我遇到的迷宫题从易到难将会单独开一篇文章来讲。以上就是这篇文章的全部内容

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐