Một vài real case (maybe đơn giản) mình được ông anh ném cho vọc.
Ban đầu khi cài đặt và mở app trên con android đã ẩn root và bật sẵn frida-server thì bị văng app sau 30s. Do khá tự tin về khả năng ẩn root, ẩn unlock botloader , ẩn usb debug trên máy của mình nên khả năng chỉ có thể là frida đã bị detech.
Thử tắt frida-server đi và thử lại thì không còn bị văng app nữa nên chắc chắn app có detech frida.
Decompile với JADX nhận thấy đây là 1 app Flutter.
Và đã được obfuscate
Search thử 1 số keyword, phát hiện ở đây đã sử dụng Talsec RASP, cụ thể là freeRASP
Cùng với namespace com.aheaditec.talsec_security, có thể xác nhận đây là thư viện Talsec.
Đầu tiên thử sử dụng các script bypass Talsec RASP sẵn nhưng không được cơm cháo gì. Ok fine.Bắt buộc phải nhảy vào rev.
Sau 1 hồi trace theo keyword X509Certificate nhưng không thấy đoạn code nào liên quan đến việc triển khai SSL Pinning, quay lại kiểm tra các lib.
Có 1 số lib khả nghi ở đây :
libsecurity.so : là thư viện Talsec Security dùng cho RASP: anti-Frida, root detection, threat checks.
libpolarssl.so : Chưa có thông tin.
Bypass Anti-Frida
Bắt đầu với libsecurity.so. Sử dụng IDA Provà 100% Claude để rev lib này, tìm thấy cách app detech Frida:
afld() @ 0x5bc4 : mở /proc/self/maps, đọc từng dòng bằng fgets , tìm substring "frida" bằng strstr . Nếu trong memory map có module / path chứa chữ frida thì coi như bị instrumentation.
ifpip() @ 0x5910 : mở /proc/self/maps,parse các memory region có quyền r-x, quét trực tiếp vùng nhớ thực thi và tìm chuỗi LIBFRIDA.
ifsl() @ 0x5b24 : tạo socket rồi connect tới 127.0.0.1:27042 (Port mặc định của Frida), nếu connect được thì kết luận đang có Frida server hoạt động.
Ok đã tìm thấy hàm detech frida, tiến hành hook vào để bypass:
var libc = Process.findModuleByName("libc.so"); // Hook strstr - hide "frida" string if (libc) { var strstrExport = libc.findExportByName("strstr"); if (strstrExport) { Interceptor.attach(strstrExport, { onEnter: function (args) { this.needle = args[1].readUtf8String(); }, onLeave: function (retval) { if (this.needle && this.needle.toLowerCase().indexOf("frida") !== -1) { retval.replace(ptr(0)); } }, }); console.log("[+] Hooked strstr (frida string hiding)"); } // Hook connect - block port 27042 var connectExport = libc.findExportByName("connect"); if (connectExport) { Interceptor.attach(connectExport, { onEnter: function (args) { this.block = false; var addrPtr = args[1]; var addrLen = args[2].toInt32(); if (addrLen >= 16) { var family = addrPtr.readU16(); if (family === 2) { var port = (addrPtr.add(2).readU8() << 8) | addrPtr.add(3).readU8(); if (port === 27042) { console.log("[+] Blocked connect() to 127.0.0.1:" + port); this.block = true; } } } }, onLeave: function (retval) { if (this.block) retval.replace(-1); }, }); console.log("[+] Hooked connect (port 27042 blocking)"); } }
// Hook afld/ifpip/ifsl in libsecurity.so functionhookSecurityModule() { var mod = Process.findModuleByName("libsecurity.so"); if (mod) { console.log("[+] Found libsecurity.so @ " + mod.base);
Interceptor.attach(mod.base.add(0x5bc4), { // afld onLeave: function (retval) { if (retval.toInt32() === 1) { console.log("[+] afld(): blocked frida in /proc/self/maps"); retval.replace(2); } }, }); Interceptor.attach(mod.base.add(0x5910), { // ifpip onLeave: function (retval) { if (retval.toInt32() === 1) { console.log("[+] ifpip(): blocked LIBFRIDA scan"); retval.replace(2); } }, }); Interceptor.attach(mod.base.add(0x5b24), { // ifsl onLeave: function (retval) { if (retval.toInt32() === 1) { console.log("[+] ifsl(): blocked port 27042 detection"); retval.replace(2); } }, }); console.log("[+] Native anti-Frida hooks installed"); } else { setTimeout(hookSecurityModule, 100); } }
Thử hook script này và test, lúc này không bị văng app nữa.
Cũng ở libsecurity.so, app có 1 số cơ chế để check root:
Tìm su trong PATH : đọc biến môi trường PATH ,thử ghép từng directory với /su, nếu tồn tại thì báo root.
Đọc /proc/self/mounts,tìm string magisk, tìm core/mirror, tìm core/img để phát hiện Magisk / root environment.
Quét /system/app , so tên package trong danh sách APK decode sẵn, tìm app root manager / suspicious app.
Tìm file nhị phân root phổ biến : Danh sách đã được obfuscate nhưng khi chạy thực tế thấy app kiểm các path kiểu: /system/app/Superuser.apk,/sbin/su, /system/bin/su, /system/xbin/su,/data/local/xbin/su,/data/local/bin/su,/data/local/su,/su/bin/su,/cache/su ,/dev/su,…
Sau khi đã bypass anti-frida và check root thành công, cần bypass nốt SSL Pinning để bắt request.
Do khi rev libsecurity.so không thấy dấu hiệu gì của việc triển khai SSL Pinning ở đây nên thử rev libpolarssl.so,tuy nhiên lib này chỉ chứa crypto/hash, không phải chỗ xử lý TLS chính của app.
Lúc này có 2 khả năng:
Flutter engine (libflutter.so) dùng BoringSSL
native custom TLS library riêng
Đã thử reverse libflutter.so:
có string liên quan SecurityContext, SecureSocket, handshake.cc
tìm được một số candidate của BoringSSL
nhưng hook các hàm candidate như verify_cert_chain, ssl_verify_peer_cert không hề trigger trong runtime
Điều này chứng tỏ app không đi qua luồng TLS mặc định của Flutter cho request thực tế.
không dùng TLS stack mặc định của Flutter cho network chính
Do đó thử Claude rev libclib.so và thấy rất nhiều symbol OpenSSL còn nguyên:
SSL_CTX_set_verify
SSL_set_verify
SSL_CTX_set_cert_verify_callback
SSL_get_verify_result
SSL_do_handshake
Đến đây chắc chắn app dùng OpenSSL custom và SSL/TLS pinning và verify chính nằm ở libclib.so.
TLS client flow
SSL_connect @ 0x3eeffc
gọi tiếp SSL_do_handshake @ 0x3eef18
App triển khai verify từ 3 hàm:
SSL_CTX_set_verify @ 0x3f115c
SSL_set_verify @ 0x3eeac4
SSL_CTX_set_cert_verify_callback @ 0x3f1154
Ta thấy app có 2 cơ chế:
set verify mode
set callback verify custom
Đây là đúng pattern của app dùng OpenSSL để tự kiểm soát cert verification / pinning.
Sau handshake app hoặc thư viện có thể đọc verify status
SSL_get_verify_result @ 0x3f2108
Tiến hành hook vào các hàm
SSL_CTX_set_verify : ép mode thành SSL_VERIFY_NONE (0)
SSL_set_verify : ép mode thành 0
SSL_CTX_set_cert_verify_callback : thay callback verify bằng callback custom luôn return success
SSL_get_verify_result : nếu return khác 0 thì ép thành 0 (X509_V_OK)
SSL_do_handshake : debug xem handshake có thực sự đi qua OpenSSL hay không
Ở đây app dùng raw OpenSSL/curl để nói chuyện TLS trực tiếp với server.
Nên ta sẽ rewrite TCP destination từ 18.x.x.x:443 / 18.x.x.x:9243 thành 172.20.10.2:8080 (Proxy của Burp), nên cần bật invisible proxy trong Burp để bắt được request.
Nhìn tổng quan thì app này cũng tương tự như case 1. Thử sửa lại offset và chạy script của app 1 thì bypass được check frida và check root, app hoạt động bình thường nhưng không bắt được request.
Nhảy vào Claude rev libclib.so thì phát hiện app này triển khai SSL khác với app 1.
Qua RE trên libclib.so, flow chính là:
sub_2575F4 khởi tạo SSL_CTX, cấu hình TLS, gọi SSL_CTX_set_verify(ctx, mode, NULL)
sub_259DD8 thực hiện SSL_connect trong loop
Sau khi handshake thành công, sub_259DD8 gọi trực tiếp sub_258204
sub_258204 làm phần verify thực tế:
kiểm tra chain
kiểm tra OCSP
kiểm tra public-key pin
public-key pin nằm ở sub_253B58
sub_253B58 so sánh public key của peer cert với pin format:
1
sha256//<base64>
Convention trả về của lớp này:
0 = success / pin match
90 = mismatch
Vì vậy bypass đúng là ép các hàm verify nội bộ này về 0, không phải 1.
Tuy nhiên sau khi hook thành công thì chỉ bắt được duy nhất request đến server của talsec :
Có thể đây là request tracking trạng thái để gửi về server của Tailsec.
Đến đây sau 1 lúc loay hoay thì mình nghĩ rằng có thể app này triển khai SSL ở chỗ khác nữa, ở libclib.so chỉ triển khai SSL của Talsec thôi.
Bypass libflutter.so
Thử với script vẫn hay dùng với những app triển khai SSL trong libflutter.so thì hook thành công và bắt được request.
Do đó có thể chắc chắn traffic API chính không đi qua libclib.so mà đi qua engine Flutter trong libflutter.so, dùng BoringSSL statically linked.
Điều này dẫn tới hai hệ quả:
Không thể hook tìm hàm theo kiểu findExportByName("SSL_CTX_set_verify") như với OpenSSL/libcurl
Muốn hook phải tìm hàm theo pattern / string / control-flow, không phải theo export name
Vì BoringSSL được compile thẳng vào libflutter.so dưới dạng static link và binary release đã strip symbol/không export các hàm SSL nội bộ, nên không còn symbol như SSL_CTX_set_verify trong .dynsym để findExportByName() tìm được; do đó phải lần theo dấu vết còn sót lại như string (ssl_client), xref và control-flow để xác định hàm verify cần hook.
Ta sẽ lần theo 2 string:
string ssl_client\0
string Socket_CreateConnect\0
Từ ssl_client\0:
scan trong .rodata
tìm instruction sequence trong .text tham chiếu tới string đó
trace ngược về prologue hàm
suy ra được hàm verify_cert_chain
Ở phía Flutter/BoringSSL, verify_cert_chain dùng convention kiểu boolean:
0 = fail
1 = success
Vì vậy chỉ cần tìm verify_cert_chain rồi hook vào onLeave,nếu retval là fail (0) thì đổi thành success (1).
Tương tự với Socket_CreateConnect, tìm Socket_CreateConnect , từ đó lần tới GetSockAddr ,hook GetSockAddr để lấy sockaddr rồi hook socket() để sửa destination IP/port sang Burp.