Đây là cuộc thi về ctf do bên UIT tổ chức, trải qua 8 tiếng căng thẳng thì:…
Tuy được hạng 4 toàn bảng (hạng nhì offline), nma cũng khá xót khi còn thiếu tận 1.000…1 bài nữa để lên top :< và còn đâu đó 1 bài forensics dang dở + 1 bài cryptography biết hướng làm nhưng không ra .
Tổng cộng hôm thi team mình được 7 bài (1 MISC + 1 WEB + 2 PWN + 3 REV), trong đó thì mình ăn first blood cả 3 bài rev :)) + thêm bài misc khá là …
Trong danh sách writeups sau, mình sẽ trình bày theo mốc thời gian làm, thay vì độ khó :>. Thứ tự độ khó tăng dần sẽ là Memory -> Baby APK -> The return of Anti Debug -> Kittensware.
Bài này là bài làm lại (đổi biến số) từ 1 bài bị lỗi (Anti Debug) của kỳ Wannagame năm trước :)). Trước hôm thi thì mình có giải lại thử, nói chung đây sẽ là dạng để bạn thể hiện khả năng đọc hiểu code assembly và hiểu cách mà một số decompiler (IDA, Ghidra, …) output thông tin
Theo gợi ý, file code.txt sẽ có nội dung như sau (bỏ qua các struct ở đầu file):
Từ thông tin trên biết được file này là elf64, dump từ IDA 7.6, và không có liên kết thêm thư viện ngoài.
Câu hỏi được đặt ra là từ file này convert sang IDA được không <(“), thì câu trả lời có vẻ là không (mặc dù file có vẻ chi tiết hơn so với challenge năm trước), có thể tìm hiểu thêm tại đây
Nơi chương trình bắt đầu
Vị trí đầu tiên (Entry point) cần phải xem xét trong đa số trường hợp sẽ là main (Đây là hàm do IDA tự đặt tên dựa trên thông tin trên file), chỉ một số ít mới cần xét đến _start (Dành cho trường hợp nanomite - chương trình tự fork và tự điều khiển chính bản thân thông qua ptrace).
Stack ở hàm main thuộc dạng bp-based, tức là các vị trí đều được tính trên base pointer. Từ đây ta sẽ cố gắng khôi phục lại kiểu dữ liệu ban đầu của từng biến
Sau đó chương trình thực hiện lệnh call tới hàm sus và kết thúc chương trình:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
mov edi, 0Ah ; c call _putchar mov eax, 0 call sus mov eax, 0 mov rcx, [rbp+var_8] xor rcx, fs:28h jz short locret_1735 call ___stack_chk_fail ; ---------------------------------------------------------------------------
locret_1735: ; CODE XREF: main+1C7↑j leave retn
Tổng quan lại những gì chương trình đã làm trong hàm main:
Tạo ptr từ file “./somethingSecret.txt”
Tạo mảng s random
Tạo mảng var_50 = (ptr ^ s) + s (element wise)
Leak 3 phần tử đầu của mảng var_50 dưới dạng thập lục phân ra màn hình.
Hàm sus
Quá trình tạo pseudo-code cũng tương tự như trên (hoặc có thể đọc trực tiếp không cần thông dịch) :> Do hàm này sử dụng lại từ challenge trước, mình sẽ chỉ nói đại ý về công dụng thôi
Đọc file /proc/self/maps, đây chính là bảng chứa virtual address của process hiện tại. Sau đó đọc qua từng dòng, đến khi nào dòng có substring là ‘[st’ (chính là [stack]) thì thực hiện parse địa chỉ đầu và cuối của stack
Mở file /proc/self/mem, đây là memory hiện tại của chương trình, sau đó seek tới stack và dump toàn bộ ra file dump
Hướng giải quyết
Như vậy, ta đã biết được các thông tin sau:
stack của chương trình
Mảng var_50 thông qua những bytes đã được leak trong log.txt
Mảng s thông qua offset với mảng var_50
Không biết được thông tin của ptr trên heap (thông tin cần tìm)
Đây là bài mình giải thứ 2, khi giải xong cũng khá bất ngờ khi mà lúc nộp server lại import thiếu flag, tốn hơn nửa tiếng để xác nhận phía tác giả + fix
Decompile với jadx
Có thể thấy được chương trình hiện tại được build trên flutter. Ở flutter có 1 cái đặc biệt là debug mode và release mode sẽ hoàn toàn khác nhau, nguyên nhân là debug mode cần đến việc hot reload (cập nhật giao diện khi sửa code) bù lại cho việc app sẽ chạy chậm & unstable hơn.
Để phân biệt giữa 2 mode, Ta chỉ cần xét đơn giản như sau
Release mode (bài APK Again): Tồn tại file libapp.so trong folder lib/< arch >, chứa mã nguồn đã được compile lại thành VM Snapshot.
Phân tích kernel_blob.bin
Đầu tiên, extract string có trong file sang file text, ví dụ
1
strings kernel_blob.bin > abc.txt
Sau đó vào file, search với những cụm từ như main.dart, wannagame_championship(do đây cũng là package của app) để tìm vị trí của chương trình, tách lấy đoạn code cần thiết, ta được thành phẩm như sau:
classMyAppextendsStatelessWidget{ const MyApp({Key? key}) : super(key: key); // This widget is the root of your application. @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter APP', theme: ThemeData( // This is the theme of your application. // // Try running your application with "flutter run". You'll see the // application has a blue toolbar. Then, without quitting the app, try // changing the primarySwatch below to Colors.green and then invoke // "hot reload" (press "r" in the console where you ran "flutter run", // or simply save your changes to "hot reload" in a Flutter IDE). // Notice that the counter didn't reset back to zero; the application // is not restarted. primarySwatch: Colors.blue, ), home: const MyHomePage(title: 'WannaGame Championship Challenge'), ); } }
classMyHomePageextendsStatefulWidget{ const MyHomePage({Key? key, requiredthis.title}) : super(key: key); // This widget is the home page of your application. It is stateful, meaning // that it has a State object (defined below) that contains fields that affect // how it looks. // This class is the configuration for the state. It holds the values (in this // case the title) provided by the parent (in this case the App widget) and // used by the build method of the State. Fields in a Widget subclass are // always marked "final". finalString title; @override State<MyHomePage> createState() => _MyHomePageState(); }
class_MyHomePageStateextendsState<MyHomePage> { @override Widget build(BuildContext context) { // This method is rerun every time setState is called, for instance as done // by the _incrementCounter method above. // // The Flutter framework has been optimized to make rerunning build methods // fast, so that you can just rebuild anything that needs updating rather // than having to individually change instances of widgets. return Scaffold( appBar: AppBar( // Here we take the value from the MyHomePage object that was created by // the App.build method, and use it to set our appbar title. title: Text(widget.title), ), body: Input_Text(), ); } }
Future<List<int>> calc_sha256(String password) async { final algorithm_hash = Sha256(); final sink = algorithm_hash.newHashSink(); // sink.add(password.codeUnits); sink.add(await bytes(password)); sink.close(); final hash = await sink.hash(); return hash.bytes; }
Ở class cuối, có lời nhắn về việc flag đúng sai khi click vào button -> Ta sẽ bắt đầu ở hàm này. Source sẽ là input từ người dùng và Sink chính là hệ thống hiện thị flag đúng.
Về cơ bản thì 2 hàm này đều để compare giữa 2 object với nhau -> Đặt vấn đề sink sẽ nằm ở đây (mục tiêu tạo giá trị = True)
Ở hàm calc_sha256, ngoại trừ việc gọi tới hàm bytes đáng ngờ ra thì phần còn lại để tính hash sha256 như bình thường, suy ra mục tiêu cần đạt được là tìm input sao cho: hash_pass == SHA256(input).
Tuy nhiên với lượng thông tin như vậy là chưa đủ (hash không nằm trong database có sẵn trên web để crack), nên cần tập trung chính vào hàm calc_chacha20. Hàm cts so sánh giữa input và giá trị trả về của calc_chacha20, cho nên đây có thể là hàm giải mã 1 chuỗi đã được mã hoá trong chương trình.
Đến đây, có 2 cách có thể thực hiện:
Dynamic: chạy chương trình và hook vào hàm cts để lấy thông tin 2 chuỗi được so sánh
Static: Reproduce lại kết quả của hàm calc_chacha20
Ở đây thì mình cũng chưa rành phương pháp dynamic lắm, nên tạm thời sẽ theo hướng static là chính :>
Chacha20 gồm 2 thành phần chính là key và nonce tạo thành stream (https://loup-vaillant.fr/tutorials/chacha20-design). Sau đó việc encrypt/decrypt chỉ đơn giản là việc xor stream với message/ciphertext.
Cộng với việc ra, rb lần lượt tạo ra mảng 32 và 12 bytes -> đoán được đây là secret và nonce (có thể từ kiểm nghiệm lại từ flow của chương trình).
Ciphertext của chương trình nằm ở hàm m (giá trị trả về của hàm làm tham số để truyền vào SecretBox). Và phần này đơn giản chỉ là xor ngược lại mảng với 0xab để tìm ciphertext gốc:
Vấn đề đặt ra ở ra, rb: làm sao để tìm lại mảng ? (cả 2 hàm đều sử dụng đến random). Để ý thấy được cả 2 đều dùng 1 giá trị cố định cho seed của Random -> giá trị ở mỗi lần chạy có thể sẽ được giữ nguyên!.
Để cho tiện thì có thể sử dụng https://dartpad.dev/ để chạy chương trình và tìm giá trị trả về của 2 hàm như sau:
Về bài này thì team mình tự hào là team duy nhất giải ra (vào cuối giờ theo đúng nghĩa đen - 2 phút trước khi kết thúc). Đồng thời đây cũng là bài tốn thời gian nhất :)) ~30p mình ngồi kiếm cách để unpack VMProtect để rồi nhận ra không cần unpack vẫn xem code được … + ~ 1 tiếng để tĩnh tâm hiểu được flow chương trình. Trong vòng 1 tiếng thì mình dùng Rubber duck debugging với đứa đồng đội kế bên và thấy khá hiệu quả :> nên nếu có khó khăn gì thì cứ thử nhé .
Lan man vậy đủ rôì :> bắt đầu reverse thôi
Suspected binary in network trafic
Khi mở file lên bằng Wireshark, đầu tiên sẽ thực hiện filter dựa trên http protocol để tìm ra những file được download và upload (Data ở dạng raw, có thể dump được do không bị mã hoá trên đường truyền ở cả 2 phía):
Dễ dàng thấy được có 1 file Powershell đáng ngờ đã được tải về (dựa theo đuôi .ps), còn lại là các request POST tới /beacon_ring.
Công việc chính của file Powershell này là từ chuỗi mã hoá base64, decode và lưu ra file challenge.exe, sau đó sẽ khởi chạy file này (1 kiểu hoạt động cơ bản của Trojan, hide -> inject -> eavesdrop :>)
Để lấy được file challenge.exe thì cần decode base64 và lưu = thành file binary (có thể dùng tool Cyberchef luôn) hoặc đơn giản hơn thì chỉ cần xoá dòng cuối Start-Process..., chạy file Powershell cho nó tự giải mã và vào 'C:\Users\Public để lấy file (Không khuyến khích chạy file nếu chưa biết nó sẽ làm gì)
Sau khi có được challenge.exe, thử analyze với Detect It Easy ta có được thông tin như sau:
Các thông tin tìm được:
Được protect bằng VMProtect ở mức min (Phần này mọi người có thể tự tìm hiểu xem nó sẽ làm gì nhé)
Compile bằng MinGW’s gcc 64bit (hầu như sẽ không gọi tới WinAPI quá nhiều - 1 điểm cộng lớn khi reverse binary Windows .)
Phân tích bằng IDA
Khi mới đầu load, chương trình sẽ xuất hiện rất nhiều mã int 4, tuy nhiên có thể đay chỉ là interrupt của VMProtect, không ảnh hưởng tới chương trình lắm (thông qua debug), + thêm việc dùng indirect calling cho function, cho nên có thể dùng regex và filter bớt đi, được đoạn code sáng sủa hơn như sau:
-> Xác thực giả thiết và v61[0] sẽ chứa key ở dạng base64!
Tạo khoá công khai với RC4
Khúc này ban đầu mình cũng khá là đau đầu :)) hàm text_68 sau khi inspect sẽ biết được nó là vsprintf, vậy mà tham số truyền vào lại là mấy chuỗi tiếng Tàu… vậy thì đống data xử lý như thế nào <(“).
Khoảng vài phút sau mới nhận ra thực ra là lỗi bên IDA :)) VMProtect đổi mấy hàm strcpy thành wcscpy để anti static analysis, thành ra IDA sẽ luôn nhận tham số thứ 2 là chuỗi Unicode, thay vì là chuỗi ascii (1 ký tự Unicode được ghép từ 2 hay nhiều bytes lại với nhau, trong khi ascii chỉ cần 1 byte để biểu diễn)
Quick fix cho vấn đề này thì phải tab qua lại với bên assembly để lấy số ban đầu, sau đó decode sang chuỗi little endian… Chú ý: cách này mình dùng trong lúc thi thôi do thời gian gấp rút, còn cho đơn giản thì có thể dùng cyberchef như sau
Hơn nữa hàm parse_json cũng sẽ được gọi khá nhiều -> Chương trình đang thiết lập giao thức socket TCP bằng AES CBC với KEY và IV ở trên. Lúc mình làm ra khúc này thì còn 10p :)) nên phần sau đây sẽ lụi luôn <(“), setup cyberchef với thông số thích hợp và lần lượt dump các tcp stream để check xem giao tiếp qua lại những gì (khúc này chắc ý định của tác giả sẽ là viết tool để decode conversation, nhưng hên là không có quá nhiều nên mình làm chay vẫn kịp giờ :)))
Nhìn sơ qua thì có vẻ chương trình đang thực hiện chạy lệnh gì đó, (maybe nhận lệnh đọc file từ server, sau đó gửi ngược lại ?). Cấu trúc gói tin của 2 bên cũng tương tự như trên: