Kiến thức nền
Về thuật ngữ source và sink:
Về Java bytecode và framework ASM:
- https://sheon.hashnode.dev/java-learning-7-java-bytecode
- https://sheon.hashnode.dev/java-learning-8-java-bytecode-asm-framework
Tổng quan về Gadget Inspector
- được công bố trong bài trình bày Automated Discovery of Deserialization Gadget Chains bởi Ian Haken tại hội thảo DEFCON26 năm 2018
- Slide của bài trình bày Automated Discovery of Deserialization Gadget Chains: https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains.pdf
- Paper của bài trình bày Automated Discovery of Deserialization Gadget Chains: https://i.blackhat.com/us-18/Thu-August-9/us-18-Haken-Automated-Discovery-of-Deserialization-Gadget-Chains-wp.pdf
- Video bài trình bày Automated Discovery of Deserialization Gadget Chains: https://www.youtube.com/watch?v=KSA7vUkXGSg
- là công cụ phân tích tĩnh (static analysis) mã nguồn, nó kiểm tra trong các thư viện nằm trên Classpath và các thư viện tích hợp sẵn của ứng dụng, từ đó tự động tìm ra các gadget chain có thể sử dụng để khai thác trong ứng dụng.
Các thư viện thực hiện Serialization & Deserialization trong Java
Trong thực tế có nhiều thư viện hỗ trợ việc serialization & deserialization khác nhau:
- JDK (Object Input Stream)
- Jackson (XML, JSON)
- Xstream (XML, JSON)
- Genson (JSON)
- JSON-IO (JSON)
- FlexSON (JSON)
- Fastjson (JSON)
Hành vi khi thực hiện deserialization của các thư viện này cũng khác nhau. Theo đó magic method khác nhau sẽ được gọi tự động và các magic method này có thể được sử dụng làm entry point (hay còn gọi là source) của quá trình deserialization. Nếu các này gọi các sub-method khác thì một sub-method trong call chain cũng có thể được sử dụng làm source, tương đương với việc biết phần trước của call chain, bắt đầu từ một sub-method để tìm các nhánh khác. Một số phương thức nguy hiểm (dangerous method) hay còn gọi là sink có thể bị chạm tới thông qua nhiều layer các method call.
- ObjectInputStream:
- Cho ví dụ: một class implement interface
Serializable
thìObjectInputStream.readobject()
sẽ tự động tìm và gọi methodreadObject()
,readResolve()
và một số methods của class khi thực hiện quá trình deserialization. - Cho ví dụ: một class implement interface
Externalizable
thìObjectInputStream.readObject()
sẽ tự động tìm và gọi methodreadExternal()
và 1 số methods của class khi thực hiện quá trình deserialization.
- Cho ví dụ: một class implement interface
- Jackson:
- Khi
ObjectMapper.readValue()
thực hiện deserialization một class, nó sẽ tự động tìm hàm constructor không có argument (no-argument constructor) của class cần deserialization, constructor có chứa base parameter, setter của property, getter của property,…
- Khi
Phân tích
Điểm khởi đầu của tool chính là hàm main()
trong file src\main\java\gadgetinspector\GadgetInspector.java. Cùng đi phân tích lần lượt những gì có trong hàm này.
1 | if (args.length == 0) { |
Mở đầu của hàm, đoạn này xác định xem tham số khởi động liệu có bị trống hay không ? Nếu có, chương trình sẽ thoát ngay lập tức.
1 | configureLogging(); |
Tiếp tục, configureLogging()
dùng để cấu hình log. Biến resume
sẽ được mô tả chi tiết phần sau.
Ta thấy dòng GIConfig config = ConfigRepository.getConfig("jserial");
khởi tạo biến config
có kiểu dữ liệu GIConfig
. Thực tế GIConfig
là 1 interface. Bước này dùng để thống nhất việc quản lý output và thống nhất cấu hình kiểu serialization. Cùng xem qua GIConfig
tại src\main\java\gadgetinspector\config\GIConfig.java
1 | public interface GIConfig { |
Do có kiểu serialization khác nhau nên ta cần triển khai (implement) các SerializableDecider
, ImplementationFinder
và SourceDiscovery
dành riêng cho từng kiểu serialization.
Trong 3 thư mục: src\main\java\gadgetinspector\jackson, src\main\java\gadgetinspector\javaserial, src\main\java\gadgetinspector\xstream tác giả đã implement các SerializableDecider
, ImplementationFinder
và SourceDiscovery
dành riêng cho từng hình thức serialization.

Sử dụng Jackson làm ví dụ để phân tích. Xem file src\main\java\gadgetinspector\jackson\JacksonSerializableDecider.java để thấy SerializableDecider
dành cho kiểu serialization trong thư viện Jackson.
1 | public class JacksonSerializableDecider implements SerializableDecider { |
Trong phần này ta chỉ cần chú ý đến method apply()
, đây là 1 method có return type là boolean. Có thể thấy rằng miễn là có một hàm constructor không có tham số (no-argument constructor) thì có nghĩa là nó có thể serialize được. Bởi vì trong Jackson, nếu không có implement cụ thể gì bên trong 1 no-argument constructor và lúc đó lại implement bên trong 1 constructor có argument thì lúc này Jackson sẽ deserialize dữ liệu JSON.
Mọi instance đều được khởi tạo (instantiated) thông qua no-argument constructor. Do đó nếu 1 class không có no-argument constructor thì nó không thể được deserialize bởi Jackson.
1 | if (classMethods != null) { |
Ta thấy khối if (method.getName().equals("<init>") && method.getDesc().equals("()V"))
dùng để kiểm tra xem class đang kiểm tra có no-argument constructor hay không? Nếu không có thì nó không thể deserialize được -> không deserialize được thì ta không thể kiểm soát được data flow -> không kiểm soát được data flow thì gadget chain tìm được sẽ không hợp lệ.
Tiếp tục, xem file src\main\java\gadgetinspector\jackson\ JacksonImplementationFinder
1 | package gadgetinspector.jackson; |
Để ý tới method JacksonImplementationFinder.getImplementations()
, bởi vì Java là ngôn ngữ đa hình nên ta chỉ có thể biết được một class đã implement gì từ một interface trong run time. Trong khi đó gadget inspector không phải công cụ tìm gadget chain trong run time. Do đó, khi gặp một số lời gọi đến một số phương thức của interface, ta cần tìm tất cả các class đã implement các method của interface và tạo thành một chain trong số chúng và từ đó tạo thành một chuỗi method call và cuối cùng thực hiện Taint Analysis.
Method getImplementations()
này được đánh giá bằng cách gọi method apply()
của class JacksonSerializableDecider, bởi vì chúng ta có thể kiểm soát việc triển khai interface hoặc subclass, nhưng việc JSON có thể deserialize được hay không thì đòi hỏi JacksonSerializableDecider
phải xác định xem liệu có no-argument constructor hay không ?
Tiếp tục, xem file src\main\java\gadgetinspector\jackson\JacksonSourceDiscovery.java để thấy SourceDiscovery
dành cho kiểu serialization trong thư viện Jackson.
1 | package gadgetinspector.jackson; |
Class này chỉ có một method là discover()
. Tuy nhiên, nó là method quan trọng nhất để tìm gadget chain , bởi vì đối với chuỗi thực thi method của gadget chain, chúng ta phải có một entry có thể được kích hoạt và vai trò của JacksonSourceDiscovery
là tìm các entry method. Khi Jackson deserialize dữ liệu JSON, nó sẽ thực thi hàm constructor không có đối số cũng như các method setter()
và getter()
. Nếu chúng ta có các trường dữ liệu có thể kiểm soát được thì các method được thực thi này sẽ kích hoạt, nếu có gadget chain, nó có thể kích hoạt việc thực thi toàn bộ source-sink chain.
Quay trở lại hàm GadgetInspector.main()
. Xem xét tiếp đoạn code sau:
1 | int argIndex = 0; |
Đoạn code chỉ đơn giản kiểm tra các tham số ta truyền vào khi khởi động chương trình. Không cần để ý nhiều. Lúc đầu tôi có nhắc tới biến resume
sẽ được nói tới sau, khối if (arg.equals("--resume"))
kiểm tra xem nếu như ta có chuyển tham số --resume
vào khi khởi động chương trình hay ko ? Nếu ko truyền thì giá trị resume = false
, ở phần sau ta sẽ thấy nếu như resume = false
thì sẽ xóa toàn bộ file .dat
Tiếp tục phần sau của GadgetInspector.main()
:
1 | final ClassLoader classLoader; |
Phần code trên parse phần “–parameter” cuối cùng khi khởi động chương trình. Phần này có thể chỉ định một package WAR hoặc nhiều package JAR và đặt vào trong ClassResourceEnumerator
. ClassResourceEnumerator
sẽ đọc tất cả các class trong WAR và JAR đã tải hoặc đọc tất cả các class trong rt.jar
của jre.
Tiếp tục phần sau GadgetInspector.main()
:
1 | if (!resume) { |
Đoạn này rất đơn giản. Như đã phân tích ở phía trên, nếu như không truyền tham số --resume
thì xóa toàn bộ file .dat.
Tiếp tục, đây là phần cuối cũng như core xử lý của method GadgetInspector.main()
:
1 | if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat")) |
5 khối code tương đương với 5 bước thực hiện của công cụ mà tác giả đã nói trong slide:
- Bước 1: Enumerate class/method hierarchy.
- Bước 2: Discover Passthrough Dataflow.
- Bước 3: Enumerate Passthrough Callgraph.
- Bước 4: Enumerate Sources Using Know Tricks.
- Bước 5: BFS on Call Graph for Chains.
Bước 1: Enumerate class/method hierarchy
Bước này chủ yếu thu thập dữ liệu class, dữ liệu method và dữ liệu mối quan hệ kế thừa của các class.
1 | if (!Files.exists(Paths.get("classes.dat")) || !Files.exists(Paths.get("methods.dat")) |
Có thể thấy ở trên, khối if
kiểm tra xem liệu 3 file classes.dat, methods.dat, inheritanceMap.dat có tồn tại không ? Nếu không tồn tại thì tạo ra 1 instance MethodDiscovery
ở và gọi tới method discover()
của instance này.
Tới src\main\java\gadgetinspector\MethodDiscovery.java để xem cách method MethodDiscovery.discover()
được triển khai.
1 | public void discover(final ClassResourceEnumerator classResourceEnumerator) throws Exception { |
Bên trong MethodDiscovery.discover()
, method classResourceEnumerator.getAllClasses()
sẽ lấy ra toàn bộ class từ trong rt.jar
, package war và jar được cấu hình với tham số khởi động chương trình. Vòng for
sẽ duyệt qua từng class. Bên trong vòng for
, dòng ClassReader cr = new ClassReader(in);
khởi tạo một instance của ClassReader
trong ASM framework.
Quay trở lại ta với đoạn code bên trên:
1 | ClassReader cr = new ClassReader(in); |
Ở sử dụng method ClassReader.accept()
để quan sát từng class với ClassVisitor
được truyền vào, ở đây ClassVisitor
được truyền vào chính là MethodDiscoveryClassVisitor
.
Code được triển khai của class MethodDiscoveryClassVisitor
cũng nằm trong class MethodDiscovery
:
1 | private class MethodDiscoveryClassVisitor extends ClassVisitor { |
Thứ tự các method mà ClassVisitor
sẽ truy cập sẽ là: visit()
->visitField()
->visitMethod()
->visitEnd()
.
Ta phân tích MethodDiscoveryClassVisitor.visit()
, đây là method được thực thi đầu tiên khi class hiện tại được quan sát bởi ClassReader
. Xem lại triển khai của MethodDiscoveryClassVisitor.visit()
:
1 |
|
Ta thấy khi MethodDiscoveryClassVisitor.visit()
được thực thi thì nó sẽ lưu lại 1 số thông tin của class đang được quan sát như sau:
this.name
: tên class hiện tạithis.superName
: tên của class cha đã kế thừathis.interfaces
: tên các inteface đã implementthis.isInterface
: liệu class hiện tại có phải là interface kothis.members
: tập hợp các field của class hiện tạithis.classHandle
: đóng gói của các tên class bên trong gadget inspector
Tiếp theo ta phân tích MethodDiscoveryClassVisitor.visitField()
, đây là method được thực thi ngay sau MethodDiscoveryClassVisitor.visit()
. Xem lại triển khai của MethodDiscoveryClassVisitor.visitField()
:
1 | public FieldVisitor visitField(int access, String name, String desc, |
Ta thấy return type của method MethodDiscoveryClassVisitor.visitField()
là FieldVisitor
. FieldVisitor
được dùng để truy cập các field của class hiện tại được quan sát. Class hiện tại được quan sát có bao nhiêu field thì method MethodDiscoveryClassVisitor.visitField()
sẽ được gọi bấy nhiêu lần.
Ta thấy mỗi lần MethodDiscoveryClassVisitor.visitField()
được gọi thì nó đều tạo ra 1 biến typeName
, khối if (type.getSort() == Type.OBJECT || type.getSort() == Type.ARRAY)
sẽ đánh giá kiểu của field, cuối cùng ở dòng members.add()
thêm typeName
vào bên trong biến this.members
mà ta biết this.members
được khởi tạo từ method MethodDiscoveryClassVisitor.visit()
.
Tiếp theo ta phân tích MethodDiscoveryClassVisitor.visitMethod()
, đây là method được thực thi ngay sau MethodDiscoveryClassVisitor.visitField()
. Xem lại triển khai của MethodDiscoveryClassVisitor.visitMethod()
:
1 |
|
Gần giống với MethodDiscoveryClassVisitor.visitField()
, MethodDiscoveryClassVisitor.visitMethod()
có return type là MethodVisitor
, nó dùng để truy cập các method của class hiện tại. Class hiện tại được quan sát có bao nhiêu method thì method MethodDiscoveryClassVisitor.visitMethod()
sẽ được gọi bấy nhiêu lần. Mỗi lần MethodDiscoveryClassVisitor.visitMethod()
được gọi thì nó sẽ lưu lại thông tin của method vào trong biến this.discoveredMethods
(biến này có dạng ArrayList).
Cuối cùng xem xét MethodDiscoveryClassVisitor.visitEnd()
, đây cũng là method được thực thi sau cùng, sau khi thực thi toàn bộ các method visit()
ở phía trước.
1 |
|
Trong MethodDiscoveryClassVisitor.visitEnd()
, toàn bộ thông tin về class đang được quan sát sẽ được lưu vào this.discoveredClass
. Các thông tin được lưu vào this.discoveredClass
có cả thông tin các field thu thập được của class đang được quan sát (điều này có được thông qua biến members
, mà members
được tạo ra từ khi thực thi visitField()
).
Tại thời điểm này, sự thực thi của MethodDiscover.discover()
đã hoàn thành. Bước tiếp theo sẽ là sự thực thi của MethodDiscover.save()
, cùng xem triển khai của MethodDiscover.save()
:
1 | public void save() throws IOException { |
Thông tin class và thông tin method từ trong this.discoveredClasses
và this.discoveredMethods
được lưu lại nhờ method DataLoader.saveData()
. Định dạng thông tin được lưu trữ được triển khai thông qua ClassReference.Factory()
và MethodReference.Factory()
(lưu ý Factory là inner class có trong cả class ClassReference
và class MethodReference
)
Triển khai của DataLoader.saveData()
:
1 | public static <T> void saveData(Path filePath, DataFactory<T> factory, Collection<T> values) throws IOException { |
Ta thấy tại dòng final String[] fields = factory.serialize(value);
, dữ liệu sẽ được serialize (lưu ý serialize này không phải serialize trong suốt bài viết mà chỉ là 1 kiểu serialize dữ liệu khác) bằng việc gọi method serialize()
trên instance của Factory
. Các đoạn code về sau của method này được dùng để in ra dữ liệu ra thành từng dòng một.
Xem qua method serialize()
của ClassReference.Factory
:
1 | public static class Factory implements DataFactory<ClassReference> { |
Cuối cùng, một entry trong file classes.dat có dạng: "Tên class" "Các parent class" "Interface A, Interface B, Interface C" "Liệu có phải là interface ko?" "Field 1! Field 1 access! Field 1 type! Field 2! Field 2 access! Field 1 type"
.
Ví dụ một entry trong file classes.dat: com/oracle/net/Sdp$1 java/lang/Object java/security/PrivilegedAction false val$o!4112!java/lang/reflect/AccessibleObject
Trong đó:
com/oracle/net/Sdp$1
: là tên classjava/lang/Object
: là tên các parent classjava/security/PrivilegedAction
: là tên interfacefalse
: class này không phải interfaceval$o!4112!java/lang/reflect/AccessibleObject
:- các giá trị được ngăn cách bởi dấu “!”
val$o
: là Field 14412
: là Field 1 accessjava/lang/reflect/AccessibleObject
: là Field 1 type
Xem qua một chút method MethodReference.Factory.serialize()
:
1 | public static class Factory implements DataFactory<MethodReference> { |
Cuối cùng, một entry trong file methods.dat có dạng: "Tên class" "Tên method" "Method descriptor" "Liệu có phải là static method ko?"
Ví dụ một entry trong file methods.dat: com/oracle/net/Sdp$1 <init> (Ljava/lang/reflect/AccessibleObject;)V false
Trong đó:
com/oracle/net/Sdp$1
: là tên class<init>
: là tên method(Ljava/lang/reflect/AccessibleObject;)V
: là method descriptorfalse
: method này không phải static method
Quay trở lại với MethodDiscover.save()
thì sau khi thông tin class và method được lưu, thông tin class thu được sẽ được tiếp tục sử dụng để tiến hành phân tích tổng hợp các mối quan hệ triển khai (implementation) và kế thừa (inheritance) của class.
1 | // Tạo mối quan hệ ánh xạ của tên class |
Phần triển khai chính nằm ở method InheritanceDeriver.derive()
(tại dòngInheritanceDeriver.derive(classMap).save();
). Cùng xem triển khai của method InheritanceDeriver.derive()
:
1 | public static InheritanceMap derive(Map<ClassReference.Handle, ClassReference> classMap) { |
Ta thấy đoạn sau gọi tới InheritanceDeriver.getAllParents()
:
1 | // Lấy toàn bộ parent class, super classes, và interface của classReference |
Cùng xem triển khai của method InheritanceDeriver.getAllParents()
:
1 | public class InheritanceDeriver { |
Cuối cùng của method InheritanceDeriver.derive()
sẽ tạo ra 1 instance của InheritanceMap
với tham số là implicitInheritance
, một entry trong implicitInheritance
có dạng "Tên parent class" "Tên subclass 1” “Tên subclass 2” …
Đi tiếp tới constructor method InheritanceMap
:
1 | public class InheritanceMap { |
Quay trở lại với MethodDiscovery.save()
:
1 | public class MethodDiscovery { |
Sau khi gọi InheritanceDeriver.derive()
, ta có được dữ liệu thể hiện mối quan hệ triển khai và kế thừa là 1 instance của InheritanceMap
, do đó dữ liệu được lưu bằng cách gọi phương thức InheritanceMap.save()
Cùng xem triển khai của InheritanceMap.save()
:
1 | public class InheritanceMap { |
Bên trong InheritanceMap.save()
) tiếp tục gọi DataLoader.saveData()
và dữ liệu sẽ được serialize bằng việc gọi method serialize()
trên instance của InheritanceMapFactory
. InheritanceMapFactory
là inner class của InheritanceMap
.
Xem qua method InheritanceMapFactory.serialize()
:
1 | public class InheritanceMap { |
Cuối cùng, một entry trong file inheritanceMap.dat sẽ có dạng: "Tên class" "Parent class hoặc Super class hoặc Interface thứ 1" "Parent class hoặc Super class hoặc Interface thứ 2" … "Parent class hoặc Super class hoặc Interface thứ n"
Ví dụ một entry trong file inheritanceMap.dat: sun/management/ManagementFactoryHelper$LoggingMXBean java/lang/Object java/lang/management/PlatformLoggingMXBean java/util/logging/LoggingMXBean java/lang/management/PlatformManagedObject
Trong đó:
sun/management/ManagementFactoryHelper$LoggingMXBean
: là tên classjava/lang/Object
: là tên parent class của classsun/management/ManagementFactoryHelper$LoggingMXBean
java/lang/management/PlatformLoggingMXBean
: là tên interface màsun/management/ManagementFactoryHelper$LoggingMXBean
đã extendjava/util/logging/LoggingMXBean
: là tên interface màsun/management/ManagementFactoryHelper$LoggingMXBean
triển khaijava/lang/management/PlatformManagedObject
: là tên interface màjava/lang/management/PlatformLoggingMXBean
đã extend
Bước 2: Discover Passthrough Dataflow
Tiếp tục sang bước 2, đây là đoạn code được thực hiện bên trong method GadgetInspector.main()
:
1 | public class GadgetInspector { |
Trong phần này, tôi chủ yếu giải thích cách hoạt động của PassthroughDiscovery
, đây cũng là phần core của toàn bộ công cụ gadget inspector.
Trước khi nói tiếp về cách thức hoạt động của PassthroughDiscovery
, tôi muốn đưa ra ví dụ sau:
1 | public void main(String args) throws IOException { |
Từ đoạn code trên, ta có thể thấy class A
và method tên method()
. Sau khi method A.method()
được truyền parameter param
vào, nó return giá trị của parameter para
vừa được truyền, sau đó giá trị được return này được gán cho biến cmd
tại dòng String cmd = new A().method(args);
trong method main()
và cuối cùng method Runtime.getRuntime().exec()
sẽ thực thi lệnh bên trong biến cmd
(trong Java, đây là method nguy hiểm có thể dẫn tới hậu quả là RCE).
Ta có thể thấy, miễn là ta có thể kiểm soát các tham số đầu vào của method, ta có thể kiểm soát giá trị trả về của phương thức của nó và kiểm soát luồng dữ liệu đến Runtime.exec()
. Việc này giống như taint analysis và trong giai đoạn xử lý của class PassthroughDiscovery
thì điều quan trọng nhất là thực hiện được một việc như vậy. Việc này được thực hiện bằng cách liên tục analyze tất cả các method, xem liệu chúng có bị ảnh hưởng bởi các parameter đầu vào hay không?
Ngoài ra, việc luồng dữ liệu (data flow) được truyền thông qua method không chỉ một hoặc hai layer mà có thể liên quan đến nhiều method trong toàn bộ gadget chain. Sau đó, để tiến hành taint analysis của tất cả các data flow của method, thứ tự analysis sẽ là điều kiện tiên quyết để có thể thành công. Để giải thích, ta tiếp tục với một ví dụ:
1 | public void main(String args) throws IOException { |
Trong đoạn code trên, có thể thấy quy trình cụ thể giữa source và sink. Sau quá trình taint analysis các data flow, chúng ta có thể nhận được kết quả (cách thể hiện kết quả như vậy giống cách tác giả Ian Haken thể hiện trong paper):
A.method1()->1
B.method2()->1
C.method3()->1
Con số 1 đứng sau A.method1()-> biểu thị cho việc parameter 1 kiểm soát return value của A.method1().
Việc đánh số cho parameter có quy tắc như sau:
- biến
this
của class sẽ được tính là parameter 0. - các tham số được truyền vào hàm sẽ được tính tăng dần từ 1. Ví dụ: nếu
public String method1(String param)
thìparam
tính là parameter 1, còn nếupublic String method1(String param, Int age)
thìparam
tính là parameter 1 cònage
tính là parameter 2.
Từ việc phân tích code, vì chúng ta có thể kiểm soát các parameter của A.method1()
và return value của nó cũng được điều khiển gián tiếp bởi các parameter.
- Return value của
A.method1()
sau đó được gán cho biếncmd
tại dòngString cmd = new A().method1(args);
, điều đó có nghĩa là ta cũng có thể kiểm soát biếncmd
. - Sau đó dòng
new B().method2(cmd);
lại gọiB.method2()
, biếncmd
được sử dụng làm parameter choB.method2()
và nó lại tiếp tục được làm parameter củaC.method3()
và cuối cùng nó chạm tớiRuntime.getRuntime().exec(param)
. Điều này có nghĩa là miễn là chúng ta có thể kiểm soátA.method1()
thì tới cuối cùng chúng ta có thể sử dụng dữ liệu này để tác động đến toàn bộ source->sink và cuối cùng thực hiện RCE.
Từ luồng code trên, miễn là ta biết tham số nào có thể bị ô nhiêm (tainted) bởi method A.method1()
, method B.method2()
và method C.method3()
thì ta có thể xác định việc lây lan sự ô nhiễm (tainted transfer) từ sink đến source. Tuy nhiên, có một vấn đề ở đây đó là: trước khi nhận được kết quả là các parameter của B.method2()
đã bị tainted, trước tiên ta phải nhận được kết quả là các parameter của C.method3()
đã bị tainted. Cụ thể thực hiện việc này như thế nào? Trong Gadget Inspector, DTS được sử dụng. Đây là một phương pháp sắp xếp đảo ngược topology (inverse topological sorting).
Về inverse topological sorting, đầu tiên chúng ta nhận được một Set
chứa các method được sắp xếp theo thứ tự ngược lại của chuỗi method call. Sau đó, thực hiện quá trình taint analysis từ tham số ở cuối và thực hiện đảo ngược. Tức là, trước tiên ta xác nhận các tham số của C.method3()
bị tainted và lưu đây lại xác nhận này. Khi analyze B.method2()
, chúng ta có thể tiếp tục phân tích dựa trên xác nhận thu được trước đó là C.method3()
bị tainted và cuối cùng thu được xác nhận rằng B.method2()
cũng bị tainted. Vậy tool thực hiện inverse topological sorting như thế nào, ta xem tiếp code triển khai của PassthroughDiscovery.discover()
:
1 | public class PassthroughDiscovery { |
Có thể thấy rằng ba thao tác đầu tiên PassthroughDiscovery.discover()
làm là tải các class, method và thông tin triển khai & kế thừa từ file.
Tiếp theo, dòng Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);
gọi method PassthroughDiscovery.discoverMethodCalls()
để sắp xếp tập hợp ánh xạ giữa tất cả các method, method của caller method và method của callee method.
Cùng xem PassthroughDiscovery.discoverMethodCalls()
:
1 | public class PassthroughDiscovery { |
Dòng MethodCallDiscoveryClassVisitor visitor = new MethodCallDiscoveryClassVisitor(Opcodes.ASM6);
sử dụng MethodCallDiscoveryClassVisitor
(được extend từ ClassVisitor
) mục đích để thu thập các method call.
Cùng xem MethodCallDiscoveryClassVisitor
:
1 | public class PassthroughDiscovery { |
Trình tự thực thi của method bên trong MethodCallDiscoveryClassVisitor
là: visit()
-> visitMethod()
-> visitEnd()
. Cụ thể:
visit()
: Trong method này, tên class hiện đang được quan sát được gán chothis.name
visitMethod()
: Trong method này ở dòngMethodCallDiscoveryMethodVisitor modelGeneratorMethodVisitor = new MethodCallDiscoveryMethodVisitor(api, mv, this.name, name, desc);
, nó tiếp tục quan sát thêm chi tiết từng method của class đang được quan sát thông quaMethodCallDiscoveryMethodVisitor
(được extend từMethodVisitor
)
Mỗi khi method đang được theo dõi bên trong visitMethod()
gọi một method khác thì MethodCallDiscoveryMethodVisitor.visitMethodInsn()
sẽ được thực thi. Ta cùng xem triển khai của MethodCallDiscoveryMethodVisitor
:
1 | public class PassthroughDiscovery { |
Tôi đã comment khá chi tiết tại đây. Khi constructor method của class MethodCallDiscoveryMethodVisitor
được thực thi thì this.calledMethods
ở dòng private final Set<MethodReference.Handle> calledMethods;
sẽ được khởi tạo. this.calledMethods
có scope là private nằm bên trong MethodCallDiscoveryMethodVisitor
. Ta biết mỗi khi method đang được quan sát từ bước MethodCallDiscoveryClassVisitor.visitMethod()
gọi tới 1 method nào đó thì MethodCallDiscoveryMethodVisitor.visitMethodInsn()
sẽ được gọi. Lúc đó nhiệm vụ của this.calledMethods
là lưu thông tin method được gọi.
Sau cùng, các thông tin của method đang được theo dõi sẽ được lưu vào methodCalls
(lí do bởi là biến methodCalls
có scope nằm tại class PassthroughDiscovery
nên sẽ thuận tiện cho các method khác cùng nằm trong class PassthroughDiscovery
truy cập), methodCalls
được lưu theo dạng: {“tên class của method đang được theo dõi, tên của method đang được theo dõi, method descriptor của method đang được theo dõi” = “tên class của method được method đang theo dõi gọi, tên của method được gọi, method descriptor của method được gọi”}
Quay trở lại với PassthroughDiscovery.discover()
, sau khi thực thi xong Map<String, ClassResourceEnumerator.ClassResource> classResourceByName = discoverMethodCalls(classResourceEnumerator);
thì sau đó thực hiện List<MethodReference.Handle> sortedMethods = topologicallySortMethodCalls();
, ta thấy ở đây gọi đến method topologicallySortMethodCalls()
:
Cùng xem triển khai của topologicallySortMethodCalls()
:
1 | public class PassthroughDiscovery { |
Đoạn dưới được sử dụng để chuyển cấu trúc dữ liệu của methodCalls
sang dạng Map<MethodReference.Handle, Set<MethodReference.Handle>>
và lưu vào outgoingReferences
1 | Map<MethodReference.Handle, Set<MethodReference.Handle>> outgoingReferences = new HashMap<>(); |
Tiếp tục, đoạn dưới gọi tới method dfsTsort()
để thực hiện inverse topological sorting.
1 | for (MethodReference.Handle root : outgoingReferences.keySet()) { |
Trước khi nói tiếp về inverse topology sorting thì tôi sẽ nói qua 1 chút về topology sorting: việc sắp xếp theo cấu trúc liên kết (topology sorting) chỉ khả dụng cho các biểu đồ theo chu kỳ có hướng (directed acyclic graphs - DAG), các biểu đồ không phải DAG không có khả năng sử dụng topology sorting.
DAG phải thỏa mãn các điều kiện sau:
- Mỗi đỉnh xuất hiện và chỉ xuất hiện một lần
- Nếu A đứng trước B trong dãy thì không có đường đi từ B đến A như trên hình.

Đồ thị như vậy là đồ thị topo có thứ tự. Cấu trúc tree thực sự có thể được chuyển thành phân loại topo, trong khi phân loại topo không nhất thiết phải chuyển thành cây.
Lấy sơ đồ sắp xếp topo ở trên làm ví dụ, sử dụng dictionary (trong python) để biểu diễn cấu trúc biểu đồ:
1 | graph = { |
Code triển khai:
1 | graph = { |
Nhưng trong method call, chúng ta hy vọng kết quả cuối cùng là ['c', 'b', 'e', 'd', 'a']
,ngược lại với ['a', 'd', 'e', 'b', 'c']
. Bước này yêu cầu ta phải sử dụng inverse topological sorting, sắp xếp thuận dùng BFS thì kết quả ngược lại mới có thể sử dụng DFS. Tại sao chúng ta cần sử dụng inverse topological sorting trong method call, điều này có liên quan đến việc tạo luồng dữ liệu Passthrough. Hãy xem ví dụ sau:
1 | ... |
Vậy có mối quan hệ nào giữa arg
và return type không? Giả sử Obj.childMethod
là:
1 | ... |
Vì return value của childMethod()
có liên quan đến param carg
nên có thể xác định rằng return value của parentMethod
có liên quan đến param arg
. Vì vậy, nếu có một lệnh gọi sub-method và truyền param của parent-method cho sub-method đó, trước tiên cần xác định mối quan hệ giữa giá trị trả về của sub-method và đối số của sub-method. Do đó, việc đánh giá sub-method cần phải được thực hiện trước, đó là lý do tại sao việc inverse topological sorting được thực hiện.
Như bạn có thể thấy trong hình bên dưới, cấu trúc dữ liệu của outgoingReferences
trong method topologicallySortMethodCalls()
là:

Nhưng ở trên đã nói rằng topology không thể tạo thành một vòng khi sắp xếp mà phải có một vòng trong chuỗi method call.
Cùng xem triển khai của dfsTsort()
để thấy tác giả tránh được bằng cách nào.
1 | public class PassthroughDiscovery { |
- biến
dfsStack
đảm bảo rằng các vòng lặp không được hình thành khi thực hiện inverse topological sorting. - biến
visitedNodes
tránh việc sắp xếp lặp lại khi chuỗi method call bị chồng chéo. - biến
sortedMethods
là kết quả cuối cùng sau khi thực hiện inverse topological sorting.
Sử dụng biểu đồ call graph sau để minh họa quy trình xử lý bên trong method dfsTsort()
: