This blog post should have been published last year when I finished reversing the DeepL client, but I delayed starting it because I was afraid that once the relevant details were made public, they would be widely adopted by others and blocked by the DeepL official. Some time ago I released DeepL Docker image of Free Api, and also made the binary public on GitHub. I believe DeepL will be in action soon, so I think it’s time to make the details public.

I reverse the Windows client of DeepL, because it is developed by C# dependent on .net, and there is no obfuscation and shelling, so I can easily reverse the source code. Through the exchange with some other researchers some time ago, I think there are already a lot of interested people also reverse, perhaps a tacit agreement, are afraid of DeepL in the discovery of modifications, so we are not open to the public, currently the network can not search any related content. The purpose of this article is to give the relevant partners a little thought, but I hope that we still do not directly disclose the code to continue to deceive DeepL to believe that no one has found their tricks yet.

In my implementation of the DeepL Free Api, I found that DeepL does not avoid interface abuse by means of signatures and other means like some interface designs I have seen before, instead, they use some deceptive tactics to obfuscate and thus try to make packet-catching analysts give up, which this article will discuss around.

Process

When I entered graduate school, I developed a scratch translation tool for myself to facilitate reading papers, and DeepL was particularly effective among many translation engines. DeepL’s official Api requires binding a credit card for authentication, but it does not operate business in mainland China, so it does not support domestic credit cards. I also tried to buy someone else’s account authenticated with a foreign credit card, but it was expensive and without abuse, DeepL blocked my account within two months, so I decided to use some other means.

Considering that DeepL offers a free version of its translation service with Web support, and that Windows, Android and iOS all have corresponding clients, I thought I would use the free interface used by these clients. Not surprisingly, with the widespread use of packaging and obfuscation techniques, the web-side js code of DeepL is not something that is human-readable, but by simply grabbing the package, I found that its interface parameters are very clear and there are no additional authentication techniques such as signatures and tokens at all. I think I should be able to handle it easily, maybe a few lines of Python code can finish the interface interfacing work.

But after testing, I found that when modifying the translation content, there is a high probability of encountering 429 Too many requests, and once 429 appears, all subsequent requests will be 429.

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

After searching GitHub, I found that there were already previous attempts to leverage the free interface to DeepL, and that they had run into this 429 issue back in 2018 and hadn’t resolved it until now.

I tried turning to the free interface for the client, which can be easily MITM on Apple devices, so I grabbed a packet on the DeepL client on my iPad. To my surprise, the client-side requests were surprisingly simpler than the web-side ones, and the number of interface parameters was only the required few, which was very beneficial to utilize. Once again, I thought I could handle it easily.

After a simple test, I was dumbfounded again. The forged request was obviously identical to the one initiated by the client, but as soon as I replaced the translated content, the return immediately became 429. fuck! I’m starting to doubt myself.

 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,
}

See for yourself how clear this interface is, but how come it can’t be forged?

I’ve thought about it and thought about it, and it’s id that’s suspicious, because I don’t know how this parameter is generated, whether it’s random or calculated according to some rules, we have no way to know. But from the results so far, it seems that the random id is not recognized by the server.

Of course, I have considered other server-side methods of determining abuse, such as certain http headers, ssl-level methods (e.g. the order of encryption algorithms during SSL negotiation in previous Go implementations, etc.). I’ve also tried to forge it, but it just doesn’t work. I was tired and didn’t want to mess with it.

The next day, I suddenly remembered his Windows client, and was surprised to find that it was C#, not yet shelled, so I decided to use dnSpy to analyze it, and found no confusion. After the analysis, everything is clear, it turns out that DeepL is simply trying to make you think you can do it.

Look at the parameters of the interface, the reason I think I can easily handle it is because this interface it is so simple. It doesn’t use abbreviations like some companies do, but every parameter here, its name tells me what it means, what it does and how it is generated.

jsonrpc is the version number, method is the method, a fixed string. params has texts which are multiple paragraphs of text to be translated, and lang has the language options for translation, which are enumerated types. timestamp is a UNIX-style timestamp, and id is the serial number. At a glance, only id is the most suspicious of these, and that was indeed the initial mistake I made.

The Truth

Now I’ll show you how DeepL actually authenticates. (The following is not the code for the DeepL client, it is the code I wrote to exploit Rust, but the logic remains the same).

 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
}

Haha! Didn’t expect it! It’s timestamp is not real!

DeepL first calculates the number of all is in the text, and then performs a small operation on the real timestamp ts - ts % i_count + i_count, which changes the timestamp by almost only milliseconds. This change would not even be noticeable if verified by the human eye, it would appear to be an ordinary timestamp and would not care about the millisecond difference.

But DeepL gets this modified timestamp and can compare it with the real time (millisecond error) and also determine whether it is a forged request by a simple operation (whether it is an integer multiple of i_count). That’s brilliant!

And it gets even better! Read on.

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\": \""
    },
);

How’s that? I think I was fooled from the beginning, their id is a purely random number, except that subsequent requests add one to the first random id, but this id also determines a small, insignificant space in the text.

According to normal thinking, in order to facilitate human reading and analysis, the first time I got the request, I would first put it into the editor to format the Json, how could I have thought that this would just destroy the features that deeple uses for authentication, so no matter how hard I tried it was hard to find.

Summary

In my past experience, the interface anti-abuse, either a user-exclusive token, or a signature or encryption of the request. These methods of combating abuse are all explicit, that is, to tell you clearly that I have a signature, how to sign, you go to analyze it, but I code confusion, you take your time to toss it.

Or is the advanced point, more technical, the use of certain client-specific implementation caused by the characteristics of authentication, I am most impressed by Go’s SSL negotiation process algorithm sequence. This type of approach requires a higher level of skill and is certainly more difficult to analyze, and finding such a method is not easy in itself.

From the method of DeepL I found another way of thinking. Exploiting the psychological weaknesses of people, making it feel very easy at first, but not getting the desired result anyway, causes psychological shock and self-doubt to the analyst, making it easy for people to give up. At the same time, it takes advantage of the inertia of human behavior, making it destroy certain key information on its own, thus creating an obstacle to the analysis that is difficult to find.

It is interesting to see that there is such a path besides technology!