從分析一個賭球APP中入門安卓逆向、開發、協議分析
APP開發的背景知識的介紹
APP開發遵循邏輯和視圖分離的思想:我們創建一個activity,android studio會自動生成其對應的xml文件。
注意,任何的activity都要在AndroidManifest.xml中定義。(一般androidstudio會自動完成)
視圖
視圖在xml中定義:可以直接可視化移動一個按鈕進視圖,也可以用代碼編寫。每個元素都會有一個id留給activity去調用。比如按鈕A對應一個id,按鈕B對應一個Id。
在xml中:
@id/id_name表示引用這個id。
@+id/button1 表示定義一個id。
邏輯
邏輯在activity中定義:activty要加載上面定義的視圖,即布局,要調用setContentView(布局文件的id)。(項目添加的任何資源都會在R文件中生成一個資源id,這里布局文件的id即為對應xml文件的id)
如果要對布局進行一些操作,也是在activity中定義。比如說監聽按鈕的點擊事件,在Java中要使用findviewByID()方法獲取布局文件中定義的元素,然后再定義該元素的函數的內容,比如按鈕元素的話就可以定義其setonclicklistener函數。
而Kotlin不需要使用findviewbyID(),直接使用元素的名字就可以調用該元素了。
(1)邏輯間如何跳轉---intent
在activity中按鈕的listen函數中定義
intent = Intet(this,另一個activity)startactivity(intent)
即可實現跳轉頁面。
(2)隱式intent
不指定跳轉到哪個activity,而是指定跳轉動作和類型,讓系統來選擇合適的activity。我們可以在AndroidManifest.xml中設置activity的可以相應的動作和類型、相應的協議類型(scheme)。
intent不僅可以打開activity,也可以打開網頁。
intent = Intet(intent.action_view)intent.data = uri.parse('www.baidu.com')startactivity(intent)
甚至可以通過intent向下一個或者上一個頁面傳遞數據。
(3)常用UI控件---textview
在activity的xml中定義,顯示文字。
width和height有三個可選值:
1、match_parent:和父布局大小一樣(即和手機屏幕大小一樣)
2、wrap_content:恰好包住里面內容。
3、固定值。
還有其他的屬性可以選:是否居中,文字顏色,文字大小。
APP逆向過程
目標:給一個APK反匯編出java源代碼。
流程:
1、先用壓縮包提取classes.dex文件。

2、用dex2jar提取出jar文件,并將這個文件拷至dex2jar工具存放目錄下。

打開控制臺,使用cd指令進入到dex2jar工具存放的目錄下。進入到dex2jar目錄下后,輸入“d2j-dex2jar.bat classes.dex”指令運行。

執行完畢,查看dex2jar目錄,會發現生成了classes.dex.dex2jar.jar文件。

3、將jar文件導入jd.gui看java源碼
上一步中生成的classes.dex.dex2jar.jar文件,可以通過JD-GUI工具直接打開查看jar文件中的代碼。

查找某個字符串在哪個頁面出現的小trick
res/values/string.xml存了APK的字符串。
同目錄下的public.xml有其對應的id,查找當前目錄下包含0x7f10002f的文件。
findstr.exe /s /i "0x7f10002f" *.*outdir\res\values\public.xml: <public type="string" name="activity_alipay_real_name_hint" id="0x7f10002f" />outdir\smali\com\happy\roulette\R$string.smali:.field public static final activity_alipay_real_name_hint:I = 0x7f10002f
賭球APP分析實戰
定位第一個APP界面
我們用apktool解析出apk的文件夾如下:(安裝apktool前要先安裝java1.8.關于apktool如何安裝參考https://www.jianshu.com/p/b027856d55ac)
安裝完并配置好環境變量后,用以下命令反編譯輸出到baz目錄。
apktool d xxx.apk -o baz

從AndroidManifest.xml搜索android.intent.action.MAIN"定位到如下:
-<activity android:name="com.happy.roulette.activity.SplashActivity" android:theme="@style/Theme.AppCompat.Light.NoActionBar.FullScreen" android:screenOrientation="portrait"> -<intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> intent-filter> activity>
所以第一個主頁面是com.happy.roulette.activity.SplashActivity,以下便是第一個界面:

我們從com.happy.roulette.activity.SplashActivity.class的oncreate函數開始看(因為Oncreate函數是進入到一個新頁面后要執行的第一個函數)。
protected void onCreate(@Nullable Bundle paramBundle) { super.onCreate(paramBundle); setContentView(2131492944);//加載定義好的布局 TextView textView = (TextView)_$_findCachedViewById(R.id.tv_version_info); //設置文字 Intrinsics.checkExpressionValueIsNotNull(textView, "tv_version_info"); textView.setText("37_2.2.40"); //設置文字 checkLocalHost();}
我們繼續看checkLocalHost()函數,每次啟動第一個界面都會檢查一下host。
private final void checkLocalHost() { String str = HostManager.INSTANCE.loadHostUrl();//先取出url HostManager.INSTANCE.setNeedGetHost(true); checkAppMaintain(str, true); //然后驗證url是否能連接}
loadHostUrl函數會先取出url, 我們繼續跟蹤loadHostUrl。
public final class HostManager { public final String loadHostUrl() { mSharedPreferencesManager = new SharedPreferencesManager(MyApplication.getAppContext()); SharedPreferencesManager sharedPreferencesManager = mSharedPreferencesManager; if (sharedPreferencesManager == null) Intrinsics.throwUninitializedPropertyAccessException("mSharedPreferencesManager"); return sharedPreferencesManager.get("key-host-url", ""); }
發現使用了sharepreferences存儲這個url在本地,以下想在本地找到保存這個url的文件。
在模擬器上運行該APP,打印出APP的package和當前頁面的acitivy,以下為APP在主頁面時運行命令得到的結果。
C:\Users\Administrator>adb shell dumpsys window | findstr mCurrentFocus mCurrentFocus=Window{b007190 u0 com.cxinc.app.n9h/com.happy.roulette.activity.MainActivity}
以下為APP在登錄頁面時運行命令得到的結果:
C:\Users\Administrator>adb shell dumpsys window | findstr mCurrentFocus mCurrentFocus=Window{ae65cbc u0 com.cxinc.app.n9h/com.happy.roulette.activity.login.LoginActivity}
通過 run-as 命令進入APP的文件目錄下:
C:\Users\Administrator>adb devicesList of devices attachedemulator-5554 deviceC:\Users\Administrator>adb -s emulator-5554 shellemulator64_x86_64:/ $ run-as com.cxinc.app.n9hrun-as: package not debuggable: com.cxinc.app.n9h
發現無法進入這個APP的目錄,因為不是debug版本的APP。所以后面會通過抓包來獲取這個url。
我們繼續看checkLocalHost函數,上面我們無法分析出loadHostUrl在本地哪個文件獲取url。
我們繼續看后面的函數調用過程,通過loadHostUrl函數獲取到url后,會調用checkAppMaintain來檢查這個url。
private final void checkAppMaintain(String paramString, boolean paramBoolean) { this.mHostApi.checkUrl(paramString, new SplashActivity$checkAppMaintain$1(paramString, paramBoolean)); }
一直跟蹤下去,發現這里會請求這個url。發現會在這個url后拼接/api/checkAppWh.do,然后發送請求。
public void checkUrl(String paramString, BaseWebApi.ResultListener paramResultListener) { this.mAppUrl = paramString; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(this.mAppUrl); stringBuilder.append("/api/checkAppWh.do"); StringRequest stringRequest = createStringRequest(0, stringBuilder.toString(), null, paramResultListener); getRequestQueue().add((Request)stringRequest);}
tcpdump抓包
tcpdump是常用的一個抓包工具,linux或android環境下已經默認安裝好。
當用android studio自帶的模擬器啟動APP后,在電腦終端輸入adb shell進入模擬器終端:
C:\Users\Administrator>adb shell
進入模擬器終端后用tcpdump命令進行抓包,包保存在/sdcard/capture.pcap
emulator64_x86_64:/ # tcpdump -i any -p -n -s 0 -w /sdcard/capture.pcap
-i是指定網卡為any;
-w表示保存為pacp;
s 0 : tcpdump 默認只會截取前 96 字節的內容,要想截取所有的報文內容,可以使用 -s number, number 就是你要截取的報文字節數,如果是 0 的話,表示截取報文全部內容。
-p : 不讓網絡接口進入混雜模式。默認情況下使用 tcpdump 抓包時,會讓網絡接口進入混雜模式。一般計算機網卡都工作在非混雜模式下,此時網卡只接受來自網絡端口的目的地址指向自己的數據。當網卡工作在混雜模式下時,網卡將來自接口的所有數據都捕獲并交給相應的驅動程序。如果設備接入的交換機開啟了混雜模式,使用 -p 選項可以有效地過濾噪聲。
抓包結束后按Cltr+C中斷后即可以保存文件。
我們在PC終端中把模擬器的抓取的包拿回來本地D盤,在本地終端執行。
C:\Users\Administrator>adb pull /sdcard/capture.pcap d:/capture.pcap/sdcard/capture.pcap: 1 file pulled, 0 skipped. 2.6 MB/s (108623 bytes in 0.040s)
通過wireshark分析如下:
首先會進行DNS請求,獲取這個域名對應的IP:發現請求9h.app00app.com這個域名,且IP為204.11.56.48。

checkmaintain執行完后會跳到下面這個回調函數里,即請求服務器后會回到以下函數。
public static final class SplashActivity$checkAppMaintain$1 implements BaseWebApi.ResultListener { SplashActivity$checkAppMaintain$1(String param1String, boolean param1Boolean) {} //如果請求失敗了調用OnError,說明當前請求的域名失效了 public void onError(@NotNull ErrorOutput param1ErrorOutput) { Intrinsics.checkParameterIsNotNull(param1ErrorOutput, "error"); Log.e("SplashActivity", "APP ); if (this.$isFailToGetHost) { SplashActivity.this.getHost(); //調用這個獲取新的url,然后發送請求:是https://9h.開頭的 return; } SplashActivity.this.showErrorRetryDialog("); } //如果請求成功了:以下代碼有兩個case:case49,case48。 public void onResult(@NotNull String param1String) { Context context; Intrinsics.checkParameterIsNotNull(param1String, "response"); Log.i("SplashActivity", "APP ); switch (param1String.hashCode()) { case 49: if (param1String.equals("1")) { context = (Context)SplashActivity.this; StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(WebServerUrl.getBaseUrl()); stringBuilder.append("/wh.html"); //通過wh.html可以看出wh是維護的縮寫,且下面也標識了“維護中”, //所以推測這是服務器維護時會返回一個code,此時會執行下面代碼的跳轉, //比如跳轉到9h.app00app.com/wh.html JumpUtil.ToWeb(context, stringBuilder.toString(), "維護中“); SplashActivity.this.finish(); return; } break; case 48: if (context.equals("0")) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("Selected host url: "); stringBuilder.append(this.$selectedHostUrl); Log.i("SplashActivity", stringBuilder.toString()); WebServerUrl.setBaseUrl(this.$selectedHostUrl); SplashActivity.this.getAppConfig(); //將服務器返回的參數用來設置APP return; } break; } onError(new ErrorOutput()); } }
發現請求這個IP,連接不上。所以會執行上面Onerror函數去獲取另外一個域名。

Onerror函數會調用gethost()
private final void getHost() { this.mHostApi.getHost(new SplashActivity$getHost$1());}
繼續跟蹤:
public void getHost(BaseWebApi.ResultListener paramResultListener) { this.i = 0; this.mClientResultListener = paramResultListener; sendGetHostRequest(getNextServerUrl());}
看getNextServerUrl,它會將“https://9h.”拼接域名list中一個域名。
private String getNextServerUrl() { try { WebServerUrl.setCurrentServerUrl(WebServerUrl.SERVER_URL_LIST.get(this.i)); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("https://9h."); stringBuilder.append(WebServerUrl.SERVER_URL_LIST.get(this.i)); stringBuilder.append("/api/getAppConfig.do"); return stringBuilder.toString(); } catch (Exception exception) { exception.printStackTrace(); return ""; }
跟蹤SERVER_URL_LIST,發現這是一個域名的list,包含以下域名。猜測這種讀博網站域名經常被封,所以要多準備幾個域名。
public static final List<String> SERVER_URL_LIST = Arrays.asList(new String[] { "app00app.com", "app66app.com", "app99app.vip", "app66app.vip", "app88app.vip" });
獲取到域名之后,又會繼續去執行checkAppMaintain這個函數去檢測域名,即重復上面步驟,直到找到一個可以連接的域名,然后會進入上面的Onresult函數的case48。
通過抓包分析,這里請求了上面域名list中的第二個域名app66app.com

然后與得到的IP地址進行TCP連接,以下為三次握手和密鑰協商過程。

為了學習TLS協議,下面我們分析一下協議的過程。

可以看到是TLS1.3協議,先看看Client hello這條消息。

我們可以看到Transport layer Security就是傳輸層安全(TLS),
TLS1.3總共有兩層,分別是握手協議(handshake protocol)和記錄協議(record protocol),握手協議在記錄協議的上層,記錄協議是一個分層協議。其中握手協議中還包括了警告協議(alert protocol)。

(圖來自https://blog.csdn.net/SkyChaserYu/article/details/104716229#t3,以下部分轉自該博客)
接下來看一下Handshake protocol:Hello中的內容:

Handshake Type:ClientHello,表示握手消息類型,此處是ClientHello
Length:508,即長度為508。
Version:TLS1.2(0x0303),表示版本號為1.2,TLS1.3中規定此處必須置為0x0303,即TLS1.2,起到向后兼容的作用。1.3版本用來協商版本號的部分在擴展當中,而之前的版本就在此處進行。
Random,隨機數,是由安全隨機數生成器生成的32個字節。
Session ID Length:會話ID的長度。
Session ID,會話ID,TLS 1.3之前的版本支持“會話恢復”功能,該功能已與1.3版本中的預共享密鑰合并。為了兼容以前的版本,該字段必須是非空的,因此不提供TLS 1.3之前會話的客戶端必須生成一個新的32字節值。該值不必是隨機的,但應該是不可預測的,以避免實現固定在特定值,否則,必須將其設置為空。
Cipher Suites Length,即下面Cipher Suites的長度。
Cipher Suites是密碼套件,表示客戶端提供可選擇的加密方式,如圖所示:

每個加密套件都包含,密鑰交換,簽名算法,加密算法,哈希算法。
Compression Methons (1 method)表示壓縮方法,長度為1,內容為空
Exentisons擴展部分,是TLS1.3才開始使用,是TLS1.3的顯著特征。每一個擴展都包含類型(type),長度(length)和數據(data)三個部分。
下面分析幾個相對重要的擴展:
1)key_share
key_share 是橢圓曲線類型對應的公鑰,如圖所示:

此處包含一個KeyShareEntry,是x25519曲線組,這是客戶端生成的代表自己支持的DH組,具體數據在KeyExchange字段中;每個KeyShareEntry都代表一組密鑰交換參數,對于有限域DH來說是g和p的值,對于橢圓域DH是橢圓曲線和基點的值,很明顯這里是用了橢圓曲線的DH。
同選定加密組件一樣,TLS 1.3定義了幾組gp值,雙方只需要協商想要使用的gp對即可。具體實施過程,為每個組生成一個DH密鑰交換的參數,將其組名和參數值封裝在key_share擴展中,服務端選定DH組后,返回一個封裝好的key_share,雙方根據交換的公鑰參數和自己持有的私鑰參數計算出DH最終密鑰。
理論上,客戶端應該將所有與密鑰協商有關的擴展(pre_shared_key、shared_key)都發送給服務端,服務端選定哪一種,再將對應選定的擴展返還給客戶端,如果服務端同時使用兩種密鑰協商,則返還所有擴展,
然后我們來看下EXDH的密鑰協商過程,首先EC的意思是橢圓曲線,這個EC提供了一個很厲害的性質,你在曲線上找一個點P,給定一個整數k,求解另外一個點Q=kP很容易,給定兩個點P,Q,知道Q =kP,求k卻是個難題。
在這個背景下,給定一個大家都知道的大數G,client在每次需要和server協商秘鑰時,生成一段隨機數a,然后發送A=aG給server,server收到這段消息(aG)后,生成一段隨機數b,然后發送B=bG給client,然后server端計算(aG)b作為對稱秘鑰,client端收到后bG后計算a(Gb),因為(aG)b = a(Gb),所以對稱秘鑰就是aGb。
攻擊者只能截獲A=aG和B=bG,由于橢圓曲線難題,知道A和G是很難計算a和b的,也就無法計算aGb了(當然,實際上的計算過程和原理證明不是這么簡單的,中間還有一個取模的過程,以及取模過程的交換律和結合律證明,但是本質思想和這個是差不多的)。留意下TLS1.3圖中的key_share,這段的功能就是直接記錄了aG,然后包含在client_hello中。然后server收到后在server_hello的key_share段中記錄bG。所以TLS1.3一個RTT就搞定握手了。
參考:https://blog.csdn.net/zk3326312/article/details/80245756
2)signature_algorithms

Signature_algorithms擴展是,客戶端提供簽名算法,讓服務器選擇
以第一個簽名算法為例,ecdsa_secp256r1_sha256,使用sha256作為簽名中的哈希,簽名算法為ecdsa。
3)psk_key_exchange_modes

TLS 1.3 與之前的協議有較大差異,相比過去的的版本,引入了新的密鑰協商機制 — PSK。TLS 1.3支持DH、PSK兩種密鑰協商機制,也支持同時使用兩者進行密鑰協商。
psk_key_exchange_modes表示psk密鑰交互模式選擇
此處的PSK模式為(EC)DHE下的PSK(貌似就是使用上面的ECDHE進行密鑰協商),客戶端和服務器必須提供KeyShare。
如果是僅PSK模式,則服務器不需要提供KeyShare。
下面分析server hello:

可以看到Record layer下面有三個協議:
1、握手協議:確定了加密套件為TLS_AES_128_GCM_SHA256,確定了密鑰協商的隨機數bG
2、密鑰交換協議
3、應用數據協議-https
4、SNI Service name indication

由于服務器能力的增強,在一臺物理服務器上部署多個虛擬主機已經成為十分流行的做法了。在過去的 HTTP 時代,解決基于名稱的主機同一 ip 地址上托管多個網站的問題并不難。
當一個客戶端請求某特定網站時,把請求的域名作為主機頭(host)放在 http header 中,從而服務器根據域名可以知道把該請求引向哪個域名服務,并把匹配的網站傳送給客戶端。但是此方式到 https 就失效了,因為 SSL 在握手的過程中,不會有 host 信息,所以服務端通常返回配置中的第一個可用證書,這就導致不同虛擬主機上的服務不能使用不同證書(但在實際中,證書通常是與服務對應。)
所以通過這個字段我們有可能能識別出APP對應的服務是什么。
參考:https://blog.csdn.net/u010217394/article/details/121713758
當APP檢測到請求成功,即網站還能被訪問時,會調用getAppConfig()。
private final void getAppConfig() { TextView textView = (TextView)_$_findCachedViewById(R.id.tv_progress_msg); Intrinsics.checkExpressionValueIsNotNull(textView, "tv_progress_msg"); textView.setText("正在獲取平臺配置,請稍等“); this.mHomeApi.getAppConfig(new SplashActivity$getAppConfig$1()); }
繼續跟蹤this.mHomeApi.getAppConfig,發現它向服務器請求了一個json文件,猜測會將服務器返回的配置信息用來配置APP。
public void getAppConfig(BaseWebApi.ResultListener paramResultListener) { StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append(WebServerUrl.getBaseUrl()); stringBuilder.append("/static/data/config.json"); StringRequest stringRequest = createStringRequest(0, stringBuilder.toString(), null, paramResultListener); getRequestQueue().add((Request)stringRequest);}
抓包如下:

發現本地請求的數據是TLS傳輸,下面能看到數據是加密的數值。
請求完之后進入回調函數如下:
public static final class SplashActivity$getAppConfig$1 implements BaseWebApi.ResultListener { //請求失敗 public void onError(@NotNull ErrorOutput param1ErrorOutput) { Intrinsics.checkParameterIsNotNull(param1ErrorOutput, "error"); Log.e("SplashActivity", "Gat App Config ); SplashActivity.this.showErrorRetryDialog("); } //請求成功,則這里傳進來的參數param1String即為服務器返回的響應。 public void onResult(@NotNull String param1String) { Intrinsics.checkParameterIsNotNull(param1String, "response"); Log.i("SplashActivity", "Gat App Config ); try { //把獲取到json配置給APP AppConfigOutput appConfigOutput = (AppConfigOutput)(new Gson()).fromJson(param1String, AppConfigOutput.class); AppConfigManager.INSTANCE.setAppConfig(appConfigOutput); SplashActivity splashActivity = SplashActivity.this; String str = appConfigOutput.defaultSkin; Intrinsics.checkExpressionValueIsNotNull(str, "appConfigOutput.defaultSkin"); //這里就會跳轉到mainactivity,即APP的主頁面 splashActivity.judgeSkin(str); return; } catch (Exception exception) { exception.printStackTrace(); onError(new ErrorOutput()); return; } }}
跟蹤judgeSkin:
private final void judgeSkin(String paramString) { if (AppConfigManager.INSTANCE.loadIsShowDefaultSkin()) { if (Intrinsics.areEqual(paramString, SystemSettingsManager.SkinStyle.BLUE.toString())) { SystemSettingsManager.INSTANCE.setColorSkin(SystemSettingsManager.SkinStyle.BLUE); } else if (Intrinsics.areEqual(paramString, SystemSettingsManager.SkinStyle.RED.toString())) { SystemSettingsManager.INSTANCE.setColorSkin(SystemSettingsManager.SkinStyle.RED); } else if (Intrinsics.areEqual(paramString, SystemSettingsManager.SkinStyle.DARK.toString())) { SystemSettingsManager.INSTANCE.setColorSkin(SystemSettingsManager.SkinStyle.DARK); } AppConfigManager.INSTANCE.saveIsShowDefaultSkin(false); } goHomePage(); }
跟蹤goHomePage:這里就會跳轉到mainactivity,即APP的主頁面MainActivity.class,并且結束當前頁面。
private final void goHomePage() { if (!isFinishing()) { startActivity(new Intent((Context)this, MainActivity.class)); finish(); }
以下即跳入主頁面:

以下我們看看MainActivity.class的oncreate函數:
protected void onCreate(@Nullable Bundle paramBundle) { super.onCreate(paramBundle); setContentView(2131492931); //設置布局 initFragment(); initNavigation(); //設置導航欄 setNavigationListener(); //設置導航欄的監聽 if (HostManager.INSTANCE.isNeedGetHost()) getHost(); }
其gethost函數會執行以下函數:
private final void getHost() { (new HostApi()).getHost(new MainActivity$getHost$1());}
繼續跟蹤getHost:
public void getHost(BaseWebApi.ResultListener paramResultListener) { this.i = 0; this.mClientResultListener = paramResultListener; sendGetHostRequest(getNextServerUrl()); }}
跟蹤getNextServerUrl,這里是請求服務器去獲取/api/getAppConfig.do這個配置文件(上面是獲取/static/data/config.json文件)。
private String getNextServerUrl() { try { WebServerUrl.setCurrentServerUrl(WebServerUrl.SERVER_URL_LIST.get(this.i)); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.append("https://9h."); stringBuilder.append(WebServerUrl.SERVER_URL_LIST.get(this.i)); stringBuilder.append("/api/getAppConfig.do"); return stringBuilder.toString(); } catch (Exception exception) { exception.printStackTrace(); return ""; }}
到這里好像也沒配置什么信息。
public static final class MainActivity$getHost$1 implements BaseWebApi.ResultListener { public void onError(@NotNull ErrorOutput param1ErrorOutput) { Intrinsics.checkParameterIsNotNull(param1ErrorOutput, "error"); Log.e("MainActivity", "Get Host ); MainActivity.this.showErrorCloseDialog("獲取服務器失敗,請先檢查網絡“); } public void onResult(@NotNull String param1String) { Intrinsics.checkParameterIsNotNull(param1String, "resultAppUrl"); Log.i("MainActivity", "Get Host ); if ((Intrinsics.areEqual(param1String, HostManager.INSTANCE.loadHostUrl()) ^ true) != 0) { HostManager.INSTANCE.saveHostUrl(param1String); MainActivity.this.showErrorCloseDialog("線路有更新,需要重啟APP”); } }}
下面我又點擊了導航欄,跳轉到其他頁面。

通過抓包發現,它又請求了其他的域名,并且后面和該域名進行了TCP連接,并且傳輸了加密的數據。


此時server name與上面不同,后面又有client hello請求。

又出現了新的server name。