# DeepL Api 设计中的欺骗战术 这篇博文本应该在去年完成 DeepL 客户端逆向的时候发布,但考虑到相关细节一旦公开,恐怕会被广泛采用而被 DeepL 官方封杀,因此迟迟未开始。前段时间我发布了 [DeepL Free Api 的 Docker 镜像](https://hub.docker.com/r/zu1k/deepl),也在 [GitHub 上公开了相关二进制程序](https://github.com/zu1k/deepl-api-rs),就下载量来看已经有不少人在使用了,相信 DeepL 不久就会有动作,因此我认为现在已经可以公开相关细节。 我逆向的是 DeepL 的 Windows 客户端,因为是 `C#` 开发依附于 `.net`,也未进行任何混淆和加壳,可以轻松逆出源码。通过前段时间与一些其他研究者交流,我认为已经有不少感兴趣的同学也进行了逆向,也许是一种默契,都害怕 DeepL 在发觉后进行修改,所以大家也都没有对外公开,目前网络中搜不到任何相关的内容。本文的目的是给相关小伙伴一点思路,不过希望大家还是不要直接公开代码,以继续欺骗 DeepL,让其相信还没有人发现他们的把戏。 在我实现 DeepL Free Api 的过程中,我发现 DeepL 并没有像之前见到的一些接口设计那样,通过签名等手段来避免接口滥用,相反,他们使用了一些欺骗战术来混淆视听,从而尝试让抓包分析者放弃,本文将围绕此进行讨论。 ## 过程 进入研究生阶段,为了方便阅读论文,为自己开发了划词翻译工具,在众多翻译引擎中 DeepL 的效果尤为出色。DeepL 官方的 Api 需要绑定信用卡进行认证,但其并未在中国大陆经营业务,所以并不支持国内的信用卡。我也尝试过从淘宝购买别人用国外信用卡认证过的帐号,价格贵不说,在没有滥用的情况下,DeepL 在两个月内封禁了我的帐号,因此我决定用一些其他手段。 考虑到 DeepL 有提供免费版本的翻译服务,支持 Web,Windows、Android 和 iOS 都有相应的客户端,我便想使用这些客户端使用的免费接口。不出所料,在广泛使用打包和混淆技术的当下,DeepL 的 Web 端 js 代码也不是人看的东西,但通过简单的抓包,我发现其接口参数非常清晰,根本没有额外的签名、token等认证技术,我觉得自己又行了,几行 Python 代码便完成了接口对接工作。 但测试下来,我发现当修改翻译内容,有极大概率遇到 429 `Too many requests`,并且一旦出现 429,后续的所有请求便都是 429 了。 ```json { "jsonrpc": "2.0", "error":{ "code":1042902, "message":"Too many requests." } } ``` 在 GitHub 搜索之后,我发现已经有前人尝试利用过 DeepL 的免费接口了,早在 2018 年他们就已经遇到了这个 429 问题,并且到现在都没有解决。 我尝试转向客户端的免费接口,苹果设备可以轻松 MITM,于是我便在 iPad 上对 DeepL 客户端进行抓包,让我意想不到的是,客户端的请求竟然比 Web 端的简单不少,接口参数数量仅有必须的几个,非常有利于利用。于是我又觉得自己行了,两三行 Python 代码完成接口对接。 简单测试,我又傻眼了。伪造的请求明明跟客户端发起的完全相同,但只要一更换翻译的内容,返回马上就变成 429。干!我都开始怀疑自己了。 ```json { "jsonrpc": "2.0", "method": "LMT_handle_texts", "params": { "texts": [{ "text": "translate this, my friend" }], "lang": { "target_lang": "ZH", "source_lang_user_selected": "EN", }, "timestamp": 1648877491942 }, "id": 12345, } ``` 你自己看看,这个接口多么清楚明白,但怎么就伪造不了呢? 我想了又想,这里面也就 `id` 比较可疑,因为这个参数我不知道它是怎么生成的,是随机的还是根据某种规则计算出来的,我们无从知道。但从目前结果来看,随机的 `id` 无法被服务器认可。 当然,我也考虑过其他的服务端判断滥用的方法,例如某些 http 头、ssl 层面的方法(例如之前 Go 实现中 SSL 协商过程中加密算法的顺序等),我也想办法进行了伪造,可就是不行。疲惫了,不想搞了。 第二天,突然想起他的 Windows 客户端,稍微一分析惊喜的发现是 `C#`,还没加壳,果断扔进 `dnSpy`,发现也没混淆,真是柳暗花明又一村啊。分析之后,也就一切都清楚明白了,原来 DeepL 根本一开始就在想方设法让你觉得你行啊。 看前面那个接口的参数,我之所以觉得我行,就是因为这个接口它太简单了。接口的参数少,参数含义又非常明确,它并不像某些厂那样用一些不知所以然的缩写,这里的每一个参数,它的名称都在告诉我它的含义、它是干什么的以及它是怎么生成的。 `jsonrpc` 是版本号,`method` 是方法,一个固定的字符串。`params` 里面 `texts` 是多段待翻译的文本,`lang` 里面是翻译的语言选项,是枚举类型。`timestamp` 是 UNIX 风格的时间戳,`id` 就是序号。大眼一看,这里面只有 `id` 是最可疑的,这也确实是我最初犯的错误。 ## 真相 现在我来告诉你,DeepL 到底是怎么认证的。(下面并不是 DeepL 客户端的代码,是我写的 Rust 利用代码,但逻辑不变) ```Rust fn gen_fake_timestamp(texts: &Vec) -> u128 { let ts = tool::get_epoch_ms(); let i_count = texts .iter() .fold( 1, |s, t| s + t.text.matches('i').count() ) as u128; ts - ts % i_count + i_count } ``` 哈哈!没想到吧!人家的时间戳不是真的! DeepL 先计算了文本中所有 `i` 的数量,然后对真正的时间戳进行一个小小的运算 `ts - ts % i_count + i_count`,这个运算差不多仅会改变时间戳的毫秒部分,这个改变如果用人眼来验证根本无法发现,人类看来就是一个普通的时间戳,不会在意毫秒级的差别。 但是 DeepL 拿到这个修改后的时间戳,既可以与真实时间对比(误差毫秒级),又可以通过简单的运算(是否是 `i_count` 的整倍数)判断是否是伪造的请求。真是精妙啊! 还有更绝的!你接着看: ```Rust let req = req.replace( "\"method\":\"", if (self.id + 3) % 13 == 0 || (self.id + 5) % 29 == 0 { "\"method\" : \"" } else { "\"method\": \"" }, ); ``` 怎么样?我觉得我一开始就被玩弄了,人家的 `id` 就是纯粹的随机数,只不过后续的请求会在第一次的随机 `id` 基础上加一,但是这个 `id` 还决定了文本中一个小小的、微不足道的空格。 按照正常的思路,为了方便人类阅读和分析,拿到请求的第一时间,我都会先扔编辑器里格式化一下 Json,我怎么会想到,这恰恰会破坏掉人家用来认证的特征,因此无论我如何努力都难以发现。 ## 总结 在我以往的经验中,接口防滥用,要不就是用户专属的 token,要不就是对请求进行签名或者加密,这些对抗滥用的方法都是明面上的,就是明白告诉你我有一个签名,怎么签的,你去分析去吧,但是我代码混淆了,你看看你是要头发还是要算法。 要不就是高级点的,更具技术性的,利用某些客户端特有的实现造成的特征进行认证,我印象中最深刻的就是 [Go 的 SSL 协商过程中的算法顺序](https://www.zackwu.com/posts/2021-03-14-why-i-always-get-503-with-golang/)。这类方法要求更高的技术,当然分析起来也肯定更加困难,并且找到这样一种方法本身也不容易。 从 DeepL 的方法中,我找到了另外一种思路。利用人心理的弱点,一开始让其感觉非常简单,但是无论如何都无法得到想要的结果,给分析者造成心理上的打击和自我怀疑,让其浅尝辄止自行放弃分析。同时利用人行为上的惯式,使其自行破坏掉某些关键信息,从而给分析造成难以发现的阻碍。 原来,除了技术以外,还有这样一条道路啊,真是有趣!