目录

DeepL Api 设计中的欺骗战术

这篇博文本应该在去年完成 DeepL 客户端逆向的时候发布,但考虑到相关细节一旦公开,恐怕会被广泛采用而被 DeepL 官方封杀,因此迟迟未开始。前段时间我发布了 DeepL Free Api 的 Docker 镜像,也在 GitHub 上公开了相关二进制程序,就下载量来看已经有不少人在使用了,相信 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 了。

1
2
3
4
5
6
7
{
    "jsonrpc": "2.0",
    "error":{
        "code":1042902,
        "message":"Too many requests."
    }
}

在 GitHub 搜索之后,我发现已经有前人尝试利用过 DeepL 的免费接口了,早在 2018 年他们就已经遇到了这个 429 问题,并且到现在都没有解决。

我尝试转向客户端的免费接口,苹果设备可以轻松 MITM,于是我便在 iPad 上对 DeepL 客户端进行抓包,让我意想不到的是,客户端的请求竟然比 Web 端的简单不少,接口参数数量仅有必须的几个,非常有利于利用。于是我又觉得自己行了,两三行 Python 代码完成接口对接。

简单测试,我又傻眼了。伪造的请求明明跟客户端发起的完全相同,但只要一更换翻译的内容,返回马上就变成 429。干!我都开始怀疑自己了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
    "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 利用代码,但逻辑不变)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
fn gen_fake_timestamp(texts: &Vec<String>) -> 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 的整倍数)判断是否是伪造的请求。真是精妙啊!

还有更绝的!你接着看:

1
2
3
4
5
6
7
8
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 协商过程中的算法顺序。这类方法要求更高的技术,当然分析起来也肯定更加困难,并且找到这样一种方法本身也不容易。

从 DeepL 的方法中,我找到了另外一种思路。利用人心理的弱点,一开始让其感觉非常简单,但是无论如何都无法得到想要的结果,给分析者造成心理上的打击和自我怀疑,让其浅尝辄止自行放弃分析。同时利用人行为上的惯式,使其自行破坏掉某些关键信息,从而给分析造成难以发现的阻碍。

原来,除了技术以外,还有这样一条道路啊,真是有趣!