Mobile Security via Flutter — ตอนที่ 1 SSL Pinning

Amorn Apichattanakul
KBTG Life
Published in
4 min readMar 24, 2021

เมื่อสถานการณ์โควิดส่งผลให้ทุกภาคส่วนทั่วโลกพยายามทำทุกอย่างให้เป็นดิจิทัลมากขึ้น ผู้พัฒนาต้องหมั่นเรียนรู้เทคโนโลยีใหม่เพื่อพัฒนา User Experience ให้ดียิ่งขึ้นตามไปด้วย ปัจจุบันเราจะเห็นว่าสิ่งของจำนวนมากเริ่มถูกทำให้เป็นออนไลน์มากขึ้น แม้กระทั่งเครื่องใช้ไฟฟ้าในบ้านอย่างตู้เย็นเอง บางทีก็ทำให้รู้สึกหวั่นๆ ว่าจะเสี่ยงต่อการโดนดักฟังข้อมูลโดยเหล่าแฮกเกอร์หรือมิจฉาชีพหรือไม่

ใครที่เคยเรียนเกี่ยวกับ Design Thinking จะต้องรู้จักกับสิ่งที่เรียกว่า Empathy หรือความเห็นใจคนอื่น การที่เราจะสร้างผลิตภัณฑ์อะไรสักอย่างให้ลูกค้า เราควรทดลองใช้เองก่อนทุกครั้ง เพราะถ้าเรายังไม่เคยใช้ หรือยังไม่ชอบของที่ทำเอง เราจะคาดหวังให้คนอื่นมาชอบของที่เราใช้ได้อย่างไร? ด้วยเหตุนี้เองที่ KBTG เราจึงมีกระบวนการทำ Dogfooding หรือ Eat Your Own Dog Food ก่อนผลิตภัณฑ์จะถึงมือลูกค้าเสมอ ทั้งนี้บางคนอาจจะมองว่าสิ่งที่ลูกค้าปรารถนามีแค่ฟีเจอร์สุดเจ๋งหรือดีไซน์สุดล้ำ แต่ชาว KBTG ไม่ได้คิดเพียงเท่านั้น สำหรับเรา Empathy คือการที่ลูกค้าไว้ใจให้เราดูแลรักษาข้อมูลที่เขาสละให้กับเรา ด้วยความเชื่อมั่นว่าข้อมูลที่ให้ไปนั้นจะถูกจัดเก็บอย่างดี ไม่มีใครสามารถเข้าถึงและนำข้อมูลส่วนนี้มาดูได้ ดังนั้นจึงนับเป็นเกียรติและความรับผิดชอบของเราที่จะต้องรักษาความลับของลูกค้าด้วยมาตรการป้องกันข้อมูลอย่างเข้มงวด ชนิดที่ว่าผู้พัฒนาเองก็ยังไม่สามารถดูข้อมูลนี้ได้

ในซีรีย์บทความนี้ ผมจึงอยากจะมาไฮไลท์ในส่วนสำคัญที่ PO หลายคนอาจจะมองข้ามไป คือเรื่องของ Security โดยผมได้รวบรวมเทคนิคต่างๆ ในการป้องกันไม่ให้มิจฉาชีพสามารถเจาะข้อมูลดูได้ง่ายๆ ซึ่งแน่นอนว่าในโลกนี้ไม่มีอะไรที่สามารถป้องกันได้ 100% แต่เราพยายามใช้ความรู้ความสามารถอย่างเต็มที่เพื่อปกป้องให้ใกล้เคียง 100% มากที่สุด ผมจะแชร์ความรู้ในส่วนของ Mobile Banking โดยที่อาจจะไม่ได้ลงลึกด้าน Security มากนัก แต่มาเล่าประสบการณ์ในการ Implement Mobile Security ผ่าน Flutter ตามมาตรฐานที่ OWASP กำหนดไว้ให้ฟังกันครับ

ทุกคนอาจจะไม่จำเป็นต้องพัฒนา Security ระดับธนาคารที่มีระบบซับซ้อน แต่ผมอยากให้ผู้พัฒนาติดตั้ง Security พื้นฐานกับทุกแอปควรมี ซึ่งแต่ละส่วนนั้นง่ายๆ ไม่ยุ่งยากมากมาย เริ่มจาก SSL Pinning

SSL Pinning

SSL Pinning คือการบอกกับแอปของเราว่าต้องเชื่อ SSL Certificate ที่เราจัดไว้ให้เท่านั้น เพื่อป้องกันการโจมตีแบบ MITM (Man in the Middle Attack) ของแฮกเกอร์นั่นเอง

คำว่าแฮกเกอร์ที่เราจะพูดถึงต่อไปนี้คือ Bad Hacker นะครับ โดยปกติแฮกเกอรจะมีทั้ง White Hat และ Dark Hat หากสนใจเรื่องความแตกต่างระหว่างสองฝ่ายนี้ สามารถไปหาอ่านกันได้หลังจบบทความนี้ครับ

ถ้าให้พูดง่ายๆ คือเมื่อคุณเชื่อมต่อกับ Public WIFI/Hotspot คนสาย IT จะสามารถดักจับข้อมูลที่คุณส่งผ่าน WIFI ตัวนั้นได้ทั้งหมด ที่น่ากลัวที่สุดคือการดักจับ Username และ Password ดังนั้นหากไม่มีการทำ SSL Pinning ไว้ ทางแฮกเกอร์จะสามารถนำ Username และ Password ของคุณไปลองทำการล็อคอินระบบที่คุณใช้ได้ ยิ่งถ้าเป็นแอปที่ผูกกับบัตรเครดิตไว้ก็เรียบร้อย สามารถล็อคอินแล้วสั่งซื้อของไปส่งที่อื่นได้เสร็จสรรพ สำหรับรายละเอียดลึกๆ สามารถตามอ่านที่ลิงก์ด้านล่างได้ครับ และท่องจำเอาไว้ให้ดีว่าอย่าใช้ Public WIFI/Hotspot เป็นอันขาด!

Credit https://www.guardsquare.com/en/blog/iOS-SSL-certificate-pinning-bypassing

ขั้นตอนการติดตั้ง

ใน Flutter ถ้าเราไล่หาใน StackoverFlow เกี่ยวกับ SSL Pinning จะเจอคำสั่งที่เรียกว่า badCertificateCallback

คือการที่เราบอกกับ Flutter ว่าเราจะไม่เชื่อ Certificate นอกจากอันที่เราไฟเขียวเท่านั้น การใช้จะเป็นตามด้านล่างนี้ครับ

HttpClient _client = new HttpClient(context: await globalContext);_client.badCertificateCallback =(X509Certificate cert, String host, int port) => false;final _ioClient = new IOClient(_client);_ioClient.get(url)

ให้สร้าง HttpClient และส่ง globalContext เข้าไป แล้วให้ badCertificateCallback Return False เสมอ

จากโค้ดด้านบนสุด เราจะได้ _ioClient มาครับ ซึ่งจะนำไปใส่ในส่วน GET, POST, PUT, DELETE ตามที่เราต้องการ แต่ผมจะส่งต่อให้ GraphQL

GlobalContext คือตัวแปรด้านล่างที่ไว้อ่านค่า Certificate จาก Flutter

Future<SecurityContext> get globalContext async {    // Note: Not allowed to load the same certificate    final sslCert1 = await   
rootBundle.load('assets/cert/certificate.pem');
final sslCert2 = await
rootBundle.load('assets/cert/certificate2.pem');
SecurityContext sc = new SecurityContext(withTrustedRoots: false); sc.setTrustedCertificatesBytes(sslCert1.buffer.asInt8List());
sc.setTrustedCertificatesBytes(sslCert2.buffer.asInt8List());
return sc;}

เราจะโหลด certificate.pem เข้าไป โดยการที่เราจะเอา certificate.pem มาได้นั้น ผมใช้สคริปด้านล่างในการดึง Public Key มาจากเซิร์ฟเวอร์ ทั้งนี้อย่าลืมเปลี่ยน your-url.com ให้เป็นของเว็บคุณ ไม่ต้องใส่ HTTP หรือ HTTPS นำหน้านะครับ เช่น ผมจะเอา Certificate จาก Google ก็ให้ใส่ google.com:443 ได้เลย ไม่ต้อง https://www.google.com เต็มยศ

openssl s_client -showcerts -connect your-url.com:443 -servername your-url.com </dev/null | sed -ne '/-BEGIN CERTIFICATE-/,/-END CERTIFICATE-/p' > certificate.pem

หลังจากได้ Certificate มาแล้ว ให้นำไปใส่ใน Flutter Project ผมใส่เอาไว้ที่โฟลเดอร์ assets/cert ซึ่งต้องไปแอดใน pubspec.yaml ตามด้านล่าง เพื่อให้ Flutter รู้จัก

assets:
- assets/cert/

คุณจะทำให้เชื่อกี่ Certificate ก็ได้ ระบบไม่ได้มีจำกัดไว้ แต่ปกติผมจะตั้งไว้ที่ 4 เท่านั้น เพราะยิ่งเยอะก็ยิ่งเสี่ยงต่อการโดนแฮค สำหรับ Firewall ทางเราเลือกใช้ Akaimai เพื่อทำหน้าที่ป้องกัน Request ที่ไม่เกี่ยวข้องหรือ DDoS ต่างๆ เพื่อให้ข้อมูลที่ไปยังเซิร์ฟเวอร์ถูกกรองว่ามาจาก User จริงๆ โดยเราจะ Pin ไว้ 4 Certificates คือ 2 สำหรับใช้ปัจจุบัน และอีก 2 สำหรับอนาคต อย่างไรก็ตามความน่ากลัวของการทำ SSL Pinning คือถ้า Certificate ที่ใช้หมดอายุแล้ว แอปเราจะใช้งานใดๆ ไม่ได้เลย เนื่องจากเราบอกแอปไปแล้วว่าเชื่อ Certificate เฉพาะที่เราให้เท่านั้น ผมเคยพยายาม Pin Cert ที่ Invalid หรือ Cert ที่ซ้ำกัน ผลออกมาคือ Dart จะฟ้องให้ว่าใช้งานไม่ได้ ฉะนั้นหลังจากทำ SSL Pinning แล้ว อย่าลืมวัน Cert หมดอายุ และอัพเดตแอปตามด้วยนะครับ

หลังจากเราทำเสร็จและทดสอบเรียบร้อย จะเห็นว่าใช้งานได้เรียบร้อย ถ้าเราลองแก้ Certificate บนเซิร์ฟเวอร์ แอปก็จะหยุดทำงาน ไม่รับ Request ใดๆ จากเซิร์ฟเวอร์นี้ ตอนแรกผมก็หลงดีใจ เอ๊ะง่ายแฮะ โค้ดไม่กี่บรรทัดจบ

แต่

แต่

ปรากฏว่ามีปัญหาเกี่ยวกับ badCertificateCallback ตาม Issue ด้านล่างครับ

กลายเป็นว่า badCertificateCallback ได้ Pin ที่ Intermediate Cert โดยที่ไม่ได้ทำการเช็ค Common Name ทำให้เกิดช่องโหว่ขึ้น เช่น สมมุติว่าผม Pin Let’s Encrypt ให้เชื่อ Cert ของผมเท่านั้น แต่หากแฮกเกอร์รู้ช่องโหว่ Flutter นี้ ก็จะไปทำ Let’s Encrypt ขึ้นอีกตัวเพื่อมาหลอกแอปของผมได้ เพราะ badCertificateCallBack จะไม่เช็ค Common Name จึงมองว่ามาจาก Let’s encrypt เหมือนกัน ฉะนั้นให้ผ่านได้

เพื่อปิดช่องโหว่ตรงนี้ เราจึงต้องมาเขียนเช็คเองครับ

Future<bool> get _isAllowList async {    const myAllowList = "xxxxxxx";
final x509Cert1 = await _readPemCert('assets/cert/certificate.pem');
X509CertificateData data = X509Utils.x509CertificateFromPem(x509Cert1); return data.sha256Thumbprint == myAllowList
}

_readPemCert มาจากโค้ดด้านล่าง อาจจะดูไม่สวยงามแต่ก็สามารถทำงานได้ตามต้องการ ผมอาศัยการตัดต่อ String เพื่อดึงเฉพาะ Leaf Cert เนื่องจาก Lib ที่ผมใช้มีแค่เท่านี้ ส่วน myAllowList เป็นส่วน Hardcode ของผมเองที่ตั้งค่าเอาไว้ โดยต้องไปหาค่า SHA256 ของ Certificate มาเทียบ

Future<String> _readPemCert(String path) async {   final sslCert = await rootBundle.load(path);   final data = sslCert.buffer.asUint8List();   final pemString = utf8.decode(data);   final pemArray = pemString.split("-----END CERTIFICATE-----");   final cert = [pemArray[0], "-----END CERTIFICATE-----"].join("");   return cert;}

ผมใช้ Libs basic_utils สำหรับดึงข้อมูล Certificate ให้ใส่ Lib นี้ลงไปใน pubspec.yaml เนื่องจาก basic_util ไม่สามารถรับ Certificate ทั้งใบได้

basic_utils: ^2.7.0-rc.4

และใช้ฟังก์ชัน X509Utils เพื่อที่จะได้ SHA1 ของ Certificate มา เรานำค่านี้มาเทียบกับที่เรา Hardcode ไว้ในแอปว่าตรงกันหรือไม่ ถ้าตรงกันก็แสดงว่าถูก และเราต้องเชื่อจากอันนี้เท่านั้น ผมก็จะนำตัวแปร _isAllowList ไปใช้ ถ้า True เราให้ทำงานต่อ ถ้า False เราจะแจ้งเตือน User ว่ากรุณาไป อัพเดตแอปตัวใหม่

การแจ้ง User ให้อัพเดตแอปตรงนี้ค่อนข้างสำคัญ เพราะ User จะไม่รู้ว่าทำไมใช้งานไม่ได้ ลงเอยด้วยการลบแอปของเราทิ้ง เราจึงจำเป็นต้องพา User ไปให้ถูกที่ครับ

เมื่อเร็วๆ นี้ ผมได้ไปเจอ Lib อีกตัว หลังจาก Implement แบบของผมไปแล้ว

ผมไม่แน่ใจว่าตัวนี้ทำงานเป็นยังไงบ้าง ผ่าน Security ไหม เพราะเราก็เช็คเองไม่เป็น แต่ตัวที่ผมทำไปด้านบนนั้นได้ผ่าน Penetration Testing เรียบร้อยครับ ถึงไม่สวยแต่ใช้งานได้

Updated ล่าสุด 10 มกราคม 2654

มีเพื่อนอีกทีมหนึงที่ใช้ Project Flutter เหมือนกัน ได้ใช้ Lib ด้านบน ก็ผ่าน Pen test ได้แบบไม่มีปัญหาใดๆเลยครับ ผมก็แนะนำให้ใช้แบบด้านบนดีกว่าครับ แบบผมมันจะลูกทุ่งไปหน่อย 😏

หวังว่าบทความนี้จะช่วยให้ Developer สามารถติดตั้ง SSL Pinning แบบง่ายๆ กับแอป Flutter ยังไงมารอติดตามภาคต่อของซีรีย์นี้ ว่าด้วยเรื่องของ Strong Device/ Strong Password กันด้วยนะครับ

สำหรับชาวเทคคนไหนที่สนใจเรื่องราวดีๆ แบบนี้ หรืออยากเรียนรู้เกี่ยวกับ Product ใหม่ๆ ของ KBTG สามารถติดตามรายละเอียดกันได้ที่เว็บไซต์ www.kbtg.tech

--

--

Amorn Apichattanakul
KBTG Life

Google Developer Expert for Flutter & Dart | Senior Flutter/iOS Software Engineer @ KBTG