本記事は特定のサービスのリバースエンジニアリングを推奨するものではありません。
リバースエンジニアリングの学習を目的とした利用を前提としています。
また、この記事は私が2021年に公開したWrite-upの日本語訳です。
内容は2018年に行ったリバースエンジニアリングの結果に基づいていますが、2020年にはいくつかの仕様が変更されたことに留意してください。
こんにちは、リバースエンジニアリングについて学んでいる らと です。
各国にはそれぞれ人気なメッセージングアプリがあると思いますが、私の国、日本ではLINEが最も多くのユーザーに利用されています。
私はLINEの通信プロトコルに非常に興味がありましたが、LINEはOSSアプリケーションではありません。
そのため、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%しか寄付していません。
このような現状には、「もったいない」精神が根源であることは事実でしょう。
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についての理解度を深める必要があります。
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は、通信データが正しいことを確認するために使用されます。
LINEはThriftを通信に使用していることがわかりましたが、パケットはシリアライズされています。
そのため、デコンパイラ/デバッガを使用してLINE LiteのTServiceClientをフックしてみましょう。
TServiceClientは通信プロトコルのコアです。
Java Apache Thrift javadoc[6]から引用
publicabstractclassTServiceClientextendsjava.lang.ObjectATServiceClient is usedtocommunicatewithaTService implementation across protocols and transports.protectedvoidsendBase(java.lang.String methodName,TBase args)throwsTExceptionThrows:TExceptionprotectedvoidreceiveBase(TBase result,java.lang.String methodName)throwsTExceptionThrows:TExceptionさて、これらの関数を使用することで、パケットをフックすることができるかもしれません!


デコンパイラでTServiceClientに辿り着きました。
おそらく、LINE Corporationが開発したORK[7]と呼ばれるllvm-obfuscatorによって難読化されていますが、理解するのは簡単です。
関数aはreceiveBaseであり、関数bはsendBaseです。
そのため、Xposedアプリケーションを作成してフックすることができました。
publicclassThriftHookerimplementsIXposedHookLoadPackage{publicvoidhandleLoadPackage(XC_LoadPackage.LoadPackageParam lparam)throwsThrowable{if(lparam.packageName.equals("com.linecorp.linelite")){ClassTServiceClient= lparam.classLoader.loadClass("w.a.a.TServiceClient");XposedHelpers.findAndHookMethod(TServiceClient,"b",String.class,"w.a.a.TProtocol",newXC_MethodHook(){@RequiresApi(api=Build.VERSION_CODES.O)@OverrideprotectedvoidbeforeHookedMethod(MethodHookParam param)throwsThrowable{XposedBridge.log("[TServiceClient sendBase]: "+" [ "+ param.args[1]+" ] "+ param.args[1].toString());}@OverrideprotectedvoidafterHookedMethod(MethodHookParam param)throwsThrowable{}});XposedHelpers.findAndHookMethod(TServiceClient,"a","w.a.a.TProtocol",String.class,newXC_MethodHook(){@RequiresApi(api=Build.VERSION_CODES.O)@OverrideprotectedvoidbeforeHookedMethod(MethodHookParam param)throwsThrowable{ArrayList<String> args=newArrayList<String>();for(Object arg: param.args){ args.add(arg.toString());}XposedBridge.log("[TServiceClient receiveBase]: "+" [ "+ param.args[1]+" ] "+ param.args[0].toString());}@OverrideprotectedvoidafterHookedMethod(MethodHookParam param)throwsThrowable{}});}}}その結果…

バーン!
パケットの大半が読み取り可能になりました。
[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)
このバイトコードでは、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ログインやメッセージングに使用されるエンドポイントのダンプです。
特に重要なのは
さて、今やLINEの振る舞いについて多く理解できました。
次に行うべきことは、Thrift IDLを再構築することです。
一つの方法として、私はSmali[9]を選びました。
Javaアプリケーションの開発者であれば、apkをビルドする際に、Dalvikバイトコードを含む.dexファイルが含まれることを知っているかもしれません。
詳細には書きませんが、Javaは中間言語であり、簡単にデコンパイルできます。
ただし、バイトコードの読み取りは簡単ではありません。
そのため、disassembly/assemblyにbaksmaliを使用し、最適な選択肢としてapktoolを使用します。
apktoolで逆アセンブルした後、Linuxコマンドでコードを検索します。
$find.-name"*.smali"|xargsgrep-E"_result|_args"
いいね!
Smaliの構文はx86-32アセンブリに似ており、私はアセンブリに精通していたので、約20分で理解することができました。
そして理解した後、SmaliからThrift IDLを自動的に再構築するプログラムをGolangとPythonで書きました。
アルゴリズムは非常にシンプルです。
語彙解析も役立つと思いますが、私はif文だけでパターン処理を作成しました。
書く必要がないほど簡単なので、アルゴリズムを箇条書きで説明します。
便宜上、引数をprogram_argsと呼びます。
_result
_args
規則
createQrCodeの再構築の例
# instance fields.fieldpublicd:Lb/a/d/a/a/b/a/j;// b/a/d/a/a/b/a/j= Response.fieldpublice:Lb/a/d/a/a/b/a/r;// b/a/d/a/a/b/a/r= Exception# direct methods.methodpublicstaticconstructor<clinit>()V.locals8.line1 new-instancev0,Lw/a/a/j/l; const-stringv1,"createQrCode_result" invoke-direct{v0,v1},Lw/a/a/j/l;-><init>(Ljava/lang/String;)V sput-objectv0,Lb/a/d/a/a/b/a/m0;->f:Lw/a/a/j/l;.line2 new-instancev0,Lw/a/a/j/c; const-stringv1,"success" const/16v2,0xc const/4v3,0x0 invoke-direct{v0,v1,v2,v3},Lw/a/a/j/c;-><init>(Ljava/lang/String;BS)V /*v1= namev2= Typev3= FieldID */ sput-objectv0,Lb/a/d/a/a/b/a/m0;->g:Lw/a/a/j/c;...# instance fields.fieldpublicd:Lb/a/d/a/a/b/a/i;// b/a/d/a/a/b/a/i= request# direct methods.methodpublicstaticconstructor<clinit>()V.locals7.line1 new-instancev0,Lw/a/a/j/l; const-stringv1,"createQrCode_args" invoke-direct{v0,v1},Lw/a/a/j/l;-><init>(Ljava/lang/String;)V sput-objectv0,Lb/a/d/a/a/b/a/l0;->e:Lw/a/a/j/l;.line2 new-instancev0,Lw/a/a/j/c; const-stringv1,"request" const/16v2,0xc const/4v3,0x1 invoke-direct{v0,v1,v2,v3},Lw/a/a/j/c;-><init>(Ljava/lang/String;BS)V /*v1= namev2= Typev3= FieldID */ sput-objectv0,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);}
再構築されたThrift IDLからPythonライブラリを生成できたようです。
あとはPythonでAPIを実装するだけです!
何かを待つ時間が長くなればなるほど、手に入れたときにそれをより高く評価することができます。なぜなら、手に入れる価値のあるものは、待つ価値があるからです。- Susan Gale
from LutwidseAPI.TalkServiceimport TalkService# ログインアルゴリズムはスパゲッティのように複雑なコードであるため省略します。msg= Messagemsg= Message(to=groupId, text="Hello World")# グループIDはパケットを解析するか、またはグループ情報を取得するためのThrift関数を使用して取得することができます。client.sendMessage(reqSeq, msg)爆発するぞ!


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 0param1 128658291param2 295818notice23moretab304stickershop234channel205denykeyword244connectioninfo148buddy256timelineinfo8themeshop41callrate43configuration348sticon52suggestdictionary144suggestsettings281usersettings0analyticsinfo289searchpopularkeyword224searchnotice169timeline99searchpopularcategory287extendedprofile254seasonalmarketing34newstab84suggestdictionaryv2106chatappsync337agreements323instantnews147emojimapping96searchbarkeywords38shopping256chateffectbg223chateffectkw27searchindex276hubtab109payruleupdated144smartch244homeservicelist296timelinestory289wallettab261podtab183reqSeq -1revision -1type 0パズルが好きなら、とても簡単に答えを出すことができます。
なぜLINEがそのようにしてキーを決定しているのかは不明ですが、
とにかく、それが正常に動作していることを確認できます。
ご覧いただき、ありがとうございました。 - らと
