Build CodeQL DB without source code

Build CodeQL DB without source code

·

7 min read

Có thể với nhiều bạn đã biết, CodeQL hỗ trợ rất mạnh trong việc tìm kiếm lỗ hổng và các biến thể của lỗ hổng thông qua việc chuyển hóa và truy vấn dữ liệu từ DB. Tuy nhiên cũng có một vấn đề nhức nhối đi kèm với nó, đó là CodeQL chỉ hỗ trợ tạo database từ một bộ mã nguồn hoàn chỉnh, không bao gồm các file library của nó. Điều này tạo ra nhiều sai sót trong quá trình tìm kiếm lỗ hổng, Ví dụ như flow path sau:

class A

private void Foo(String arg1){
    String var1 = ExternalLibrary.doDecode(arg1);
    Runtime.getRuntime().exec(var1);
}

Theo như cách hoạt động, bóc tách code để insert vào DB của CodeQL mà mình đã nói tại bài viết trước (tại đây), thì thông tin về đoạn ClassA.Foo() -> ExternalLibrary.doDecode() vẫn sẽ được nhận biết để insert vào DB. Tuy nhiên tới đoạn ExternalLibrary.doDecode() thì do không có mã nguồn nên đương nhiên là thông tin về đoạn call này sẽ bị bỏ qua, và kéo theo đó là cũng không biết được kết quả trả về của ExternalLibrary.doDecode() là gì? có thể passthrough được hay không? Từ đó sẽ gây ra chuyện flow path trên bị đứt đoạn -> bỏ sót lỗ hổng trong quá trình rà soát mã nguồn. Đây là chuyện không thể chấp nhận được với một công cụ sinh ra chuyên biệt để tìm kiếm các biến thể 🤷‍♀️

image.png Mặc dù phía phát triển của CodeQL bằng nhiều cách, đã giảm thiểu sai sót này bằng cách tìm bằng cơm các passthrough variable, method. Nghĩa là dựa trên những lỗ hổng, các signature đã biết về các method có thể truyền dữ liệu qua, và có giữ nguyên dạng hay không. Ví dụ như với flow path vừa kể trên, lúc này trong thư viện "chuẩn" .qll của codeql, sẽ định nghĩa rằng: dữ liệu truyền vào ExternalLibrary.doDecode(), vẫn sẽ được tainted vào dữ liệu trả về. Như vậy thì khi query, codeql sẽ hiểu rằng biến "var1" là biến đổi của "arg1", do đó sẽ tiếp tục query tiếp tới đoạn "var1" được truyền vào .exec()! Nếu bạn đọc vẫn còn mơ hồ về điều này thì có thể trực tiếp kiểm nghiệm qua bộ thư viện chuẩn của codeql tại TaintTrackingUtil.qll#L320

private predicate taintPreservingArgumentToMethod(Method method, int arg) {
  method.getDeclaringType().hasQualifiedName("org.apache.commons.codec.binary", "Base64") and
  (
    method.getName() = "decodeBase64" and arg = 0
    or
    method.getName().matches("encodeBase64%") and arg = 0
  )
  or
  method.(TaintPreservingCallable).returnsTaintFrom(arg)
}

.

.

.

Tầm này năm ngoái thì mình có làm cái luận văn về cách hoạt động của CodeQL, trong quá trình nghiên cứu thì cũng có ý tưởng làm 1 cách nào đó để cho CodeQL có thể build được DB mà không cần source code.

Ý tưởng ban đầu của mình đó là sử dụng OW2 ASM để traverse bytecode, và sau đó lưu trữ các thông tin này vào trap file để tạo DB. Tuy nhiên phương thức này nói thì dễ, làm mới thấy vấn đề nhiều như thế nào. Ví dụ như các lệnh loop, try catch, ... sẽ không tường minh như khi traverse với source code. Như vậy sẽ yêu cầu viết lại một bộ thư viện chuẩn mới của CodeQL, dành riêng cho java bytecode để có thể biên dịch lại các bytecode này thành các lệnh loop, try catch.

Project đã bị bỏ dở hơn 2 tháng kể từ khi mình gặp vấn đề này, sau đó một ngày đẹp trời, đồng nghiệp mình có hỏi chi tiết về cách hoạt động của CodeQL sau series của mình (tại đây). Và có trình bày ý tưởng về một hướng đi khác để build DB cho CodeQL, ban đầu thì ý tưởng này gặp ngay sự phản đối của mình. Ý tưởng đó như thế này: Decompile nguyên cái file jar, sau đó tạo 1 script để javac compile các file đã được compile này. Ban đầu mình có phản đối ý tưởng này, đơn giản là vì mặc dù tỉ lệ decompile chính xác của fernflower decompiler hiện tại khá cao (lên tới 95%), nhưng số 5% còn lại đó vẫn là 1 vấn đề lớn, khi build cả 1 project chỉ cần 1 file lỗi thì sẽ khiến cho cả quá trình build bị dừng lại và sẽ không thể tiếp tục quá trình Extractor của CodeQL được. Tuy nhiên sau khi xem demo thì mình đã biết phương thức này không phải là vô lý, và hoàn toàn có thể biến nó thành sự thật được. Quay trở lại với phần 2 của series How CodeQL works, tại đây mình đã nói qua về cách setup môi trường để tự debug quá trình Extract của CodeQL, khuyến nghĩ bạn đọc nên xem qua để tránh một số bỡ ngỡ về sau! Config để debug như sau: image.png Còn đây là nội dung của file javac.args:

image.png Trong đó có bao gồm các file mã nguồn java sẽ được truyền vào javac để biên dịch. Theo cách làm của đồng nghiệp mình, khi đó các file java này sẽ là các file đã được decompile từ các file jar. Ở đây mình lấy apache tomcat ra làm ví dụ, các file đã được decompile từ catalina.jar và truyền vào arg của javac:

image.png Với lần đầu chạy thử, mình đã gặp ngay rất nhiều lỗi và đã bị crash chương trình, đây là output log khi chạy Extractor:

image.png

Những log về syntax error này rất nhiều, tuy nhiên chưa làm cho chương trình crash. Mò mẫm một hồi mới phát hiện ra nguyên nhân gây crash chương trình là đây:

image.png

Stacktrace:

image.png

Sai sót gây ra crash ở đây là do chương trình throw một AssertionError(), mà không có đoạn nào trong chương trình catch lỗi này cả, fix lỗi này đơn giản bằng cách thêm vài dòng code để catch AssertionError() này là xong:

image.png Sau khi patch lại file extractor, chương trình chạy khá là mượt và không gặp thêm lỗi gì nữa. Ước tính tầm 80% file đã được chuyển đổi sang file trap:

image.png

Các file trap đã được tạo thành công, bước tiếp theo đơn giản chỉ là import các file trap này thành dataset, phục vụ cho việc truy vấn tiếp theo, câu lệnh để import như sau:

codeql dataset import --dbscheme=.\semmlecode.dbscheme .\db-java .\trap\*

image.png

Với semmlecode.dbscheme là scheme của java, được tìm thấy trong folder binary của codeql:

image.png Tới đây thì việc tạo codeql DB đã gần xong rồi, chỉ còn 1 bước cuối đó là tạo file meta khai báo DB này, File meta của DB có dạng sau:

---
sourceLocationPrefix: "D:\\Research2021\\codeql\\newdb\\src"
unicodeNewlines: false
columnKind: "utf16"

Trong đó sourceLocationPrefix là vị trí của mã nguồn đã được decompile trên máy. Sau khi tạo metafile cho DB xong thì việc cuối cùng là import vào DB thôi:

image.png Thử nghiệm 1 query với DB mới này:

image.png

image.png

Như vậy là việc xây dựng CodeQL DB mã không cần mã nguồn đã không còn là viển vông nữa, Dù rằng tỉ lệ chính xác của phương pháp này không thể đạt được 100% như với mã nguồn gốc, nhưng bằng một vài phương pháp kết hợp với mã nguồn gốc các thứ, có thể sẽ giúp giảm tỉ lệ bỏ sót lỗ hổng, các tainted path khi truy vấn với CodeQL. Hạn chế hiện tại của CodeQL có chăng chỉ là tốc độ truy vấn mã nguồn mà thôi. Mình đã từng thử build một bộ DB của product X, kết quả được DB khá lớn ~20GB, và thử thực thi một vài truy vấn trên này thì thời gian phản hồi rất chi là lâu, mình đã đợi 1,2 ngày cho đến 1 tuần mà vẫn không thấy có kết quả gì, query server chỉ báo là running trong vô vọng ╮(╯_╰)╭. Trên đây là một vài chia sẻ của mình về cách build DB cho CodeQL mà không cần mã nguồn gốc của chương trình. Khuyến cáo: việc decompile để build DB này có thể sẽ vi phạm vào một vài quy tắc nào đó của các enterprise product, hãy suy nghĩ kỹ. Do at your own risk! Cảm ơn đồng nghiệp @tuyenlx đã chia sẻ idea này và biến nó thành sự thật! Cảm ơn bạn đọc đã theo dõi! Jang