Dropbox客户端逆向工程

TL; DR。 本文讨论了Dropbox客户端的反向开发,用Python破解了客户端的混淆和反编译机制,以及更改了程序以激活正常模式下隐藏的调试功能。 如果您只对适当的代码和说明感兴趣,请滚动到末尾。 在撰写本文时,该代码与基于CPython 3.6解释器的最新Dropbox版本兼容。

引言


Dropbox立即使我着迷。 这个概念看似仍然简单。 这是文件夹。 将文件放在那里。 已同步。 转到另一台设备。 再次同步。 文件夹和文件现在也在那里出现了!

隐藏的后台工作量真是惊人。 首先,在为主要桌面操作系统(OS X,Linux,Windows)创建和维护跨平台应用程序时必须解决的所有问题都不会消失。 除此之外,还支持各种Web浏览器,各种移动操作系统。 我们只是在谈论客户端。 我对Dropbox后端也很感兴趣,Dropbox后端使我得以实现这种可扩展性和低延迟,而十亿用户的工作量却如此之大。

出于这些原因,我一直喜欢观察Dropbox的功能以及这些年来的发展。 大约八年前,当我注意到在旅馆中广播未知流量时,我首先尝试找出Dropbox客户端的实际工作方式。 调查表明,这是Dropbox功能LanSync的一部分,该功能称为LanSync,如果同一LAN上的Dropbox主机可以访问相同的文件,则可以使您更快地进行同步。 但是,该协议没有记录在案,我想了解更多。 因此,我决定更详细地研究一下,结果,我对几乎整个程序进行了逆向工程。 这项研究从未发表过,尽管我有时会与一些人分享笔记。

当我们开设Anvil Ventures时,克里斯和我赞赏许多用于文档存储,共享和协作的工具。 显然,其中之一是Dropbox,对我来说,这是挖掘旧研究并在客户端的当前版本上进行检查的另一个原因。

解密和模糊处理


首先,我下载了Linux客户端,并很快发现它是用Python编写的。 由于Python许可相当宽松,因此人们可以轻松修改和分发Python解释器以及其他依赖项,例如商业软件。 然后,我开始进行逆向工程,以了解客户端的工作方式。

当时,具有字节码的文件位于ZIP文件中,并带有可执行二进制文件。 主要的二进制文件只是一个经过修改的Python解释器,该解释器是通过捕获Python导入机制加载的。 随后的每个导入调用都通过ZIP文件解析重定向到此二进制文件。 当然,从二进制文件中提取此ZIP很容易。 例如,有用的binwalk工具将其与所有字节编译的.pyc文件一起检索。

然后,我无法破坏.pyc文件的加密,但是最后,我将标准Python库的通用对象重新编译,并在内部注入了后门。 现在,Dropbox客户端正在加载该对象,我可以轻松地在工作的解释器中执行任意Python代码。 尽管我自己发现了此方法,但Florian Leda和Nicolas Raff在2012年关于Hack.lu的演示中使用了相同的方法。

探索和操纵Dropbox中正在运行的代码的能力已经揭示了很多。 该代码使用了一些保护性技巧,使其难以转储代码对象 。 例如,在常规的CPython解释器中,很容易恢复代表函数的已编译字节码。 一个简单的例子:

>>> def f(i=0): ... return i * i ... >>> f.__code__ <code object f at 0x109deb540, file "<stdin>", line 1> >>> f.__code__.co_code b'|\x00|\x00\x14\x00S\x00' >>> import dis >>> dis.dis(f) 2 0 LOAD_FAST 0 (i) 2 LOAD_FAST 0 (i) 4 BINARY_MULTIPLY 6 RETURN_VALUE >>> 

但是在Objects / codeobject.c的编译版本中,co_code属性已从打开列表中删除。 该成员列表通常看起来像这样:

  static PyMemberDef code_memberlist[] = { ... {"co_flags", T_INT, OFF(co_flags), READONLY}, {"co_code", T_OBJECT, OFF(co_code), READONLY}, {"co_consts", T_OBJECT, OFF(co_consts), READONLY}, ... }; 

co_code属性的消失使得无法转储这些代码对象。

此外,还删除了其他库,例如标准的Python 反汇编程序 。 最后,我仍然设法将代码对象转储到文件中,但是仍然无法反编译它们。 我花了一些时间才意识到Dropbox解释器使用的操作码与Python的标准操作码不匹配。 因此,有必要了解新的操作码,以便将代码对象改写回原始的Python字节码。

一种选择是操作码重新映射。 据我所知,该技术由Rich Smith开发,并在Defcon 18上引入。 在那次演讲中,他还展示了用于对内存中的Python字节码进行逆向工程的pyREtic工具。 似乎对pyREtic代码的支持不佳,该工具针对的是“旧的” Python 2.x二进制文件。 要熟悉Rich提出的技术,强烈建议观看他的表演。

操作码转换方法采用标准Python库的所有代码对象,并将它们与从Dropbox二进制文件中提取的对象进行比较。 例如,来自hashlib.pycsocket.pyc的代码对象在标准库中。 假设,如果每次0x43操作码对应于解模糊的0x21 0x21 ,我们就可以逐步构建一个转换表来重写代码对象。 然后,这些代码对象可以通过Python反编译器传递。 要转储,您仍然需要带有正确co_code对象的正确解释器。

另一种选择是破解序列化格式。 在Python中,序列化称为封送处理 。 以常规方式反序列化模糊处理的文件不起作用。 在IDA Pro中对二进制文件进行反向工程时,我发现了解密步骤。 据我所知,第一个对此主题进行公开发布的人是Hagen Fritch在他的博客上 。 他在那里提到了Dropbox新版本中的更改(当Dropbox从Python 2.5切换到Python 2.7时)。 该算法的工作原理如下:

  • 解压缩pyc文件时,会读取标头以确定封送处理的版本。 除了CPython本身的实现之外,没有记录此格式。
  • 该格式定义了其中编码的类型的列表。 类型为TrueFalsefloats等,但最重要的是上述Python code object的类型code object
  • 加载code object ,首先从输入文件中读取两个附加值。
  • 第一个是32位random值。
  • 第二个是一个32位length值,指示序列化代码对象的大小。
  • 然后,将randlengthrand到一个生成seed的简单RNG函数。
  • 该种子被传递到Mersenne涡旋 ,后者生成四个32位值。
  • 这四个值结合在一起,为序列化数据提供了加密密钥。 然后,加密算法使用Tiny Encryption Algorithm解密数据。

在我的代码中,我从头开始编写Python解组过程。 解密代码对象的部分看起来像下面的片段。 应当注意,该方法将必须递归调用。 pyc文件的顶级对象是一个代码对象,其本身包含代码对象,这些对象可以是类,函数或lambda。 反过来,它们也可以包含方法,函数或lambda。 这些都是层次结构中的所有代码对象!

  def load_code(self): rand = self.r_long() length = self.r_long() seed = rng(rand, length) mt = MT19937(seed) key = [] for i in range(0, 4): key.append(mt.extract_number()) # take care of padding for size calculation sz = (length + 15) & ~0xf words = sz / 4 # convert data to list of dwords buf = self._read(sz) data = list(struct.unpack("<%dL" % words, buf)) # decrypt and convert back to stream of bytes data = tea.tea_decipher(data, key) data = struct.pack("<%dL" % words, *data) 

解密代码对象的能力意味着在反序列化过程之后,您需要重写实际的字节码。 代码对象包含有关行号,常量和其他信息的信息。 实际的字节码在co_code对象中。 建立操作码转换表时,我们可以简单地用标准的Python 3.6等效项替换模糊的Dropbox值。

现在,代码对象采用通常的Python 3.6格式,并且可以将其传递给反编译器。 由于R. Bernstein的uncompyle6项目,Python反编译器的质量得到了显着提高 。 反编译提供了相当不错的效果,并且我能够将所有内容放到一个工具中,该工具会尽其所能反编译当前版本的Dropbox客户端。

如果克隆此存储库并按照说明进行操作,则结果将如下所示:

  ...
     __main__-信息-成功反编译了保管箱/客户端/功能/ browse_search / __初始化__。pyc
     __main__-信息-解密,修补和反编译_bootstrap_overrides.pyc
     __main__-信息-成功反编译_bootstrap_overrides.pyc
     __main__-信息-处理了3713个文件(成功反编译了3591个文件,失败了122个文件)
     opcodemap-警告-由于未设置强制覆盖,因此未写入操作码映射 

这意味着您现在拥有一个out/目录,其中包含反编译版本的Dropbox源代码。

启用Dropbox跟踪


在开源中,我开始寻找一些有趣的东西,以下片段吸引了我的注意力。 仅当程序集未冻结或在第1430行中未设置限制功能的魔术键或cookie时,才安装out/dropbox/client/high_trace.py中的跟踪处理程序。

  1424 def install_global_trace_handlers(flags=None, args=None): 1425 global _tracing_initialized 1426 if _tracing_initialized: 1427 TRACE('!! Already enabled tracing system') 1428 return 1429 _tracing_initialized = True 1430 if not build_number.is_frozen() or magic_trace_key_is_set() or limited_support_cookie_is_set(): 1431 if not os.getenv('DBNOLOCALTRACE'): 1432 add_trace_handler(db_thread(LtraceThread)().trace) 1433 if os.getenv('DBTRACEFILE'): 1434 pass 

提及冻结的构建是指Dropbox的内部调试构建。 在同一个文件中,您可以找到以下几行:

  272 def is_valid_time_limited_cookie(cookie): 273 try: 274 try: 275 t_when = int(cookie[:8], 16) ^ 1686035233 276 except ValueError: 277 return False 278 else: 279 if abs(time.time() - t_when) < SECONDS_PER_DAY * 2 and md5(make_bytes(cookie[:8]) + b'traceme').hexdigest()[:6] == cookie[8:]: 280 return True 281 except Exception: 282 report_exception() 283 284 return False 285 286 287 def limited_support_cookie_is_set(): 288 dbdev = os.getenv('DBDEV') 289 return dbdev is not None and is_valid_time_limited_cookie(dbdev) build_number/environment.py 

从第287行的limited_support_cookie_is_set方法中可以看到,只有在名为DBDEV的环境变量正确设置为寿命有限的cookie时,才启用跟踪。 好吧,这很有趣! 现在,我们知道了如何生成此类限时Cookie。 根据名称判断,Dropbox工程师可以生成此类cookie,然后在需要支持客户的某些情况下临时启用跟踪。 重新启动Dropbox或重新启动计算机后,即使指定的cookie仍然存在,它也会自动过期。 我想这应该防止例如由于连续跟踪而导致的性能下降。 这也使反向工程Dropbox变得困难。

但是,一个小的脚本可以简单地不断生成和设置这些cookie。 像这样:

  #!/usr/bin/env python3 def output_env(name, value): print("%s=%s; export %s" % (name, value, name)) def generate_time_cookie(): t = int(time.time()) c = 1686035233 s = "%.8x" % (t ^ c) h = md5(s.encode("utf-8?") + b"traceme").hexdigest() ret = "%s%s" % (s, h[:6]) return ret c = generate_time_cookie() output_env("DBDEV", c) 

然后创建一个基于时间的cookie:

  $ python3 setenv.py DBDEV=38b28b3f349714; export DBDEV; 

然后,将该脚本的输出正确加载到环境中并运行Dropbox客户端。

  $ eval `python3 setenv.py` $ ~/.dropbox-dist/dropbox-lnx_64-71.4.108/dropbox 

这包括跟踪输出,彩色格式等。 看起来像这个未注册的客户端:



实施新守则


这一切都有些有趣。 进一步研究反编译的代码,我们找到out/build_number/environment.pyc 。 有一个功能可以检查是否安装了特定的魔术钥匙。 该密钥未在代码中进行硬编码,但会与SHA-256哈希进行比较。 这是相应的代码段。

  1 import hashlib, os 2 from typing import Optional, Text 3 _MAGIC_TRACE_KEY_IS_SET = None 4 5 def magic_trace_key_is_set(): 6 global _MAGIC_TRACE_KEY_IS_SET 7 if _MAGIC_TRACE_KEY_IS_SET is None: 8 dbdev = os.getenv('DBDEV') or '' 9 if isinstance(dbdev, Text): 10 bytes_dbdev = dbdev.encode('ascii') 11 else: 12 bytes_dbdev = dbdev 13 dbdev_hash = hashlib.sha256(bytes_dbdev).hexdigest() 14 _MAGIC_TRACE_KEY_IS_SET = dbdev_hash == 'e27eae61e774b19f4053361e523c771a92e838026da42c60e6b097d9cb2bc825' 15 return _MAGIC_TRACE_KEY_IS_SET 

从代码的不同位置多次调用此方法,以检查是否设置了魔术跟踪键。 我尝试使用开膛手约翰(John the Ripper)蛮力破解SHA-256哈希,但是简单的蛮力花费了很长时间,并且由于无法猜测内容,我无法减少选项的数量。 在Dropbox中,开发人员可以具有特定的硬编码开发密钥,必要时可以安装该密钥,从而激活客户端的“魔术密钥”模式进行跟踪。

这让我很烦,因为我想找到一种快速简便的方法来启动具有这组用于跟踪的键的Dropbox。 因此,我编写了一个封送处理程序,该程序根据Dropbox加密生成加密的pyc文件。 因此,我能够输入自己的代码或简单地替换上面的哈希。 Github存储库中的此代码位于patchzip.py文件中。 结果,该哈希被ANVILVENTURES的SHA-256哈希ANVILVENTURES 。 然后,将代码对象重新加密并放在zip中,所有混淆后的代码都存储在其中。 这使您可以执行以下操作:

  $ DBDEV = ANVILVENTURES; 导出DBDEV;
     $〜/ .dropbox-dist / dropbox-lnx_64-71.4.108 / dropbox 

现在,在系统托盘中的Dropbox图标上单击鼠标右键时,将显示所有调试功能。



在文件dropbox/webdebugger/server.py文件中进一步研究了反编译的源,我发现了一个名为is_enabled的方法。 似乎正在检查是否启用内置的Web调试器。 首先,他检查提到的魔术钥匙。 由于我们替换了SHA-256哈希,因此我们可以简单地将值设置为ANVILVENTURES 。 第201202行的第二部分检查是否存在一个名为DB<x>的环境变量,该变量的x等于SHA-256哈希。 正如我们已经看到的,环境的价值设置了有时间限制的cookie。

  191 @classmethod 192 def is_enabled(cls): 193 if cls._magic_key_set: 194 return cls._magic_key_set 195 else: 196 cls._magic_key_set = False 197 if not magic_trace_key_is_set(): 198 return False 199 for var in os.environ: 200 if var.startswith('DB'): 201 var_hash = hashlib.sha256(make_bytes(var[2:])).hexdigest() 202 if var_hash == '5df50a9c69f00ac71f873d02ff14f3b86e39600312c0b603cbb76b8b8a433d3ff0757214287b25fb01' and is_valid_time_limited_cookie(os.environ[var]): 203 cls._magic_key_set = True 204 return True 205 206 return False 

使用完全相同的技术,用以前使用的SHA-256替换此哈希,现在我们可以将以前编写的setenv脚本更改为以下内容:

  $ cat setenv.py … c = generate_time_cookie() output_env("DBDEV", "ANVILVENTURES") output_env("DBANVILVENTURES", c) $ python3 setenv.py DBDEV=ANVILVENTURES; export DBDEV; DBANVILVENTURES=38b285c4034a67; export DBANVILVENTURES $ eval `python3 setenv.py` $ ~/.dropbox-dist/dropbox-lnx_64-71.4.108/dropbox 

如您所见,启动客户端后,将打开一个新的TCP端口以进行侦听。 如果环境变量设置不正确,它将无法打开。

  $ netstat --tcp -lnp |  grep保管箱
     tcp 0 0 127.0.0.1:4242 0.0.0.0:* LISTEN 1517 /投递箱 

在代码的进一步部分,您可以在webpdb.pyc文件中找到WebSocket接口。 这是标准python pdb实用程序的包装。 通过该端口上的HTTP服务器访问接口。 让我们安装websocket客户端尝试一下:

  $ websocat -t ws://127.0.0.1:4242 / pdb
     -返回-
    
     > /home/gvb/dropbox/webdebugger/webpdb.pyc(101)run()->无
     >
     (Pdb)从build_number.environment导入magic_trace_key_is_set为ms
     (Pdb)毫秒()
    是的 

因此,现在我们在客户端中拥有一个成熟的调试器,该调试器在所有其他方面都像以前一样工作。 我们可以执行任意的Python代码,并且能够启用内部调试菜单和跟踪功能。 所有这些将大大有助于进一步分析Dropbox客户端。

结论


我们能够对Dropbox进行反向工程,编写代码解密和注入工具,这些工具可与基于Python 3.6的当前Dropbox客户端一起使用。 我们对各个隐藏功能进行了反向工程并激活了它们。 显然,调试器将确实有助于进一步的黑客攻击。 特别是由于decompyle6的缺点,许多文件无法成功反编译。

代号


可以在Github上找到该代码。 那里的使用说明。 该存储库还包含我在2011年编写的旧代码。 如果有人具有基于Python 2.7的较旧版本的Dropbox,则它应该仅需进行少量修改即可使用。

该存储库还包含用于广播操作码的脚本,用于设置Dropbox环境变量的说明以及更改zip文件所需的一切。

致谢


感谢Anvil Ventures的Brian审核了我的代码。 这段代码的工作持续了好几年,我不时地对其进行了更新,引入了新的方法并重写了片段以使其恢复以在新版本的Dropbox上运行。

如前所述,Rich Smith,Florian Ledoux和Nicolas Raff以及Hagen Fritch的工作是反向工程Python应用程序的一个很好的起点。 特别是他们的工作与世界上最大的Python应用程序之一-Dropbox客户端的反向开发有关。

应当指出的是,由于R. Bernstein领导的uncompyle6项目,Python代码的反编译有了很大的进步。 该反编译器已经编译并改进了许多不同的Python反编译器。

也非常感谢Brian,Austin,Stefan和Chris的同事审阅了这篇文章。

Source: https://habr.com/ru/post/zh-CN452276/


All Articles