Category index

Cyber Security

4276 articles

4276 ARTICLES

FEATURED REPORT

Bringing Rust to the Pixel Baseband

Posted by Jiacheng Lu, Software Engineer, Google Pixel Team Google is continuously advancing the security of Pixel devices. We have been focusing on hardening the cellular baseband modem against exploitation. Recognizing the risks associated within the complex modem firmware, Pixel 9 shipped with mitigations against a range of memory-safety vulnerabilities. For Pixel 10, Google is advancing its proactive security measures further. Following our previous discussion on “Deploying Rust in Existing Firmware Codebases”, this post shares a concrete application: integrating a memory-safe Rust DNS(Domain Name System) parser into the modem firmware. The new Rust-based DNS parser significantly reduces our security risk by mitigating an entire class of vulnerabilities in a risky area, while also laying the foundation for broader adoption of memory-safe code in other areas. Here we share our experience of working on it, and hope it can inspire the use of more memory safe languages in low-level environments. Why Modem Memory Safety Can’t Wait In recent years, we have seen increasing interest in the cellular modem from attackers and security researchers. For example, Google’s Project Zero gained remote code execution on Pixel modems over the Internet. Pixel modem has tens of Megabytes of executable code. Given the complexity and remote attack surface of the modem, other critical memory safety vulnerabilities may remain in the predominantly memory-unsafe firmware code. Why DNS? The DNS protocol is most commonly known in the context of browsers finding websites. With the evolution of cellular technology, modern cellular communications have migrated to digital data networks; consequently, even basic operations such as call forwarding rely on DNS services. DNS is a complex protocol and requires parsing of untrusted data, which can lead to vulnerabilities, particularly when implemented in a memory-unsafe language (example: CVE-2024-27227). Implementing the DNS parser in Rust offers value by decreasing the attack surfaces associated with memory unsafety. Picking a DNS library DNS already has a level of support in the open-source Rust community. We evaluated multiple open source crates that implement DNS. Based on criteria shared in earlier posts, we identified hickory-proto as the best candidate. It has excellent maintenance, over 75% test coverage, and widespread adoption in the Rust community. Its pervasiveness shows its potential as the de-facto DNS choice and long term support. Although hickory-proto initially lacked no_std support, which is needed for Bare-metal environments (see our previous post on this topic), we were able to add support to it and its dependencies. Adding no_std support The work to enable no_std for hickory-proto is mostly mechanical. We shared the process in a previous post. We undertook modifications to hickory_proto and its dependencies to enable no_std support. The upstream no_std work also results in a no_std URL parser, beneficial to other projects. https://github.com/hickory-dns/hickory-dns/pull/2104 https://github.com/servo/rust-url/pull/831 https://github.com/krisprice/ipnet/pull/58 The above PRs are great examples of how to extend no_std support to existing std-only crates. Code size study Code size is the one of the factors that we evaluated when picking the DNS library to use. Code size by category Rust implemented Shim that calls Hickory-proto on receiving a DNS response 4KB core, alloc, compiler_builtins (reusable, one-time cost) 17KB Hickory-proto library and dependencies 350KB Sum 371KB We built prototypes and measured size with size-optimized settings. Expectedly, hickory_proto is not designed with embedded use in mind, and is not optimized for size. As the Pixel modem is not tightly memory constrained, we prioritized community support and code quality, leaving code size optimizations as future work. However, the additional code size may be a blocker for other embedded systems. This could be addressed in the future by adding additional feature flags to conditionally compile only required functionality. Implementing this modularity would be a valuable future work. Hook-up Rust to modem firmware Before building the Rust DNS library, we defined several Rust unit tests to cover basic arithmetic, dynamic allocations, and FFI to verify the integration of Rust with the existing modem firmware code base. Compile Rust code to staticlib While using cargo is the default choice for compilation in the Rust ecosystem, it presents challenges when integrating it into existing build systems. We evaluated two options: Using cargo to build a staticlib before the modem builds. Then add the produced staticlib into the linking step. Directly work with rustc and integrate the Rust compilation steps into the existing modem build system. Option #1 does not scale if we are going to add more Rust components in the future, as linking multiple staticlibs may cause duplicated symbol errors. We chose option #2 as it scales more easily and allows tighter integration into our existing build system. Our existing C/C++ codebase uses Pigweed to drive the primary build system. Pigweed supports Rust targets (example) with direct calls to rustc through rust tools defined in GN. We compiled all the Rust crates, including hickory-proto, its dependencies, and core, compiler_builtin, alloc, to rlib. Then, we created a staticlib target with a single lib.rs file which references all the rlib crates using extern crate keywords. Build core, alloc, and compiler_builtins Android’s Rust Toolchain distributes source code of core, alloc, and compiler_builtins, and we leveraged this for the modem. They can be included to the build graph by adding a GN target with crate_root pointing to the root lib.rs of each crate. Pixel modem firmware already has a well-tested and specialized global memory allocation system to support some dynamic memory allocations. alloc support was added by implementing the GlobalAlloc with FFI calls to the allocators C APIs: use core::alloc::{GlobalAlloc, Layout}; extern “C” { fn mem_malloc(size: usize, alignment: usize) -> *mut u8; fn mem_free(ptr: *mut u8, alignment: usize); } struct MemAllocator; unsafe impl GlobalAlloc for MemAllocator { unsafe fn alloc(&self, layout: Layout) -> *mut u8 { mem_malloc(layout.size(), layout.align()) } unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) { mem_free(ptr, layout.align()); } } #[global_allocator] static ALLOCATOR: MemAllocator = MemAllocator; Pixel modem firmware already implements a backend for the Pigweed crash facade as the global crash handler. Exposing it into Rust panic_handler through FFI unifies the crash handling for both Rust and C/C++ code. #![no_std] use core::panic::PanicInfo; extern “C” { pub fn PwCrashBackend(sigature: *const i8, file_name: *const i8, line: u32); } #[panic_handler] fn panic(panic_info: &PanicInfo) -> ! { let mut filename = “”; let mut line_number: u32 = 0; if let Some(location) = panic_info.location() { filename = location.file(); line_number = location.line(); } let mut cstr_buffer = [0u8; 128]; // Never writes to the last byte to make sure cstr_buffer is always zero // terminated. let (_, writer) = cstr_buffer.split_last_mut().unwrap(); for (place, ch) in writer.iter_mut().zip(filename.bytes()) { *place = ch; } unsafe { PwCrashBackend( “Rust panic\0”.as_ptr() as *const i8, cstr_buffer.as_ptr() as const i8, line_number, ); } loop {} } Link Rust staticlib The Pixel modem firmware linking has a step that calls the linker to link all the objects generated from C/C++ code. By using llvm-ar -x to extract object files from the Rust combined staticlib and supplying them to the linker, the Rust code appears in the final modem image. There was a performance issue we experienced due to weak symbols during linking. The inclusion of Rust core and compiler-builtin caused unexpected power and performance regressions on various tests. Upon analysis, we realized that modem optimized implementations of memset and memcpy provided by the modem firmware are accidentally replaced by those defined in compiler_builtin. It seems to happen because both compiler_builtin crate and the existing codebase defines symbols as weak, linker has no way to figure out which one is weaker. We fixed the regression by stripping the compiler_builtin crate before linking using a one line shell script. llvm-ar -t | grep compiler_builtins | xargs llvm-ar -d Integrating hickory-proto Expose Rust API and calling back to C++ For the DNS parser, we declared the DNS response parsing API in C and then implemented the same API in Rust. int32_t process_dns_response(uint8_t, int32_t); The Rust function returns an integer standing for the error code. The received DNS answers in the DNS response are required to be updated to in-memory data structures that are coupled with the original C implementation, therefore, we use existing C functions to do it. The existing C functions are dispatched from the Rust implementation. pub unsafe extern “C” fn process_dns_response( dns_response: *const u8, response_len: i32, ) -> i32 { //… validate inputs dns_response and response_len. // SAFETY: // It is safe because dns_response is null checked above. response_len // is passed in, safe as long as it is set correctly by vendor code. match process_response(unsafe { slice::from_raw_parts(dns_response, response_len) }) { Ok(()) => 0, Err(err) => err.into(), } } fn process_response(response: &[u8]) -> Result<()> { let response = hickory_proto::op::Message::from_bytes(response)?; let response = hickory_proto::xfer::DnsResponse::from_message(response)?; for answer in response.answers() { match answer.record_type() { hickory_proto::RecordType:… => { // SAFETY: // It is safe because the callback function does not store // reference of the inputs or their members. unsafe { callback_to_c_function(…)?; } } // … more match arms omitted. } } Ok(()) } In our case, the DNS responding parsing function API is simple enough for us to hand write, while the callbacks back to C functions for handling the response have complex data type conversions. Therefore, we leveraged bindgen to generate FFI code for the callbacks. Build third-party crates Even with all features disabled, hickory-proto introduces more than 30 dependent crates. Manually written build rules are difficult to ensure correctness and scale poorly when upgrading dependencies into new versions. Fuchsia has developed cargo-gnaw to support building their third party Rust crates. Cargo-gnaw works by invoking cargo metadata to resolve dependencies, then parse and generate GN build rules. This ensures correctness and ease of maintenance. Conclusion The Pixel 10 series of phones marks a pivotal moment, being the first Pixel device to integrate a memory-safe language into its modem. While replacing one piece of risky attack surface is itself valuable, this project lays the foundation for future integration of memory-safe parsers and code into the cellular baseband, ensuring the baseband’s security posture will continue to improve as development continues. Special thanks to Armando Montanez, Bjorn Mellem, Boky Chen, Cheng-Yu Tsai, Dominik Maier, Erik Gilling, Ever Rosales, Hungyen Weng, Ivan Lozano, James Farrell, Jeffrey Vander Stoep, Jiacheng Lu, Jingjing Bu, Min Xu, Murphy Stein, Ray Weng, Shawn Yang, Sherk Chung, Stephan Chen, Stephen Hines.

BY Edward Fernandez
MIN READ 8 MIN READ
EXPLORE north_east
Bringing Rust to the Pixel Baseband
Protecting Cookies with Device Bound Session Credentials
CYBERSECURITY

Protecting Cookies with Device Bound Session Credentials

Posted by Ben Ackerman, Chrome team, Daniel Rubery, Chrome team and Guillaume Ehinger, Google Account Security team Following our April 2024 announcement, Device Bound Session Credentials (DBSC) is now entering public availability for Windows users on Chrome 146, and expanding to macOS in an upcoming Chrome release. This project represents a significant step forward in our ongoing efforts to combat session theft, which remains a prevalent threat in the modern security landscape. Session theft typically occurs when a user inadvertently downloads malware onto their device. Once active, the malware can silently extract existing session cookies from the browser or wait for the user to log in to new accounts, before exfiltrating these tokens to an attacker-controlled server. Infostealer malware families, such as LummaC2, have become increasingly sophisticated at harvesting these credentials. Because cookies often have extended lifetimes, attackers can use them to gain unauthorized access to a user’s accounts without ever needing their passwords; this access is then often bundled, traded, or sold among threat actors. Crucially, once sophisticated malware has gained access to a machine, it can read the local files and memory where browsers store authentication cookies. As a result, there is no reliable way to prevent cookie exfiltration using software alone on any operating system. Historically, mitigating session theft relied on detecting the stolen credentials after the fact using a complex set of abuse heuristics – a reactive approach that persistent attackers could often circumvent. DBSC fundamentally changes the web’s capability to defend against this threat by shifting the paradigm from reactive detection to proactive prevention, ensuring that successfully exfiltrated cookies cannot be used to access users’ accounts. How DBSC Works DBSC protects against session theft by cryptographically binding authentication sessions to a specific device. It does this using hardware-backed security modules, such as the Trusted Platform Module (TPM) on Windows and the Secure Enclave on macOS, to generate a unique public/private key pair that cannot be exported from the machine. The issuance of new short-lived session cookies is contingent upon Chrome proving possession of the corresponding private key to the server. Because attackers cannot steal this key, any exfiltrated cookies quickly expire and become useless to those attackers. This design allows large and small websites to upgrade to secure, hardware-bound sessions by adding dedicated registration and refresh endpoints to their backends, while maintaining complete compatibility with their existing front-end. The browser handles the complex cryptography and cookie rotation in the background, allowing the web app to continue using standard cookies for access just as it always has. Google rolled out an early version of this protocol over the last year. For sessions protected by DBSC, we have observed a significant reduction in session theft since its launch. An overview of the DBSC protocol showing the interaction between the browser and server. Private by design A core tenet of the DBSC architecture is the preservation of user privacy. Each session is backed by a distinct key, preventing websites from using these credentials to correlate a user’s activity across different sessions or sites on the same device. Furthermore, the protocol is designed to be lean: it does not leak device identifiers or attestation data to the server beyond the per-session public key required to certify proof of possession. This minimal information exchange ensures DBSC helps secure sessions without enabling cross-site tracking or acting as a device fingerprinting mechanism. Engagement with the ecosystem DBSC was designed from the beginning to be an open web standard through the W3C process and adoption by the Web Application Security Working Group. Through this process we partnered with Microsoft to design the standard to ensure it works for the web and got input from many in the industry that are responsible for web security. Additionally, over the past year, we have also conducted two Origin Trials to ensure DBSC effectively serves the requirements of the broader web community. Many web platforms, including Okta, actively participated in these trials and their own testing and provided essential feedback to ensure the protocol effectively addresses their diverse needs. If you are a web developer and are looking for a way to secure your users against session theft, refer to our developer guide for implementation details. Additionally, all the details about DBSC can be found on the spec and the corresponding github. Feel free to use the issues page to report bugs or provide feature requests. Future improvements As we continue to evolve the DBSC standard, future iterations will focus on increasing support across diverse ecosystems and introducing advanced capabilities tailored for complex enterprise environments. Key areas of ongoing development include: Securing Federated Identity: In modern enterprise environments, Single Sign-On (SSO) is ubiquitous. We are expanding the DBSC protocol to support cross-origin bindings, ensuring that a relying party (RP) session remains continuously bound to the same original device key used by the Identity Provider (IdP). This guarantees that the high-assurance security of the initial device binding is maintained throughout the entire federated login process, creating an unbroken chain of trust. Advanced Registration Capabilities: While DBSC provides robust protection for established cookies, some environments require an even stronger foundation when the session is first created. We are developing mechanisms to bind DBSC sessions to pre-existing, trusted key material rather than generating a new key at sign-in. This advanced capability enables websites to integrate complementary technologies, such as mTLS certificates or hardware security keys, creating a highly secure registration environment. Broader Device Support: We are also actively exploring the potential addition of software-based keys to extend protections to devices without dedicated secure hardware.

5 MIN READ arrow_forward
Master C and C++ with our new Testing Handbook chapter
CYBERSECURITY

Master C and C++ with our new Testing Handbook chapter

We added a new chapter to our Testing Handbook: a comprehensive security checklist for C and C++ code. We’ve identified a broad range of common bug classes, known footguns, and API gotchas across C and C++ codebases and organized them into sections covering Linux, Windows, and seccomp. Whereas other handbook chapters focus on static and dynamic analysis, this chapter offers a strong basis for manual code review. LLM enthusiasts rejoice: we’re also developing a Claude skill based on this new chapter. It will turn the checklist into bug-finding prompts that an LLM can run against a codebase, and it’ll be platform and threat-model aware. Be sure to give it a try when we release it. And after reading the chapter, you can test your C/C++ review skills against two challenges at the end of this post. Be in the first 10 to submit correct answers to win Trail of Bits swag! What’s in the chapter The chapter covers five areas: general bug classes, Linux usermode and kernel, Windows usermode and kernel, and seccomp/BPF sandboxes. It starts with language-level issues in the bug classes section—memory safety, integer errors, type confusion, compiler-introduced bugs—and gets progressively more environment-specific. The Linux usermode section focuses on libc gotchas. This section is also applicable to most POSIX systems. It ranges from well-known problems with string methods, to somewhat less known caveats around privilege dropping and environment variable handling. The Linux kernel is a complicated beast, and no checklist could cover even a part of its intricacies. However, our new Testing Handbook chapter can give you a starting point to bootstrap manual reviews of drivers and modules. The Windows sections cover DLL planting, unquoted path vulnerabilities in CreateProcess, and path traversal issues. This last bug class includes concerns like WorstFit Unicode bugs, where characters outside the basic ANSI set can be reinterpreted in ways that bypass path checks entirely. The kernel section addresses driver-specific concerns such as device access controls, denial of service through improper spinlock usage, security issues arising from passing handles from usermode to kernelmode, and various sharp edges in Windows kernel APIs. Linux seccomp and BPF features are often used for sandboxing. While more modern tools like Landlock and namespaces exist for this task, we still see a combination of these older features during audits. And we always uncover a lot of issues. The new Testing Handbook chapter covers sandbox bypasses we’ve seen, like io_uring syscalls that execute without the BPF filter ever seeing them, the CLONE_UNTRACED flag that lets a tracee effectively disable seccomp filters, and memory-level race conditions in ptrace-based sandboxes. Test your review skills We’ve provided two challenges below that contain real bug classes from the checklist. Try to spot the issues, then submit your answers. If you’re in the first 10 to submit correct answers, you’ll receive Trail of Bits swag. The challenge will close April 17, so get your answers in before then. Stuck? Don’t worry. We’ll be publishing the answers in a follow-up blog post, so don’t forget to #like and #subscribe, by which we mean add our RSS feed to your reader. The many quirks of Linux libc In this simple ping program, there are two libc gotchas that make the program trivially exploitable. Can you find and explain the issues? If you can’t, check out the handbook chapter. Both bugs are covered in the Linux usermode section. #include <stdio.h> #include <stdlib.h> #include <string.h> #include <arpa/inet.h> #define ALLOWED_IP “127.3.3.1” int main() { char ip_addr[128]; struct in_addr to_ping_host, trusted_host; // get address if (!fgets(ip_addr, sizeof(ip_addr), stdin)) return 1; ip_addr[strcspn(ip_addr, “\n”)] = 0; // verify address if (!inet_aton(ip_addr, &to_ping_host)) return 1; char *ip_addr_resolved = inet_ntoa(to_ping_host); // prevent SSRF if ((ntohl(to_ping_host.s_addr) » 24) == 127) return 1; // only allowed if (!inet_aton(ALLOWED_IP, &trusted_host)) return 1; char trusted_resolved = inet_ntoa(trusted_host); if (strcmp(ip_addr_resolved, trusted_resolved) != 0) return 1; // ping char cmd[256]; snprintf(cmd, sizeof(cmd), “ping ‘%s’”, ip_addr); system(cmd); return 0; } Windows driver registry gotchas This Windows Driver Framework (WDF) driver request handler queries product version values from the registry. There are several bugs here, including an easy-to-exploit denial of service, but one of them leads to kernel code execution by messing with the registry values. Can you figure out the bug and how to exploit it? NTSTATUS InitServiceCallback( In WDFREQUEST Request ) { NTSTATUS status; PWCHAR regPath = NULL; size_t bufferLength = 0; // fetch the product registry path from the request status = WdfRequestRetrieveInputBuffer(Request, 4, &regPath, &bufferLength); if (!NT_SUCCESS(status)) { TraceEvents( TRACE_LEVEL_ERROR, TRACE_QUEUE, “%!FUNC! Failed to retrieve input buffer. Status: %d”, (int)status ); return status; } / check that the buffer size is a null-terminated Unicode (UTF-16) string of a sensible size */ if (bufferLength < 4 || bufferLength > 512 || (bufferLength % 2) != 0 || regPath[(bufferLength / 2) - 1] != L’\0’) { TraceEvents( TRACE_LEVEL_ERROR, TRACE_QUEUE, “%!FUNC! Buffer length %d was incorrect.”, (int)bufferLength ); return STATUS_INVALID_PARAMETER; } ProductVersionInfo version = { 0 }; HandlerCallback handlerCallback = NewCallback; int readValue = 0; // read the major version from the registry RTL_QUERY_REGISTRY_TABLE regQueryTable[2]; RtlZeroMemory(regQueryTable, sizeof(RTL_QUERY_REGISTRY_TABLE) * 2); regQueryTable[0].Name = L"MajorVersion"; regQueryTable[0].EntryContext = &readValue; regQueryTable[0].Flags = RTL_QUERY_REGISTRY_DIRECT; regQueryTable[0].QueryRoutine = NULL; status = RtlQueryRegistryValues( RTL_REGISTRY_ABSOLUTE, regPath, regQueryTable, NULL, NULL ); if (!NT_SUCCESS(status)) { TraceEvents( TRACE_LEVEL_ERROR, TRACE_QUEUE, “%!FUNC! Failed to query registry. Status: %d”, (int)status ); return status; } TraceEvents( TRACE_LEVEL_INFORMATION, TRACE_QUEUE, “%!FUNC! Major version is %d”, (int)readValue ); version.Major = readValue; if (version.Major < 3) { // versions prior to 3.0 need an additional check RtlZeroMemory(regQueryTable, sizeof(RTL_QUERY_REGISTRY_TABLE) * 2); regQueryTable[0].Name = L"MinorVersion"; regQueryTable[0].EntryContext = &readValue; regQueryTable[0].Flags = RTL_QUERY_REGISTRY_DIRECT; regQueryTable[0].QueryRoutine = NULL; status = RtlQueryRegistryValues( RTL_REGISTRY_ABSOLUTE, regPath, regQueryTable, NULL, NULL ); if (!NT_SUCCESS(status)) { TraceEvents( TRACE_LEVEL_ERROR, TRACE_QUEUE, “%!FUNC! Failed to query registry. Status: %d”, (int)status ); return status; } TraceEvents( TRACE_LEVEL_INFORMATION, TRACE_QUEUE, “%!FUNC! Minor version is %d”, (int)readValue ); version.Minor = readValue; if (!DoesVersionSupportNewCallback(version)) { handlerCallback = OldCallback; } } SetGlobalHandlerCallback(handlerCallback); } We’re not done yet Our goal is to continuously update the handbook, including this chapter, so that it remains a key resource for security practitioners and developers who are involved in the source code security review process. If your favorite gotcha is not there, please send us a PR. Checklist-based review, even combined with skilled-up LLMs, is only a single step in securing a system. Do it, but remember that it’s just a starting point for manual review, not a substitute for deep expertise. If you need help securing your C/C++ systems, contact us.

5 MIN READ arrow_forward