[Cloudflare]Cloudflare protects customers from cache poisoning

A few days ago, Cloudflare — along with the rest of the world — learned of a “practical” cache poisoning attack. In this post I’ll walk through the attack and explain how Cloudflare mitigated it for our customers. While any web cache is vulnerable to this attack, Cloudflare is uniquely able to take proactive steps to defend millions of customers.

In addition to the steps we’ve taken, we strongly recommend that customers update their origin web servers to mitigate vulnerabilities. Some popular vendors have applied patches that can be installed right away, including DrupalSymfony, and Zend.

How a shared web cache works

Say a user requests a cacheable file, index.html. We first check if it’s in cache, and if it’s not not, we fetch it from the origin and store it. Subsequent users can request that file from our cache until it expires or gets evicted.

Although contents of a response can vary slightly between requests, customers may want to cache a single version of the file to improve performance:

cache-ok

(See this support page for more info about how to cache HTML with Cloudflare.)

How do we know it’s the same file? We create something called a “cache key” which contains several fields, for example:

  • HTTP Scheme
  • HTTP Host
  • Path
  • Query string

In general, if the URL matches, and our customer has told us that a file is cacheable, we will serve the cached file to subsequent users.

How a cache poisoning attack works

In a cache poisoning attack, a malicious user crafts an HTTP request that tricks the origin into producing a “poisoned” version of index.html with the same cache key as an innocuous request. This file may get cached and served to other users:

cache-poison

We take this vulnerability very seriously, because an attacker with no privileges may be able to inject arbitrary data or resources into customer websites.

So how do you trick an origin into producing unexpected output? It turns out that some origins send back data back from HTTP headers that are not part of the cache key.

To give one example, we might observe origin behavior like:

An HTTP response that reflects back data in an HTTP request header

Because this data is returned, unescaped, from the origin, it can be used in scary ways:

An HTTP response that reflects back malicious JavaScript from an HTTP request header

Game over — the attacker can now get arbitrary JavaScript to execute on this webpage.

Notifying customers who are at risk

As soon as we learned about this new vulnerability, we wanted to see if any of our customers were vulnerable. We scanned all of our enterprise customer websites and checked if they echoed risky data. We immediately notified these customers about the vulnerability and advised them to update their origin.

Blocking the worst offenders

The next step was to block all requests that contain obviously malicious content — like JavaScript — in an HTTP header. Examples of this include a header with suspicious characters like < or >.

We were able to deploy these changes immediately for all customers who use our WAF. But we weren’t done yet.

A more subtle attack

There are other versions of the attack that could trick a client into downloading an unwanted but innocuous-looking resource, with harmful consequences.

Many requests that have traveled through another proxy before reaching Cloudflare contain the X-Forwarded-Host header. Some origins may rely on this value to serve web pages. For example:

An HTTP request may look innocuous, but contain malicious data that gets reflected by an origin

In this case, there’s no way to just block requests with this X-Forwarded-Host header, because it may have a valid purpose. However, we need to ensure that we don’t return this content to any users who didn’t request it!

There are a few ways we could defend against this type of attack. An obvious first answer is to just disable cache. This isn’t a great solution, though, as disabling cache would result in a tremendous amount of traffic on customer origin servers, which defeats the purpose of using Cloudflare.

Another option is to always include every HTTP header and its value in the cache key. However, there are many headers, and many different innocuous values (e.g. User-Agent). If we always included them in our default cache key, performance would degrade, because different users asking for the same content would get different copies, when they could all be effectively served with one.

Solution: include “interesting” header values in the cache key

Instead, we decided to change our cache keys for a request only if we think it may influence the origin response. Our default cache key got a bunch of new values:

  • HTTP Scheme
  • HTTP Host
  • Path
  • Query string
  • X-Forwarded-Host header
  • X-Host header
  • X-Forwarded-Scheme header

In order to prevent unnecessary cache sharding, we only include these header values when they differ from what’s in the URL or Host header. For example, if the HTTP Host is http://www.example.com, and X-Forwarded-Host is also http://www.example.com, we will not add the X-Forwarded-Host header to the cache key. Of course, it’s still crucial that applications do not send back data from any other headers!

One side effect of this change is that customers who use these headers, and also rely on Purge by URL, may need to specify more headers in their Purge API calls. You can read more detail in this support page.

Conclusion

Cloudflare is committed to protecting our customers. If you notice anything unusual with your account, or have more questions, please contact Cloudflare Support.

(Source link: https://blog.cloudflare.com/cache-poisoning-protection/)

[Vietnamese] Phương pháp cập nhật (update) phần mềm hiệu quả

Có bao giờ các bạn tự hỏi các hãng lớn cập nhật phần mềm của họ thế nào? Chúng ta dùng dạo qua dăm ba câu hỏi để có chút gợi ý trả lời cho câu hỏi trên.

– Tại sao việc cập nhật cho phần mềm chạy diện rộng lại là vấn đề?
Một trong các vấn đề cần xử lý trong các phần mềm chạy diện rộng là vấn đề cập nhật phần mềm hoặc các file data từ server về client. Giả dụ chúng ta có khoảng một triệu client trên toàn bộ hệ thống cần cập nhật phiên bản sửa lỗi mới nhất (hoặc file data có sửa đổi) thì làm thế nào cho hiệu quả. Nếu như cách thông thường thì ta download toàn bộ file cần cập nhật từ server về và thay thế file local tương ứng. Việc này thì đơn giản và thông dụng nhưng không hiệu quả nếu chúng ta có rất nhiều client cần cập nhật hoặc file quá lớn mà chỉ thay đổi phần nhỏ trong đó.  Việc không hiệu quả ở đây dễ thấy nhất là rất tốn băng thông và tài nguyên server, việc cập nhật cũng diễn ra chậm hơn làm ảnh hưởng đến trải nghiệm người dùng. Vậy có phương pháp nào để chúng ta cập nhật phần mềm hiệu quả? Chúng ta dùng xem xem liệu có cách nào hiệu quả cho vấn đề này.

– Ý tưởng nào đơn giản để có thể thực hiện?
Như ta đã biết không phải lúc nào cập nhật chúng ta cũng thay đổi toàn bộ source code của ứng dụng (hoặc file data) với trường hợp của file thực thi thì khi chúng ta sửa một phần source code thì khi biên dịch trình biện dịch (compiler) chỉ thay đổi ở phần code mới các phần code khác dù có dịch lại (cùng trình biên dịch) thì vẫn y như cũ do đó ta có thể dùng cách băm file binary ra thành nhiều phần để tính toán phần thay đổi hoặc bổ xung. Với file data, khi ta chỉ sửa hoặc bổ xung phần nhỏ cho file data và cũng có thể áp dụng tương tự như file thực thi.

– Ý tưởng cụ thể hơn ta có thể học hỏi?
Đơn giản nhất là ta hình dung theo cách làm của các chương trình quản lý source code – source code control (RCS, SVN, ClearCase, Git,..). Các ứng dụng này cho ta cái nhìn rất rõ về cách quản lý các phiên bản/thay đổi của source code và ta có thể học hỏi ý tưởng đó để triển khai.

– Nhưng có cách nào dễ dàng hơn không?
Việc phải tự nghiên cứu và làm từ đầu thì có quá nhiều việc nên xem ra khó khả thi và tốn kém. May mắn thay chúng ta có thể học hỏi và sử dụng cách của các hãng đã làm và ở đây có cách phổ biến nhất mà nhiều hãng dùng đó là Delta update (theo như tôi biết thì các hãng Microsoft, Google, Redhat đã từng/ đang dùng). Hiện Google dùng Courgette cho việc cập nhật Chrome Browser các bạn có thể tìm hiểu và áp dụng cho hệ thống của chúng ta.

(Vì lý do bận nên tôi chỉ có thể viết bài mang tính giới thiệu nên các bạn thông cảm, hi vọng tương lai tôi có thể bổ xung thêm thông tin cho bài viết).

Posted in AV

[Vietnamese] Làm thế nào để viết phần mềm diệt vi-rút – B.2 Cơ chế bảo vệ realtime, chức năng bảo vệ 24/7 cho máy tính

Bài trước chúng ta đã nói về cách quét và kiểm tra xem file có phải là vi-rút hay không bằng mã hash. Ở bài này chúng ta sẽ tìm hiểu về cơ chế bảo vệ realtime trong av ra sao. Ở bài này tui sẽ chỉ nói về cơ chế bảo vệ thời gian thực liên quan đến việc truy xuất file.

Chúng ta thường thấy cụm từ bảo vệ thời gian thực (realtime protection) trong các phần mềm av và cũng mường tưởng ra đôi chút về tính năng của nó giờ chúng ta sẽ cùng tìm hiểu cơ chế và cách viết tính năng realtime cho av.

Cơ chế bảo vệ realtime là gì tại sao nên bật nó? Thông thường sao các lần quét vi-rút toàn bộ hệ thống thì các av đã đảm bảo máy tính cho an toàn một phần rồi nhưng do quá trình sử dụng chúng ta thường hay download file, copy file từ ổ usb, cd/dvd hoặc thẻ nhớ, truy cập các thư mục chia sẻ trong công ty/trường học, cài đặt các phần mềm,… cho đến hiện tại đó là tác nhân gây lây nhiễm vi-rút lớn nhất cho máy tính. Dựa trên cơ chế lây nhiễm mà realtime được sinh ra để đảm bảo mỗi khi có file được truy cập (download, copy, thực thi,… ) sẽ luôn được kiểm tra xem có bị nhiễm vi-rút hay không và thông qua đó bảo vệ an toàn cho máy tính.

Vậy cách đơn giản nhất để làm cơ chế bảo vệ realtime như thế nào? Trước khi trả lời câu hỏi thì chúng ta thống nhất đặt tên cho tính năng bảo vệ realtime là realtime engine (và giả sử nó chạy trên hệ điều hành Windows). Để tạo ra realtime engine thì đơn giản chúng ta chỉ cần dùng cơ chế hook để hook các hàm liên quan đến truy cập file như createfile, openfile, writefile, createprocess, movefile… là chúng ta có thông tin được truy cập và dùng tính năng quét bằng hash trong bài trước để kiểm tra xem file được truy cập có bị nhiễm vi-rút hay không. Việc hook các hàm này diễn ra ở user-mode nên sẽ có nhiều hạn chế nên thường chúng ta chỉ dùng với mục đích cá nhân và học hỏi là chính.

Cơ chế hook có nhiều hạn chế vậy nếu muốn bảo vệ tốt hơn thì làm thế nào? Để có thể chặn và kiểm tra được mọi truy cập đến file thì chúng ta phải dùng các driver chạy ở tầng kernel-mode. Một trong các kiểu driver chúng ta có thể dùng là filter driver. Việc viết driver là tương đối khó, đòi hỏi chúng ta có hiểu biết nhất định về kiến trúc hệ điều hành Windows. Nếu bạn không muốn tốn quá nhiều công sức vào việc viết driver thì may mắn cho bạn là có các hãng thứ ba cung cấp driver dạng này và chúng ta chỉ cần dùng API ở tầng User-mode để làm việc với driver.

Realtime engine đơn giản thế thì tại sao lại khó mà không phải hãng nào cũng làm tốt? Việc chặn truy cập file và quét kiểm tra đơn giản là làm gia tăng độ trễ của việc load file của hệ thống và các ứng dụng (với rất nhiều phần mềm av bạn sẽ thấy máy tính chạy nhanh một cách đáng kể khi bạn tắt realtime đi) Và thường thì một ứng dụng được chạy (load lên memory) thì kéo theo việc load tầm vài chục đến cả trăm thư viện khác. Giả sử mỗi file bạn chặn để quét và kiểm tra mất 5-50ms thì với con số file được load như đã nêu thì bạn có thể ước lượng được nó chậm ra sao. Với một số ứng dụng, ứng dụng dạng dịch vụ, COM,… thì việc load chậm sẽ làm cho lỗi và có thể treo ứng dụng và hệ thống. Và việc xử lý không tốt trong engine mà gây lỗi có thể khiến máy tính treo hoặc BSOD (lỗi màn hình xanh). Dó đó việc làm realtime engine đòi hỏi phải tính toán và test cẩn thận trước khi đưa ra.

Vậy làm sao để tối ưu realtime engine khi nó làm máy tính chạy quá chậm? Ngoài các kỹ thuật tối ưu như đã nêu ở bài trước thì các bạn cần phải có thêm một phần mới gọi là cache. Đối với file sạch (không bị nhiễm vi-rút) sau mỗi lần quét ta có thể lưu lại và coi như một local DB chứa các file sạch, trước khi quét ta cần kiểm tra xem file cần quét có nằm trong DB sạch không nếu không thì ta mới tiến hành quét còn có thì ta có thể tiến hành bỏ qua. Việc cache này nếu có đủ thông tin về file sẽ có thể giúp chúng ta dễ dàng giảm độ trễ xuống <1ms cho mỗi file (cho một cấu hình máy tính bình thường dùng ổ HDD).

Vậy cách cache thông tin file ra sao? Trường hợp này sẽ ưu tiên cho tốc độ nên ta thường không lưu hash được cho cả file. Đơn giản chúng ta có thể tính hash đường dẫn + phần đầu + phần cuối file + file size + date modified để làm thông tin so sánh. Mã hash ở đây để có dùng là CRC32/64. Bạn có thể dùng binary search tree để quản lý tìm kiếm trên DB Cache. Các file trên hệ thống thường không cố định (có thể bị xóa hoặc để sang thư mục khác)  nên bạn sẽ phải có timeout cho cache của từng file tránh làm rác DB cache dẫn đến tốn memory và giảm hiệu xuất tìm kiếm.

Vậy khi cập nhật mẫu mới mà trùng với file sạch trong DB cache thì sao? Để giải quyết vấn đề này ta có thể lưu thêm hash của file sạch để lọc ra khỏi DB cache sau mỗi lần cập nhật mẫu.

Kết luận: Vậy là ta có thể hình dung ra cách làm một realtime engine đơn giản cho av ra sao.

Bài tiếp: Làm thế nào để quét và kiểm tra các dòng lây file?

>VVM.

 

 

[Vietnamese] Làm thế nào để viết phần mềm diệt vi-rút – B.1 Quét file bằng mã hash

Giới thiệu: Tui (VVM) làm phần mềm diệt vi-rút (hay còn gọi là anti-virus software – gọi tắt là av) trong khoảng độ 6 năm cho bên CMC InfoSec. Hiện tại tui không còn làm về av nhưng khi nói chuyện và trao đổi với các bạn trong ngành IT thì phần lớn các bạn không hình dung ra cách làm av và nghĩ là làm av rất khó vì vậy tôi viết loạt bài về “Làm thế nào để xây dựng phần mềm diệt vi-rút” để các bạn có thể hình dung ra cách xây dựng một av (mức đơn giản) như thế nào.

AV ngay từ thời cổ xưa của nó vốn khởi chỉ là quét file và kiểm tra xem file đó có phải là vi-rút hay không. Và đến ngày nay tính năng quét file vẫn là tính năng chính và cơ bản nhất của một av. Tui cùng các bạn chúng ta bắt đầu với những câu hỏi.
Vậy làm sao ta biết một file có phải là vi-rút hay không? Có nhiều cách nhưng tui xin chỉ ra một cách cơ bản nhất và phổ biến nhất để nhận biết 1 file có phải là vi-rút hay không, đó là so sánh mã hash (vd: MD5, SHA1, SHA2,…) của file ta cần kiểm tra và tập mẫu vi-rút ta có, nếu 2 mã hash đó trùng nhau thì ta có thể khẳng định đó là file chứa vi-rút và nếu là file không can thiệp sâu vào hệ thống thì đơn giản ta chỉ cần xóa các file chứa vi-rút đi là coi như đã an toàn.

  *hash: Là dạng mã độc nhất được sinh ra từ nội dung nhất định. Tức là mỗi nội dụng khác nhau ta sẽ có một mã hash khác nhau (theo lý thuyết vẫn có tỷ lệ trùng lặp nhất định nhưng rất nhỏ nên ta có thể bỏ qua).
Câu hỏi đặt ra là làm sao ta có mẫu vi-rút? Các nguồn cung cấp mẫu phổ biến là từ diễn đàn chuyên ngành, từ các tổ chức chuyên ngành, từ các dịch vụ như virustotal.com, từ chương trình trao đổi mẫu giữa các hãng phần mềm diệt vi-rút, và từ quá trình thu nhập mẫu từ phía người dùng.

Giả sử bạn có tầm vài trăm đến tầm vài nghìn mẫu vi-rút rồi thì làm thế nào tìm kiếm trong tập mẫu đó? Cách đơn giản nhất là bạn đem so mã hash của file với từng mã hash của các mẫu vi-rút bạn có, nhưng tốt hơn là bạn nên áp dụng các thuật toán sắp xếp đơn giản (vd: Quick Sort, Bubble Sort, Merge Sort,…) và dùng thuật toán binary search để tìm kiếm.

Câu hỏi dẫn thêm là làm thế nào để so sánh hiệu quả với tập mẫu tầm vài triệu mẫu trở lên (đôi khi là cả chục triệu mẫu)? Để trả lời câu hỏi này thì ta bắt đầu phải dùng đến bài toán tối ưu, nhưng trong trường hợp cơ bản nhất thì các bản có thể dùng binary search tree (cây nhị phân tìm kiếm) và rất nhiều phần mềm cũng tận dụng thuật toán này để xây dựng database (DB) cho riêng cho mình. Và nếu dùng binary search tree thì bạn nên dùng self-balancing binary search tree (red-black tree là một dạng đó) hoặc không nếu bạn chỉ làm để tìm hiểu hoặc cung ứng dạng dịch vụ online/cloud thì các bạn có thể dùng hẳn DB (RocksDB, Redis, Riak, MongoDB, PostgresSQL,…) để khỏi phải mất công viết và thử nghiệm DB.

Đó là với một file còn cả một thư mục hay ổ đĩa với cả trăm nghìn (đôi khi là triệu) file + kích thức file khác nhau thì làm thế nào? Thứ nhất về việc quét nhiều file thì các bạn có thể mở nhiều thread/process để duyệt và kiểm tra file (tối ưu hơn thì các bạn có thể phân luồng công việc (vd: thread thì quét file, thread thì tính hash, thread thì tìm kiếm và so sánh với DB,…), và tối ưu cho các thread/process dựa trên tài nguyên của hệ thống). Thứ hai với file lớn thì các bạn có thể phân làm 2 giai đoạn: giai đoạn một chỉ quét vài KByte đầu để kiểm tra xem dữ liệu có trong DB mẫu hay không và nếu có chuyển qua giai đoạn 2 là lấy thêm hash + file size hoặc là tính hash toàn file để chắc chắn là trùng mẫu trong DB (với cách này thì mẫu vi-rút dung lượng lớn các bạn cũng phải làm tương tự).

Vậy ta còn có thể tối ưu được nữa không? Vâng ta vẫn còn có thể tối ưu thêm được nữa . Nếu các bạn hiểu các CPU tính toán thì sẽ biết là CPU sẽ tính toán nhanh với các phép tính dạng số nguyên và nếu tính toán tốt để hạn chế sự trùng lặp thì các bạn có thể dùng CRC32/CRC64 kết hợp với file size để kiểm tra hash một cách nhanh chóng hơn MD5/SHA1/SHA2 tương đối. Ngoài ra cũng nên tận dụng các kỹ thuật tối ưu về cấp phát bộ nhớ (memory), các dạng thuật toán hỗ trợ lock-free/lockless, inline function để có tăng tốc độ tính toán. Và nếu sau này ta có gắn thêm Realtime Engine thì ta còn có thể tận dụng làm tăng tốc độ quét nhanh lên tương đới nữa (bằng cách lưu lịch sử trạng thái file,…).

Vậy còn DB mẫu vi-rút thì quản lý thế nào? Nếu là DB bạn tự xây dụng thì đây cũng là một bài toán cần cân nhắc và tính toán kỹ. DB mẫu có thể tổ chức dưới định dạng nhất định để av sau khi cập nhật về có thể load ngay lên mà không cần thêm công đoạn import,… . Việc thêm và loại bỏ mẫu vi-rút cũng là việc diễn ra thường xuyên, và để hạn chế việc download lại phần không cần nhiết nhiều thì các bạn có thể chia nhỏ DB mẫu thành nhiều tập con dưới dạng các file khác nhau (có thể phân loại dựa theo thời gian, mức độ nguy hiểm, nguồn mẫu,…). Và cũng nói thêm là DB mẫu thì thường rất nhẹ nên chỉ chứa thông tin về mã hash của vi-rút, tên virus (do cách bạn tự đặt hoặc mượn từ các nguồn khác), mức độ nguy hiểm và id để truy vấn thông tin thêm khi cần.

Kết luận: Vậy ta đã biết cách dùng hash để kiểm tra một file có phải là vi-rút hay không. Về quét vi-rút lây file hay còn gọi là vi-rút đa hình là trường hợp đặc biệt tui sẽ nói trong các bài sau.

Bài kế tiếp: Cơ chế bảo vệ realtime, chức năng bảo vệ 24/7 cho máy tính

>VVM.

Posted in AV

New Linux rootkit injects malicious HTML into Web servers

On Tuesday, November 13, 2012, a previously unknown Linux rootkit was posted to the Full Disclosure mailing list by an anonymous victim. The rootkit was discovered on a web server that added an unknown iframe into any HTTP response sent by the web server.

The victim has recovered the rootkit kernel module file and attached it to the mailing list post, asking for any information on this threat. Until today, nobody has replied on this email thread. CrowdStrike has performed a brief static analysis of the kernel module in question, and these are our results. Our results seem to be in line with Kaspersky’s findings; they also already added detection.

Key Findings

  • The rootkit at hand seems to be the next step in iframe injecting cyber crime operations, driving traffic to exploit kits. It could also be used in a Waterhole attack to conduct a targeted attack against a a specific target audience without leaving much forensic trail.
  • It appears that this is not a modification of a publicly available rootkit. It seems that this is contract work of an intermediate programmer with no extensive kernel experience.
  • Based on the Tools, Techniques, and Procedures employed and some background information we cannot publicly disclose, a Russia-based attacker is likely.

Functional Overview

The kernel module in question has been compiled for a kernel with the version string 2.6.32-5. The -5 suffix is indicative of a distribution-specific kernel release. Indeed, a quick Google search reveals that the latest Debian squeeze kernel has the version number 2.6.32-5.

The module furthermore exports symbol names for all functions and global variables found in the module, apparently not declaring any private symbol as static in the sources. In consequence, some dead code is left within the module: the linker can’t determine whether any other kernel module might want to access any of those dead-but-public functions, and subsequently it can’t remove them.

The module performs 6 different tasks during start-up:

  1. Resolution of a series of private kernel symbols using a present System.map file or the kernel’s run-time export of all private symbols through /proc/kallsyms
  2. Initialization of the process and file hiding components using both inline hooks and direct kernel object manipulation
  3. Creating an initial HTTP injection configuration and installing the inline function hook to hijack TCP connection contents
  4. Starting a thread responsible for updating the injection configuration from a command and control server (hereafter “C2”)
  5. Ensuring persistence of the rootkit by making sure the kernel module is loaded at system startup
  6. Hiding the kernel module itself using direct kernel object manipulation
The remainder of this blog post describes those tasks and the components they initialize in detail.

Ghetto Private Symbol Resolution

The rootkit hijacks multiple private kernel functions and global variables that don’t have public and exported symbols. To obtain the private addresses of these symbols, the rootkit contains code to scan files containing a list of addresses and private symbols. Those System.map called files are usually installed together with a kernel image in most Linux distributions. Alternatively, the kernel exports a pseudo-file with the same syntax via procfs at /proc/kallsyms to userland.

The code contains the function search_export_var that receives one parameter: the symbol name to resolve. This function merely wraps around the sub-functionsearch_method_export_var that receives an integer parameter describing the method to use for symbol resolution and the symbol name. It first attempts method 0 and then method 1 if the previous attempt failed.

search_method_export_var then is a simple mapping of 1 tosearch_method_exec_command or 2 to search_method_find_in_file. Any other method input will fail. The attentive reader will notice that therefore the rootkit will always attempt to resolve symbols using search_method_exec_command, because method 0 is not understood by search_method_export_var and 2 is never supplied as input.

search_method_exec_command uses the pseudo-file /proc/kallsyms to retrieve a list of all symbols. Instead of accessing these symbols directly, it creates a usermode helper process with the command line "/bin/bash", "-c", "cat /proc/kallsyms > /.kallsyms_tmp" to dump the symbol list into a temporary file in the root directory. It then uses a function shared with search_method_find_in_file to parse this text representation of addresses and symbols for the desired symbol. Due to the layout of the call graph, this will happen for every symbol to be resolved.

Symbol Resolution Method Identifier Confusion

The alternative (but effectively dead) function search_method_find_in_file is, unfortunately, as ugly. Despite the fact that the System.map file is a regular file that could be read without executing a usermode helper process, the author found an ingenious way to use one anyway.

Since multiple kernels might be installed on the same system, the System.map file(s) (generated at kernel build time) include the kernel version as a suffix. Instead of using a kernel API to determine the currently running kernel version, the rootkit starts another usermode helper process executing "/bin/bash", "-c", "uname -r > /.kernel_version_tmp".uname is a userland helper program that displays descriptive kernel and system information.

So instead of using the kernel version this module is built for at build time (it’s hardcoded in other places, as we’ll see later), or at least just calling the same system call that uname uses to obtain the kernel version, they start a userland program and redirect its output into a temporary file.

The kernel version obtained in this way is then appended to the System.map filename so that the correct file can be opened. Recall that this code path is never taken due to a mistake at another place, though.

When starting up, the rootkit first iterates over a 13-element array of fixed-length, 0-padded symbol names and resolves them using the previously described functions. The name of the symbol and its address are then inserted into a linked list. Once a symbol’s address needs to be used, the code iterates over this linked list, searching for the right symbol and returning its address.

Berserk Inline Code Hooking

To hook private functions that are called without indirection (e.g., through a function pointer), the rootkit employs inline code hooking. In order to hook a function, the rootkit simply overwrites the start of the function with an e9 byte. This is the opcode for a jmp rel32 instruction, which, as its only operand, has 4 bytes relative offset to jump to.

The rootkit, however, calculates an 8-byte or 64-bit offset in a stack buffer and then copies 19 bytes (8 bytes offset, 11 bytes unitialized) behind the e9 opcode into the target function. By pure chance the jump still works, because amd64 is a little endian architecture, so the high extra 4 bytes offset are simply ignored.

To facilitate proper unhooking at unload time, the rootkit saves the original 5 bytes of function start (note that this would be the correct jmp rel32 instruction length) into a linked list. However, since in total 19 bytes have been overwritten, unloading can’t work properly:

.text:000000000000A32E       xor     eax, eax
.text:000000000000A330       mov     ecx, 0Ch
.text:000000000000A335       mov     rdi, rbx
.text:000000000000A338       rep stosd
.text:000000000000A33A       mov     rsi, rbp
.text:000000000000A33D       lea     rdi, [rbx+8]
.text:000000000000A341       lea     rdx, [rbx+20h]
.text:000000000000A345       mov     cl, 5
.text:000000000000A347       rep movsd
.text:000000000000A349       mov     [rbx], rbp
.text:000000000000A34C       mov     esi, 14h
.text:000000000000A351       mov     rdi, rbp
.text:000000000000A354       mov     rax, cs:splice_func_list
.text:000000000000A35B       mov     [rax+8], rdx
.text:000000000000A35F       mov     [rbx+20h], rax
.text:000000000000A363       mov     qword ptr [rbx+28h], offset splice_func_list
.text:000000000000A36B       mov     cs:splice_func_list, rdx
.text:000000000000A372       call    set_addr_rw_range
.text:000000000000A377       lea     rax, [rbp+1]
.text:000000000000A37B       mov     byte ptr [rbp+0], 0E9h
.text:000000000000A37F       lea     rsi, [rsp+38h+target_offset]
.text:000000000000A384       mov     ecx, 19
.text:000000000000A389       mov     rdi, rax
.text:000000000000A38C       rep movsb
.text:000000000000A38E       mov     rdi, rax

To support read-only mapped code, the rootkit contains page-table manipulation code. Since the rootkit holds the global kernel lock while installing an inline hook, it could simply have abused thewrite-protect-enable-bit in cr0 for the sake of simplicity, though.

Since the rootkit trashes the hooked function beyond repair and is not considering instruction boundaries, it can never call the original function again (a feature that most inline hooking engines normally posses). Instead, the hooked functions have all been duplicated (one function even twice) in the sourcecode of the rootkit.

File and Would-be Process Hiding

Unlike many other rootkits, this rootkit has a rather involved logic for hiding files. Most public Linux rootkits define a static secret and hide all files and directories, where this secret is part of the full file or directory name. This rootkit maintains a linked list of file or directory names to hide, and it hides them only if the containing directory is called "/" or "sound" (the parent directory of temporary files and the module file, respectively).

The actual hiding is done by inline hooking the vfs_readdir function that’s called for enumerating directory contents. The replacement of that function checks if the enumerated directory’s name is either  "/" or "sound" as explained above.

If that’s the case, the function provides an alternative function pointer to the normally usedfilldir or filldir64 functions. This alternative implementation checks the linked list of file names to hide and will remove the entry if it matches.

Interestingly, it will also check a linked list of process names to hide, and it will hide the entry if it matches, too. That, however, doesn’t make sense, since the actual directory name to hide would be the process id. Also, the parent directory for that would be "/proc", which isn’t one of the parent directories filtered. Therefore, the process hiding doesn’t work at all:

Improperly Hidden Kernel Threads
The list of hidden files is:

  • sysctl.conf
  • module_init.ko (the actual rootkit filename)
  • zzzzzz_write_command_in_file
  • zzzzzz_command_http_inject_for_module_init

The real module’s name gets added to the linked list of file names to hide by the module hiding code.

Interestingly, the rootkit also contains a list of parent path names to hide files within. However, this list isn’t used by the code:

  • /usr/local/hide/first_hide_file
  • /ah34df94987sdfgDR6JH51J9a9rh191jq97811

Since only directory listing entries are being hidden but access to those files is not intercepted, it’s still possible to access the files when an absolute path is specified.

Command and Control Client

As part of module initialization, the rootkit starts a thread that connects to a single C2 server. The IP address in question is part of a range registered to Hetzner, a big German root server and co-location provider.
The rootkit uses the public ksocket library to establish TCP connections directly from the Linux kernel. After the connection has been successfully initiated, the rootkit speaks a simple custom protocol with the server. This very simple protocol consists of a 1224-byte blob sent by the rootkit to the server as an authentication secret. The blob is generated from “encrypting” 1224 null bytes with a 128-byte static password, the C2 address it’s talking to, and, interestingly, an IP address registered to Zattoo Networks in Zurich, Switzerland, that is not otherwise used throughout the code.
C2 Connection Attempt

The server is then expected to respond with the information about whether an iframe or a JavaScript snippet should be injected, together with the code to be injected. The server’s response must contain a similarily generated authentication secret for the response to be accepted. If this check passes, the rootkit then copies the injection information into a global variable.

This protocol is obviously vulnerable to simply generating the secret blob once using dynamic analysis and replaying it, and therefore it merely serves for a little obfuscation. We didn’t invest further time investigating this specific “encryption” algorithm.

TCP Connection Hijacking

In order to actually inject the iframes (or JavaScript code references) into the HTTP traffic, the rootkit inline hooks the tcp_sendmsg function. This function receives one or multiple buffers to be sent out to the target and appends them to a connections outgoing buffer.

The TCP code will then later retrieve data from that buffer and encapsulate it in a TCP packet for transmission. The replacement function is largely a reproduction of the original function included in the kernel sources due to the inline hooking insufficiencies explained above.

A single call to the function formation_new_tcp_msg was added near the head of the original function; if this function returns one, the remainder of the original function is skipped and internally a replacement message is sent instead. This function always considers only the first send buffer passed, and we’ll implicitly exclude all further send buffers passed to a potentialsendmsg call in the following analysis.

The formation_new_tcp_msg function invokes a decision function that contains 4 tests, determining whether injection on the message should be attempted at all:

  1. An integer at +0x2f0 into the current configuration is incremented. Only if its value modulo the integer at +0x2e8 in the current configuration is equal to zero, this test passes. This ensures that only on every n-th send buffer an injection is attempted.
  2. Ensure that the size of all the send buffers to be sent is below or equal to 19879 bytes.
  3. Verify that originating port (server port for server connections) is :80.
  4. Ensure that the destination of this send is not 127.0.0.1.
  5. Make sure that none of the following three strings appears anywhere in the send buffer:
    • “403 Forbidden”
    • “304 Not Modified”
    • ” was not found on this server.”
  6. Make sure the destination of this send is not in a list of 1708 blacklisted IP addresses, supposedly belonging to search engines per the symbol namesearch_engines_ip_array.

There are several shortcomings in the design of these tests that ultimately led to the discovery of this rootkit as documented in the Full Disclosure post. Since the check to only attempt an inject once every n-th send buffer is not performed per every m-th connection and before all other tests, it will trigger on more valid requests than one might expect when defining the modulus.

Also, doing a negative check on a few selected error messages instead of checking for a positive “200” HTTP status led to the discovery, when an inject in a “400” HTTP error response was found.

The rootkit then tries to parse a HTTP header being sent out by looking for the static header strings “Content-Type”, “Content-Encoding”, “Transfer-Encoding” and “Server”. It matches each of the values of these headers against a list of known values, e.g., for Content-Type:

  • text/html
  • text/css
  • application/x-javascript

The Content-Type of the response and the attacker specified Content-Type of the inject have to match for injection to continue. The code then searches for an attacker-specified substring in the message and inserts the inject after it.

What is notable is the support for both chunked Transfer-Encoding and gzip Content-Encoding. The chunked encoding handling is limited to handling the first chunk sent because the HTTP headers parsed need to present in the same send buffer. However, it will adjust the length of the changed chunk correctly.

When encountering a  gzip Content-Encoding, the rootkit will use the zlib kernel module to decompress the response, potentially patch it with the inject, and then recompress it. While this is a technically clever way to make sure your inject ends up in even compressed responses, it will potentially severely degrade the performance of your server.

Reboot Persistence

After running most of the other initialization tasks, the rootkit creates a kernel thread that continously tries to modify /etc/rc.local to load the module at start-up. The code first tries to open the file and read it it all into memory. Then it searches for the loading command in the existing file.

If it’s not found, it appends the loading command “insmod /lib/modules/2.6.32-5-amd64/kernel/sound/module_init.ko” by concatenating the “insmode” command with the directory path and filename. However, all those 3 parts are hardcoded (remember that the kernel version now hardcoded was determined dynamically for symbol resolution earlier?).

If opening the file fails, the thread will wait for 5 seconds. After successfully appending the new command, the thread will wait for 3 minutes before checking for the command and potentially re-adding it again.

Additionally, the rootkit installs an inline hook for the vfs_read function. If the read buffer (no matter which file it is being read from) contains the fully concatenated load command, the load command is removed from the read buffer by copying the remainder of the buffer over it and adjusting the read size accordingly. Thereby, the load command is hidden from system administrators if the rootkit is loaded.

Successful Persistence Command Hiding

The screenshot above showcases a problem already with this technique of persistence: since the command is appended to the end of rc.local, there might actually be shell commands that result in the command not being executed as intended. On a default Debian squeeze install, /etc/rc.local ends in an exit 0 command, so that the rootkit is effectively never loaded.

Module Hiding

Hiding itself is achieved by simple direct kernel object manipulation. The rootkit iterates about the kernel linked list modules and removes itself from the list using list_del. In consequence, the module will never be unloaded and there will be no need to remove the inline hooks installed earlier. In fact, the remove_splice_func_in_memory function is unreferenced dead code.

Conclusion

Considering that this rootkit was used to non-selectively inject iframes into nginx webserver responses, it seems likely that this rootkit is part of a generic cyber crime operation and not a targeted attack. However, a Waterhole attack, where a site mostly visited from a certain target audience is infected, would also be plausible. Since no identifying strings yielded results in an Internet search (except for the ksocket library), it appears that this is not a modification of a publicly available rootkit. Rather, it seems that this is contract work of an intermediate programmer with no extensive kernel experience, later customized beyond repair by the buyer.
Although the code quality would be unsatisfying for a serious targeted attack, it is interesting to see the cyber-crime-oriented developers, who have partially shown great skill at developing Windows rootkits, move into the Linux rootkit direction. The lack of any obfuscation and proper HTTP response parsing, which ultimately also led to discovery of this rootkit, is a further indicator that this is not part of a sophisticated, targeted attack.

Based on the Tools, Techniques, and Procedures employed and some background information we cannot publicly disclose, a Russia-based attacker is likely. It remains an open question regarding how the attackers have gained the root privileges to install the rootkit. However, considering the code quality, a custom privilege escalation exploit seems very unlikely.
(via blog.crowdstrike.com)

How To Set Up An IPS (Intrusion Prevention System) On Fedora 17

Vuurmuur is a linux firewall manager. It takes a human readable rule syntax and turns it into the proper iptables commands. It supports logviewing, traffic shaping, connection killing and a lot of other features. Suricata is a relatively new network IDS/IPS. It’s multithreaded for performance, supports IDS and IPS modes, can extract files from HTTP streams and has a lot of other features.

Fedora 17 includes both Vuurmuur and Suricata in its repository. In this howto I’ll describe how to get a functional IPS using only Fedora packages.

Installing Vuurmuur and Suricata

Install both Vuurmuur and Suricata through “yum”:

yum install suricata Vuurmuur-daemon Vuurmuur-tui

Running Suricata in IDS mode

As an IPS will block traffic is it is misconfigured, it’s adviced to first test in the passive IDS mode.

We’re getting the free Emerging Threats IDS rules optimized for Suricata and setting them up in /etc/suricata/rules/

cd /etc/suricata/
curl -O https://rules.emergingthreatspro.com/open/suricata/emerging.rules.tar.gz
tar xzvf emerging.rules.tar.gz
ln -s /etc/suricata/rules/reference.config /etc/suricata/reference.config
ln -s /etc/suricata/rules/classification.config /etc/suricata/classification.config
cp /etc/suricata/rules/suricata-1.2-prior-open.yaml /etc/suricata/suricata.yaml

Test Suricata with:

suricata -c /etc/suricata/suricata.yaml -i eth0

Leave it running for a few minutes and check /var/log/suricata/stats.log and /var/log/suricata/http.log to confirm that things work. Make sure to generate some traffic by for example opening a browser and visiting your favorite sites.

Running Suricata in IPS mode

To make sure Suricata can inspect the traffic, iptables needs to be set up to pass traffic to Suricata. We’re using Vuurmuur to manage the firewall.

Open vuurmuur_conf, go to “Rules” and add a new rule with the following properties:

accept service any from any to any log

The rule looks like this:

Click to enlarge

Next, to be able to start the Vuurmuur service we need to add an interface to it’s configuration.

Go to “Interfaces” and add a new interface, name it however you like. In device add “eth0”:

Click to enlarge

When this is done, exit vuurmuur_conf.

For Vuurmuur’s logging to work properly we need to adapt the rsyslog configuration. Edit /etc/rsyslog.conf and add:

*.debug /var/log/debug

Close the file and restart rsyslog to effectuate the changes:

service rsyslog restart

We can now start Vuurmuur:

service vuurmuur start

Make sure Vuurmuur gets started on boot:

systemctl enable vuurmuur.service

Open vuurmuur_conf, go to the logviewer and check if traffic is passing:

Click to enlarge

If thats all good, lets get to passing the traffic to Suricata for deep inspection.

First, change the rule in vuurmuur to:

nfqueue service any from any to any

The rule looks like this:

Click to enlarge

This will pass all traffic to Suricata.

Then “apply changes” in vuurmuur_conf, this will automatically update the firewall. The logview will now show:

Click to enlarge

Then, start suricata:

suricata -c /etc/suricata/suricata.yaml -q0

Open your browser and check if traffic is flowing. Open /var/log/suricata/stats.log and /var/log/suricata/http.log to see if things are working as expected.

stats.log:

-------------------------------------------------------------------
Date: 10/8/2012 -- 17:20:08 (uptime: 0d, 01h 39m 02s)
-------------------------------------------------------------------
Counter                   | TM Name                   | Value
-------------------------------------------------------------------
decoder.pkts              | Decode1                   | 3147
decoder.bytes             | Decode1                   | 1453192
decoder.ipv4              | Decode1                   | 3147
decoder.ipv6              | Decode1                   | 0
decoder.ethernet          | Decode1                   | 0
decoder.raw               | Decode1                   | 0
decoder.sll               | Decode1                   | 0
decoder.tcp               | Decode1                   | 2426
decoder.udp               | Decode1                   | 589
decoder.sctp              | Decode1                   | 0
decoder.icmpv4            | Decode1                   | 0
decoder.icmpv6            | Decode1                   | 0
decoder.ppp               | Decode1                   | 0
decoder.pppoe             | Decode1                   | 0
decoder.gre               | Decode1                   | 0
decoder.vlan              | Decode1                   | 0
decoder.avg_pkt_size      | Decode1                   | 461.770575
decoder.max_pkt_size      | Decode1                   | 1492
defrag.ipv4.fragments     | Decode1                   | 0
defrag.ipv4.reassembled   | Decode1                   | 0
defrag.ipv4.timeouts      | Decode1                   | 0
defrag.ipv6.fragments     | Decode1                   | 0
defrag.ipv6.reassembled   | Decode1                   | 0
defrag.ipv6.timeouts      | Decode1                   | 0
tcp.sessions              | Decode1                   | 76                                                                                                                                                                                   
tcp.ssn_memcap_drop       | Decode1                   | 0                                                                                                                                                                                    
tcp.pseudo                | Decode1                   | 5                                                                                                                                                                                    
tcp.invalid_checksum      | Decode1                   | 0                                                                                                                                                                                    
tcp.no_flow               | Decode1                   | 0                                                                                                                                                                                    
tcp.reused_ssn            | Decode1                   | 0                                                                                                                                                                                    
tcp.memuse                | Decode1                   | 6029312.000000                                                                                                                                                                       
tcp.syn                   | Decode1                   | 76                                                                                                                                                                                   
tcp.synack                | Decode1                   | 101                                                                                                                                                                                  
tcp.rst                   | Decode1                   | 19                                                                                                                                                                                   
tcp.segment_memcap_drop   | Decode1                   | 0                                                                                                                                                                                    
tcp.stream_depth_reached  | Decode1                   | 0                                                                                                                                                                                    
tcp.reassembly_memuse     | Decode1                   | 11292544.000000                                                                                                                                                                      
tcp.reassembly_gap        | Decode1                   | 0                                                                                                                                                                                    
flow_mgr.closed_pruned    | FlowManagerThread         | 75                                                                                                                                                                                   
flow_mgr.new_pruned       | FlowManagerThread         | 5                                                                                                                                                                                    
flow_mgr.est_pruned       | FlowManagerThread         | 101                                                                                                                                                                                  
flow.memuse               | FlowManagerThread         | 3690424.000000                                                                                                                                                                       
flow.emerg_mode_entered   | FlowManagerThread         | 0                                                                                                                                                                                    
flow.emerg_mode_over      | FlowManagerThread         | 0                                                                                                                                                                                    
detect.alert              | Detect                    | 0

http.log:

10/08/2012-17:24:02.447292 www.howtoforge.com [**] / [**] Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1 [**] 192.168.122.48:48396 -> 188.40.16.205:80
10/08/2012-17:24:02.544458 static.howtoforge.com [**] /misc/drupal.css [**] Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1 [**] 192.168.122.48:52942 -> 178.63.27.110:80
10/08/2012-17:24:02.549184 static.howtoforge.com [**] /modules/copyright/copyright.css [**] Mozilla/5.0 (X11; Linux x86_64; rv:15.0) Gecko/20100101 Firefox/15.0.1 [**] 192.168.122.48:52944 -> 178.63.27.110:80

Close the running suricata (ctr-c on it’s console) and open /etc/sysconfig/suricata. Change “OPTIONS” to look like this:

OPTIONS="-q 0 -D --pidfile /var/run/suricata.pid"

Start Suricata through the service and make sure it’s getting started at boot:

service suricata start
systemctl enable suricata.service

Dropping Traffic

So far nothing was dropped. However, we’re an IPS so lets start dropping something. All the rules we downloaded default to “alert”, so nothing is dropped yet.

Open /etc/suricata/suricata.yaml in your editor and scroll down to the “stream” section. There, set “inline” to “yes”. This will force Suricata to do stream reassembly in a IPS aware way.

stream:
  memcap: 32mb
  checksum_validation: yes      # reject wrong csums
  inline: yes

Add local.rules to the rules to be loaded by Suricata:

default-rule-path: /etc/suricata/rules/
rule-files:
 - local.rules
 - emerging-ftp.rules
 - emerging-policy.rules

Create local.rules in /etc/suricata/rules/ using a text editor. Add on a single line:

drop tcp any any -> any any (msg:"facebook is blocked"; content:"facebook.com"; http_header; nocase; classtype:policy-violation; sid:1;)

Restart Suricata:

service suricata restart

Now open Firefox, and try to go to http://www.facebook.com/, the request should time out.

The logfile /var/log/suricata/fast.log will have:

10/06/2012-11:40:49.018377  [Drop] [**] [1:1:0] facebook is blocked [**] [Classification: Potential Corporate Privacy Violation] [Priority: 1] {TCP} 192.168.122.48:57113 -> 173.252.100.16:80
10/06/2012-11:40:49.020955  [Drop] [**] [1:1:0] facebook is blocked [**] [Classification: Potential Corporate Privacy Violation] [Priority: 1] {TCP} 192.168.122.48:57114 -> 173.252.100.16:80
10/06/2012-11:40:51.991876  [Drop] [**] [1:1:0] facebook is blocked [**] [Classification: Potential Corporate Privacy Violation] [Priority: 1] {TCP} 192.168.122.48:57115 -> 173.252.100.16:80

Obviously there is a lot more to managing an IPS, but this should get you started!

Recommended further reading:

(via Howtoforge.com)