SlideShare a Scribd company logo
How to reverse engineer
Android applications
Finding Vulnerabilities through Reverse Engineering
Hasso Plattner Institute, Potsdam
Hubert Hesse, Lukas Pirl,
Christoph Matthies, Conrad Calmez
using a popular word game
as an example
??
Images: “Freepik” on flaticon.com (CC BY 3.0), Google (CC BY 3.0)
1 Get the .apk
23 4
Extract the .apk
5
Decompilation
to Smali
Debugging
6Putting it
together
7 8Automation
Proxy
Decompilation
to Java
Our Example—a word game
● Top 10 word game in 145 countries (as of July 2014)
● More than 10.000.000 installs
● Over 50 million players
● Play online (with friends)
● 14 languages
● Free and premium version
1:58 0 points
S N B I
L U SF
E I T
T E RP
A
1:58 15 points
S N B I
L U SF
E I T
T E RP
A
FLUT +15
● APK (application package file),
archive file, based on JAR format
● Similar to Deb packages (in Ubuntu) or
MSI packages (in Windows)
● Contains program code, resources, assets, certificates, and
manifest file
● Can’t be directly downloaded from App Store
1
Get the .apk
Download using online “APK Downloader”
(http://apps.evozi.com/apk-downloader/)
- or -
Install on device and download using SDK tools
(adb pull <app_path> downloaded.apk)
2
Extract the .apk
● Normal decompression using unzip fails
● Special tool: APKTool
○ Standard is APKTool 1.5.2. (not able to recompress correctly) (https:
//code.google.com/p/android-apktool/downloads/list)
○ APKTool 2.0.0 Beta 9 works
(http://connortumbleson.com/2014/02/apktool-2-0-0-beta-9-released/)
Decrompressing:
apktool d -d game.apk -o outdir
2
Extract the .apk
2
Modifying resources
● Change arbitrary resources
● Repack into .apk file and install
Recrompressing:
apktool b -d outdir -o com.company.game.free_patch.apk
● Recompression works, Android fails with “can’t install”, wrong
certificate
○ APKTool tries to reuse as much as possible, doesn’t
recompute signature
2Manually sign repacked apk:
● Create custom CA
● Java JAR Signing and Verification Tool
(http://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html)
jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my-
release-key.keystore com.company.game.free_patch.apk alias_name
Modifying resources
How to reverse engineer Android applications
.apk contains compiled code
● Dalvik bytecode interpreted by
the Dalvik Process virtual machine
● Stored in .dex (Dalvik EXecutable) files
APKTool translates this to “smali” (https://code.google.com/p/smali/)
● Abstraction of bytecode, closer to Java
● Dalvik opcodes (http://s.android.com/tech/dalvik/dalvik-bytecode.html)
● Can be edited directly
3Decompilation to Smali
.class public LHelloWorld;
.super Ljava/lang/Object;
.method public static main([Ljava/lang/String;)V
.registers 2
sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream;
const-string v1, "Hello World!"
invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V
return-void
.end method
3
Smali Hello World
Interactive debugging
● Set debuggable=”true” in AndroidManifest.xml
○ Repack using APKTool
● Need to connect smali sources to binary
● Workaround: pretend we have valid Java code
4
Debugging
<application android:allowBackup="true" android:hardwareAccelerated="true"
android:icon="@drawable/launcher_icon" android:label="@string/app_name"
android:name="com.company.game.core.GameApplication" android:theme="
@style/Theme.GameTheme" android:debuggable="true">
a=0;// .class public abstract La;
a=0;// .super Ljava/lang/Object;
a=0;//
a=0;//
a=0;// # instance fields
a=0;// .field protected final a:Ljava/lang/Object;
a=0;//
a=0;// .field private final b:Landroid/os/Handler;
a=0;//
4
Debugging
Smali code in comments
Placeholder
Java
Two ways to obtain java code
● Convert .dex files to .jar
○ Use standard java bytecode decompilers
● Disassemble .dex directly to .java
5
Decompilation to Java
Using dex files
● Androguard (https://code.google.com/p/androguard/)
○ Maps DEX format into full Python objects
○ Works in memory (My 4GB machine wasn’t enough)
○ Doesn’t immediately dump code into Java files
5
Decompilation to Java
Using jar files
● dex2jar (https://code.google.com/p/dex2jar/)
○ dex2jar, jar2dex, apk-sign
○ Supports recreating .dex from Java
● JD-GUI (http://jd.benow.ca/)
○ Popular jar-decompiler
○ Works 100% with “Hello World” app
5
Decompilation to Java
Combining Java decompilation and Smali
● Java more readable than Smali
● Unfortunately Java decompilation not
100% perfect
○ Invalid Java constructs or only
method signatures
○ Cannot recompile from Java sources
6
Putting it together
private void fixSpecialChars()
{
int i;
char ac[];
int j;
int k;
i = 0;
ac = tiles;
j = ac.length;
k = 0;
_L9:
if(k >= j)
break MISSING_BLOCK_LABEL_161;
ac[k];
JVM INSTR lookupswitch 6: default 80
// 40: 125
// 41: 137
// 47: 149
// 91: 89
// 92: 101
// 93: 113;
goto _L1 _L2 _L3 _L4 _L5 _L6 _L7
_L4:
break MISSING_BLOCK_LABEL_149;
_L1:
break; /* Loop/switch isn't completed */
_L5:
break; /* Loop/switch isn't completed */
_L10:
i++;
k++;
if(true) goto _L9; else goto _L8
_L8:
6When Decompilation fails
an example
Goto not supported in Java
Bare JVM instructions
Combining Java decompilation and Smali
● Approach: Use multiple Java decompilers
○ They tend to fail in different places
6
Putting it together
1. Find interesting parts in Java source
2. Check corresponding smali sources
3. Edit those
protected void roundEnd(boolean paramBoolean)
{
// …
this.resultData.setTotalScore(this.totalScore);
// …
startRoundSummary();
if (!this.isPractice)
{
this.currentRound.setWordsInRound(this.resultData.getMoves().size());
// …
this.currentRound.setPlayer1Moves(GameHelper.encodeMoves(this.resultData.
getMoves()));
this.currentRound.setPlayer1Score(this.totalScore);
// …
6
Manipulating the score
Opportunities for manipulation
● Server validation disallows this
a=0;// sget-boolean v0, Lcom/company/game/core/statics/Statics;->DEBUGGING:Z
a=0;//
a=0;// #v0=(Boolean);
-a=0;// if-eqz v0, :cond_0
+a=0;// #if-eqz v0, :cond_0
a=0;//
6
Enable Logging
public class Toolkit
{
// …
public static void Logw(String s, String s1)
{
if(Statics.DEBUGGING)
Log.w(s, s1);
}
// …
a=0;// # static fields
a=0;// .field public static ROUND_DURATION_IN_SECONDS_FOR_NORMAL_GAME:I
a=0;// .field public static ROUND_DURATION_IN_SECONDS_FOR_TUTORIAL:I
a=0;//
a=0;// .method static constructor <clinit>()V
a=0;// .locals 1
…
-a=0;// const/16 v0, 0x78
+a=0;// const/16 v0, 0x12c
a=0;//
a=0;// #v0=(PosByte);
a=0;// sput v0, Lcom/company/game/core/statics/GameStatics;->ROUND_DURATION_IN_SECONDS_FOR_NORMAL_GAME:I
6
More time per round
120s
300s
public static boolean allowPremiumContent(PremiumType premiumtype, Context context)
{
if(premiumIsPurchased(context))
return true;
synchronized(lock)
{
if(!isLicensed(context))
break MISSING_BLOCK_LABEL_31;
}
return true;
6
Getting Premium
a=0;// .line 129
-a=0;// invoke-static {p0}, Lcom/company/game/util/PremiumCampaignHelper;->premiumIsPurchased(…;)Z
+a=0;// # invoke-static {p0}, Lcom/company/game/util/PremiumCampaignHelper;->premiumIsPurchased(…;)Z
a=0;//
-a=0;// move-result v0
+a=0;// # move-result v0
a=0;//
-a=0;// #v0=(Boolean);
-a=0;// if-eqz v0, :cond_0
+a=0;// #v0=(One);
+a=0;// # if-eqz v0, :cond_0
6
Getting Premium
free version premium (stats unlocked, no ads)
7
Proxy
Route all app traffic through custom proxy
● Used MitMProxy (https://github.com/mitmproxy/mitmproxy)
● Retrieve real server URL via Wireshark
● Redirect app traffic via /etc/hosts on device
● Custom SSL certificate
○ Install own CA in device
○ No certificate pinning
● Avoid compressed responses via HTTP header
○ Accept-Encoding: gzip;q=0,deflate,sdch
7
Proxy
AES encryption
● Shared key in decompiled code
● No key derivation function
● AES initialization vector in HTTP header
○ Payload-session: 2e2f6a61642f7372…
○ Unencrypted
// file APIConnector.java
private static byte sharedKey[] = {
57, -116, 126, 39, 116, -25, -95, -106, -81, 48,
-33, -19, 120, 118, 35, 40, 66, 126, 31, 30,
-83, 76, 31, 93, 13, -122, -50, 68, -108, -114, 28, -80
};
SSL
MitM
Proxy SSLHTTP
Server by “aLf “, thenounproject.com (CC BY 3.0 US)
Spy by “Hopstarter ”, iconarchive.com (CC BY-NC-ND 4.0)
#! python
#decrypt AES
#using IV
7
ProxyHeader: AES IV
AES payload
HTTP
# /etc/hosts
# redirect
# to proxy
7
Proxy
{
"cacheTimestamp": "1405377910521",
"userId": "0",
"conversationId": "-1",
"player1MostWordsInRound": "32",
"id": "6602198229545556683",
"player1Score": "214",
"player1LongestWord": "HEAPS",
"player1User": {
"username": "username",
"ranking": "0",
"premium": "false",
"recruits": "0",
"deleted": "false",
"newUser": "false",
"bestScoreInMatch": "0",
"userId": "3005807464",
"bestScoreInRound": "0",
"online": "false",
"facebookConnected": "false",
"avatarId": "0",
"matchesPlayed": "0",
"useFacebookImage": "false",
"mostWordsInRound": "0"
},
{"rounds": [
{
"seed3": "14657688",
"player2MoveErrors": "0",
"gameId": "6602198229545556683",
"player2SwipeDistance": "681",
"player2Moves":
"1AB2BAE2EAB216227612AEF2DA73840127652567354013DAB723673
B7654EAB72",
"player1MoveErrors": "19",
"player2Done": "true",
"seed1": "2073207065",
"seed2": "680974433",
"player1SwipeDistance": "1608",
"board": {
"bonus": [" ", " ", " ", " ",
" ", " ", "D", " ",
" ", " ", " ", " ",
" ", " ", " ", "T"
],
"board": ["A", "T", "E", "H",
"E", "P", "O", "T",
"H", "S", "A", "S",
"T", "F", "T", "E"
],
"words": [
"TATE",
"SOTS",
"HOST",
"SAPS",
"FATSOS",
…
Server response
request size up to 100kB
8
Automation
Play the game automatically
● Generic external approach
○ No modification of binary necessary
○ Works for any app
Monkeyrunner (http://developer.android.com/tools/help/monkeyrunner_concepts.html)
● Test apps at the functional/framework level
● Able to simulate keystrokes, take screenshots
● Python bindings
8Obtain all possible words
to play correctly
● apk contains .jet “dictionary” for
each language
● Btw, also a wordlist (probably)
used to check for cheaters
Automation
8
Automation
Ruzzle .jet files
● Binary files
● Trie / Radix tree structure
● Optimal for the way the game
is played
● No duplicate encoding
of characters
● List of all excepted
words constructable
G
GA
GAM
GAME
GO
GOD GOT
G
O
D T
A
M
E
8
Automation
Achieving the highscore
● Get all 16 letters
○ Input by hand / screenshot + OCR
● Find all valid words using the extracted
dictionary
● Simulate keystrokes for found words
○ Actually not enough time to enter all
valid words
8
Automation
DEMO
Achievements
Found possibilities to:
✓ Enable logging
✓ Unlock premium features
✓ Achieve insanely high score through automation
✓ Extract protocol via man-in-the-middle attack
Backup slides
Pinned certificate
(installed at dev.
time)
App
Server
Get current
server
certificate
1
Compare
current and
pinned
certificates
2
if identical:
establish
connection
else: reject
3
Certificate Pinning

More Related Content

How to reverse engineer Android applications

  • 1. How to reverse engineer Android applications Finding Vulnerabilities through Reverse Engineering Hasso Plattner Institute, Potsdam Hubert Hesse, Lukas Pirl, Christoph Matthies, Conrad Calmez using a popular word game as an example ?? Images: “Freepik” on flaticon.com (CC BY 3.0), Google (CC BY 3.0)
  • 2. 1 Get the .apk 23 4 Extract the .apk 5 Decompilation to Smali Debugging 6Putting it together 7 8Automation Proxy Decompilation to Java
  • 3. Our Example—a word game ● Top 10 word game in 145 countries (as of July 2014) ● More than 10.000.000 installs ● Over 50 million players ● Play online (with friends) ● 14 languages ● Free and premium version
  • 4. 1:58 0 points S N B I L U SF E I T T E RP A
  • 5. 1:58 15 points S N B I L U SF E I T T E RP A FLUT +15
  • 6. ● APK (application package file), archive file, based on JAR format ● Similar to Deb packages (in Ubuntu) or MSI packages (in Windows) ● Contains program code, resources, assets, certificates, and manifest file ● Can’t be directly downloaded from App Store 1 Get the .apk Download using online “APK Downloader” (http://apps.evozi.com/apk-downloader/) - or - Install on device and download using SDK tools (adb pull <app_path> downloaded.apk)
  • 7. 2 Extract the .apk ● Normal decompression using unzip fails ● Special tool: APKTool ○ Standard is APKTool 1.5.2. (not able to recompress correctly) (https: //code.google.com/p/android-apktool/downloads/list) ○ APKTool 2.0.0 Beta 9 works (http://connortumbleson.com/2014/02/apktool-2-0-0-beta-9-released/) Decrompressing: apktool d -d game.apk -o outdir
  • 9. 2 Modifying resources ● Change arbitrary resources ● Repack into .apk file and install Recrompressing: apktool b -d outdir -o com.company.game.free_patch.apk ● Recompression works, Android fails with “can’t install”, wrong certificate ○ APKTool tries to reuse as much as possible, doesn’t recompute signature
  • 10. 2Manually sign repacked apk: ● Create custom CA ● Java JAR Signing and Verification Tool (http://docs.oracle.com/javase/7/docs/technotes/tools/windows/jarsigner.html) jarsigner -verbose -sigalg SHA1withRSA -digestalg SHA1 -keystore my- release-key.keystore com.company.game.free_patch.apk alias_name Modifying resources
  • 12. .apk contains compiled code ● Dalvik bytecode interpreted by the Dalvik Process virtual machine ● Stored in .dex (Dalvik EXecutable) files APKTool translates this to “smali” (https://code.google.com/p/smali/) ● Abstraction of bytecode, closer to Java ● Dalvik opcodes (http://s.android.com/tech/dalvik/dalvik-bytecode.html) ● Can be edited directly 3Decompilation to Smali
  • 13. .class public LHelloWorld; .super Ljava/lang/Object; .method public static main([Ljava/lang/String;)V .registers 2 sget-object v0, Ljava/lang/System;->out:Ljava/io/PrintStream; const-string v1, "Hello World!" invoke-virtual {v0, v1}, Ljava/io/PrintStream;->println(Ljava/lang/String;)V return-void .end method 3 Smali Hello World
  • 14. Interactive debugging ● Set debuggable=”true” in AndroidManifest.xml ○ Repack using APKTool ● Need to connect smali sources to binary ● Workaround: pretend we have valid Java code 4 Debugging <application android:allowBackup="true" android:hardwareAccelerated="true" android:icon="@drawable/launcher_icon" android:label="@string/app_name" android:name="com.company.game.core.GameApplication" android:theme=" @style/Theme.GameTheme" android:debuggable="true">
  • 15. a=0;// .class public abstract La; a=0;// .super Ljava/lang/Object; a=0;// a=0;// a=0;// # instance fields a=0;// .field protected final a:Ljava/lang/Object; a=0;// a=0;// .field private final b:Landroid/os/Handler; a=0;// 4 Debugging Smali code in comments Placeholder Java
  • 16. Two ways to obtain java code ● Convert .dex files to .jar ○ Use standard java bytecode decompilers ● Disassemble .dex directly to .java 5 Decompilation to Java
  • 17. Using dex files ● Androguard (https://code.google.com/p/androguard/) ○ Maps DEX format into full Python objects ○ Works in memory (My 4GB machine wasn’t enough) ○ Doesn’t immediately dump code into Java files 5 Decompilation to Java
  • 18. Using jar files ● dex2jar (https://code.google.com/p/dex2jar/) ○ dex2jar, jar2dex, apk-sign ○ Supports recreating .dex from Java ● JD-GUI (http://jd.benow.ca/) ○ Popular jar-decompiler ○ Works 100% with “Hello World” app 5 Decompilation to Java
  • 19. Combining Java decompilation and Smali ● Java more readable than Smali ● Unfortunately Java decompilation not 100% perfect ○ Invalid Java constructs or only method signatures ○ Cannot recompile from Java sources 6 Putting it together
  • 20. private void fixSpecialChars() { int i; char ac[]; int j; int k; i = 0; ac = tiles; j = ac.length; k = 0; _L9: if(k >= j) break MISSING_BLOCK_LABEL_161; ac[k]; JVM INSTR lookupswitch 6: default 80 // 40: 125 // 41: 137 // 47: 149 // 91: 89 // 92: 101 // 93: 113; goto _L1 _L2 _L3 _L4 _L5 _L6 _L7 _L4: break MISSING_BLOCK_LABEL_149; _L1: break; /* Loop/switch isn't completed */ _L5: break; /* Loop/switch isn't completed */ _L10: i++; k++; if(true) goto _L9; else goto _L8 _L8: 6When Decompilation fails an example Goto not supported in Java Bare JVM instructions
  • 21. Combining Java decompilation and Smali ● Approach: Use multiple Java decompilers ○ They tend to fail in different places 6 Putting it together 1. Find interesting parts in Java source 2. Check corresponding smali sources 3. Edit those
  • 22. protected void roundEnd(boolean paramBoolean) { // … this.resultData.setTotalScore(this.totalScore); // … startRoundSummary(); if (!this.isPractice) { this.currentRound.setWordsInRound(this.resultData.getMoves().size()); // … this.currentRound.setPlayer1Moves(GameHelper.encodeMoves(this.resultData. getMoves())); this.currentRound.setPlayer1Score(this.totalScore); // … 6 Manipulating the score Opportunities for manipulation ● Server validation disallows this
  • 23. a=0;// sget-boolean v0, Lcom/company/game/core/statics/Statics;->DEBUGGING:Z a=0;// a=0;// #v0=(Boolean); -a=0;// if-eqz v0, :cond_0 +a=0;// #if-eqz v0, :cond_0 a=0;// 6 Enable Logging public class Toolkit { // … public static void Logw(String s, String s1) { if(Statics.DEBUGGING) Log.w(s, s1); } // …
  • 24. a=0;// # static fields a=0;// .field public static ROUND_DURATION_IN_SECONDS_FOR_NORMAL_GAME:I a=0;// .field public static ROUND_DURATION_IN_SECONDS_FOR_TUTORIAL:I a=0;// a=0;// .method static constructor <clinit>()V a=0;// .locals 1 … -a=0;// const/16 v0, 0x78 +a=0;// const/16 v0, 0x12c a=0;// a=0;// #v0=(PosByte); a=0;// sput v0, Lcom/company/game/core/statics/GameStatics;->ROUND_DURATION_IN_SECONDS_FOR_NORMAL_GAME:I 6 More time per round 120s 300s
  • 25. public static boolean allowPremiumContent(PremiumType premiumtype, Context context) { if(premiumIsPurchased(context)) return true; synchronized(lock) { if(!isLicensed(context)) break MISSING_BLOCK_LABEL_31; } return true; 6 Getting Premium a=0;// .line 129 -a=0;// invoke-static {p0}, Lcom/company/game/util/PremiumCampaignHelper;->premiumIsPurchased(…;)Z +a=0;// # invoke-static {p0}, Lcom/company/game/util/PremiumCampaignHelper;->premiumIsPurchased(…;)Z a=0;// -a=0;// move-result v0 +a=0;// # move-result v0 a=0;// -a=0;// #v0=(Boolean); -a=0;// if-eqz v0, :cond_0 +a=0;// #v0=(One); +a=0;// # if-eqz v0, :cond_0
  • 26. 6 Getting Premium free version premium (stats unlocked, no ads)
  • 27. 7 Proxy Route all app traffic through custom proxy ● Used MitMProxy (https://github.com/mitmproxy/mitmproxy) ● Retrieve real server URL via Wireshark ● Redirect app traffic via /etc/hosts on device ● Custom SSL certificate ○ Install own CA in device ○ No certificate pinning ● Avoid compressed responses via HTTP header ○ Accept-Encoding: gzip;q=0,deflate,sdch
  • 28. 7 Proxy AES encryption ● Shared key in decompiled code ● No key derivation function ● AES initialization vector in HTTP header ○ Payload-session: 2e2f6a61642f7372… ○ Unencrypted // file APIConnector.java private static byte sharedKey[] = { 57, -116, 126, 39, 116, -25, -95, -106, -81, 48, -33, -19, 120, 118, 35, 40, 66, 126, 31, 30, -83, 76, 31, 93, 13, -122, -50, 68, -108, -114, 28, -80 };
  • 29. SSL MitM Proxy SSLHTTP Server by “aLf “, thenounproject.com (CC BY 3.0 US) Spy by “Hopstarter ”, iconarchive.com (CC BY-NC-ND 4.0) #! python #decrypt AES #using IV 7 ProxyHeader: AES IV AES payload HTTP # /etc/hosts # redirect # to proxy
  • 30. 7 Proxy { "cacheTimestamp": "1405377910521", "userId": "0", "conversationId": "-1", "player1MostWordsInRound": "32", "id": "6602198229545556683", "player1Score": "214", "player1LongestWord": "HEAPS", "player1User": { "username": "username", "ranking": "0", "premium": "false", "recruits": "0", "deleted": "false", "newUser": "false", "bestScoreInMatch": "0", "userId": "3005807464", "bestScoreInRound": "0", "online": "false", "facebookConnected": "false", "avatarId": "0", "matchesPlayed": "0", "useFacebookImage": "false", "mostWordsInRound": "0" }, {"rounds": [ { "seed3": "14657688", "player2MoveErrors": "0", "gameId": "6602198229545556683", "player2SwipeDistance": "681", "player2Moves": "1AB2BAE2EAB216227612AEF2DA73840127652567354013DAB723673 B7654EAB72", "player1MoveErrors": "19", "player2Done": "true", "seed1": "2073207065", "seed2": "680974433", "player1SwipeDistance": "1608", "board": { "bonus": [" ", " ", " ", " ", " ", " ", "D", " ", " ", " ", " ", " ", " ", " ", " ", "T" ], "board": ["A", "T", "E", "H", "E", "P", "O", "T", "H", "S", "A", "S", "T", "F", "T", "E" ], "words": [ "TATE", "SOTS", "HOST", "SAPS", "FATSOS", … Server response request size up to 100kB
  • 31. 8 Automation Play the game automatically ● Generic external approach ○ No modification of binary necessary ○ Works for any app Monkeyrunner (http://developer.android.com/tools/help/monkeyrunner_concepts.html) ● Test apps at the functional/framework level ● Able to simulate keystrokes, take screenshots ● Python bindings
  • 32. 8Obtain all possible words to play correctly ● apk contains .jet “dictionary” for each language ● Btw, also a wordlist (probably) used to check for cheaters Automation
  • 33. 8 Automation Ruzzle .jet files ● Binary files ● Trie / Radix tree structure ● Optimal for the way the game is played ● No duplicate encoding of characters ● List of all excepted words constructable G GA GAM GAME GO GOD GOT G O D T A M E
  • 34. 8 Automation Achieving the highscore ● Get all 16 letters ○ Input by hand / screenshot + OCR ● Find all valid words using the extracted dictionary ● Simulate keystrokes for found words ○ Actually not enough time to enter all valid words
  • 36. Achievements Found possibilities to: ✓ Enable logging ✓ Unlock premium features ✓ Achieve insanely high score through automation ✓ Extract protocol via man-in-the-middle attack
  • 38. Pinned certificate (installed at dev. time) App Server Get current server certificate 1 Compare current and pinned certificates 2 if identical: establish connection else: reject 3 Certificate Pinning