Chuyện dev kể: fix bug ứng dụng Đặt Vé Máy Bay và thu về món hời to bự

3
3043

Bài viết này mình đọc trên Reddit, thấy khá hay nên có nhã ý dịch lại cho anh em. Nói về quá trình làm sao để:

  • Phát hiện bug, tìm mã lỗi để suy đoán lỗi ở đâu.
  • Decode 1 app Android và cách trích xuất mã bytecode từ 1 app không thể bị dịch ngược.
  • Truy xuất bytecode để dễ dàng chỉnh sửa, reimplement 1 phương thức nào đó.
  • Có nói rõ các công cụ được sử dụng luôn, rất clear cho anh em ham học hỏi.
  • Chú ý các mục lớn, luôn là tìm hiểu và xác định vấn đề, thử nghiệm và fix,….

Nói chung khá là giống cách bạn patch 1 bản vá hay nói cho dễ hiểu nó tương tự như “tiêm thuốc” phần mềm Photoshop trên Windows để xài không phải trả tiền vậy. Chỉ khác đây là 1 bug chưa được fix và thanh niên này patch 1 bản vá để fix bug này thôi.

Sẽ là 1 bài dịch dài hơi với nhiều câu chữ theo phong cách Na ngố, cũng không dịch bám bản gốc, tốt nhất anh em hãy chuẩn bị 1 tách cafe để đọc hết nhé.

Reading…

Azul Linhas Aéreas là một hãng hàng không đến từ Brazil cung cấp vé rẻ hơn cho khách hàng mua từ ứng dụng di động của họ. Nếu khách hàng mua vé trên website của họ thì phải trả thêm phí (convenience fee), điều đó kích thích người dùng sử dụng app hơn thay vì book trên website.

Thật không may, ứng dụng đặt vé máy bay trên di động của họ đã gặp lỗi trong vài tháng gần đây. Cụ thể là khi người dùng chọn phương thức thanh toán (Master Card, Visa, Paypal,…) ứng dụng sẽ tự đóng. Trong khuôn khổ bài viết này, chúng tôi sẽ vá ứng dụng bằng bytecode để sửa lỗi trên.

Xác định vấn đề:

Khi book 1 chuyến bay trên website của hãng hàng không Azul, họ sẽ tính thêm 1 khoản phí gọi là “convenience fee” khoảng 60 BRL cho mỗi hành khách tương đương khoảng 374.000 VNĐ. Hài cái là phí tiện lợi (convenience fee) nó thường tính đắt hơn cả phí thuê sân bay (embarkation fee).

  • (convenience fee) – trong bài này mình sẽ dịch là phí tiện lợi. Phí này của nhà nước mỗi khi bay thì cộng vào tiền vé máy bay của hành khách luôn. Ý của hãng hàng không Azul nó charge phí này vì đặt trên web/app của nó tiện hơn ra quầy mua í.
  • (embarkation fee) – là thuế sân bay/phí duy trì dịch vụ sân bay.
Phần tô vàng chính là phí tiện lợi

May mắn thay, các chuyến bay được đặt bằng ứng dụng di động của họ được miễn phí phí tiện lợi.

Đặt trên app không phải chịu thêm phí tiện lợi, easy game

Tuy nhiên, ứng dụng book vé trên di động của họ lại gặp lỗi sau khi chọn phương thức thanh toán.

Tới bước chọn phương thức thanh toán như Visa, MasterCard là ứng dụng tự out

Debugging – gỡ lỗi vấn đề:

Cần chuẩn bị 1 vài công cụ bên dưới:

Vì ứng dụng đột nhiên bị đóng, giả thuyết đầu tiên là xuất hiện một số ngoại lệ (uncaught exception) chưa được tester tìm ra trước đây đang lộng quyền và ứng dụng không có cơ chế báo cáo mã lỗi rõ ràng trước khi đóng (kiểu như không đặt try – catch) hoặc dev đã tắt chế độ thông báo lỗi.

Để kiểm tra giả thuyết này, chúng tôi khởi chạy adb logcat và thử đặt 1 chuyến bay. Ứng dụng ngay lập tức gặp sự cố khi chọn phương thức thanh toán và chúng tôi nhận được 1 ít log từ stack trace bên dưới:

Toẹt vời. Ít nhất còn có log để chúng ta lần mò tiếp. Từ log trên chúng ta nhận thấy rằng ứng dụng dường như đang cố gắng chuyển đổi số tiền đến hạn (tính bằng xu) thành kiểu long. Tuy nhiên, có một số khoảng trắng ở hàng đầu trong chuỗi, khiến việc chuyển đổi không thành công. Liệu họ quên cắt chuỗi?

Chúng tôi truy xuất tệp apk từ thiết bị và cố gắng mở nó bằng chương trình Bytecode Viewer. Không có may mắn xảy ra vì không thể giải mã một số tài nguyên trong resource.arsc. Tuy nhiên, đây không phải là vấn đề vì chúng tôi chỉ có ý định hiểu (và sửa đổi) bytecode, do đó chúng tôi trích xuất class.dex và mở nó.

Sau khi dịch ngược và tìm đến method removerSeparadoresDeValor là thủ phạm gây lỗi ở Stack Trace bên trên:

Ô hay! Họ đã cắt chuỗi mà nhỉ. Vậy tại sao lại có khoảng trắng ở hàng đầu?

Sau khi mở output trong logcat ở trình soạn thảo hex, chúng tôi nhận ra các ký tự khoảng trắng được tạo bởi 2 byte c2 a0 (UTF-8), trong bộ mã Unicode thì đây là 1 khoảng trắng 00A0 (no-break space) – nó cũng tựa tựa như   trong HTML. Phương thức cắt chuỗi removerSeparadoresDeValor có hàm trim() đã bỏ qua ký tự đặc biệt này và không ảnh hưởng đến nó.

Fix bug nhanh 1 cách tạm bợ:

Trình dịch ngược (decompiler) hiếm khi dịch được hoàn chỉnh code 1 class Java thông thường, do cấu trúc ngôn ngữ Java khi decompile sẽ không đảm bảo tính toàn vẹn cho nên việc re-build lại ứng dụng gần như bất khả thi. Để dễ dàng hơn, chúng ta nên làm việc trực tiếp với mã bytecode (có giải thích bên dưới) bằng cách thay đổi từ chế độ Bytecode Viewer’s sang Smali/DEX để có thể chỉnh sửa dễ dàng.

Bắt đầu với phương thức removeSeparadoresDeValor, chúng ta sẽ từng bước chèn các mã hướng dẫn bên dưới ngay dòng cuối sau khi gọi hàm replace.

Đoạn mã trên sẽ giúp .replace ("\ u00a0", "") được gọi trước .trim ().

Bytecode Viewer có thể lưu trữ đoạn code đã sửa đổi bên trên vào file classes.dex (bằng cách gói vào Smali). Tuy nhiên, chúng ta cần zip nó vào file apk 1 cách thủ công, vì chúng tôi không mở file apk trực tiếp trong Bytecode Viewer.

Đoạn này cần giải thích 1 chút vì cách này rất hay được sử dụng để: mod ứng dụng (mod Zing VIP, mod Nhaccuatui vĩnh viễn tới 2050,…), ROM cook và rất nhiều thứ khác.

  • Thông thường 1 ứng dụng Android được viết bằng ngôn ngữ bậc cao Java và thực thi mã trên Virtual Machine, bytecode chạy trên Dalvik Virtual Machine (DVM) được chuyển đổi từ JVM byte code truyền thống sang dex-format  (.dex file) nhờ vào convertion tool dx.
  • Mã code thực thi trên DVM được gọi là mã Smali, Smali có tập lệnh phức tạp tương tự như Assembly trên Windows. Đương nhiên để đọc và hiểu mã Smali là 1 việc không hề dễ dàng.
  • Việc chèn code Smali vào ứng dụng là cách hay được dùng nhất để re-build lại ứng dụng (1 tệp apk mới đã được chỉnh sửa) và chạy bình thường.

Lưu ý: hacker rất hay lợi dụng việc chèn code Smali vào ứng dụng do đó việc sử dụng các tệp tin phân phát trên chợ đen hoặc 1 file apk từ 1 nguồn không đáng tin cậy rất nguy hiểm. Nhẹ thì thêm ads, nặng thì ghi âm,… nói chung đừng nên tải và cài đặt file apk không rõ nguồn gốc vì có trời mới biết.

Tiếp theo, chúng tôi sign file apk với debug key như sau:

Cuối cùng, bạn chỉ cần gỡ ứng dụng gốc và cài đặt file apk đã sửa đổi của chúng tôi.

Kết quả:

Thay vì đặt vé từ trang web với 229,98 BRL (khoảng 1tr4 VNĐ) thì giờ đây thôi chỉ cần trả 110,38 BRL (khoảng 688 VNĐ) thông qua ứng dụng đã được fix bug. Giờ thì tôi đang cùng vợ đi du lịch và rất hạnh phúc.

Đôi khi bỏ ra chút thời gian để fix bug bạn nhận được nhiều hơn 1 niềm vui khi bug được fix.

Tôi nhận được gì?

Hiện tại thì tôi đã tiết kiệm được 1 số tiền cho mỗi chuyến bay, điều đó thật tốt. Tuy nhiên tôi tin rằng bài viết này sẽ giúp được rất nhiều lập trình viên khác.

Nguyên nhân gốc rễ ở đây là gì?

  • Before: 110VNĐ_
  • After: 110 BRL

Điều thú vị đầu tiên là vấn đề sẽ không xảy ra nếu removerSeparadoresDeValor chạy trong OpenJDK. Hóa ra OpenJDK có đoạn mã Java thuần implementation cho DecimalFormat. Hãy nhìn vào locale data mà nó sử dụng, chúng ta có thể thấy nó chèn 1 khoảng trắng (\u0020) sau ký hiệu tiền tệ (\u00A4).

Mặt khác, việc triển khai DecimalFormat của Android là 1 trình wrap cho icu4c. Nhìn vào locale data mà nó sử dụng, chúng ta có thể thấy nó chèn 1 khoảng trắng (\u00A0) sau ký hiệu tiền tệ (\u00A4).

Từ khi nào bug này bắt đầu xuất hiện?

Locale data đại diện cho format tiền tệ đã được thay đổi lần cuối vào 26/09/2017. Trước khi thay đổi, không có bất kỳ khoảng trắng nào giữa ký hiệu tiền tệ và giá trị số.

Tuy nhiên, 1 sự thật thú vị là mã icu4c hiện tại sẽ tự động chèn khoảng trắng vào giữa ký hiệu tiền tệ và giá trị số tiền ngay cả khi nó không có trong dữ liệu gốc. Từ khi nào điều này xảy ra? Định dạng mới chịu trách nhiệm cho việc thêm khoảng trắng được commit lần đầu vào 26/09/2017, và các bài unit test sửa đổi lần cuối nào ngày 17/04/2018. Vì vậy, bug này có thể xuất hiện sau thời gian này.

Từ khi nào Android gửi locale data? Android 9 có sẵn ngay ở bản phát hành đầu tiên. Android 8 thì không có sẵn, tôi đã kiểm tra ở các bản phát hành gần đây.

Tại sao các nhà phát triển ứng dụng đã không phát hiện bug này mặc dù nó tồn tại trong vài tháng?

Rõ ràng là họ đã không test ứng dụng trên Android 9.

Câu hỏi được đặt ra là:
  • Có phải cần dùng Android 9 mới là cách duy nhất để họ phát hiện ra lỗi này?
  • Họ có thu thập các báo cáo lỗi từ người dùng?
Câu dưới mình không biết dịch sao cho hay nên xin phép để nguyên gốc:

It is funny that the only uncaught exception handler the app installs is related to app rating. Yes, it serves the only purpose of avoiding to ask users to rate the app if the app crashed at least once for them. Of course, these users would not give many stars.

Kiểu như: nếu sử dụng UnCaught exception handler trong ứng dụng sẽ ảnh hưởng tới việc rating app. Chuẩn, nó phục vụ mục đích duy nhất là tránh yêu cầu người dùng xếp hạng ứng dụng nếu ứng dụng bị sập (chỉ cần sập 1 lần). Tất nhiên, những người dùng này sẽ không cho nhiều sao khi đánh giá ứng dụng.

Kết luận:

Có lẽ dễ dàng hơn khi chỉ cần khởi chạy trình giả lập Android 8 và đặt chuyến bay từ đó.

Thực sự là tôi không biết điều đó, tôi nghĩ rằng ứng dụng này có vấn đề và tìm hiểu. Trên hết là 1 lập trình viên, tôi luôn muốn tìm hiểu điều gì đã khiến nó hoạt động, điều gì đã khiến nó không hoạt động.

Chúng tôi thực sự khuyên các bạn dev ở Azul Linhas Aéreas:

  • Hãy viết lại phương thức removeSeparadoresDeValor bằng cách triển khai đáng tin cậy hơn (cách khắc phục nhanh của chúng tôi không phải là một ví dụ!).
  • Tránh phụ thuộc vào dữ liệu bản địa (locale data) để ứng dụng hoạt động 1 cách chính xác.
  • Bắt đầu thu thập các báo cáo sự cố và có kế hoạch sửa lỗi nhanh chóng.

Tham khảo: Fixing an airline app bug for fun and profit

5.0
06

3
Bình Luận Bài Viết

avatar
2 Comment threads
1 Thread replies
0 Followers
 
Most reacted comment
Hottest comment thread
2 Comment authors
Bà NaChiến Nguyễn Recent comment authors

Chiến Nguyễn | <span class="wpdiscuz-comment-count">8 comments</span>
Khách
Chiến Nguyễn | 8 comments
Off

Anh có thể viết thêm nhiều bài dạng như vậy được không? Em đang học lập trình nhưng thích đọc mấy bài như vậy.