2 điểm bởi GN⁺ 2024-09-15 | 1 bình luận | Chia sẻ qua WhatsApp
  • FlowTracker là một Java agent theo dõi quá trình chương trình Java đọc, thao tác và ghi dữ liệu, cho thấy đầu ra đến từ đầu vào, tệp, mạng hay hằng số mã nào
  • Bằng cách quan sát chương trình đang chạy, công cụ hiển thị I/O tệp và mạng, đặc biệt theo dõi mối tương ứng giữa đầu vào và đầu ra, giúp hiểu đầu ra của chương trình Java có ý nghĩa gì và vì sao nó được tạo ra
  • Trong demo Spring PetClinic, có thể lần theo header của phản hồi HTTP, template Thymeleaf, giá trị cơ sở dữ liệu cho tới script chèn SQL, qua đó khám phá nhiều tầng của stack phần mềm
  • Về nội bộ, công cụ instrument bytecode tại thời điểm JVM tải lớp, kết hợp hook phương thức JDK, phân tích luồng dữ liệu, theo dõi lời gọi dựa trên ThreadLocalClassOriginTracker để duy trì ánh xạ nguồn gốc tập trung vào chuỗi, ký tự và byte
  • Trạng thái hiện tại gần với bằng chứng khái niệm hơn là sẵn sàng cho production; dù hoạt động tốt với một số chương trình ví dụ, nó không phù hợp với mọi chương trình và làm tốc độ chạy chậm đi đáng kể do overhead lớn

FlowTracker theo dõi những gì

  • FlowTracker là một Java agent theo dõi cách dữ liệu được đọc, truyền đi, biến đổi và ghi trong chương trình Java
  • Không chỉ hiển thị I/O tệp và mạng, nó còn liên kết đầu ra của chương trình với đầu vào mà đầu ra đó bắt nguồn từ
  • Mục tiêu là giúp hiểu đầu ra của chương trình Java có ý nghĩa gì và vì sao chương trình đã ghi đầu ra đó
  • Dự án hiện tại là một proof-of-concept nhằm khám phá những insight có thể thu được khi nhìn hành vi chương trình từ góc độ này

Demo: Theo dõi nguồn gốc phản hồi HTTP trong Spring PetClinic

  • FlowTracker PetClinic demo cho phép xem trong trình duyệt quá trình Spring PetClinic xử lý yêu cầu HTTP và tạo trang HTML dựa trên template cùng dữ liệu cơ sở dữ liệu
  • Màn hình hiển thị phản hồi HTTP mà PetClinic gửi qua mạng; khi bấm vào một phần của thân phản hồi, bạn có thể kiểm tra phần đó đến từ đâu ở khung xem phía dưới
  • Có thể chọn đầu vào/nguồn gốc hoặc đầu ra/sink đã được theo dõi từ cây bên trái, hoặc từ nút phía dưới bên trái trên di động
  • Tầng xử lý HTTP

    • Khi bấm vào "HTTP/1.1" hoặc header HTTP, có thể thấy phần phản hồi này được tạo bởi các lớp Apache Coyote trong package org.apache.coyote
    • FlowTracker hiển thị đoạn mã nào đã tạo ra đầu ra nào
  • Tầng template Thymeleaf

    • Khi bấm vào tên thẻ HTML như "html" hoặc "head", có thể thấy phần HTML tương ứng đến từ tệp layout.html
    • Sau khi bấm vào layout.html, nếu nhấn nút màu + ở phía dưới, mọi phần đến từ tệp đó sẽ được hiển thị cùng một màu
    • Khi cuộn xuống, có thể xác nhận một phần phản hồi đến từ tệp khác là ownerDetails.html
    • Khi bấm vào ký tự < hoặc >, có thể thấy ký tự đó được ghi bởi thư viện template Thymeleaf
  • Nguồn gốc của giá trị cơ sở dữ liệu

    • Bảng trong trang HTML chứa thông tin đến từ cơ sở dữ liệu
    • Khi bấm vào George trong bảng, giá trị đó không chỉ được truy ngược tới mức nó đến từ cơ sở dữ liệu, mà còn tới tận script SQL ban đầu đã chèn giá trị vào cơ sở dữ liệu
    • Lý do demo này truy vết được tới script SQL là vì nó dùng cơ sở dữ liệu in-memory, nên nội dung cơ sở dữ liệu không rời khỏi JVM

Demo MySQL và tính độc lập với framework

  • Khi chạy cùng demo PetClinic với cơ sở dữ liệu MySQL, các giá trị được truy vết tới điểm kết nối cơ sở dữ liệu
  • Trong trường hợp này, có thể thấy truy vấn SQL đã được gửi trước đó để tạo giá trị, cũng như chi tiết cách driver MySQL JDBC giao tiếp với cơ sở dữ liệu
  • FlowTracker PetClinic mysql demo cũng cho thấy FlowTracker chặn được nội dung đã giải mã được truyền qua kết nối SSL tới cơ sở dữ liệu
  • Spring PetClinic chỉ là một ví dụ; FlowTracker không phụ thuộc vào framework hay thư viện cụ thể nào
  • javac demo cho thấy FlowTracker giúp quan sát trình biên dịch Java để hiểu định dạng tệp class được tạo ra và bytecode bên trong đó như thế nào

Cách sử dụng và lưu ý

  • Hiện tại FlowTracker gần với proof-of-concept hơn là trạng thái sẵn sàng cho production
  • Nó hoạt động tốt với nhiều chương trình ví dụ, nhưng không đảm bảo hoạt động tốt với mọi chương trình
  • Công cụ thêm nhiều overhead, khiến chương trình chạy chậm hơn đáng kể
  • Quy trình sử dụng:
    • Tải agent jar flowtracker-*.jar từ Github releases pages
    • Thêm -javaagent:path/to/flowtracker.jar vào dòng lệnh Java
    • Để tắt một số tối ưu hóa JVM gây cản trở FlowTracker, cũng thêm đầu ra của java -jar flowtracker.jar jvmopts vào dòng lệnh
    • Theo mặc định, FlowTracker khởi động web server trên cổng 8011, nên chỉ cần mở http://localhost:8011/ trong trình duyệt
  • Các tùy chọn cấu hình chi tiết hơn có trong USAGE.md

Cơ chế nội bộ: Instrument bytecode và mô hình Tracker

  • FlowTracker là một agent instrument chèn mã vào class file, tức bytecode, khi JVM tải lớp
  • Mã được chèn duy trì ánh xạ giữa dữ liệu trong bộ nhớ và nguồn gốc của dữ liệu đó trong khi chương trình đọc, truyền và ghi dữ liệu
  • Trọng tâm theo dõi là dữ liệu văn bản và nhị phân như String, char, byte[], chứ không tập trung vào số, dữ liệu có cấu trúc hay dữ liệu được tính toán
  • Cách được sử dụng:
    • Thay thế một số lời gọi phương thức JDK bằng lời gọi tới phiên bản phương thức của FlowTracker
    • Chèn mã vào các vị trí cốt lõi của JDK để theo dõi đầu vào và đầu ra
    • Thực hiện phân tích luồng dữ liệu và instrument sâu hơn để theo dõi biến cục bộ và giá trị trên stack bên trong phương thức
    • Thêm mã trước/sau lời gọi phương thức và ở đầu/cuối phương thức được gọi để theo dõi đối số và giá trị trả về bằng ThreadLocal
  • Mô hình dữ liệu Tracker

    • Tracker: lưu nội dung và thông tin nguồn gốc của đối tượng được theo dõi
    • content: dữ liệu như mọi byte đã đi qua InputStream hoặc OutputStream
    • source: liên kết một phạm vi cụ thể của nội dung với một phạm vi cụ thể của tracker khác
    • TrackerRepository: lưu một Map<Object, Tracker> toàn cục lớn liên kết các đối tượng được quan tâm với Tracker tương ứng
    • TrackerPoint: trỏ tới một vị trí trong tracker, biểu diễn một giá trị primitive đơn lẻ như nguồn gốc của một byte

Instrumentation cơ bản: hook JDK và ASM

  • FlowTracker chèn lời gọi phương thức hook khi một số phương thức JDK nhất định được gọi để giữ Tracker luôn được cập nhật
  • Ví dụ đơn giản nhất là System.arraycopy
    • Thay thế lời gọi java.lang.System.arraycopy bằng lời gọi com.coekie.flowtracker.hook.SystemHook.arraycopy
    • SystemHook gọi arraycopy thật, rồi lấy tracker của mảng nguồn và mảng đích từ TrackerRepository, sau đó cập nhật để tracker đích trỏ tới nguồn
  • Việc instrumentation như vậy sử dụng thư viện thao tác bytecode ASM
  • Phần lớn hook được thêm vào phía được gọi bên trong phương thức JDK, chứ không phải phía caller
    • Ví dụ, thêm lời gọi FileInputStreamHook.afterReadByteArray vào cuối FileInputStream.read(byte[])
    • Instrumentation này được triển khai bằng một micro-framework tự xây dựng dựa trên annotation, sử dụng AdviceAdapter của ASM
  • FlowTracker thêm hook vào các lớp liên quan đến I/O của JDK như java.io.FileInputStream, java.io.FileOutputStream, sun.nio.ch.FileChannelImpl, sun.nio.ch.IOUtil, sun.nio.ch.NioSocketImpl
  • Triển khai liên quan:

Theo dõi giá trị primitive và phân tích luồng dữ liệu bên trong phương thức

  • Các giá trị primitive như byte không có identity như đối tượng, nên không thể theo dõi an toàn bằng khóa Map của TrackerRepository
  • FlowTracker viết lại mã để lưu riêng nguồn gốc của giá trị primitive vào biến cục bộ bên trong phương thức
  • Ví dụ, sau byte b = x[1], nó lấy tracker của b bằng ArrayHook.getElementTracker(x, 1), rồi khi y[2] = b thì ghi nguồn gốc vào mảng đích bằng ArrayHook.setElementTracker(y, 2, bTracker)
  • Để làm việc này, FlowTracker thực hiện diễn giải biểu tượng (symbolic interpretation) trên nền tảng tính năng phân tích của ASM
  • Tại từng điểm trong phương thức, nó mô hình hóa giá trị trong biến cục bộ và stack đến từ đâu và đi tới đâu
  • Triển khai liên quan:
    • FlowValueArrayLoadValue
    • MergedValue: xử lý các tình huống giá trị có thể đến từ nhiều vị trí do luồng điều khiển như câu lệnh if hoặc vòng lặp
    • FlowInterpreter: diễn giải lệnh bytecode bằng cách mở rộng Interpreter của ASM và tạo FlowValue phù hợp
    • StoreArrayStore
    • FlowTransformer: điều phối toàn bộ quá trình phân tích và instrumentation
  • Không theo dõi mọi giá trị primitive; trọng tâm là bytechar, còn intlong được xử lý hạn chế hơn

Luồng dữ liệu vượt qua lời gọi phương thức

  • Chỉ phân tích bên trong phương thức thì không thể xử lý các tình huống giá trị primitive chảy sang tham số và giá trị trả về của phương thức khác
  • FlowTracker lưu PointTracker của tham số và giá trị trả về vào Invocation, rồi đưa nó vào ThreadLocal ngay trước khi gọi phương thức
  • Ở điểm bắt đầu của phương thức được gọi, có thể lấy thông tin từ ThreadLocal bằng Invocation.start(...) để sử dụng nguồn gốc của tham số primitive
  • Bằng cách này, ngay cả khi truyền giá trị primitive vào phương thức như out.write(b), tracker của value vẫn có thể được kế thừa bên trong write(byte value)
  • Triển khai liên quan:

Xem chính mã nguồn như nguồn gốc dữ liệu

  • Các nguồn chính mà FlowTracker theo dõi là I/O và các giá trị đến từ chính mã nguồn
  • Các giá trị đến từ mã nguồn bao gồm hằng primitive và String như 'a', "abc"
  • Với các hằng như vậy, hệ thống tạo một ClassOriginTracker cho từng lớp và lưu giữ nội dung biểu diễn lớp cùng các tham chiếu hằng dưới dạng văn bản
  • Khi một hằng được tham chiếu, tracker của giá trị đó sẽ trỏ tới vị trí trong biểu diễn văn bản này
  • Mô hình này xem hằng như thể được đọc từ biểu diễn văn bản của mã nguồn, nên trở nên tương tự mô hình theo dõi I/O
  • Vì lý do hiệu năng, FlowTracker dùng ConstantDynamic (JEP 309) để tránh việc phương thức constantPoint bị gọi mỗi lần thực thi phương thức
  • Triển khai liên quan:

Xử lý String literal và các ràng buộc

  • String literal tạo một bản sao String mới và liên kết byte[] bên trong String.value với ClassOriginTracker
  • Câu lệnh như String s = "abc"; được viết lại thành dạng String s = StringHook.constantString("abc", 1234, 81);
  • Cách này phá vỡ đảm bảo String interning mà JVM thường cung cấp
    • Ban đầu, mọi lần xuất hiện của cùng một hằng String phải tham chiếu tới cùng một instance
    • Sau instrumentation, mã phụ thuộc vào đảm bảo này có thể bị lỗi
  • FlowTracker có một số cơ chế để giảm thiểu vấn đề này
    • Dùng ConstantDynamic để dù cùng một String literal trên cùng một dòng được thực thi nhiều lần thì vẫn trả về cùng một instance mỗi lần
    • Viết lại một số biểu thức stringA == stringB thành Objects.equals(stringA, stringB) để ở một góc nhìn nhất định trông như cùng một instance
    • Vô hiệu hóa theo dõi String literal trong một số package như java.lang.*
    • Hành vi này có thể cấu hình bằng breakStringInterning trong USAGE.md
  • Triển khai liên quan:

Fallback cho các giá trị không được theo dõi

  • FlowTracker không theo dõi mọi giá trị trong chương trình
  • Lý do là các lo ngại về hiệu năng, những phần chưa được triển khai, các giá trị ít liên quan, và việc biểu diễn các giá trị sinh ra từ sự kết hợp của nhiều nguồn sẽ cần một mô hình dữ liệu phức tạp hơn
  • Khi một giá trị trước đó không được theo dõi đi tới vị trí cần bắt đầu theo dõi, FlowTracker liên kết nó với ClassOriginTracker tương tự như hằng và biểu diễn vị trí đó bằng "<?>"
  • Ví dụ, độ dài mảng không được theo dõi, nên khi gọi write(array.length), một PointTracker trỏ tới vị trí mã nguồn của lời gọi write sẽ được truyền cho Invocation
  • Kết quả là trong đầu ra ở định dạng nhị phân, ngay cả khi không thấy được nguồn gốc ban đầu, đôi khi vẫn có thể nhanh chóng diễn giải ý nghĩa của giá trị thông qua các chuỗi đã được theo dõi xung quanh và vị trí trong mã nguồn

Các chủ đề triển khai có thể bàn thêm

  • MergedValue xử lý việc theo dõi các giá trị đi qua nhánh rẽ và vòng lặp, được xem là một trong những phần khó nhất trong phân tích luồng dữ liệu
  • String concatenation được xử lý thông qua indification (JEP 280) bằng cách thêm hook vào MethodHandle do StringConcatFactory trả về
  • Việc tìm mã nguồn, dịch ngược bằng Vineflower, và liên kết bytecode với dòng mã nguồn cũng được bao gồm trong phần triển khai
  • Cấu hình ClassLoader tập trung vào việc tránh phụ thuộc bootclasspath và xung đột với ứng dụng, đồng thời duy trì chu kỳ phát triển nhanh mà không cần shading và nested jar
  • Việc theo dõi các giá trị primitive được lưu trong field cũng được bao gồm trong phần triển khai
  • Frontend gồm web server dựa trên Jetty và JAX-RS, cùng web UI dựa trên Svelte

1 bình luận

 
GN⁺ 2024-09-15
Bình luận trên Hacker News
  • Tuyệt vời. Tôi cũng đã làm một công cụ cho Clojure theo hướng tương tự tên là FlowStorm http://www.flow-storm.org/
    Để instrument, nó dùng một bản fork của trình biên dịch Clojure chính thức thay vì một instrumentation agent, và tận dụng đặc tính của Clojure là có thể dễ dàng thay trình biên dịch trong lúc phát triển để chèn thêm bytecode
    Điểm thú vị trong bản ghi thực thi của chương trình Clojure là phần lớn giá trị là bất biến, nên chỉ cần giữ con trỏ là có thể chụp snapshot
    Bài demo gốc là khám phá ứng dụng web, nên tôi cũng để lại một demo debug ứng dụng web bằng FlowStorm cho ai quan tâm https://www.youtube.com/watch?v=h8AFpZkAwPo
    • Thật sự rất hay. Tôi tò mò vì sao bạn chọn JavaFX. Sau khi chọn JavaFX, bạn có xem qua cljfx không?
    • Hay đấy. Tôi cũng muốn biết bạn có thích cách dùng metadata của cấu trúc dữ liệu để theo dõi giá trị không
  • Thật sự ấn tượng
    Tôi rất thích vì các công cụ trong hệ sinh thái Java/JVM quá xuất sắc. Lần trước tôi ngạc nhiên như vậy là khi xem jitwatch https://github.com/AdoptOpenJDK/jitwatch
    FlowTracker gợi tôi nhớ đôi chút đến taint analysis, nơi theo dõi cách đầu vào người dùng chưa được kiểm chứng hoặc giá trị bí mật di chuyển trong chương trình để tránh bị rò rỉ hoặc bị dùng mà chưa kiểm chứng
    Từ khóa để tìm là “dynamic taint tracking/analysis”
    https://github.com/gmu-swe/phosphor
    https://github.com/soot-oss/SootUp
    https://github.com/feliam/klee-taint
  • Demo lần ngược từ một phần tử HTML tới tận câu lệnh SQL đã thêm giá trị đó vào cơ sở dữ liệu thật sự rất ấn tượng
    Tôi hoàn toàn có thể hình dung kiểu công cụ này sẽ trở thành tuyến phòng thủ đầu tiên khi truy vết lỗi trong tương lai
    • Cảm ơn
      Trong quá trình phát triển FlowTracker, rất nhiều công việc xuất phát từ việc làm cho việc truy vết hoạt động với các chương trình ví dụ cụ thể
      Tôi biết kết quả mong muốn là gì, nhưng rất khó đoán để một ví dụ cụ thể hoạt động thì cần hỗ trợ những cơ chế cấp thấp nào, và điều đó thường phụ thuộc vào chi tiết cài đặt nội bộ của JDK hay thư viện mà dữ liệu đi qua
      Nhưng việc một phần tử HTML được nối tới script SQL đã đưa dữ liệu đó vào DB thì lại không phải kiểu như vậy
      Tôi không kỳ vọng hay chủ đích làm ra điều đó; nó đơn giản là tự nhiên xảy ra, và vì thế tôi cũng khá bất ngờ, đồng thời càng háo hức muốn xem còn có thể làm được gì khác với cách tiếp cận này
    • Nghĩ kỹ thì nếu có một cách tiêu chuẩn để theo dõi nguồn gốc và tính xác thực của dữ liệu, rất nhiều vấn đề hẳn đã được ngăn chặn, và nhiều quy tắc nghiệp vụ cũng sẽ dễ biểu đạt hơn nhiều
      Cũng sẽ hay nếu có cách theo dõi liệu dữ liệu chỉ là tạm thời hay cần được ghi lại lần nữa
      Có thể mô tả những ràng buộc kiểu này ở phía trước càng nhiều thì càng tốt
  • Tôi vẫn chưa hiểu hoàn toàn bức tranh lớn hay cách dùng nó, nhưng nó làm tôi nghĩ đến môi trường Smalltalk, nơi mọi thứ đều có thể được kiểm tra
    Trong Smalltalk, mọi thứ đều là đối tượng và thông điệp, nên có thể lần ngược và tương tác
  • Rất hay. Video demo cũng tốt, và nó rõ ràng có vẻ hữu ích khi đào sâu vào một codebase xa lạ
  • Vài năm trước tôi từng thử nghiệm một ý tưởng tương tự[1]. Tôi muốn áp dụng thứ gì đó giống source map của JavaScript cho HTML
    Tôi đã không dành thời gian để mở rộng nó hơn, nhưng các công cụ phát triển web có lẽ sẽ hưởng lợi rất lớn từ kiểu theo dõi quy gán full-stack này
    Tuy vậy, việc tích hợp loại giải pháp này vào các framework hiện có có vẻ là một thách thức lớn
    [1] HTML Source Maps - https://github.com/connorjclark/html-source-maps https://docs.google.com/document/d/19XYWiPL9h9vA6QcOrGV9Nfkr...
  • Theo nghĩa tích cực, nó làm tôi nhớ đến demo Eve-lang khi debug chương trình chỉ đơn giản hỏi “Tại sao nó không ở đây?”. Công việc xuất sắc
    https://www.youtube.com/watch?v=TWAMr72VaaU&t=164shttps://witheve.com/
  • Nếu tôi nhớ không nhầm thì từng có một bài báo về một công cụ tương tự dùng để tìm SQL injection động trong chương trình Java. Đây có phải cùng một công cụ không?
    • Không, có lẽ là công cụ khác
      Nếu mở rộng những gì FlowTracker làm, nó có thể tìm ra SQL hoặc các lỗ hổng injection khác. Vì vậy có thể công cụ bạn nghĩ tới đã dùng cách tiếp cận tương tự
  • Có thời gian tôi từng tưởng tượng việc truy vết dữ liệu qua cả Internet. Ví dụ như một hình ảnh đến từ đâu, từng nằm ở CDN nào
    Hoặc những câu hỏi kiểu “chuỗi này đã đi qua những gì từ lúc được tạo ra cho đến khi tới màn hình của tôi”
    Đây có vẻ là một bước đi theo hướng đó
  • Tôi đang thử chạy công cụ này trong VSCode cùng với dự án mà tôi muốn tìm hiểu
    Giờ tôi phải dừng lại một chút, nhưng rất mong chờ lúc làm cho nó chạy được để nghịch thử mọi thứ