🕵️‍♂️

LINEの通信プロトコルを解析する方法

2023/03/16に公開

前置き

本記事は特定のサービスのリバースエンジニアリングを推奨するものではありません。
リバースエンジニアリングの学習を目的とした利用を前提としています。
また、この記事は私が2021年に公開したWrite-upの日本語訳です。
内容は2018年に行ったリバースエンジニアリングの結果に基づいていますが、2020年にはいくつかの仕様が変更されたことに留意してください。

Shh

0. LINEの解析について

こんにちは、リバースエンジニアリングについて学んでいる らと です。
各国にはそれぞれ人気なメッセージングアプリがあると思いますが、私の国、日本ではLINEが最も多くのユーザーに利用されています。
私はLINEの通信プロトコルに非常に興味がありましたが、LINEはOSSアプリケーションではありません。
そのため、LINEをリバースエンジニアリングすることに決めました。

1. LINEってなに?

WikipediaのLine (software)[1]から引用

Line(LINEとしてすべて大文字)は、スマートフォン、タブレットコンピュータ、パーソナルコンピュータなどの電子機器での即時通信に使用されるフリーウェアアプリです。Lineのユーザーは、テキスト、画像、動画、音声をやり取りし、無料のVoIP通話やビデオ会議を行います。

  • 競合するKakaoが韓国のメッセージング市場を席巻したことから、Naver Corporationは2011年2月に韓国でメッセンジャーアプリケーション「NAVER Talk」を立ち上げました。しかし、韓国のメッセージング市場はKakaoに支配されていたため、NAVER Talkのビジネスは抑圧されました。Naver Corporationはメッセージングアプリケーションを拡大し、まだ開発されていない他の国のメッセージング市場にターゲットを絞りました。Naver Corporationは、2011年に日本のメッセージング市場に向けて彼らのメッセージングアプリケーションをリリースし、その後LINEに名称を変更しました。LINEが大成功を収めると、NAVER TalkとLINEを2012年3月に統合しました。

LINEが日本で大成功したことは本当に興味深いことです。
現在、日本には国内のメッセージングアプリが存在せず、人々は新しいサービスに移行することを避ける傾向があるため、作成されたとしても広く普及することはありません。
私の知るカテゴリでは、ブロックチェーン技術を使用した国内の安全なアプリケーションのクラウドファンディング[2]が行われています。ただし、2020年11月16日現在、寄付者はわずか56人であり、15歳から64歳までのスマートフォンを定期的に使用している人口の割合[3]を計算すると、人口の約0.0000007%しか寄付していません。
このような現状には、「もったいない」精神が根源であることは事実でしょう。

2. 通信プロトコルの概要

LINEの公開する暗号化技術についてのホワイトペーパー[4]から引用

LINEモバイルクライアントで使用されている主要な転送プロトコルは、SPDY 2.0に基づいています。通常、SPDYプロトコルでは、TLSを使用して暗号化されたチャネルを確立しますが、LINEの実装では、転送鍵を確立するために軽量なハンドシェイクプロトコルが使用されています。このハンドシェイクプロトコルは、TLS v1.3の0-RTTハンドシェイクをベースにしています。LINEの転送暗号化プロトコルでは、楕円曲線暗号学(ECC)とsecp256k1曲線を使用して、鍵交換とサーバーのID検証を実装しています。対称暗号化にはAESを使用し、HKDFを使用して対称鍵を導出します。プロトコルについては、以下で詳しく説明します。

要するに、LINEのAndroid/iOSアプリは、通信にSPDY 2.0を使用しています。SPDYプロトコルは、Googleが開発したもので、HTTPをサポートすることを目的としています。ただし、HTTP/2の開発を優先するために2016年に廃止されました。

では、パケットから何かを取得できるか見てみましょう。
パケットのキャプチャにはHttpCanaryを使用しました。
また、効率的なデバッグのために、LINE / LINE Liteとarmv7/armv8を両方使用しました。

QrCode - ログインセッション


LINEは、"/acct/lgn/sq/v1"に接続するためにApache Thrift Compact Protocolを使用して通信していることがパケットから分かります。これはおそらくAPIで、"/account/login/"に関連しています。
また、LINEは、createSession関数を使用して、Thrift APIエンドポイントでセッションを生成していることが分かります。

そのため、Thriftについての理解度を深める必要があります。

3. Apache Thrift の理解度を深めてみよう

Apache Thrift[5]は、スケーラブルなクロス言語サービス開発のためにFacebookによって作られたプロトコルです。
Thriftは、IDL(インターフェース記述言語)と呼ばれるものです。開発者は.thriftファイルでデータ型を定義し、Thriftコンパイラで定義ファイルをコンパイルして、どんなプログラミング言語でも使用できるようにします。

以下は、Thrift IDL定義の簡単な例です。

exception HelloError
{
  1:i32 errcode,
  2:string message
}

struct HelloResponse
{
  1:string message;
}

struct HelloRequest{}

service HelloService
{
  HelloResponse HelloWorld(
    1:HelloRequest request)
    throws (1:HelloError err);
}

Thrift IDLには6つの型があります。

  • ベースタイプ
  • 特殊タイプ
  • 構造体
  • コンテナ
  • 例外
  • サービス

そして、式はFieldID:型です。
FieldIDは、通信データが正しいことを確認するために使用されます。

4. LINEの通信データをデシリアライズしてみよう

LINEはThriftを通信に使用していることがわかりましたが、パケットはシリアライズされています。
そのため、デコンパイラ/デバッガを使用してLINE LiteのTServiceClientをフックしてみましょう。
TServiceClientは通信プロトコルのコアです。

Java Apache Thrift javadoc[6]から引用

public abstract class TServiceClient
extends java.lang.Object
A TServiceClient is used to communicate with a TService implementation across protocols and transports.
protected void sendBase(java.lang.String methodName,
                        TBase args)
                 throws TException
Throws:
TException
protected void receiveBase(TBase result,
                           java.lang.String methodName)
                    throws TException
Throws:
TException

さて、これらの関数を使用することで、パケットをフックすることができるかもしれません!


デコンパイラでTServiceClientに辿り着きました。
おそらく、LINE Corporationが開発したORK[7]と呼ばれるllvm-obfuscatorによって難読化されていますが、理解するのは簡単です。
関数aはreceiveBaseであり、関数bはsendBaseです。
そのため、Xposedアプリケーションを作成してフックすることができました。

public class ThriftHooker implements IXposedHookLoadPackage {
    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam lparam) throws Throwable {
        if (lparam.packageName.equals("com.linecorp.linelite")) {
            Class TServiceClient = lparam.classLoader.loadClass("w.a.a.TServiceClient");

            XposedHelpers.findAndHookMethod(TServiceClient, "b", String.class, "w.a.a.TProtocol", new XC_MethodHook() {
                @RequiresApi(api = Build.VERSION_CODES.O)

                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    XposedBridge.log("[TServiceClient sendBase]: " + " [ " + param.args[1] + " ] " + param.args[1].toString());
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                }
            });

            XposedHelpers.findAndHookMethod(TServiceClient, "a", "w.a.a.TProtocol", String.class, new XC_MethodHook() {
                @RequiresApi(api = Build.VERSION_CODES.O)

                @Override
                protected void beforeHookedMethod(MethodHookParam param) throws Throwable {
                    ArrayList<String> args = new ArrayList<String>();
                    for (Object arg: param.args) {
                        args.add(arg.toString());
                    }
                    XposedBridge.log("[TServiceClient receiveBase]: " + " [ " + param.args[1] + " ] " + param.args[0].toString());
                }

                @Override
                protected void afterHookedMethod(MethodHookParam param) throws Throwable {
                }
            });
        }
    }
}

その結果…

バーン!
パケットの大半が読み取り可能になりました。

[TServiceClient sendBase]:  [ getServerTime_args() ] getServerTime_args()
[TServiceClient sendBase]:  [ createSession_args(request:CreateQrSessionRequest()) ] createSession_args(request:CreateQrSessionRequest())
[TServiceClient receiveBase]:  [ getServerTime ] getServerTime_result(success:0, e:null)
[TServiceClient receiveBase]:  [ createSession ] createSession_result(success:null, e:null)
[TServiceClient sendBase]:  [ createQrCode_args(request:CreateQrCodeRequest(authSessionId:**********************************************************39587a69)) ] createQrCode_args(request:CreateQrCodeRequest(authSessionId:**********************************************************39587a69))
[TServiceClient receiveBase]:  [ createQrCode ] createQrCode_result(success:null, e:null)
[TServiceClient sendBase]:  [ verifyCertificate_args(request:VerifyCertificateRequest(authSessionId:**********************************************************39587a69, certificate:********************************************************ec433014)) ] verifyCertificate_args(request:VerifyCertificateRequest(authSessionId:**********************************************************39587a69, certificate:********************************************************ec433014))
[TServiceClient receiveBase]:  [ verifyCertificate ] verifyCertificate_result(success:null, e:null)
[TServiceClient sendBase]:  [ qrCodeLogin_args(request:QrCodeLoginRequest(authSessionId:**********************************************************39587a69, systemName:G011A, autoLoginIsRequired:true)) ] qrCodeLogin_args(request:QrCodeLoginRequest(authSessionId:**********************************************************39587a69, systemName:G011A, autoLoginIsRequired:true))
[TServiceClient receiveBase]:  [ qrCodeLogin ] qrCodeLogin_result(success:null, e:null)

5. QrCodeログインの方法についての理解度を更に深めてみよう

このバイトコードでは、LINEがQrCodeログインのためのURLを生成しています。
LINEアプリケーションでURLを開くと、PinCodeの確認が表示されます。
コードによると、Curve25519で計算されたecdhと呼ばれる鍵ペアがあります。

cr.yp.to[8]より引用

ユーザーの32バイトのシークレットキーが与えられると、Curve25519はユーザーの32バイトの公開鍵を計算します。また、ユーザーの32バイトのシークレットキーと他のユーザーの32バイトの公開鍵が与えられると、Curve25519は両方のユーザーによって共有される32バイトのシークレットを計算します。このシークレットは、2人のユーザー間でのメッセージの認証と暗号化に使用することができます。

したがって、最終的なURLは次のようになります。

https://line.me/R/au/g/authSessionId?secret=ecdh&e2eeVersion=version

調査の結果、フックできない関数が存在することに気付きました。

この関数は、QrCodeログインを待機処理しているようです。
他にも存在しますが、ここでは省略します。

以下は、QrCodeログインやメッセージングに使用されるエンドポイントのダンプです。
特に重要なのは

  • SECONDARY_LOGIN /ACCT/lgn/sq/v1
    • QrCode ログイン
  • SECONDARY_LOGIN_PERMIT /ACCT/lp/lgn/sq/v1
    • QrCode ログイン検証
  • TalkService /S4
    • メッセージング API
  • PollingService /P4
    • オペレーションの受取

6. ThriftバイトコードからIDLを再構築する

さて、今やLINEの振る舞いについて多く理解できました。
次に行うべきことは、Thrift IDLを再構築することです。
一つの方法として、私はSmali[9]を選びました。
Javaアプリケーションの開発者であれば、apkをビルドする際に、Dalvikバイトコードを含む.dexファイルが含まれることを知っているかもしれません。

詳細には書きませんが、Javaは中間言語であり、簡単にデコンパイルできます。
ただし、バイトコードの読み取りは簡単ではありません。
そのため、disassembly/assemblyにbaksmaliを使用し、最適な選択肢としてapktoolを使用します。
apktoolで逆アセンブルした後、Linuxコマンドでコードを検索します。

$find . -name "*.smali" | xargs grep -E "_result|_args"

いいね!
Smaliの構文はx86-32アセンブリに似ており、私はアセンブリに精通していたので、約20分で理解することができました。
そして理解した後、SmaliからThrift IDLを自動的に再構築するプログラムをGolangとPythonで書きました。
アルゴリズムは非常にシンプルです。
語彙解析も役立つと思いますが、私はif文だけでパターン処理を作成しました。
書く必要がないほど簡単なので、アルゴリズムを箇条書きで説明します。

便宜上、引数をprogram_argsと呼びます。

  • _result

    • Struct
    • Exception
  • _args

    • Struct

規則

  • サービス名とその関数はImplから知ることができます。
    • jp\naver\line\android\thrift\client\impl
  • _resultからResponseを知ることができます。
    • _resultにStructがない場合、void関数です。
  • _argsからRequestを知ることができます。
  • invoke-directからFieldID、Types、nameが分かります。
  • インスタンスフィールドはdirect methodsのprogram_argsにリンクされます。
    • 型は必ずTypesまたはStructまたはEnumです。
    • インスタンスフィールドがprogram_argsにリンクするのに十分ではない場合は、オプション引数であり、invoke-directから知ることができます。
  • 名前に「Exception」を含むStructは必ずException関数です。
  • Enumはinvoke-directを読むことで再構築できます。
  • typedefはdirect methodsから知ることができます。

createQrCodeの再構築の例

# instance fields
.field public d:Lb/a/d/a/a/b/a/j;

// b/a/d/a/a/b/a/j = Response

.field public e:Lb/a/d/a/a/b/a/r;

// b/a/d/a/a/b/a/r = Exception


# direct methods
.method public static constructor <clinit>()V
    .locals 8

    .line 1
    new-instance v0, Lw/a/a/j/l;

    const-string v1, "createQrCode_result"

    invoke-direct {v0, v1}, Lw/a/a/j/l;-><init>(Ljava/lang/String;)V

    sput-object v0, Lb/a/d/a/a/b/a/m0;->f:Lw/a/a/j/l;

    .line 2
    new-instance v0, Lw/a/a/j/c;

    const-string v1, "success"

    const/16 v2, 0xc

    const/4 v3, 0x0

    invoke-direct {v0, v1, v2, v3}, Lw/a/a/j/c;-><init>(Ljava/lang/String;BS)V

    /*
    v1 = name
    v2 = Type
    v3 = FieldID
    */

    sput-object v0, Lb/a/d/a/a/b/a/m0;->g:Lw/a/a/j/c;

...
# instance fields
.field public d:Lb/a/d/a/a/b/a/i;

// b/a/d/a/a/b/a/i = request

# direct methods
.method public static constructor <clinit>()V
    .locals 7

    .line 1
    new-instance v0, Lw/a/a/j/l;

    const-string v1, "createQrCode_args"

    invoke-direct {v0, v1}, Lw/a/a/j/l;-><init>(Ljava/lang/String;)V

    sput-object v0, Lb/a/d/a/a/b/a/l0;->e:Lw/a/a/j/l;

    .line 2
    new-instance v0, Lw/a/a/j/c;

    const-string v1, "request"

    const/16 v2, 0xc

    const/4 v3, 0x1

    invoke-direct {v0, v1, v2, v3}, Lw/a/a/j/c;-><init>(Ljava/lang/String;BS)V

    /*
    v1 = name
    v2 = Type
    v3 = FieldID
    */

    sput-object v0, Lb/a/d/a/a/b/a/l0;->f:Lw/a/a/j/c;

...

enum g_a_c_u0_a_c_b_c
{
  INTERNAL_ERROR = 0;
  ILLEGAL_ARGUMENT = 1;
  VERIFICATION_FAILED = 2;
  NOT_ALLOWED_QR_CODE_LOGIN = 3;
  VERIFICATION_NOTICE_FAILED = 4;
  RETRY_LATER = 5;
  INVALID_CONTEXT = 100;
  APP_UPGRADE_REQUIRED = 101;
}

exception SecondaryQrCodeException
{
  1:g_a_c_u0_a_c_b_c code;
  2:string alertMessage;
}

struct CreateQrCodeResponse
{
  1:string callbackUrl;
}

struct CreateQrCodeRequest
{
  1:string authSessionId;
}

service SecondaryQrcodeLoginService
{
  CreateQrCodeResponse createQrCode(
  1:CreateQrCodeRequest request) throws (1:SecondaryQrCodeException e);
}

7. CLIからLINEにログインして、関数を使用してみよう

再構築されたThrift IDLからPythonライブラリを生成できたようです。
あとはPythonでAPIを実装するだけです!
何かを待つ時間が長くなればなるほど、手に入れたときにそれをより高く評価することができます。なぜなら、手に入れる価値のあるものは、待つ価値があるからです。- Susan Gale

from LutwidseAPI.TalkService import TalkService

# ログインアルゴリズムはスパゲッティのように複雑なコードであるため省略します。
msg = Message
msg = Message(to=groupId, text="Hello World")
# グループIDはパケットを解析するか、またはグループ情報を取得するためのThrift関数を使用して取得することができます。
client.sendMessage(reqSeq, msg)

爆発するぞ!


8. オペレーションの受取

LINEを長時間フックしていると、数分ごとにfetchOpsと呼ばれる関数が呼び出されていることに気付くかもしれません。

WikipediaのLong polling[10]及びPush technology[11]から引用

long polling(不可算名詞)

(コンピューティング)クライアントが即時の応答を期待せずにサーバから情報を要求する技術。

つまり、この関数は「あなたの友達があなた/グループにメッセージを送信した」「誰かがグループのアイコンを変更した」など、ユーザーのアクションを待機して受け取るために使用されます。

[TServiceClient sendBase]:  [ fetchOps_args(localRev:393831, count:50, globalRev:295818, individualRev:1286) ] fetchOps_args(localRev:393831, count:50, globalRev:295818, individualRev:1286)

しかし、globalRevとindividualRevはどのように決定されるのでしょうか?
そこで、自作したAPIでオペレーションをデバッグしてみましょう。
さて、今あなたは返り値の奇妙なシーケンスについて疑問を抱いているはずです。

createdTime 0
param1 128658291
param2 295818notice23moretab304stickershop234channel205denykeyword244connectioninfo148buddy256timelineinfo8themeshop41callrate43configuration348sticon52suggestdictionary144suggestsettings281usersettings0analyticsinfo289searchpopularkeyword224searchnotice169timeline99searchpopularcategory287extendedprofile254seasonalmarketing34newstab84suggestdictionaryv2106chatappsync337agreements323instantnews147emojimapping96searchbarkeywords38shopping256chateffectbg223chateffectkw27searchindex276hubtab109payruleupdated144smartch244homeservicelist296timelinestory289wallettab261podtab183
reqSeq -1
revision -1
type 0

パズルが好きなら、とても簡単に答えを出すことができます。

  • fetchOps
    • localRev = あなたのアカウントで取得された操作の数
    • count = 何件の操作を取得するか
    • individualRev = param1の最初の数字
    • globalRev = param2の最初の数字

なぜLINEがそのようにしてキーを決定しているのかは不明ですが、
とにかく、それが正常に動作していることを確認できます。

ご覧いただき、ありがとうございました。 - らと

脚注
  1. https://en.wikipedia.org/wiki/Line_(software) ↩︎

  2. https://camp-fire.jp/projects/view/315308 ↩︎

  3. https://www.stat.go.jp/data/jinsui/2019np/index.html ↩︎

  4. https://scdn.line-apps.com/stf/linecorp/en/csr/line-encryption-whitepaper-ver1.0.pdf ↩︎

  5. https://thrift.apache.org/docs/idl ↩︎

  6. https://people.apache.org/~thejas/thrift-0.9/javadoc/org/apache/thrift/TServiceClient.html ↩︎

  7. https://engineering.linecorp.com/ja/blog/ork-vol-1/ ↩︎

  8. https://cr.yp.to/ecdh.html ↩︎

  9. https://github.com/JesusFreke/smali ↩︎

  10. https://en.wiktionary.org/wiki/long_polling ↩︎

  11. https://en.wikipedia.org/wiki/Push_technology ↩︎

Discussion