❌

Visualização normal

Antes de ontemAnalyst
  • ✇bellingcat
  • How Online Maps Lay Bare the US Military Bases Targeted by Iran Sebastian Vandermeersch
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. Become a member and support our work here. In September, CBS News obtained new images of the aftermath of attacks on US bases in the Middle East from active service members. One told the news channel: “This is major damage to our bases that hasn’t been communicated to the American public. “We’re standing there with our eyes closed getting punched in the face,” they added. While reports have
     

How Online Maps Lay Bare the US Military Bases Targeted by Iran

5 de Novembro de 2026, 06:46

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. Become a member and support our work here.

In September, CBS News obtained new images of the aftermath of attacks on US bases in the Middle East from active service members. One told the news channel: “This is major damage to our bases that hasn’t been communicated to the American public. “We’re standing there with our eyes closed getting punched in the face,” they added.

While reports have detailed how Iran used Russian intelligence, including satellite imagery, to identify US troops, ships and aircraft, Bellingcat found that publicly available mapping platforms also reveal detailed and potentially sensitive information about US military bases in the Middle East. 

Google Maps and other online mapping services contain dozens of labels identifying facilities including headquarters, accommodation, gyms and fuel points. Publicly available photographs and videos, including material published by the US military, provide further details about how these facilities are used.

Experts told Bellingcat that such information could have helped inform Iranian targeting decisions by revealing where personnel gather and how bases operate. However, they cautioned that Iran would also have had access to other sources of intelligence. Reporting from Reuters earlier this year also highlighted how US officials believed commercially available location data was being used to target US troops in war zones.

The bases examined by Bellingcat are not secret and have operated for years in a region where US forces maintained a strong sense of security. Joseph Jarnecki, a research fellow at the Royal United Services Institute, said the US had been able to maintain aerial superiority and was understood to have significant air defence capabilities, but that Iran’s missile and drone campaign has undermined both assumptions. 

Iranian strikes have damaged at least 20 US military sites across Qatar, Bahrain, the United Arab Emirates (UAE), Jordan, Kuwait, Iraq and Saudi Arabia since the conflict began in February this year.

Governments can request that Google blur or remove sensitive information from its services and Google publishes data on government requests for content removal.

Bellingcat contacted the US Department of Defense who asked us to reach out to CENTCOM about the details in this story. We also contacted Google. However, we did not receive responses to questions from these respondents before publication.

We examined four of the largest and most heavily affected US military installations by Iran’s missile and drone campaign: Camp Buehring and Camp Arifjan in Kuwait, Muwaffaq Salti Air Base in Jordan and Al Udeid Air Base in Qatar. At each one, we found publicly available Google Maps information providing details about facilities inside the bases and how they are used.

A map detailing four US bases that were attacked by Iran during 2026 conflict.

Camp Buehring

At Camp Buehring in Kuwait, Google Maps identifies what is labelled as the main headquarters building, as well as a gym and a self-help facility, all of which were among locations destroyed by recent Iranian strikes. In addition to Google Maps place markers there are dozens of reviews and user-uploaded photographs.

Left: Screengrab showing “Camp Beuhring Main HQ, Bldg. 100” Google Maps label. Right: Same building seen destroyed on satellite image published by Iran’s Tasnim news agency.
Left: Screengrab showing “Camp Beuhring Main HQ, Bldg. 100” Google Maps label. Right: Same building seen destroyed on satellite image published by Iran’s Tasnim news agency.

Bellingcat verified that this was a headquarters building by matching the flagpoles and layout in a photo published by the US military’s Defense Visual Information Distribution Service (DVIDS) showing soldiers posing in front of the  building to the Google Maps satellite image.

Geolocation diagram using dvids photo to verify Google Maps Buehring main headquarters location, confirming the accuracy of the Google Maps place marker.
Geolocation diagram using dvids photo to verify Google Maps Buehring main headquarters location, confirming the accuracy of the Google Maps place marker.

We also identified details about one of the camp’s gyms, including the building’s layout and roof structure, as well as Google reviews that indicate when it is busiest.

Left: Google Maps label showing “Camp Beuhring Gym” Right: Destroyed gym building on satellite image published by Iran’s Tasnim news agency on 6 April 2026.
Left: Google Maps label showing “Camp Beuhring Gym” Right: Destroyed gym building on satellite image published by Iran’s Tasnim news agency on 6 April 2026.

Among the images published by CBS News in September was that of a heavily damaged tent-like structure with smoke rising from it. Bellingcat matched the location of this image to the above satellite imagery, verifying that it shows the gym building pinned on Google Maps.

⚡🚨 Exclusive images released showing damage to E-3 Sentry AWACS aircraft at Prince Sultan Air Base in Saudi Arabia, plus damage to US military camps Arifjan and Buehring in Kuwait from Iranian strikes. Images sent to CBS by active US military personnel. (IRIB News) https://t.co/1urA44f4xz

— Middle East Observer (@ME_Observer_) September 15, 2026
Left: Google Maps shows images of the gym’s interior and exterior. Right: Google Maps reviews indicate when the gym is busy (names blurred by Bellingcat).

A building labeled on Google as “DPW Self-Help Center” was also destroyed at Camp Buehring. Its Google Maps listing includes opening hours, and a photo posted by a Master Sergeant in 2017 labeled “My office in Kuwait” shows the interior of an office. Some of the notices on the walls inside the office are legible, including what appears to be a floor plan.

Left: Google maps “DPW Self-Help Center” listing. Right: Same building seen destroyed on a satellite image published by Iran’s Tasnim news agency on April 6, 2026.
Left: Google maps “DPW Self-Help Center” listing. Right: Same building seen destroyed on a satellite image published by Iran’s Tasnim news agency on April 6, 2026.

A Google Maps placemark labelled “Transient Tent” identifies a large accommodation area, while another review includes a ground-level photograph of one of the tents. The dining facility can also be seen from the inside, including information regarding meal times.

Left: Google image that appears to show an accommodation area. Right: Photos of the dining facility, including information on meal times (names blurred by Bellingcat).

William Goodhind of Contested Ground, a visual forensics consultancy that uses OSINT and satellite imagery to analyse military sites, said the information available on Google Maps could reveal “the camp’s layout, the division between secure and non-secure areas, confirms likely accommodation areas, and shows day/time periods of low and high activity.”

Camp Arifjan

A satellite image of Kuwait’s Camp Arifjan, captured by Planet on March 4, 2026 and published by the The Washington Post, shows damage to numerous structures at the base. Bellingcat compared the image with publicly available mapping data to identify two damaged facilities.

A Google Maps listing for a lodging facility labelled “TPE FOR ARIFJAN” includes a soldier’s review discussing room reservations, accommodation shortages and cleaning schedules, indicating that the building was used for military housing. Although Google’s satellite imagery of the base is blurred, the facility is visible in unblurred Bing Maps imagery using coordinates obtained from its Google Maps listing. The satellite image published by The Washington Post shows heavy damage to the facility.

Left and centre: Blurred Google Maps satellite image with Google review (name blurred by Bellingcat). Right: Unblurred Bing maps satellite image showing the same location.

A “24hr Fuel Point” tagged on Google Maps is also blurred, but when the coordinates are copied and pasted into Bing Maps a clear satellite image of the fuel station appears. This facility is also seen destroyed on the satellite imagery published by The Washington Post.

Left and centre: Blurred Google Maps satellite image with Google review. Right: Unblurred Bing Maps satellite image.
Left and centre: Blurred Google Maps satellite image with Google review. Right: Unblurred Bing Maps satellite image.

Bellingcat contacted Microsoft to ask why the facility was not blurred out in the Bing Maps service. A spokesperson said: “When an entity requests removal from visibility on Bing Maps, we work through our process and act accordingly.”

Muwaffaq Salti Air Base

Three US service members were killed in Iranian ballistic missile strikes on Muwaffaq Salti Air Base in Jordan on July 17, 2026. The strikes appear to have caused extensive damage to a barracks in the north of the base, with satellite imagery released by Iran showing at least 20 structures destroyed.

Less than 40 metres from the nearest destroyed structure, and within the same living quarters area, Google Maps reveals the location of an “Airman’s Attic”. These are volunteer-run thrift shops for US Air Force personnel. 

A five-star review posted last year by a soldier includes a ground-level image of the facility, showing uniform drop-off points and fortifications along the perimeter. The listing provides information about the area’s use, while photographs of the shop’s exterior make it possible to match the satellite imagery to the location on the ground.

A Sky News investigation also found that 80 percent of the runs shared publicly on the sporting app Strava at Muwaffaq Salti Air Base during the April ceasefire started or finished in this northern barracks.

Satellite image showing damage, highlighted in red, published by Iran’s Fars News Agency.
Satellite image showing damage, highlighted in red, published by Iran’s Fars News Agency.

Less than 100 meters from the destroyed area, “LSA DFAC” is pinned on Google maps. “DFAC” is the standard US military acronym for “dining facility”, according to Stars and Stripes. Several Google reviews also discuss the steak being served in this location.

Left: Google reviews for the dining facility (names blurred by Bellingcat). Right: Google Earth satellite image with the Airman’s Attic and dining facility visible in the area targeted by Iran.

Al Udeid Air Base

Similar mapping information is available for Al Udeid Air Base in Qatar, the largest US military installation in the Middle East. 

The base appears unblurred on Google Maps and entries on the platform identify the functions of numerous buildings. These include lodging facilities, which videos and reviews indicate are used by US Air Force crews and show the interior layout of the facility.

Google reviews for “Al Udeid Lodging” indicate the presence of US Air Force crews (names blurred by Bellingcat).

Meanwhile, Russia’s Yandex Maps details the site of two cinemas, a bar and a school on the base. This appears to match information provided on Google Maps.

While Al Udeid Air Base was damaged during Iranian attacks earlier this year, the areas that were struck do not appear to be among those listed on Google. 

Business Insider reported that a geodesic dome that housed secure communications equipment was hit in the strikes. ABC News detailed how another building had been damaged near the centre of the base. Google Maps shows a large warehouse-like structure at the location where ABC News reported this strike.

A New Reality in the Middle East

Former US Air Force targeting specialist Wes J. Bryant said the problem facing US bases in the Middle East reflected a broader assumption within the military that bases in allied countries were safe from serious attack. He said the belief that these were rear-area bases where “nothing’s going to happen out here” was a fundamental miscalculation, and that the Google Maps issue was an example of the military failing to account for that vulnerability.

Matthew Savill, Director of Military Sciences at the Royal United Services Institute, said Western militaries do not have consistent policies governing what information can be publicly posted about sensitive sites. He said the issue needs to be considered before a conflict begins, rather than when attacks are already underway. “The start point should be, can we enforce operational security discipline?” Savill pointed to restrictions already in place at some UK military facilities, including Faslane, a naval base that houses the UK’s nuclear submarines, where he said personnel are not permitted to use mobile phones on site.

Savill told Bellingcat that the different pieces of data available on Google Maps, even if outdated, provided a “pattern of life” about US bases in the Middle East. But he added that the use of Google to help with the military targeting of Western bases is not new, saying he encountered the issue while deployed to Iraq more than 20 years ago.

“When I was in Iraq, we knew for a fact that insurgents and militants had been using commercial imagery, including Google Earth, to try and map our bases,” he said. “We have known for more than 20 years that there is a vulnerability that if you don’t stop the collection and layering of data early on, that your bases can be heavily mapped.” 

The use of open-source information to support military targeting has been documented in Ukraine. In 2022, Foreign Policy detailed how Ukrainian private intelligence company Molfar used photographs of Russian soldiers published by Russian media alongside Google Earth satellite imagery to identify their location. Molfar passed its findings to Ukrainian intelligence, and the site was subsequently struck by the Ukrainian military.

The security risks of publicly mapping military installations were recognised by the US military in 2008, when the Pentagon banned Google’s Street View from documenting the interiors of its bases. Since the Iran war started, US military commanders have also warned service members about posting photographs and videos from bases after Iranian attacks, cautioning that such footage could help Iran assess the effects of its strikes.

Iranian attacks have forced US personnel to evacuate many additional locations that were deemed too vulnerable to further strikes. 

In total, 23 US service members have been killed and 865 wounded in the US-Iran war, according to the Pentagon.


Bellingcat’s Jake Godin, Charlotte Maher and Merel Zoet contributed to this report.

Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. You can also subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post How Online Maps Lay Bare the US Military Bases Targeted by Iran appeared first on bellingcat.

  • ✇bellingcat
  • Inside the Secretive, Contradictory World of Campaign Ads Funded by AI Super PACs Kolina Koltai
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. Become a member and support our work here. An online ad for Ben McAdams, the Democratic candidate for Congress in Utah, emphasised that he could be trusted because he “fought back against MAGA”. Source: Think Big / Google Ad Transparency Center In Texas, another ad promoted Republican candidate Chris Gober, calling him a “MAGA warrior”. Source: American Mission / Google Ad Transparency
     

Inside the Secretive, Contradictory World of Campaign Ads Funded by AI Super PACs

2 de Novembro de 2026, 13:11

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. Become a member and support our work here.

An online ad for Ben McAdams, the Democratic candidate for Congress in Utah, emphasised that he could be trusted because he “fought back against MAGA”.

Source: Think Big / Google Ad Transparency Center

In Texas, another ad promoted Republican candidate Chris Gober, calling him a “MAGA warrior”.

Source: American Mission / Google Ad Transparency Center

What’s interesting is that both ads were ultimately paid for with money from the same organisation – Leading the Future (LTF), a Super PAC which opposes regulations that “constrain” innovation in AI. The network has received US$125 million in funding in total from OpenAI president Greg Brockman, his wife Anna Brockman, tech-focused venture capital firm Andreessen Horowitz (a16z) and a16z’s founders, Federal Election Commission (FEC) filings show. 

Directly opposing LTF is Public First Action (PFA), which calls for AI safety regulations and guardrails. PFA has received $40 million in funding from AI company Anthropic. 

Like the ads run by organisations funded by LTF, the Super PACs funded by PFA also ran ads that appeared supportive or critical of Trump depending on the voters they were targeting. 

Ads in support of Republican Carlos De La Cruz (above) and Democrat Colin Allred (below) in Texas, paid for by Defending Our Values PAC, and Jobs and Democracy PAC, respectively. Source: Defending Our Values PAC / Jobs and Democracy PAC / Google Ad Transparency Center

None of these ads mentions AI.

As the US 2026 midterm elections approach on Nov. 3, Super PAC spending on political advertising has reportedly soared to record highs, surpassing US$11 billion last week and exceeding the amount spent on any other election cycle to date. 

Super PACs are a particular type of political action committee (PAC) that may receive unlimited contributions from individuals, corporations, labour unions and other PACs for the purpose of financing independent expenditures and other independent political activity.

In this year’s elections, LTF’s and PFA’s Super PACs, funded by the AI industry heavyweights, have played an outsized role in supporting or opposing candidates through advertising. But as polls show distrust of AI crosses party lines, the advertisements that Silicon Valley’s leaders are paying millions of dollars to try to sway the results in their chosen candidates’ favour often do not mention AI at all. 

Photo: Reuters/Shannon Stapleton

While LTF is a registered Super PAC itself, FEC filings show it mainly disburses money to other Super PACs in its network which run political ads. 

Of about $52 million that LTF has disbursed to date, $20 million went to Think Big, which targets the Democratic voter base, and another $20 million went to American Mission, which focuses on Republicans. 

PFA, meanwhile, is registered as a “social welfare”, or 501(c)(4), organisation. This type of non-profit is not required to disclose its donors or disbursements. However, PFA contributes funds to three Super PACs: the bipartisan Public First PAC, Jobs and Democracy for Democratic candidates and Defending Our Values for Republicans. FEC filings for these Super PACs show Public First PAC received $1 million, Jobs and Democracy received $15.4 million and Defending Our Values received $7.9 million from PFA. Public First PAC’s FEC filings also show that $4 million of its $4.4 million in disbursements disclosed so far went to Jobs and Democracy, while another $300,000 went to Defending Our Values. 

Disbursements to Think Big, American Mission, Jobs and Democracy and Defending Our Values from Leading the Future, Public First PAC and Public First Action. Graphic: Bellingcat

Super PACs are allowed to accept unlimited contributions from other political committees, and other similar networks of Super PACs have been observed in the current election cycle, such as cryptocurrency-linked Fairshake and its affiliated Super PACs.

Bellingcat analysed data on more than 300 online video ad campaigns paid for by Think Big, American Mission, Jobs and Democracy and Defending Our Values from Google’s Ad Transparency Center. These ad campaigns, many of which used the same videos in slightly different formats or across different platforms, supported or opposed 40 candidates. 

Estimated Super PAC Google Ad spend by state

Loading map…

Estimate total ad spend for selected Super PACs: $0

Source: Google Ad Transparency Center, analysis by Bellingcat

Only ads supporting or opposing two candidates mentioned AI, and none mentioned data centres, a top issue this election cycle. A New York Times analysis of 95 video ads paid for by the same four Super PACs, using data gathered by media tracking firm AdImpact, had similar findings. FEC rules state that Super PACs can spend money to support or oppose a candidate, but cannot coordinate with the candidate or their campaign on this. There is no evidence that any of the candidates or campaigns for which the Super PACs ran ads have coordinated with them.

Michael Beckel, Director of Money in Politics Reform at cross-partisan political reform group Issue One, told Bellingcat that voters are at a disadvantage when special interests fund Super PACs running ads that don’t mention the priority issues of those underwriting them. 

“Special interests seem keen on moving voters with messaging around hot-button issues, all while advancing a specific policy agenda that viewers of ads are kept in the dark about,” he said. The result of this, Beckel added, was that “the industry gains a stronger hand in Washington, while voters get little chance to judge the agenda their votes could help advance”. 

A representative for PFA stated: “We endorse officials who support regulating AI. The aligned PACs run ads on the issues that matter most immediately to voters in each race, and for most electorates right now that means the economy, affordability, public safety, and other issues that they are facing on a daily basis.” 

Jobs and Democracy, Defending Our Values, LTF, Think Big and American Mission did not respond to questions about their political advertising as of publication. 

A recent survey by think tank Data for Action and non-profit Guardrails Action found that about three-quarters of likely voters would be less inclined to vote for a candidate if they found out they had accepted a donation from an AI Super PAC. 

The ads distributed by the AI-backed Super PACs supporting or opposing 29 candidates mentioned economic issues such as taxes, the cost of living, consumer rights and local jobs or small businesses. Another 16 mentioned immigration or law enforcement and 17 mentioned US President Donald Trump or his “Make America Great Again” (MAGA) movement. 

chart visualization

Like the ads mentioning Trump, those on immigration were sharply divided in messaging depending on whether they were targeting Democratic or Republican voters, even when they were paid for by the AI Super PACs in the same network. 

LTF’s Think Big sponsored an ad for Democratic candidate Jeremy Moss in Michigan that talks about how he stood up to Immigration and Customs Enforcement (ICE), while American Mission’s ad for Republican Clay Fuller in Georgia mentions how he is focused on securing the southern border. 

Ads for Moss (above) and Fuller (below) sponsored by Think Big and American Mission respectively. Source: Think Big / American Mission / Google Ad Transparency Center

We found similar messaging in ads mentioning immigration that were paid for by Super PACs in PFA’s network. Defending Our Values ran an ad in support of Republican John Cowan in Georgia emphasising that he will combat illegal immigration, whereas Jobs and Democracy’s ad for Democrat Valerie Foushee in North Carolina said she was “leading the fight to hold ICE accountable”.

Ads for Cowan (above) and Foushee (below) sponsored by Defending our Values and Jobs and Democracy respectively. Source: Defending Our Values PAC / Jobs and Democracy PAC / Google Ad Transparency Center

For the ads that did mention AI, almost all were in support of Alex Bores’ primary in New York. PFA’s Jobs and Democracy PAC ran 152 ads supporting Bores’ candidacy, at least some of which mentioned that he was in favour of safeguards on AI and holding Big Tech accountable. 

LTF’s Think Big, on the other hand, ran 22 ads opposing Bores’ candidacy. While they did not mention his stance on AI, many of Think Big’s ads focused on him receiving funding from Anthropic, “dark money Super PACs” and his former employer Palantir, the software giant whose technology, including a new AI system, is used in ICE’s immigration enforcement operations. 

Ads supporting Bores sponsored by Jobs and Democracy. Source: Jobs and Democracy PAC / Google Ad Transparency Center
Ads opposing Bores, sponsored by Think Big. Source: Think Big / Google Ad Transparency Center

Jobs and Democracy’s advertiser page also showed two ads for Josh Gottheimer in New Jersey that focused on the threats of AI scams and AI-generated media, urging voters to call Gottheimer to tell him to “stand strong for AI safeguards”. 

Ad for Josh Gottheimer that mentions AI, listed on Jobs and Democracy PAC’s Google Ads Transparency Center advertising page. Source: Jobs and Democracy PAC / Google Ad Transparency Center

Major donors to LTF that Bellingcat reached out to – Greg Brockman, a16z, and a16z founders Marc Andreessen and Ben Horowitz – did not respond to requests for comment on their funding to the Super PACs as of publication. We could not find any publicly available contact information for Anna Brockman, but we also asked about her contributions to LTF in questions to her husband.

OpenAI did not respond to questions about the Brockmans’ contributions to LTF as of publication. However, the company published a statement on the topic in June, which said “OpenAI does not direct the activities of LTF, or have visibility into their operations” and that “any engagement with that organisation has been in a personal capacity, not on behalf of the company”. 

PFA’s main public donor, Anthropic, did not respond to Bellingcat’s queries about its funding for PFA and PFA’s political advertising as of publication. It said in a statement on its website in July that its donations to PFA were “to help promote policies that will maintain meaningful safeguards, sustain America’s AI leadership, and demand transparency from the companies building the most powerful AI models”.

Unknown Vendors

Federal campaign finance laws require federal political committees to file periodic campaign finance reports disclosing their receipts and disbursements. 

Our analysis of FEC filings for LTF and the Super PACs in its network revealed that they were channelling more than $36.4 million in total towards four companies that were newly set up, with no apparent online presence outside of these filings. 

A complaint to the FEC by the non-profit Campaign Legal Center (CLC) in May cited American Mission and Think Big’s payments to Summit Ridge and Lantern Productions, alleging that the Super PACs had “unlawfully evaded federal reporting requirements by funnelling their payments for goods and services through shell companies set up to conceal how the super PACs are actually spending their money”. The FEC has not yet ruled on the complaint. LTF, American and Think Big did not respond to Bellingcat’s queries about the complaint as of publication.

FEC filings show that LTF paid a company called Prusik Partners LLC more than $966,000 for operational expenditures for a range of services including communications and strategy consulting, as well as legal fees. 

The network’s Democrat-focused Super PAC, Think Big, paid a company called Lantern Production Consultants LLC more than $20 million, based on filings for disbursements from Aug. 20, 2025 to Oct. 14, 2026. 

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

The payments included $3.17 million for operational expenditures for a range of services such as media consulting and research, and another $17 million for independent expenditures. The FEC defines an independent expenditure as one that pays for “a communication that expressly advocates for the election or defeat of a clearly identified candidate”. The highest independent expenditures paid to Lantern ($8 million) were to oppose Bores’ candidacy, the filings show.

Think Big also declared in its filings that it had paid a company called Blue Horizon Compliance Consultants LLC almost $55,700 for “compliance consulting”. 

Republican-focused American Mission, on the other hand, paid a company called Summit Ridge Media LLC more than $13 million, based on filings for disbursements from Aug. 20, 2025 to Oct. 14, 2026. 

This included $2.2 million for operating expenditures such as polling and website services, and $10.9 million in independent expenditures, with the highest amount (about $960,400) going towards communications in support of James Morris Kingston, the Republican candidate for the House of Representatives in Georgia.

Bellingcat found that all four of these LLCs listed mailing offices and office supply goods stores in Nevada as their mailing addresses. We could not find records of them doing business with any entity outside of the LTF network – in fact, they appeared to have no online presence aside from their names appearing in the filings and business registrations. 

Despite their addresses being listed in Nevada, none of the companies could be found in the state’s public business registry. Instead, Bellingcat found registrations for Prusik, Lantern Production and Summit Ridge across the country – in Delaware. 

While Delaware is the second-smallest state in the US, more than half of all Fortune 500 companies are incorporated there according to the Delaware Secretary of State. The state’s business-friendly laws mean companies can reportedly incorporate in minutes and are only required to name a representative – who can be a paid agent unrelated to the business – in public registration documents, not the beneficial owners.

Articles of incorporation from the Delaware Secretary of State showed that Prusik was registered on July 22, 2025, while Lantern Production and Summit Ridge were both registered on Oct. 30, 2025. We also found a Delaware registration for a company called “Blue Horizon Compliance Group LLC”, registered on Oct. 30, 2025. While the name differs slightly from the Blue Horizon Compliance Consultants LLC that Think Big declared payments for, the registration was signed by the same agent listed in the documents for Lantern Production and Summit Ridge. 

Subscribe to the Bellingcat newsletter

Subscribe to our newsletter for first access to our published content and events that our staff and contributors are involved with, including interviews and training workshops.

The filings show that all of these companies – including Blue Horizon Compliance Group, if this was indeed the same entity referred to in Think Big’s filings – began receiving payments from the Super PACs within two to three months of their business registrations.

Saurav Ghosh, CLC’s Director of Federal Campaign Finance Reform, told Bellingcat that the group filed the complaint because “when groups funded by big corporate special interests evade transparency laws, voters lose”. 

“Federal reporting requirements help ensure transparency in our elections and protect voters’ legal right to know where the money is going,” Ghosh said. “Secret spending schemes like this make it virtually impossible to ensure that our elected officials are staying accountable to voters instead of wealthy special interests.”

We were unable to reach out to the LLCs as none of them had phone numbers or email addresses listed online. LTF, Think Big, and American Mission did not respond to questions about the LLCs, including whether “Blue Horizon Compliance Consultants LLC” listed in Think Big’s filings was the same company as Delaware-registered “Blue Horizon Compliance Group LLC”. However, a WIRED story on these same LLCs quoted a spokesperson from LTF as saying that the complaint was “a desperate ploy for attention from Public First, Anthropic, and the groups aligned with their outside organisations”. WIRED also quoted a Public First spokesperson denying that the group had anything to do with the complaint.  

Beckel suggested that stricter transparency measures relating to Super PAC spending would help inform and empower voters. For instance, Congress could pass legislation to require ads to include the names of the top donors, he said, adding that similar rules had been passed with cross-partisan support in places like Alaska and Arizona. 

“Congress would be doing voters a huge service by passing more robust transparency requirements for political ads. Voters deserve to know who is behind the ads they’re seeing,” Beckel said. 

About the Data

Google Ads Analysis

We reviewed ads available through Google’s Ads Transparency Center. These are ads served on Google platforms like Search, Display, Gmail and YouTube. We filtered specifically for ads shown between Jan. 1 and Oct. 23, 2026 in the US. 

We focused only on video ads classified as political advertisements, not text- or image-based ads. We looked at ads purchased by the following advertisers: Think Big, THINK BIG, American Mission, Jobs and Democracy PAC, JOBS AND DEMOCRACY PAC, Defending Our Values PAC and DEFENDING OUR VALUES PAC. 

In total, there were 336 advertisements in the dataset: 86 from Think Big, 57 from American Mission, 22 from Defending Our Values and 171 from Jobs and Democracy. Each ad listed on the platform was counted individually, although many of these were duplicates of the same ad that was run over a different period or platform. Some of the ads have also been removed for ad policy violations, but we were able to see which candidates they were for because they appeared when searching for ads specific to those candidates within the advertiser’s page. 

For the coding of themes, we looked at whether each of the themes mentioned (AI, data centres, economy, Trump, and immigration or law enforcement) appeared in any of the ads for each candidate. This does not account for how many times each theme may have appeared in ads for a candidate.

Our notes on the Google ad data and count of themes for each candidate can be found here. 

For the accompanying map showing spending by each Super PAC by state, we used data from the Google Ad Transparency Center showing the total spend per advertiser and the spending range per ad to estimate the spend per ad (as the data does not show the exact spend per ad). For each advertiser, we separately added up the minimum and maximum spend amounts for all ads. We then calculated where in that range the total spend fell (e.g. 45 percent between the minimum and maximum spend). The per ad spend was estimated by applying this same proportion to the range of each ad and rounding down to the nearest dollar.

FEC Campaign Finance Data

Data from FEC filings for LTF, Think Big and American Mission are based on disclosures from Aug. 20, 2025 to Oct. 14, 2026. Data from FEC filings for Public First, Defending Our Values and Jobs and Democracy are based on disclosures for Jan. 1, 2026 to Sept. 30, 2026, which were the latest available as of publication.

The amounts LTF and PFA contributed to their associated Super PACs are based on the recipient Super PACs’ receipts for contributions from “Leading the Future”, “Public First”, “Public First Action” and “Public First Action, Inc”. 

Links to the source pages for these filings, as well as the data we exported for our analysis, are available here.


This investigation was supported by a grant from the Tarbell Center for AI Journalism. 

Data visualisations by Galen Reich. Melissa Zhu contributed research to this piece.

Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. You can also subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post Inside the Secretive, Contradictory World of Campaign Ads Funded by AI Super PACs appeared first on bellingcat.

  • ✇Schneier on Security
  • New Serious PHP Vulnerabilities Released and Patched Bruce Schneier
    PHP maintainers have released fixes for two critical vulnerabilities: PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two critical vulnerabilities that can allow unauthenticated remote code execution. All PHP users are strongly encouraged to update immediately. Who is at risk: all PHP installations serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. We consider the risk highest for mod_php, with a demonstrated exploit where an unauthenticated request can t
     

New Serious PHP Vulnerabilities Released and Patched

22 de Outubro de 2026, 10:26

PHP maintainers have released fixes for two critical vulnerabilities:

PHP 8.5.12, 8.4.27, 8.3.36 and 8.2.35 fix two critical vulnerabilities that can allow unauthenticated remote code execution. All PHP users are strongly encouraged to update immediately.

Who is at risk: all PHP installations serving web requests, including PHP-FPM, Apache with mod_php, php-cgi, FrankenPHP and LiteSpeed. We consider the risk highest for mod_php, with a demonstrated exploit where an unauthenticated request can trigger the attack before any application code runs.

The vulnerabilities:

  • RCE via OOM corruption, CVE-2026-109618 (GHSA-w8hw-w9fx-j79w): out-of-memory handling can interrupt PHP while internal request data is only partially initialized. Request cleanup may then treat the invalid data as valid.
  • RCE via stale global state, CVE-2026-109619 (GHSA-6wmf-4rv5-p3rp): request-owned internal state can survive after its backing memory has been freed. A later request handled by the same worker may use that stale state before normal initialization resets it.

Updating PHP is the only way to regain security. Do it now.

More news.

Good explanations.

  • ✇bellingcat
  • Can LLMs Supercharge Geolocations? Our Tests Show Big Improvements But Humans Remain Key Foeke Postma
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. In September, the AI company Anthropic published a blog post that claimed its models were approaching “superhuman capabilities for geolocating outdoor photos”. Anthropic made this claim by comparing its models’ performance to that of players of the popular online game GeoGuessr, which challenges users to geolocate Google Street View images.  Bellingcat has been running its own benchmark test
     

Can LLMs Supercharge Geolocations? Our Tests Show Big Improvements But Humans Remain Key

22 de Outubro de 2026, 08:06

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published.

In September, the AI company Anthropic published a blog post that claimed its models were approaching “superhuman capabilities for geolocating outdoor photos”. Anthropic made this claim by comparing its models’ performance to that of players of the popular online game GeoGuessr, which challenges users to geolocate Google Street View images. 

Bellingcat has been running its own benchmark tests on AI models since June 2025, right around the time when Large Language Models (LLMs) began to challenge reverse image searches for assisting with geolocation tasks. In our first round, GPT 4o-mini from OpenAI appeared to be the best tool for geolocating, a title which was taken by Google AI mode when we performed our second round of tests a few months later in August 2025. 

This time, we significantly upped the ante for the models, providing increasingly challenging photos and asking for only one set of coordinates. We tasked not only closed-weight models from Claude, Grok, Gemini, ChatGPT, and Mistral, but also open-weight models from Moonshot AI’s Kimi, commercial provider GeoSeer and the non-generative geolocation model GeoClip. In total, we fed each model 52 photos that had not been published anywhere on the internet before.

The results showed that the current generation of models have markedly improved geolocation capabilities. Anthropic’s Opus and Fable, as well as Google’s recent Gemini 4 Argon model, performed the best on geolocation tasks, narrowing down locations from images faster than a human could under specific conditions. Preliminary tests of a set of agentic skills and tools being developed internally by Bellingcat also showed promising results in further improving the geolocation performance of generic-use AI agents.

But crucially, the tests also showed why humans remain necessary for accurate and trustworthy geolocation work, as no models were foolproof. This is particularly important given the increased use of AI in military contexts. The Anthropic blog post focused specifically on geolocating photos for military and intelligence purposes such as targeting, a historically labour-intensive process requiring significant human expertise. 

While our tests showed some models performed extremely well at narrowing down results to the correct general area, none were close to being perfect at finding the exact spot a picture was taken. That accuracy is extremely important in geolocation, especially in military contexts where the cost of incorrect information can be life or death. It is also important for organisations like Bellingcat whose reputation relies on pinpoint factual accuracy.

Our Methods

To compare model results, we used two metrics built on the distance in kilometres between a prediction and the real location. GeoGuessr Score was used to assess whether models could systematically identify the general area, while the Mean Log Score (explained below) measured which models most accurately pinpointed the correct location in the final few kilometres. 

The GeoGuessr Score metric is based on the online game of the same name, whose scoring system Stanford University academics reverse-engineered in a 2024 study. This system rewards answers close to the general area an image was taken, but does not strongly distinguish between guesses close to the precise location. For example, an answer 200km from the correct location with more points than one 2,000km away. However, an answer 500m from where a picture was taken and another 5km away achieve a similar score. For real-world geolocations, that distance matters a great deal.

For more precision, we introduced a custom metric that grades models higher if they narrow down the search in the last few kilometres. This metric, the Mean Log Score, assigns a value from 0 (completely inaccurate) to 100 (perfect model) and works on a logarithmic scale instead of a linear one like GeoGuessr score.

Therefore, a 10-fold improvement in distance means the same whether it is from 5km to 500m or from 500km to 50km. You can compare both metrics here.

Below are the model results sorted by either metric, showing how many photos each model geolocated within distance bands from the true location.

chart visualization

Several models placed at least 10 out of the 52 photos correctly within one kilometre, and the top-performing models placed over 50 percent of the photos within 25 kilometres.

NOTE: Geoseer-fast refused to answer on four of the 52 photos, claiming “This image is not suitable for geolocation”, while Mistral-large-4 refused once with “insufficient information in image to identify location”. Kimi-k2.6 also failed to provide a set of coordinates on one photo, and Gemini 4 Argon timed out repeatedly on one photo.

Our Test

To independently assess the models, we sought to give them very specific instructions and some images that were harder to geolocate than those provided in our past tests. The prompt asked for a single set of coordinates reflecting the photographer’s location. The purpose of restricting the response in this way was to prevent hedging, such as multiple guesses or confidence scores, and make the grading more uniform across different models. Models were allowed to use their providers’ built-in tools: search, code execution and page fetch. We capped their token output at 64,000 and ran each model under its default effort setting with a maximum time of 15 minutes per photo. If a model did not return coordinates on the first prompt, we also allowed a single rescue prompt asking it to do so using its previous reasoning and without using any tools.

The images provided were unique, from around the world, not published on the internet before, scrubbed of metadata and provided without any context. They did not contain significant landmarks. We also included images in snowy, foggy and nighttime conditions.

The results show that the LLMs tested could, in many circumstances, successfully narrow down a location much faster than a human can geolocate even without any context. This is not limited to Google Street View-style images. Take, for example, this image of vegetation and a very small part of a rooftop.

An example of a photo used for testing, with annotation added after the test to highlight the only man-made object visible. Credit: Bellingcat
An example of a photo used for testing, with annotation added after the test to highlight the only man-made object visible. Credit: Bellingcat.

For an average person, identifying the country this photo was taken in without any context could take a significant amount of time. However, multiple models quickly narrowed it down to Kenya. You can navigate their answers in the map below. Notice how several correctly inferred enough vegetation details to narrow down the exact part of the world and the park itself, but also that for many, the final response was a best guess only. It is important to note that even though some models were correct, others were significantly off.

The full model results for all 52 photos can be seen in the table below.

table visualization

Of the models tested, Gemini 4 Argon scored highest on the Mean Log Score, and Claude’s models (Opus and Fable) were at the top of the table regardless of the score metric. Even Claude Sonnet, the weakest of its lineup, consistently came close to the real location while being the fastest to answer across all models.

This was followed by GPT-6 Astra and GPT-6.1 Sol, as well as other Claude and Gemini models, and then the geolocation-specific model, GeoSeer Fast, which provided good results but also timed out frequently. We tested GeoSeer’s more competent Agent model, but it refused to answer on eight photos and regularly timed out.

scatter visualization

For secondary factors like time and cost, Claude also performed well on both metrics, and Gemini Argon, while much slower, came at a similar cost to Opus. Based on API rates, Argon and Opus models offered the best balance of accuracy and cost, especially compared to Fable and Grok. Grok was the most expensive option and had an underwhelming bottom-half performance. Meanwhile, GeoClip was freely available but only outperformed Claude Haiku.

Model Limitations

While we sought to analyse the most prominent models, there was one that we could not include as it has yet to be fully released.

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

In its blog post, Anthropic claimed its Mythos model beats “even the strongest human baseline on median distance error, scoring 37.0km and 47.2km across 6,000 photos (placing 23.7 percent and 23.1 percent within 1km).“ But Nuno Moniz, Associate Research Professor at the Lucy Family Institute for Data & Society at the University of Notre Dame, told Bellingcat that “the median represents a strong simplification of what would be acceptable to compare AI-based system performance with regards to humans”. 

Anthropic compared its models to competitive GeoGuessr players and their success in identifying the location of Google Street View images. The entire set of test images was online and there was no guarantee the models were processing the imagery for the first time. 

While Anthropic has been relatively open about these methods, not all developers have been as forthcoming. It is therefore difficult to assess whether the same limitations will impact all other models, or whether there will be further caveats that we are simply not aware of. 

Moniz said that “the increasing availability of benchmarking tools and datasets has made it somewhat difficult to measure reliability in test results such as these”. Specifically, due to the ”uncertainty as to whether that data was included in its training”. This is why we ran our Bellingcat tests with non-published photos sourced from Bellingcat staff and the Bellingcat Volunteer Community.

A further limitation of any models comparing performance to GeoGuessr scores is that the game uses images from Google Street View, which is limited to a select number of countries, and within these, a limited number of roads. Street View is also intentionally skewed to feature clear, high-quality photos taken during daytime that are useful for navigation, and users can pan around their view. There are also some well-known tricks that allow GeoGuessr players to bypass actual geolocation to correctly guess the location of photos: for example, Google is known to use a black car in Jordan and a white car in the UAE to capture imagery. While this wouldn’t necessarily impact model performance, it is a drawback of measuring against GeoGuessr score as the game has a set of workarounds that can bypass geolocation skill.

Anthropic’s data consisted of thousands of social media photos of various scenes. Once again, though, these photos were online and may have featured in the model’s training data. Anthropic’s blog post contained four images as an example of this dataset.

Images used as part of Anthropic's dataset. Credit: Anthropic
Images used as part of Anthropic’s dataset. Credit: Anthropic.

The examples Anthropic shared are relatively easy to geolocate as they feature recognisable buildings and signs. But footage that requires geolocation often comes from regions where access is limited and which do not feature on social media or Street View.

Specifically, in a military and intelligence context, footage often involves low-resolution material and intentionally obscured details recorded in remote or inaccessible areas. Context that could be found online can be uncertain or plain wrong, especially during a developing situation, such as the image below that Bellingcat investigators geolocated when verifying a US strike on a school in the city of Minab in Iran that killed at least 120 children earlier this year.

An example of footage geolocated by Bellingcat. Screen shot from Mehr News.

Anthropic researchers acknowledge this caveat, writing that it “does not as clearly emulate the task of precisely pinning down a location in near-real-time in a less populated battlefield setting; that is a task for future research.”

As noted, it is also possible that similar caveats need to be applied to other models when seeking to understand and interpret any geolocation-related benchmarking.

Supercharged Models

In parallel to the above models, we also tested a set of agentic skills and tools being developed internally by Bellingcat. These are researcher-informed instructions and code that can equip generic-use AI agents to augment their geolocation ability. For instance, we designed skills such as allowing it to download Environmental Systems Research Institute (ESRI) satellite imagery tiles. We also designed another skill that enabled direct access to data from Bellingcat’s OpenStreetMaps search tool, which aims to assist geolocation efforts by comparing items identified in imagery (like the position of a pylon compared to a nearby railway line) to OpenStreetMaps data. GeoSleuth is a similar project that has already been published, which we used for comparison. We picked Claude’s Opus 5.5 model to run both skills customisations because of its strong performance and reasonable pricing, and ran all the photos through the supercharged models under the same conditions – except that they had no timeout enforced as the supercharged models required more time to utilise their extra skills. 

The different timeout conditions and expanded toolset do not allow for a direct comparison of the customised models to the previous off-the-shelf models’ results, even if they both outperformed the barebones Opus model they were based on, and all other models on GeoGuessr Score, with only Argon ranking above Bellingcat’s agent in Mean Log Score.

table visualization

Still, this test allows us to showcase why skill customisations can bring value. Take the following photo.

An image captured at the Global Campus of Human Rights in Venice. Credit: Bellingcat.

This is not an exceptionally hard geolocation, and 11 of 17 models answered within 5km of the actual capture location in Venice, Italy. GPT-Sol answered with the correct monastery, but only Bellingcat’s agent and GeoSleuth answered with the exact camera position coordinates. This was possible because both model customisations can download and compare satellite imagery tiles, which allows them to validate not just which monastery the photo was taken at but also the correct courtyard position and orientation from visual cues. You can click the buttons in the list below to audit each model’s final answer.

While in this instance the supercharged models got the exact spot, there were other instances where they did not, as the scores in the results table above show. This again emphasises that even with extra skill sets added, these models were still far from infallible.

Tests Conclusions

Despite increased model improvement, our analysis shows that important caveats remain and it is important to keep a human in the loop when seeking to guarantee a geolocation is precise and correct.

Our tests show that the coordinates models provide as answers are rarely an exact match, even with supercharged models. Invariably, even the best models, with the best tools, can confidently claim a wrong position. Anthropic says its Mythos model was able to pinpoint less than a quarter of its social media photos dataset within a 1km radius. But narrowing down a location is rarely the trickiest part of geolocation work. A large part of important geolocation work is in the final stretch: finding specific coordinates from a general area. This level of specificity is needed because a general area is not a match. Matching features and perspective are necessary for an accurate geolocation, and in many cases that is done on very low-quality images or even by cross-referencing multiple sources. 

This level of precision is essential for investigations where it matters what building a person could be found at, what lines of sight individuals might have had, or what impact an air strike might have led to. 

Since LLMs can still confidently get things wrong, as our analysis shows, all results still have to be verified independently. Strong human geolocation skills therefore remain crucial, as the more one understands about geolocations, the better one can verify or take to completion an automated result. 

Though it can speed up geolocation work significantly, especially in narrowing down an area of interest, AI models remain a useful assistant rather than a tool we can blindly delegate geolocations to.


Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. You can also subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post Can LLMs Supercharge Geolocations? Our Tests Show Big Improvements But Humans Remain Key appeared first on bellingcat.

  • ✇Krebs on Security
  • Feds Suspect Ransom Negotiator Ran ShinyHunters Extortion Site BrianKrebs
    U.S. federal investigators pursuing the ShinyHunters hacking group that recently stole highly sensitive data from the FBI have identified executives at two different ransomware negotiation firms who maintained unusually close relations with the extortion group, KrebsOnSecurity has learned. Sources close to the investigation say one of the executives in the United States even helped ShinyHunters operate a website that pressured victims into paying a ransom. Hackers aligned with ShinyHunters have
     

Feds Suspect Ransom Negotiator Ran ShinyHunters Extortion Site

19 de Outubro de 2026, 10:48

U.S. federal investigators pursuing the ShinyHunters hacking group that recently stole highly sensitive data from the FBI have identified executives at two different ransomware negotiation firms who maintained unusually close relations with the extortion group, KrebsOnSecurity has learned. Sources close to the investigation say one of the executives in the United States even helped ShinyHunters operate a website that pressured victims into paying a ransom.

Hackers aligned with ShinyHunters have claimed credit for dozens of major data breaches since 2019, when the group was primarily operated by cybercriminals living in France. As those original members were arrested or jailed one by one, control over the ShinyHunters brand eventually passed to “Rey,” identified last year as a teenage hacker from Amman, Jordan. According to the FBI, ShinyHunters has extorted more than $70 million from victims so far this year.

In late September, ShinyHunters hacked the FBI recruitment website fbijobs.gov and taunted the agency by threatening to publish the stolen files, which reportedly included medical and psychological records, Social Security numbers and home addresses, and information about each employee’s unit and specialization.

The FBI jobs site hack has prompted the agency to move with uncommon speed to arrest, detain and question people who had any communications with Rey or other ShinyHunters suspects. Reuters reported on October 3 that Rey had been detained by Jordanian authorities and was cooperating with the FBI.

Rey was able to assume control over the hacking group because one of its alleged senior members had just been nabbed. On the evening of September 15, 2026, authorities in the Netherlands arrested convicted hacker and ShinyHunters suspect Pepijn van der Stap. Dutch police reportedly used flash-bang grenades in an effort to stun and apprehend Van der Stap as he was logged in to various online accounts.

According to a source close to the investigation, the authorities arrested Van der Stap while he was using his Signal instant messaging account, which allowed them to read recent communications with frequent contacts — including ransomware negotiators based in Canada and the United States.

Multiple sources with knowledge of the matter told KrebsOnSecurity that Van der Stap’s messages were the key to unraveling a murder-for-hire plot that emerged immediately following his arrest. The Dutch news outlet RTL reported last month the two murders were allegedly to be committed abroad, and that there are indications Van der Stap gave the order for the attacks (there is no indication those orders were ever carried out).

On October 9, KrebsOnSecurity broke the news that the FBI arrested a 54-year-old Canadian cybersecurity executive named Edward Dubrovsky in connection with the ShinyHunters investigation. Dubrovsky, who recently published a book on handling cyber extortion that recommended always engaging with the hackers, was charged with threatening to disclose stolen or hacked data and interfering with interstate commerce through extortion, according to court records. Politico reports prosecutors have asked that Dubrovsky remain in custody pending trial, and that records related to his case remain under seal.

A source close to the investigation told KrebsOnSecurity that Van der Stap’s Signal messages showed he had frequent conversations with Mr. Dubrovsky and with at least one executive at a large U.S.-based consulting firm, and that these executives often knew the details about new ShinyHunters victims before the victims themselves had even learned of the breach.

That source shared another potentially explosive finding by federal investigators: the Dutch hacker’s chats showed Mr. Dubrovsky knew ShinyHunters had stolen highly sensitive data from FBI servers, and that he never informed the bureau of the data breach prior to ShinyHunters threatening on Twitter/X to publish the agency’s deeply personal personnel files.

SHINYWIRE

KrebsOnSecurity has learned from two sources close to the ShinyHunters probe that FBI investigators believe at least one executive at a U.S.-based consulting firm played an active role in operating the now-defunct ShinyHunters victim extortion website “shinywire[.]com.” KrebsOnSecurity is not naming the U.S. company or the suspected executive(s) for now because they have not yet been charged with a crime, but it is a prestigious firm with more than 1,000 associates.

A section of the homepage for the now-defunct Shinywire website.

The Shinywire website was active from approximately May 3, 2026 until October 16 and had entries for nearly 100 organizations hacked by ShinyHunters. Each entry urged victims to engage with the cybercrime group and discuss terms for payment. The pages also included samples of stolen files and promised those files would be published online if the victim ignored their demands.

The following screenshot shows a cached portion of the Shinywire extortion page for Intuitive Surgical, an American medical technology company that was breached in March 2026 after an employee fell for a targeted phishing email.

The Shinywire website, as indexed by urlscan.io. This listing is an extortion demand targeting Intuitive Surgical, an American medical technology company breached in March 2026 by a targeted phishing email attack that compromised an employee’s credentials.

Victim organizations that refused to engage or negotiate after seven days would find all of their stolen data published on the ShinyHunters victim shaming blog (a website that is only reachable via the anonymity network Tor).

In a story published October 15, The Globe and Mail wrote that a few months before Edward Dubrovsky was arrested by the FBI, the Canadian cybersecurity executive alleged in court documents he had become collateral damage in a campaign by a former business partner to “destroy” another industry executive.

The story noted that Dubrovsky filed the lawsuit in Ontario this year against Daniel Tobok, the former chief executive of Cypfer, a cybersecurity firm founded in Toronto with offices in the U.S., Britain, the Netherlands, the United Arab Emirates, and Mexico. The two first became business partners in 2017 and later developed Cypfer together, where Mr. Dubrovsky was a managing partner and chief operating officer. But by 2024, the two had a falling out, with Dubrovsky accusing Tobok of gradually excluding him from business matters and making “increasingly capricious business decisions.”

Mr. Tobok reportedly denied the allegations and filed a counterclaim, accusing Mr. Dubrovsky of accessing confidential information and unfairly competing with Cypfer through work at another firm called CyberSteward Inc. He also said Mr. Dubrovsky has misrepresented himself as a Cypfer founder, and is not a shareholder.

GHOSTBYTE

According to interviews with several former colleagues of Mr. Tobok, as far back as eight years ago he was known to ransomware purveyors by a particular nickname: Ghostbyte. In 2018, a Canadian company was hacked by Dharma, a prolific Russian-language ransomware affiliate program that first surfaced in 2016. The victim company told the Dharma ransomware representative they were leaning toward paying because some of the files were unrecoverable after being locked, but that they needed a better guarantee that Dharma would keep their word if paid.

A redacted copy of the Dharma rep’s reply is pictured below, and it shows the ransomware group urged the victim to contact a person at a Canadian IT firm that helps companies seeking such guarantees. The Dharma rep recommended the victim contact the email address ghostbyte@tutanota.com.

“If you want guarantees, you can write to the IT company,” the Dharma reply advised. “Take the phone and negotiate with them. This IT company often solves such problems in Canada. You can contact them and sign a contract for services. You’ll still have to pay.”

An email reply from the Dharma ransomware group to a victim, recommending they contact an email address that is connected to Mr. Tobok.

It’s unclear if Mr. Tobok ever used that Ghostbyte address offered by the Dharma representative, and he has not responded to multiple requests for comment. However, the breach tracking service Constella Intelligence finds an email address Mr. Tobok has frequently used — dtobok@me.com — is associated with two online accounts where the chosen username was “ghostbyte,” including a Snapchat account.

Incidentally, Constella also finds the Dharma rep’s email address luckygoodlucky@tuta.io is associated with a cryptocurrency address that processed 38 transactions totaling 43 bitcoin, ~$3.5 million at today’s value (the most recent transaction for this wallet is dated February 2019).

In a statement shared on October 14, Cypfer said the company’s business does not include making or facilitating ransomware payments, and that Cypfer learned of Dubrovsky’s arrest when the news first broke in the media.

“We had no prior knowledge of Mr. Dubrovsky’s arrest,” a spokesperson told KrebsOnSecurity via email. “CYPFER has not received any outreach from law enforcement and, to our knowledge, is not the target of any law enforcement investigation.”

The statement said CyberSteward and Cypfer are separate companies with no officers, directors or employees in common, and that Cypfer was recently acquired by the Apros Group, a holding company led by Jason Hogg, a former FBI special agent and former CEO of Aon Cyber Solutions Group. In 2020, Aon aquired Cytelligence, a security consulting firm where Mr. Tobok was founder and CEO and Mr. Dubrovsky was a managing partner.

CONFLICTING INTERESTS

It is not illegal in the United States to pay hackers for an encryption key to unlock servers infected with ransomware, or in exchange for a promise to delete any stolen files and refrain from publishing them online. While victim organizations can land in legal hot water if they pay a ransom to a group that is sanctioned by the U.S. Treasury Department, ShinyHunters is not among the ransomware groups currently sanctioned by the United States or by any other government.

Experts say ransomware victims are typically directed by their insurance provider to work with a specific incident response firm. But not all cyber forensics and breach response companies will agree to facilitate payments to ransomware groups on behalf of their clients. As a result, once victim firms make a decision to pay, the firms that eschew rewarding extortionists are invariably replaced by companies that have no such qualms.

In December 2024, ShinyHunters began extorting the education technology company PowerSchool, stealing private information on 62 million students and 9.5 million teachers in the U.S. and Canada. PowerSchool reportedly paid a $2.85 million ransom demand, and a “frequently asked questions” statement (PDF) about the breach says PowerSchool was assisted in that effort by CyberSteward (ShinyHunters affiliate Matthew Lane, 20, of Sterling, Mass., was sentenced in November 2025 to four years in prison for his admitted role in the PowerSchool hack).

But that ransom payment did not stop future extortion attempts using the same stolen data. By May 2025, individual school districts were reportedly receiving separate ransom demands using samples of the same stolen files. While nobody has alleged ShinyHunters was behind the second wave of extortion against PowerSchool users, the incident drives home the reality that there is no technical way to verify the threat actors have deleted the stolen data as promised, or prevent others from abusing the same records.

A number of leading cyber incident response firms, including Coveware and Unit 221B, have consistently warned victims against paying ShinyHunters, noting that “destroyed” victim data is often kept, resold or used to extort the company’s clients directly later on.

The FBI echoed those sentiments in an alert on May 15, 2026 advising victims against paying the ShinyHunters group. Meanwhile, ShinyHunters members have been harassing and threatening employees at Coveware and Unit 221B for offering this advice.

The following screenshots show just how annoyed the ShinyHunters leaders were with Coveware’s relentless advice against paying the group. The first image is from a conversation on May 6, 2026 that took place between the group’s main account on Telegram and a co-conspirator. In that chat, snippets of which were shared with KrebsOnSecurity by a trusted source, ShinyHunters complained that Coveware was harming the group’s payment rates.

The operator of the ShinyHunters account on Telegram speaking privately with a co-conspirator and complaining that Coveware was harming their business.

“Go on my website, majority of those companies would’ve paid if it weren’t for Coveware giving them misinformation,” the ShinyHunters account wrote. “Someone (100% Coveware) is going out of their way to make sure i am not paid.”

In another conversation on April 11, 2026, the ShinyHunters Telegram account privately said they were trying to “eliminate the problem,” which they said was Coveware. Coveware declined to comment for this story.

“I’m sure the company will pay but in this specific case I am aware of Coveware and their involvement, and I’ve probably been not getting a lot of other negotiations mostly likely because Coveware keeps getting involved,” the ShinyHunters account bemoaned.

The operator of the ShinyHunters account on Telegram telling a co-conspirator they were “trying to eliminate the problem,” which they identified as Coveware advising victims not to pay the cybercrime group.

Mark Camillo is chief executive at CyberAcuView, an industry group founded in 2021 by a consortium of major cyber insurance carriers. Camillo said some firms charge a flat retainer fee, while those that specialize in helping companies negotiate a ransom payment tend to charge a percentage of the total ransom amount paid.

That latter dynamic, he said, is an inherent conflict of interest, as the negotiator has every incentive to make things more favorable to the attackers because they are financially incentivized to negotiate and pay out, and to not negotiate down on the price.

“It has come to the industry’s attention that many of the ransomware payers charge a percentage of the final payment which may or may not be disclosed,” Camillo said. “This makes no sense if they are also the negotiator. There needs to be more transparency in the process as no one in the ecosystem should be profiting purely on the size of the ransomware transaction or payment itself.”

Camillo said the firms doing this work would argue the fees are needed because there may be unexpectedly higher costs to secure larger quantities of cryptocurrency or more risk carrying the float, and because cryptocurrency exchanges may charge higher fees for high risk transactions.

“There definitely needs to be more visibility around the transaction and clarity on the increasing value of the crypto and the exchange’s percent in the transaction to get to the bottom of this and remove any misaligned incentives,” he said.

In recent months, the U.S. Department of Justice has prosecuted a number of ransomware negotiators alleged to have conspired with cybercriminals to defraud their clients. On October 7, the DOJ charged MonsterCloud CEO Zohar Pinhasi, 50, with wire fraud and conspiracy for allegedly charging customers to remediate ransomware attacks without paying the hackers. According to charging documents, Pinhasi secretly went behind his customers’ backs and paid hackers anyway, paying ransomware groups $8 million while charging customers $19 million.

In July, Angelo Martino, a 41-year-old ransomware negotiator from Land O’Lakes, Florida, was sentenced to 70 months in prison for conspiring with the Blackcat/ALPHV ransomware group to extort multiple victims. According to the DOJ, Martino was paid by BlackCat attackers to provide confidential information about the negotiating position and strategy of his employer’s clients and enable the ransomware actors to maximize the ransoms paid by the victims.

  • ✇bellingcat
  • Satellite Images Show Extent of Trump Admin’s Tree-Felling at DC Golf Course Jake Godin
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. Satellite imagery obtained by Bellingcat lays bare the extent of tree-felling at a Washington DC golf course that President Trump is reportedly seeking to transform into a championship-level course, despite moves to protect the site by conservationists.  The extent and circumstances surrounding the rapid changes prompted a judge to call an emergency hearing earlier this week, banning any fur
     

Satellite Images Show Extent of Trump Admin’s Tree-Felling at DC Golf Course

15 de Outubro de 2026, 07:33

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published.

Satellite imagery obtained by Bellingcat lays bare the extent of tree-felling at a Washington DC golf course that President Trump is reportedly seeking to transform into a championship-level course, despite moves to protect the site by conservationists. 

The extent and circumstances surrounding the rapid changes prompted a judge to call an emergency hearing earlier this week, banning any further changes until later this month.

Reports on Wednesday detailed that 430 trees were cut down at the East Potomac Golf Links  which is owned by the Federal government and managed by the National Park Service (NPS). This is despite a judge reportedly stating earlier this year that no more than 10 trees at the course could be cut down without first notifying the court. 

NPR reported that officials notified the court just before 5pm on Friday Oct. 9 of plans to remove up to 529 trees in phases starting  the next day on Saturday Oct. 10. That prompted US District Court Judge Ana Reyes to call an emergency hearing on Oct. 12. By that time, 430 trees had reportedly been cut down.

Reyes is reported to have said she was “furious” with the Justice Department for the lack of time afforded the court and plaintiffs before the tree removal was carried out.

She was also reported to have said that the government’s actions had cost it “all of its credibility with me in this case.”

Images taken at the course earlier this week show trunks and branches strewn out across fairways.

Just total devastation wrought on East Potomac Park this past weekend. 450+ trees. For an admin that professes to support “beautification” of DC, that’s sure not what we or anybody else who has seen what they did on Saturday would describe it as. East Potomac is (was?) a thriving, beautiful gem in the heart of Washington DC. A beautiful, public, accessible place that hundreds of thousands use annually. The assault on this place should infuriate every American. The impacts of what took place Saturday will take years to fix. We cannot sit idly by while this admin treats our public lands like they are for the taking.

— Save East Po (@SaveEastPo) October 13, 2026

But satellite imagery affords a broader look at exactly what sections of the course have been impacted.

The Image below from Planet Labs taken on October 8 shows the golf course before the tree-felling was carried out.

A satellite image from Planet Labs PBC shows the East Potomac Golf Links on October 8.
A satellite image from Planet Labs PBC shows the East Potomac Golf Links on October 8. Larger version visible here.

Planet Labs imagery captured on Oct. 13 shows large barren sections towards the south and west of the course, where trees were removed.

A satellite image from Planet Labs PBC shows the East Potomac Golf Links on October 13. Larger version visible here
A satellite image from Planet Labs PBC shows the East Potomac Golf Links on October 13. Larger version visible here.

A before and after slider allows for easy comparison of the Oct. 8 and Oct.13 images.

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

Reuters reported that an organisation called The ​DC Preservation League said in a February ​2026 lawsuit that President Donald Trump was flouting ⁠environmental laws to “irreparably destroy a priceless piece of American history” so he could “convert a public recreation space into an exclusive, high-end golf course.”

Kevin Griess, an NPS official, reportedly stated at Monday’s hearing that tree removal was part of a 2019 plan that predates Trump’s aims for the course and sought to restore links-style vegetation.

Bellingcat contacted NPS as well as the Department of Justice but did not receive a response before publication.

A golfer hits toward felled trees at the East Potomac Golf Links. REUTERS/Kevin Lamarque
A golfer hits toward felled trees at the East Potomac Golf Links. REUTERS/Kevin Lamarque

The changes to the golf course are not the only ones President Trump has sought in Washington DC and the surrounding area since the beginning of his second term. In October last year  demolition began on the East Wing of the White House with the aim of building a new ballroom claiming it was “the safest building ever built.”  

Trump has also proposed a 250-foot-tall “Triumphal Arch” to be built just outside the Arlington National Cemetery in Virginia.


Correction: This article was updated on October 15 to reflect that a judge told the US government not to cut down more than 10 trees before providing notice. The article previously said that no more than 10 trees could be felled without the court’s approval. The headline has also been changed to reflect this.

Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. You can also subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post Satellite Images Show Extent of Trump Admin’s Tree-Felling at DC Golf Course appeared first on bellingcat.

  • ✇bellingcat
  • Groups Associated With India’s Far-Right Receive Millions Through US Donor-Advised Funds Pooja Chaudhuri
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. US non-profits whose founding leaders or organisations have been associated with India’s controversial Rashtriya Swayamsevak Sangh (RSS) – or initiatives the Hindu nationalist group has endorsed – received millions of dollars over recent years through grants that allow donors to remain anonymous, a Bellingcat analysis has found.  The right-wing and Hindu nationalist volunteer organisation is
     

Groups Associated With India’s Far-Right Receive Millions Through US Donor-Advised Funds

6 de Outubro de 2026, 13:00

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published.

US non-profits whose founding leaders or organisations have been associated with India’s controversial Rashtriya Swayamsevak Sangh (RSS) – or initiatives the Hindu nationalist group has endorsed – received millions of dollars over recent years through grants that allow donors to remain anonymous, a Bellingcat analysis has found. 

The right-wing and Hindu nationalist volunteer organisation is widely considered the ideological parent of India’s ruling Bharatiya Janata Party (BJP). Prime Minister Narendra Modi, a former RSS worker, is one of its most famous supporters. 

But the RSS’ vision of India as a Hindu nation is controversial both at home and abroad: it was previously banned in India three times, and a bipartisan US commission on religious freedom recently called for the US government to sanction the group “for their responsibility and tolerance of severe violations of religious freedom”. Almost a hundred US politicians, both Democratic and Republican, recently pledged not to take funding from RSS-linked entities. 

India’s far-right Hindu nationalist volunteer group RSS remains an unregistered entity in India. Source: Prabhat Kumar Verma/ZUMA Press Wire via Reuters Connect

While there have been efforts to map out RSS-related entities, it can be challenging to establish concrete links between the organisations because the RSS remains an unregistered entity in India and has no formal membership, maintaining that it is a “body of individuals” with shared ideological views.

“The RSS functions without independent oversight of its resources, finances, influence, and activities,” said Dr Angana P Chatterji, an anthropologist and scholar of South Asia studies at the University of California, Berkeley. “This is not proportional to the vast power, reach, and influence it possesses. Its leadership and cadre are protected from accountability and scrutiny,” she added.

The RSS had not responded to multiple requests for comment, as of publication.

Bellingcat identified two US-registered non-profits that appeared to have past and present ties to the RSS, or organisations associated with them, based on the non-profits’ or RSS websites and publications, as well as statements by the non-profits’ founders and spokespeople. The non-profits were the Ekal Vidyalaya Foundation of USA Inc (Ekal USA) and Sewa International Inc (referred to as Sewa USA). 

We looked at all Form 990s – tax returns that US non-profit organisations are required to file annually – listing Ekal USA and Sewa USA as grant recipients, as well as the Form 990s of the charities themselves, from 2020 to 2024. As of September, the latest available Form 990s for Sewa USA were for the fiscal year ending December 2024, while Ekal USA also filed a form for the fiscal year ending December 2025, which was excluded for consistency, although the same pattern was observed during this period.

This analysis showed that the charities received at least US$24.4 million from 34 organisations that manage donor-advised funds (DAFs), or 20.7 percent of the total $117.8 million they reported receiving in contributions and grants during this period. 

The actual amount from DAFs may be higher, as we excluded filings that covered partial years outside of this period – for example, June 2019 to June 2020. The filings from the two charities we identified were based on the calendar year.

The Form 990s also revealed that the vast majority of the money spent by these two charities on grants to other organisations or individuals was in India or the South Asia region rather than in the US: Ekal USA donated $33.7 million in foreign grants – all of which went towards the Ekal Vidyalaya project in India according to its filings –  and none in domestic grants in the five years from 2020 to 2024, while Sewa USA reported $29.7 million in foreign grants and $622,613 in domestic grants during the same period. 

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

DAFs are common vehicles for people in the US who want to make donations to charitable causes while receiving tax relief or benefits. Donors do not control the money once they donate it but can advise on which charities the money eventually ends up at.

These funds are legally maintained and operated by non-profit sponsoring organisations. As public charities, DAF sponsor organisations detail the amounts and recipients of grants that they make on behalf of their donors in their Form 990s. However, they do not disclose who their donors are, and donors can choose to remain anonymous. (Note: Bellingcat US receives grants from donor-advised funds, and some grants do not disclose the name of the grant recommender. However, many of our donors are listed on our funding page.)

There are many more organisations reportedly linked to the RSS that are not included in this analysis, but we focused on these two because of the large amounts of grants they received through DAFs, with the original sources of the funds untraceable from public information, as well as evidence pointing to both historical and current connections between them and the RSS or organisations associated with the RSS. 

It is worth noting that some of the evidence we found pointing towards a connection between the two charities and the RSS or organisations associated with the RSS was from before or after the period we analysed (2020 to 2024). There is no evidence that DAF grants funded the RSS, only that they went to organisations that appear to have connections with the RSS. Experts told us that DAF grants undergo less regulatory scrutiny than other charitable donations and that DAF sponsor organisations have discretion over whether to restrict or permit donations to certain recipients.

Ekal USA

Houston, Texas-based Ekal USA is a non-profit that describes its mission in its Form 990s as “to provide free elementary education and free primary healthcare to children in remote and rural India”. The USA branch of Ekal Vidyalaya, according to its website, was founded in 2001 as part of efforts to “reach out to the global diaspora” in order to achieve a goal to “expand the reach of Ekal to 100,000 villages”. 

RSS chief Mohan Bhagwat has appeared at multiple Ekal Vidyalaya events in India promoting the initiative over the years, as far back as 2014 and most recently in May.

Ekal USA was started in the late 1990s as a programme of the Vishwa Hindu Parishad of America (VHPA), or World Hindu Council of America, VHPA spokesperson Shyam Tiwari told Bellingcat in an email response. He added that it was set up as an independent organisation in early 2000. According to VHPA’s website, Ekal USA was established in 2002.

VHPA is based in the US, while the similarly named Vishwa Hindu Parishad (VHP) is in India. VHP lists VHPA in the “Hindus Abroad” section of its website. A 2017 article from VHPA’s magazine states that late founder Mahesh Mehta was trained by the RSS since he was 10 years old, and that the VHPA was born after Mehta was advised by the former supreme leader of the RSS to “build VHP in USA”. The national spokesperson of VHP in India, Vinod Bansal, said that it is an independent organisation that “sometimes shares ideology” with VHPA.

RSS’ website states that VHP was launched to organise Hindus and “reconvert all those who had been knowingly or unknowingly proselytised to alien faiths and are now desirous of coming back to the Hindu fold”. Bansal referred to VHP as an “independent part of the RSS” and an “ideological brother”. 

Ekal USA Chairman-Advisor Subhash Gupta wrote in an Ekal USA publication this year that in the ‘80s and ‘90s, he was “president of the VHP’s Houston chapter” and “often received many high-profile guests from RSS and VHP”.

Related articles by Bellingcat

How Four Hindu Nationalist Websites Make Their Money
Asia-Pacific

How Four Hindu Nationalist Websites Make Their Money

Gupta did not respond to requests for comment as of publication.

From 2020 to 2024, Ekal USA reported receiving more than $48.4 million in contributions and grants. Of this amount, at least $8.3 million was from DAF grants, based on Form 990s that listed Ekal USA as a grantee. 

According to Ekal USA’s Form 990s, it transferred $33.7 million in foreign grants during this period and none in domestic grants for organisations or individuals. The forms also show that all the foreign grant money went towards building Ekal Vidyalaya schools in India. The rest of its expenses involved operational costs such as staff salaries.

The Ekal Vidyalaya movement has been criticised over reports that allege the schools are used to indoctrinate millions of India’s poorest and most vulnerable children and women with Hindu nationalist ideology, including that Muslims and Christians are “enemies of Hinduism”. Ekal Vidyalaya in India did not respond to multiple requests for comment.

Ekal USA did not respond to Bellingcat’s questions about these reports or any relationship it may have with the RSS. However, on its FAQ page, the group states that it does not teach religion in its schools and “does not discriminate based on caste, religious affiliation or linguistic affiliations”.

Sewa USA

Sewa USA, also based in Houston, describes itself as a “Hindu faith-based, humanitarian, nonprofit service organisation” and is listed under the heading of “Sewa Movement” on the website of India-based charity group Sewa International. 

Sewa USA Vice President of Public Affairs Rakhi Israni told Bellingcat over email that the organisation “does not operate as a branch or affiliate of the RSS” or participate in political activity. 

But multiple pages on the organisation’s website appear to indicate a connection between the non-profit and the RSS. In 2022, Sewa USA reposted an article on its website referring to Sewa International as “the US-based charitable non-profit organisation with links to the Rashtriya Swayamsevak Sangh (RSS)”. In a press release about the death of its founding member Ved Prakash Nanda in 2023, it noted that Nanda was “immersed in the ideals of the [RSS]”. And two different blog posts from 2021 and 2024 on Sewa USA’s website had volunteers citing RSS-led activities as the starting point for their involvement.

In addition, Sewa USA has also posted multiple times on social media and its website about an India-based organisation – Sewa Bharati – that appears to have the support of the RSS. 

Sewa USA referred to Sewa Bharati as a “sister organisation” in an X post in 2014, and its regional chapter in Purvanchal as a “partner organisation” in a Facebook post in 2022.

On its website, Sewa USA said in a post that volunteers from its “partner organisations in India” had participated in rescue operations related to the 2023 train accident in Odisha, and listed several initiatives by Sewa Bharati in India. It also launched a fundraiser for families affected by floods in Maharashtra in September 2025, which mentioned that over 100 Sewa Bharati volunteers were already on the ground clearing debris and assisting communities. 

“Foreign grants awarded to partner agencies in India, including service-oriented NGOs like Sewa Bharati, are strictly project-based, fully audited, and evaluated based on measurable humanitarian impact in education, health, and disaster relief,” Israni told Bellingcat.

Subscribe to the Bellingcat newsletter

Subscribe to our newsletter for first access to our published content and events that our staff and contributors are involved with, including interviews and training workshops.

RSS’ website links directly to the main website of Sewa Bharati through a tab on its main navigation bar labelled “Sewa”, and Sewa Bharati is mentioned in RSS’ 2016 annual report under the heading “Seva Vibhag”, a Sanskrit term for “service department”. 

A blog post on Sewa Bharati’s website from December 2024 about an event the group organised described Indian Defence Minister Rajnath Singh as highlighting the role of RSS leaders in establishing Sewa Bharati in his address and noting that “RSS not only builds the nation but also the individual”. RSS Joint General Secretary Alok Kumar also spoke at the Sewa Bharati event, according to the blog post. 

In response to Bellingcat’s queries, Israni said that Sewa USA had “no role in Sewa Bharati’s governance or operations”. Sewa Bharati did not respond to multiple requests for comment as of publication.

Sewa USA received almost $69.4 million in contributions and grants from 2020 to 2024, according to its Form 990s. Filings listing Sewa USA as a grantee show that at least $16 million was from DAFs. In its expenditure statements, Sewa USA reported giving $29.7 million in foreign grants to groups in South Asia during this period and about $622,613 in grants domestically in the US, while the rest of its expenditures included operational costs such as salaries and travel. It is not specified in the forms how much funding, if any, Sewa USA transferred to Sewa Bharati during this period, and Sewa USA did not respond to questions about any amounts transferred as of publication. 

‘Cause-Neutral’ Funding

Experts told Bellingcat that there was less regulatory scrutiny of grants distributed through DAFs than some other types of charitable donations.

“For donor-advised funds, there’s actually less oversight by the IRS because donor-advised funds are considered public charities rather than private foundations,” said Samuel Brunson, a law professor at Loyola University Chicago.

“If a private foundation were giving money to organisations in India, that would be a little bit trickier. But if it’s a public charity … there’s not really a ton of oversight on that as long as the money that the US tax-exempt organisation is giving meets its charitable purpose.”

Lloyd Hitoshi Mayer, a law professor at the University of Notre Dame who specialises in non-profits, said that it was “irrebuttably presumed” that funds granted to US organisations with the non-profit 501(c)(3) tax status were being used for good charitable purposes.

“The existing federal tax laws related to donor-advised funds put some restrictions on what grants donor-advised fund sponsor organisations, which actually control the funds, can make. But the big exception is that Congress said, ‘If you make a grant to a Section 501(c)(3) charity, you’re good.’ You don’t have to do any additional oversight.”

Some DAF sponsor organisations have previously faced criticism for granting money to charities that have promoted homophobic, anti-immigrant or anti-Muslim views. Several were also criticised for pausing funding to the Southern Poverty Law Centre earlier this year, although this case demonstrated how DAFs can prevent funds from going to some groups recommended by donors, if the DAFs deem them unsuitable. 

Brunson told Bellingcat that DAF sponsor organisations “absolutely have the right and ability” to halt processing donations to certain organisations. “Whether we want them to do that or not is a separate question,” he said. 

Bellingcat reached out to the top three DAF sponsor organisations that donated to the two non-profits we identified – Fidelity Investments Charitable Gift Fund (Fidelity Charitable), American Online Giving Foundation (AOGF) and Charles Schwab-affiliated Donor Advised Charitable Giving (DAFgiving360; known before 2024 as Schwab Charitable) – to ask about their internal policies on assessing charities receiving donations through their funds. In total, the three DAFs accounted for almost $20 million, or 81.7 percent, of the DAF grants to Ekal USA and Sewa International for full reporting years between 2020 and 2024. 

Fidelity Charitable, AOGF and DAFgiving360 all told Bellingcat that they had due diligence processes in place when evaluating non-profits. 

Fidelity Charitable’s website states that it is a “cause-neutral” charity, but it ensures grants are used solely for charitable purposes. AOGF said non-profits must comply with eligibility requirements regarding hate speech, discrimination and the use of funds. 

Meanwhile, DAFgiving360 said that a grant recipient may be subject to monitoring or placed under temporary suspension “due to circumstances that DAFgiving360 deems appropriate”, such as if there is a public interest consideration or if the recipient does not further DAFgiving360’s mission.


Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. You can also subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post Groups Associated With India’s Far-Right Receive Millions Through US Donor-Advised Funds appeared first on bellingcat.

  • ✇Schneier on Security
  • How American Political Campaigns Are Using AI—and What They’re Spending on the Tools Bruce Schneier
    This essay was written with Nathan E. Sanders, and originally appeared in The Guardian. New campaign finance disclosure data shines a light on which US political campaigns are using AI tools and how much they are spending on them. Candidates’, parties’ and committees’ spending reveals that AI is fast becoming an essential tool of politics. The candidates themselves are quiet about how they are using the technology in their own campaigns. It’s a sensitive issue that we have been tracking closely
     

How American Political Campaigns Are Using AI—and What They’re Spending on the Tools

2 de Outubro de 2026, 08:02

This essay was written with Nathan E. Sanders, and originally appeared in The Guardian.

New campaign finance disclosure data shines a light on which US political campaigns are using AI tools and how much they are spending on them.

Candidates’, parties’ and committees’ spending reveals that AI is fast becoming an essential tool of politics. The candidates themselves are quiet about how they are using the technology in their own campaigns. It’s a sensitive issue that we have been tracking closely since we started writing our book, Rewiring Democracy, which examined how AI is beginning to influence politics. A September 2025 Pew survey of Americans found that more than 70% would think less of a candidate if they used AI to help write a speech.

Itemized expenditure disclosure data from the US Federal Election Commission, dating back to 2020, reveals at least $17m in disclosed spending on AI technology vendors across 523 federal candidates and campaigns. Data from four states, California, Colorado, Massachusetts and Washington, provides a more localized picture going back to 2022.

Beginning with the AI behemoths, at least 80 federal campaigns and committees have reported spending with OpenAI since 2024. The total spending is not huge: only about $50,000 reported, skewing slightly more Republican than Democratic. The Republican National Committee is the largest overall buyer, with nearly $10,000 in reported expenses. Top individual users include the campaigns of Republicans Mike Lawler, John Kennedy and Bill Cassidy, as well as the California Democrats Ro Khanna and Ted Lieu. Most of these expenses are listed as office expenses, subscriptions to ChatGPT for staff, or research tools, rather than as specific political services. The company’s policies prohibit some political uses of their ChatGPT tool.

OpenAI’s biggest competitor, Anthropic, has rapidly built a similar level of usage, but with a different split. At least 65 candidates or committees now report paying the Claude maker in 2026, up from essentially zero in previous years, with a nearly two-to-one Democrat-to-Republican ratio. However, the largest individual user is the campaign of Tom Cotton, a Republican senator from Arkansas, who reported more than $4,000 in spend on Anthropic software in his June filing. Other major users are the Montana independent Senate candidate Seth Bodnar and Jason Knapp, who lost a Democratic House primary in Virginia, and the Democratic Alaska Senate candidate Mary Peltola.

Candidates use either Claude or ChatGPT, rarely both, according to the disclosures. Only about 12% of campaigns or committees using either tool reported expenditures to both vendors. The Democratic lean of Anthropic usage may reflect the company’s alleged liberal skew and clashes with the Trump administration.

In contrast, Elon Musk’s xAI caters to Republican interests and, accordingly, its meager usage comes almost entirely from the political right. Just seven federal and two state-level candidates or committees have reported paying xAI, a total of about $5,000, the majority of which was spent by the presidential campaign of RFK Jr in 2024, but also includes Republicans Dave McCormick and Thomas Massie.

More dollars go to the vendors specializing in political campaign applications of AI. For years, AmplifAI, which provides automated text messaging, essentially a new iteration on robocalling technology, was a dominant target of spending, soaking up $4.7m in campaign spending in the 2022 cycle alone. It was used heavily by Democratic candidates including Mark Kelly, Joe Biden, Bernie Sanders and Adam Schiff. Spending on AmplifAI, now owned by the troubled media conglomerate Triller, seems to have tapered off in the years since 2022.

The new rising Democratic solution for AI-powered text messaging is Daisychain, which has so far garnered about $300,000 in reported candidate spend in the 2026 cycle—up from only about $50,000 reported in 2024. More than half of this year’s spending comes from the Senate campaign of Democrat Abdul El-Sayed in Michigan.

The closest equivalent on the Republican side has been Campaign Nucleus, associated with former Trump campaign manager Brad Parscale. The AI-powered voter engagement tool has attracted six-figure spending from the Republican National Committee, multiple PACs aligned with Donald Trump, and five-figure investments from Mike Johnson, Kari Lake and other candidates. It is displacing the legacy Republican-serving texting vendor Prompt.io, which has retained about $375,000 in 2026 spending to date, down from more than $500,000 in the 2022 cycle. But it continues to be used: the A More Affordable California PAC sponsored by Uber has single-handedly spent more than $1m on Prompt.io in 2026. The Republican Massachusetts gubernatorial nominee Michael Minogue has been a recurring customer, as has the failed Republican California gubernatorial candidate Ché Ahn and Republican-aligned Super PAC Neighbors for a Better Colorado.

At the state level

At the state level, the AI spending is smaller but growing fast. Across the four states studied, we found a total of at least $92,000 in spending confidently attributable to modern generative AI vendors since 2022. The spending is spread across at least 108 candidates and committees. The growth has been explosive; there has already been about 10 times the amount of state-level AI spending reported in 2026 as there was in all of 2024.

Much of the state spending mirrors federal patterns. Daisychain again has the highest overall spend, and OpenAI and Claude dominate among the general-purpose AI vendors. DonorAtlas—the AI-powered prospect research tool—sticks out for its usage in these states, sitting behind only Daisychain and OpenAI and buoyed up by nearly $4,000 in spending by the California Democratic party.

Even though it has dominated so much media conversation, few candidates seem to be reporting spending on AI tools designed specifically to create synthetic audio and video, also known as “deepfakes”. We found just six federal candidates or committees reporting spending on the popular AI audio generator tool from ElevenLabs, with total spending of about $1,400 led by independent candidate for Colorado’s sixth congressional district Samir Witta. The AI image generator service Midjourney has five reported federal campaign or committee users reporting about $1,600, led by Sholdon Daniels, the Republican primary runner-up in the Texas 30th district. Combined, those two firms had less than $100 in reported spend across the four states.

However, recent data from the Wesleyan Media Project shows that at least 164 political ads in this cycle have included AI-generated media, supported by at least $80m in ad spending. What this illustrates is that candidate and committee disclosure reports are just the tip of the iceberg. They don’t cover spending on AI by political consultants, media firms and other vendors hired by the campaigns or by PACs, or independent committees raising and spending money aimed at boosting candidates’ campaigns. Those entities aren’t required to disclose detailed expenditure reports, and are very likely where the bulk of campaign AI usage is happening.

Since a large fraction of all spending in the campaign cycle will happen in the final weeks leading to November, much remains to be seen about the totality of how campaigns will leverage AI and what impact its use will have on voters’ decisions.

  • ✇Schneier on Security
  • Using Device Linking to Eavesdrop on WhatsApp and Signal Bruce Schneier
    Modern messaging apps allow users to link their phone accounts to their computer desktop. Eavesdroppers are taking advantage of this capability: Apps such as WhatsApp Web and Signal Desktop allow people to use their accounts on other devices, such as laptops or desktop computers. Germany’s Customs Office has been using these features to connect a police-controlled computer to a suspect’s account. Once connected, messages can be delivered to that computer without the police having to crack the en
     

Using Device Linking to Eavesdrop on WhatsApp and Signal

29 de Setembro de 2026, 08:02

Modern messaging apps allow users to link their phone accounts to their computer desktop. Eavesdroppers are taking advantage of this capability:

Apps such as WhatsApp Web and Signal Desktop allow people to use their accounts on other devices, such as laptops or desktop computers.

Germany’s Customs Office has been using these features to connect a police-controlled computer to a suspect’s account.

Once connected, messages can be delivered to that computer without the police having to crack the encryption protecting them.

Netzpoltik details that police are able to gain access in this way either through physical access to someone’s phone or by intercepting verification codes via a state-sanctioned phishing attack or intercepting SMS messages via telephone surveillance.

That last paragraph is important. Making this work requires user consent.

What we want is a feature that displays connected devices, so users could notice if a new device gets connected to their account.

  • ✇Krebs on Security
  • Dutch Police Arrest ‘Reformed’ Hacker in Shiny Hunters Investigation BrianKrebs
    Authorities in the Netherlands have arrested a 24-year-old convicted cybercriminal on suspicion of aiding in data thefts and extortions by the prolific hacker group ShinyHunters. In the days immediately following the suspect’s arrest, remaining ShinyHunters members dramatically escalated their attacks, stealing highly sensitive data from the FBI and extorting the Russian ransomware group Cl0p. According to three sources familiar with the matter, the Dutch man arrested by authorities this month i
     

Dutch Police Arrest ‘Reformed’ Hacker in Shiny Hunters Investigation

28 de Setembro de 2026, 12:08

Authorities in the Netherlands have arrested a 24-year-old convicted cybercriminal on suspicion of aiding in data thefts and extortions by the prolific hacker group ShinyHunters. In the days immediately following the suspect’s arrest, remaining ShinyHunters members dramatically escalated their attacks, stealing highly sensitive data from the FBI and extorting the Russian ransomware group Cl0p.

According to three sources familiar with the matter, the Dutch man arrested by authorities this month is Pepijn van der Stap, a convicted cybercriminal from Almere and Lelystad in the Netherlands. Van der Stap was previously convicted in 2023 in connection with a string of data thefts and extortions that prosecutors said earned between €1.5 million and €2.7 million.

At his trial in late 2023, van der Stap admitted that he lived a Dr. Jekyll and Mr. Hyde existence, secretly using the hacker handle “Umbreon” to extort victims and post their data on English language hacking communities like the now-defunct RaidForums and Breached. By day, however, van der Stap was working as a software engineer at the Amsterdam-based cybersecurity startup Hadrian, while volunteering at the Dutch Institute for Vulnerability Disclosure (DIVD), a nonprofit security research group.

Pepijn van der Stap’s alter ego “Umbreon” selling a database on RaidForums, offering information on 2.3 million people from The Netherlands in September 2021. This user’s avatar is a depiction of the Pokemon character Umbreon. Image: KELA.

Van der Stap confessed to his data theft and extortion activity, and was sentenced to four years in prison (one of which was suspended). During his trial, van der Stap opted to remain in custody for a time rather than at home, saying he could not find better treatment on the outside for his ongoing psychological issues, which he claimed included PTSD related to childhood trauma. He was released from prison in December 2025.

In an interview with KrebsOnSecurity on September 9, 2026, Van der Stap cast himself as a reformed hacker who was trying to turn his life around and make a positive contribution to society. Van der Stap is currently employed as offensive security lead at the Dutch company Neo Security, which did not respond to requests for comment.

Van der Stap said he was still dealing with civil lawsuits and restitution related to his previous cybercrime victims, and that he was trying his best to make amends. But not long after that interview, the Dutch hacker abruptly stopped replying to messages. Efforts by others close to him also repeatedly failed to elicit a response for the past two weeks.

The LinkedIn profile for Pepijn van der Stap.

According to two sources with knowledge of the matter, Van der Stap was arrested by Dutch authorities on or around September 16, and has been held in custody for questioning since. One source said a colleague of theirs personally witnessed Dutch authorities carting items out of Van der Stap’s residence.

Authorities in the Netherlands have been asking the public for help in identifying the voice in a recorded telephone call from February 2026 in which a native Dutch-speaking ShinyHunters member social engineered their way into Odido, the nation’s largest mobile telecommunications provider. In that intrusion, ShinyHunters tricked an Odido employee into logging in at a spoofed website, and then used that access to steal data on more than 6.2 million Dutch people.

Responding to Dutch news media, ShinyHunters confirmed that the suspect in the audio clip is indeed a member of the hacker collective.

“Our team member has our full support – emotionally, mentally, and financially,” the hackers said. “Everything has been arranged, including a criminal defense lawyer. We do not look down on our staff and members; we take excellent care of them,” reads a statement ShinyHunters shared with NL Times. It remains unclear if the Dutch police have matched the Odido caller to a confirmed real-life identity. The Dutch police unit handling the Odido incident did not respond to requests for comment.

The group also lashed out at the authorities in the Netherlands. “The Dutch police will need all the luck in the world – and everyone’s prayers – if they want to catch him before we carry out another large-scale data theft in the Netherlands,” the ShinyHunters statement said. “Frankly, the Dutch police are a big joke; they are incapable of doing anything. Incompetent. Irrelevant. Unimportant. Useless.”

FBI, CL0P HACKS

Just days after sources say Van der Stap was detained by Dutch authorities, ShinyHunters claimed credit for an unusually brazen breach at the FBI’s job application site apply.fbijobs.gov. According to reporting from 404 Media, the data stolen from the FBI site includes Social Security numbers and personal information on more than 5,000 officials.

404 Media and Reuters reported the FBI data included each person’s job title or team, such as special agent, threat intake examiner, major cybercrimes unit, and those investigating cyber threats from foreign state-backed actors. Reuters examined documents shared by ShinyHunters and found they included sensitive psychiatric and medical files of FBI staff. The FBI issued a brief statement confirming the hack.

ShinyHunters said it gained access to the FBI site and other victims by exploiting a recently patched vulnerability (CVE-2026-35273) in PeopleSoft, a software-as-a-service platform from the software giant Oracle that is broadly used by companies to manage hiring and human resources, benefits and payroll. Oracle quickly issued a fix for the Peoplesoft vulnerability that ShinyHunters reportedly began exploiting as a zero-day in June, and at the time Mandiant released web application firewall rules intended for organizations who couldn’t apply the security update quickly enough.

But on Friday, BleepingComputer reported that ShinyHunters used a URL-encoding trick to bypass Mandiant’s suggested web application firewall rules designed to mitigate the threat from the PeopleSoft flaw. In a report released Sept. 25, security experts at Mandiant and the Google Threat Intelligence Group (GTIG) confirmed that ShinyHunters had mass-exploited the PeopleSoft vulnerability to steal data from dozens of systems across a range of industries, including higher education, technology, healthcare, agriculture, transportation and government.

Van der Stap’s former hacker alias Umbreon was hidden in plain sight throughout the imagery ShinyHunters used to spread news about the FBI hack: The defacement image that ShinyHunters left behind on the hacked FBI jobs site included an ASCII art design featuring the Pokemon character Umbreon. The message at the top read, “This site has been seized by ShinyHunters. rooting your systems since ’19 ;)” The image appears identical to a defacement message ShinyHunters used in their 2020 hack of the English-language cybercrime community Hackforums.

The defacement message left by ShinyHunters on the FBI jobs site included an ASCII art rendition of the Pokemon character Umbreon. Image: Bleeping Computer.

Multiple sources close to the ShinyHunters investigation said the group’s recent risky attacks against the FBI and one of Russia’s most venerated ransomware groups amounted to a major pivot away from the more measured tenor of the hacking gang’s operations. Those sources said the sudden shift came about after ShinyHunters was taken over by a teenage cybercriminal from Amman, Jordan who goes by the nickname Rey and operates as part of a cybercrime group called ScatteredLapsussHunters (SLSH), which experts say is an amalgamation of three hacking groups — Scattered Spider, LAPSUS$ and ShinyHunters.

Those sources said Rey had an ongoing beef with the Dutch hacker over control of the ShinyHunters brand and data, and that the inclusion of the oversized Umbreon Pokemon image in the FBI jobs site defacement was likely an attempt by Rey to pin the hack on the Dutchman.

Rey was first publicly identified by the cybersecurity firm KELA in March 2025. In advance of our November 2025 profile of Rey, KrebsOnSecurity messaged Rey’s father and asked for permission to interview his teenage son. Rey’s dad merely forwarded the message to his son, who admitted to participating in ransomware attacks and said he was trying to extricate himself from the SLSH hacker group.

BLAMING UMBREON

Immediately after news of the FBI jobs site hack was picked up in the media, Rey’s main account on Twitter/X (Ryan Moran/@rmoskovy) was taunting the Cl0p ransomware group and the FBI, crudely depicting them as the twin towers in New York being struck by planes labeled “cl0p drama” and “fbi breach claim.” In the foreground of the city is the giant Pokemon figure of Umbreon.

A taunting meme uploaded to Twitter/X by Rey’s now-defunct account on Sept. 22. A giant float-sized version of the Pokemon character Umbreon can be seen in the bottom left.

On Sept. 24, KrebsOnSecurity again contacted Rey’s dad, asking to interview him and his son for a story on Rey’s apparent ascendency as the head of ShinyHunters. Just hours after that request, Rey deleted his longtime Twitter/X account. Meanwhile, Rey’s dad, who works for the Royal Jordanian Airlines, has failed to respond to a half-dozen emailed requests for comment about his son’s alleged activities.

Where does the bad blood between SLSH and ShinyHunters come from? According to a story in Wired this month, ShinyHunters and SLSH members briefly partnered earlier this year to help better monetize important stolen credentials collected by TeamPCP, an upstart group that was having great success compromising global code supply chains with malicious software but hadn’t been able to profit much from their stolen data (two alleged leaders of TeamPCP were arrested last month in Australia, and in an interview the TeamPCP leader claimed they made just $20,000).

The Wired story noted how Mandiant had infiltrated TeamPCP and was secretly responsible for having the crime group’s stolen credentials burned so quickly: Mandiant was secretly feeding those credentials to the major cloud providers like Amazon and Microsoft, who quickly invalidated the stolen keys. Meanwhile, the formerly cooperating hacker groups began to blame one another for causing the credentials to become worthless.

Wired’s Andy Greenberg reported that a few weeks after partnering with TeamPCP, “ShinyHunters went rogue, carrying out its own extortions with TeamPCP’s credentials but without giving the supply-chain hackers their cut.”

Mandiant researcher Austin Larsen told KrebsOnSecurity earlier this month that ShinyHunters has been enjoying a successful extortion spree so far this year, and is on track to pull in nearly $100 million in extortion payments from cybercrime victims in 2026.

Van der Stap claims he was never motivated by money and that his earlier hacker activity was driven by a desire to have the world’s most complete collection of stolen databases. Speaking with reporters from Bloomberg in 2024, Van der Stap said that singular focus in turn fueled his desire to carry out cyberattacks.

“The hacking was very easy for me, and it wasn’t a compulsion,” he told Bloomberg. “My habit was collecting. Collecting data, organizing data, downloading data, creating folders.”

DIVD, the nonprofit security research group where Van der Stap previously served as a volunteer, disclosed on LinkedIn last week that the organization was dealing with an internal cybersecurity incident that appears to have involved the malicious use of artificial intelligence. DIVD has released few details about that incident, but a spokesperson for the nonprofit told KrebsOnSecurity it does not appear related to ShinyHunters, nor are there any signs the matter involves the work of a previous volunteer.

Update: 3:44 p.m. ET: Corrected Van der Stap’s age, which is 24 (not 23).

Update, 4:54 p.m. ET: The Dutch police have confirmed the arrest of a 24-year-old in connection with the ShinyHunters investigation. In a statement on Twitter/X, the Dutch police said the man will appear on Tuesday, September 29 before the chambers of the Rotterdam District Court, and that it will provide more information tomorrow.

Sept. 29, 9:42 a.m. ET: The Dutch news outlet RTL reports that investigators suspect Van der Stap tried to orchestrate at least two murders. RTL reported the two murders were allegedly to be committed abroad, and that there are indications the suspect gave the order for this.

The FBI released a short video message on the ShinyHunters investigation from Brett Leatherman, assistant director of the FBI’s cyber division, who thanked Dutch law enforcement partners for their assistance and urged remaining ShinyHunters members to turn themselves in.

“Arrests have a way of changing who is willing to talk, and seized infrastructure has a way of showing us who’s left,” Leatherman said. “The longer you stay in this, the more we learn about you. You know how to find us, and we know how to find you. I suggest you reach out to us while the choice is still yours.”

  • ✇bellingcat
  • Christy Kinahan’s UAE Residence Visa Cancelled Financial Investigations Team
    This article is the result of a collaboration with The Sunday Times. You can find their corresponding piece here.  Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. Crime boss Christy Kinahan leads a $1.5 billion illicit network involved in drug trafficking, money laundering and arms smuggling from the cartel’s base in Dubai. Christy Kinahan, the Irish founder of the Kinahan Organised Crime Group who is being pursued by authorities aroun
     

Christy Kinahan’s UAE Residence Visa Cancelled

19 de Setembro de 2026, 14:46

This article is the result of a collaboration with The Sunday Times. You can find their corresponding piece here. 

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published.

Crime boss Christy Kinahan leads a $1.5 billion illicit network involved in drug trafficking, money laundering and arms smuggling from the cartel’s base in Dubai.

Christy Kinahan, the Irish founder of the Kinahan Organised Crime Group who is being pursued by authorities around the world from his bolthole in the United Arab Emirates (UAE), has had his residence permit cancelled.

The visa was cancelled at the end of last month, just weeks after the extradition of his eldest son Daniel Kinahan from Dubai to Ireland, where he is charged with directing the activities of a criminal organisation.

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

It follows a review of public immigration records by Bellingcat and The Sunday Times, published in August, which revealed that Christopher Kinahan Jr, another of Christy Kinahan’s sons, was recently issued with a new two-year visa in the UAE. 

This is despite his status as a sanctioned individual who, like his father and older brother, is the subject of a $5 million reward from the US government for information leading to his arrest or conviction.

Last month, we also exposed the residence statuses of the crime gang’s other sanctioned leaders who remain at large in Dubai. These include cartel lieutenant Bernard Clancy, whose residence permit was renewed in January, as well as padel poster boy Ian Dixon and Christy Kinahan, who have been living in Dubai on expired permits.

Wanted posters for Irish drug smugglers Daniel Kinahan, 49, Christy Kinahan, 69, and Christopher Kinahan Jr, 45. Senior cartel members Ian Dixon, 37, Sean McGovern, 40, Bernard Clancy, 49, and Johnny Morrissey, 66, were also sanctioned by the US government in 2022. Source: US Department of the Treasury

The cartel members had all obtained employment-based residence visas sponsored by Dubai companies, which are required to apply for employee work permits through the UAE’s Ministry of Human Resources and Emiratization.  

Christy Kinahan’s most recent visa listed his employer as OSA Management Consultancies DWC LLC, which is based at the same Dubai address as cartel-linked firm CV Aviation Consulting Services DWC LLC. The visa expired on April 1.

A new search of Emirate government portals shows the visa was subsequently cancelled on August 31. 

Screenshots of Christy Kinahan’s visa record taken by Bellingcat in July and September show the change in status. Source: UAE Federal Authority for Identity & Citizenship, Customs & Port Security

According to information published by a UAE government-authorized service provider, visa cancellation applies to people who leave the country permanently, alter their visa category or change employer. For employment-based residence visas, the cancellation process must be initiated by the employer or an authorised representative. 

Related articles by Bellingcat

Welcome to Dubai: Kinahan Cartel’s Visas Revealed
Europe

Welcome to Dubai: Kinahan Cartel’s Visas Revealed

In the case of Kinahan’s visa, the specific reason for the cancellation is unknown. However, his last date for leaving the country is listed as September 29 in the government immigration file. This is in line with the 30-day grace period granted to holders of standard employment visas after cancellation.

The Sunday Times reports today that Europe’s intelligence services believe Kinahan is still in Dubai, though rumours that he has moved to Africa have sparked speculation he has already fled the Gulf. It also reports that the cartel leader has moved assets out of the Emirates as the crime group breaks into rival factions.  

The Kinahan Organised Crime Group is a $1.5 billion transnational network involved in drug trafficking, money laundering and arms smuggling. Investigators have connected it to Iran’s intelligence services and the Lebanon-based militant group Hezbollah.

The UAE foreign ministry has been contacted for comment.


Connor Plunkett, Peter Barth, Beau Donelly and John Mooney contributed to this article.  

Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. You can also subscribe to our Patreon channel here. Subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post Christy Kinahan’s UAE Residence Visa Cancelled appeared first on bellingcat.

  • ✇bellingcat
  • “A Recurring Pattern”: Civilians Paying the Price in Mali’s Drone Campaign Sebastian Vandermeersch
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. WARNING: This report contains links to graphic images and footage. A joint Bellingcat and Jeune Afrique investigation into drone strikes carried out by the Malian military adds new details about the attacks which frequently are carried out in civilian areas and have reportedly resulted in the deaths of civilians. The incidents, concentrated in northern Mali, often involved strikes on vehi
     

“A Recurring Pattern”: Civilians Paying the Price in Mali’s Drone Campaign

16 de Setembro de 2026, 13:04

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published.

WARNING: This report contains links to graphic images and footage.

A joint Bellingcat and Jeune Afrique investigation into drone strikes carried out by the Malian military adds new details about the attacks which frequently are carried out in civilian areas and have reportedly resulted in the deaths of civilians.

The incidents, concentrated in northern Mali, often involved strikes on vehicles or commercial activity. Video footage of the strikes is often broadcast on national television, portrayed as precise attacks against terrorists and their logistics. 

Mali’s military government acquired its first Turkish-made Bayraktar TB2 drones in late 2022, and regularly releases footage of drone strikes to be broadcast on national television, describing them as “precise” and describing the targets as “terrorists and their logistics”. This month marks three years since Mali joined with Burkina Faso and Niger to form the Alliance of Sahel States (AES). Its military government has made restoring national sovereignty a central part of its political message. 

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

Bellingcat geolocated more than a dozen drone strikes including instances in which civilian deaths and injuries were reported. However, this represents just a fraction of the strikes that have taken place in northern Mali, with many more conducted for which footage was not available or further details were harder to verify as the locations are so remote.  

Yvan Guichaoua Senior Researcher at the Bonn International Centre for Conflict Studies told us the Malian military’s drone strikes appeared to fit a pattern of “guilt by location” in which people may be treated as targets based on where they are rather than who they are. 

He added that the broadcast of drone strikes on national television, often accompanied by dramatic commentary created a familiar narrative for veiwers that was akin to daily episodes “of a national saga”. Yet the impact of the drone war on civilians and non-combatants is seldom covered in such reports. 

Mali’s civil war involves separatist and Islamist groups and has been ongoing since 2012, predating the formation of the AES. But the conflict has spiked in 2026 as Tuareg separatists from the Azawad Liberation Front (FLA) and militants from the al-Qaeda affiliated Jama’at Nusrat al-Islam wal-Muslimin (JNIM) seized control of parts of the country’s north in coordinated attacks against the Malian military and Russia’s Africa Corps forces. The Malian military (known hereafter by their official name Forces Armées Maliennes or FAMa) has relied heavily upon drone strikes in response. 

We contacted FAMa and put our findings to them but did not received a substantive response at time of publication.

Destruction in Zouera

In one notable case – on July 8, 2025 FAMa presented a drone strike in the town of Zouera, in the Timbuktu region, as a successful operation against a supply vehicle for terrorists in footage broadcast on ORTM1 [4:50-5:31]. Drone footage shows a person walking near the vehicle before it is struck. The explosion immediately ignites a nearby structure, while several people can be seen running away.

One video of this incident, apparently showing the aftermath of the attack, shows the remains of a person inside the burnt-down structure, which was located directly beside the vehicle that was hit in the strike. We also checked satellite imagery and found a noticeable burn scar at the location in imagery taken on Jul 9, 2025, that wasn’t visible the previous day.

Bellingcat geolocated the strike site to the coordinates 16.84589, -3.85213, inside Zouera. Several videos filmed in the immediate aftermath show the destroyed vehicle, a large plume of black smoke, and the nearby structure engulfed in flames.

Geolocation diagram showing Google Earth satellite image (middle) with a screengrab of aftermath footage blurred by Bellingcat (left) and drone-footage shown on ORTM 1 (right). Sources: X/@AmakanessAg and Youtube.

Information gathered by Médecins Sans Frontières (MSF) and posted on X on July 11, 2025, stated four people were killed in the strike, including three young girls. MSF added that five others were treated for injuries at a local health centre, one of whom was in a serious condition. 

#Mali🇲🇱 : Après une frappe aérienne ayant touchée le marché de #Zouéra, cercle de Goundam, région de #Tombouctou, les équipes MSF ont pris en charge ce mardi 8 juillet 5 blessés, dont un cas grave, au centre de santé local. 4 personnes ont perdu la vie, dont 3 jeunes filles. pic.twitter.com/cTdLiY9bG8

— MSF Western & Central Africa (@MSF_WestAfrica) July 11, 2025

Amasrakad Strike

Zouera was not an isolated incident. Footage from March 2024 that was released by the Malian military and again aired on ORTM1 showed two drone strikes in the town of Amasrakad that resulted in the deaths of civilians, according to witness testimony and previous reporting by Amnesty International.

The footage shows an initial strike hitting a parked pick-up truck before cutting to a second strike on a nearby compound, where several people can be seen running moments before impact. The first strike was geolocated to the coordinates 17.08402, 0.76204, by France 24.

Bellingcat geolocated the second strike to a structure within a nearby compound at coordinates 17.081613, 0.768012, where the drone footage shows several people scrambling before impact. Separate graphic footage published by the Spokesperson for the Awazad Liberation Front on X the following day shows several dead people including what appeared to be women lying in and around the compound. France 24’s previous reporting on the strike included images of covered bodies inside a destroyed tent, as well as an image of a child that was reportedly injured during the strike. 

Our partner Jeune Afrique obtained testimony from the man who filmed the footage. He said the second strike killed 13 members of two families, including several children, who had gathered in a tent for shelter after the first strike. He added that he helped take eight injured people to health centres. The man told Jeune Afrique he has now joined the separatist FLA group following the attacks.  

This testimony would appear to support a 2024 Amnesty International report that stated at least 13 civilians, including seven children, were killed and more than a dozen people injured. Amnesty reported speaking to seven survivors and eyewitnesses who said the first strike hit a Toyota Hilux used by the local health centre. Believing the vehicle had been the target, residents fled to a nearby compound to seek shelter. The second strike then hit the compound directly, the Amnesty report stated.

Geolocation diagram showing Google earth satellite image (right), screengrab of aftermath footage (top-left) and screengrab of drone footage (bottom-left). Sources: X/@MohmedRAMADANE and Youtube.

The FAMa claimed in an official statement just after that attack that: “These operations neutralised several terrorists and destroyed a significant quantity of military equipment and vehicles.” While the pro-Azawad separatist group, the CSP gave a different account, condemning the strikes and claiming that they had killed 13 of which a majority were women and children. 

We contacted FAMa to ask whether it maintains that those killed were members of an armed group, whether steps were taken to establish if civilians were present before the strike was carried out and whether an investigation was done into the reports of civilian deaths. We did not receive a reply at time of publication.

Two Strikes in Tinzaouaten

Some of the cases we geolocated had been documented previously. However, we were able to add new details about several strikes. The town of Tinzaouaten near Mali’s northeastern border with Algeria endured two major drone strikes in 2024, reportedly in retaliation for a major assault carried out by separatist and Islamist forces against Malian troops and Russian mercenaries. On July 30, 2024, the Malian military stated it had launched an air campaign around Tinzaouaten in coordination with the military government of Burkina Faso and had targeted “high-value targets”, including caches, logistics hubs and vehicles.

Bellingcat geolocated this footage posted on the day of the strike to 19.92476, 2.98003. The footage shows several damaged structures and what appears to be one casualty.

Satellite images of the geolocated area in Tinzaouaten from 13 July 2024 (left) and 31 July 2024 (right), the day after the strike. Heavy destruction visible in the area highlighted by Bellingcat. Satellite imagery credit: ©2026 Vantor.

Pits and infrastructure which appear to be consistent with a mine are visible in satellite imagery of the area.

Diagram using Google Earth satellite imagery showing artisanal gold mining area in Tinzaouaten (19.93382, 2.98605), close to the Algerian border. 

Bellingcat geolocated an additional video posted by a pro-Azawad media outlet showing destruction posted on X to the coordinates 19.92502, 2.98013. In another video geolocated to this site, a man claims he is at the “Inkharaban mine, near the Azawad-Algeria border.”

Geolocation diagram using aftermath footage showing destruction in Tinzaouaten, matched to 31 July 2024 satellite image provided by ©2026 Vantor. Sources – Top-left: satellite image annotated by Bellingcat, Top-right: X/@tankaranet, bottom: X/@AhmadouAtafa.

The Second Strike

A month later, on August 25, 2024, FAMa stated it had identified pickup trucks carrying military equipment in Tinzaouaten. It said that, “After careful surveillance, a series of strikes destroyed these terrorist targets and neutralized around twenty armed individuals.” On news channel ORTM1 the newsreader described the operation as “precision strikes targeting terrorist objectives”. 

Bellingcat geolocated footage shared online by the pro-separatist Press and Media Agency of Azawad showing the destruction at the coordinates 19.95107, 2.97284, in Tinzaouaten.

Bellingcat also geolocated two images showing casualties at this location. One image shows a group of around eight bodies, while another graphic image shows the bodies of two young boys around 20-meters away (19.9512, 2.97307). We could not confirm whether or not those pictured were civilians though a resident told Reuters that civilians were killed in drone strikes on the town.

Geolocation diagram using Google earth satellite image, with screengrabs showing casualties. Victims blurred by Bellingcat. Sources: X/@InfosMinutesFR.

Bellingcat contacted FAMa to ask about the two strikes in Tinzaouten in the summer of 2024. We asked the military what steps were taken to establish whether civilians were present when the strikes were carried out and whether any investigation has been carried out into the two strikes, but we had not received a response at time of publication.

In addition to the above incidents Bellingcat geolocated additional drone strikes based on footage broadcast on ORTM and added them to the below map. We were not able to confirm who was killed in the additional strikes but evidence we analysed show strikes often taking place within populated villages or against vehicles and fuel supplies.

  • ✇Schneier on Security
  • 25 Years of Mass Surveillance Is Enough Bruce Schneier
    This essay was written with Cindy Cohn, and originally appeared in Lawfare. One of the many legacies of the terrorist attacks of Sept. 11 is the government-wide shift from targeted surveillance—such as individual wiretaps or pen register/trap and trace orders—to mass surveillance techniques—such as tapping into the internet backbone or mass collection of telephone or internet metadata. The legal and technical architecture of modern mass surveillance, initially framed as a necessary defense again
     

25 Years of Mass Surveillance Is Enough

15 de Setembro de 2026, 08:01

This essay was written with Cindy Cohn, and originally appeared in Lawfare.

One of the many legacies of the terrorist attacks of Sept. 11 is the government-wide shift from targeted surveillance—such as individual wiretaps or pen register/trap and trace orders—to mass surveillance techniques—such as tapping into the internet backbone or mass collection of telephone or internet metadata. The legal and technical architecture of modern mass surveillance, initially framed as a necessary defense against terrorist threats, has grown far beyond that justification and national security in general. Mass surveillance is now a routine tool used by law enforcement. ICE uses it in immigration actions and against people exercising their First Amendment rights to protest. It’s also increasingly part of private security systems, such as facial recognition at venues such as Madison Square Garden and networked Flock license plate capture systems on roads and in parking lots.

The interrelation between private and governmental mass surveillance is worth examining. Surveillance is the business model of the internet; companies like Google and Facebook constantly spy on their users’ behavior. From the National Security Agency relying on data collected by telecommunication and internet companies, to local sheriffs and ICE agents relying on cellphone location data and privately managed automatic license plate readers, governments primarily obtain the mass surveillance information through private companies. Increasingly, access doesn’t just come through legal processes, either. FBI Director Kash Patel recently confirmed in congressional testimony that the agency is purchasing information on Americans from data brokers and intends to continue to do so.

This pipeline from private collection to governmental collection means that as companies collect more information for surveillance capitalism purposes, more is available to law enforcement as well. And as the technology for mass surveillance and analysis improves, especially with the increased use of AI technologies, the problems attendant to mass surveillance grow as well.

After 9/11, the idea that the government could surveil the population to safety took hold. In 2001, the fear of terrorism reached a frequency and intensity never before seen. Along with that came the fear that the enemy could be anyone, anywhere. As a result, the government’s response was to watch everyone, everywhere. This line of reasoning underpinned the shift from targeted to mass surveillance. Or, in the words of an internal National Security Agency (NSA) presentation that was made public as part of Edward Snowden’s 2013 disclosures, a government that can “Collect it All,” “Process it All,” “Exploit it All,” “Partner it All,” and “Sniff it All,” will ultimately, “Know it All.” Similar rationales support the rise of domestic mass surveillance: if law enforcement could see and hear everything, it could more effectively interdict and solve serious crimes.

The national security community has never provided a full analysis of the costs and benefits of these mass surveillance programs, either in terms of taxpayer dollars or diversion of resources from other efforts—or any demonstration that those techniques stopped attacks that otherwise they would not have been able to prevent. While the NSA occasionally presents examples of the successes due to its mass surveillance programs, especially when those techniques are under public pressure, the examples also regularly fall apart upon serious scrutiny. And even if some utility exists, it must be seriously weighed against the costs.

Similarly, there has never been any comprehensive analysis about whether domestic immigration or law enforcement’s use of these techniques actually makes people safer, or whether other techniques could produce the same results. Instead, both the police and the companies selling these tools float anecdotes and dubious data. For example, Flock’s data equates the number of law enforcement hits in their database with actually solving crimes.

Twenty-five years after 9/11, it seems reasonable to step back and evaluate the costs of this shift to mass surveillance, especially in terms of Americans’ rights and freedoms.

The Shift

The easiest place to see a shift to mass surveillance was in the government’s decision immediately after 9/11 to collect Americans’ telephone records. The program started under an argument of pure executive power as the “President’s Surveillance Program.” But in 2006, that argument secretly shifted to a novel interpretation of Section 215 of the Patriot. Act which had only previously authorized more targeted access to record. While some media and public interest organizations struggled to force the government to reveal the program as early as late 2005, the government only officially confirmed it after the 2013 Snowden disclosures. In 2015, the Second Circuit Court of Appeals rejected the government’s interpretation of Section 215 as allowing mass collection of telephone records. Later the same year, Congress passed the USA Freedom Act. While this new law still allows collection of a tremendous amount of domestic telephone records, it ended the indiscriminate mass collection that had occurred for nearly fourteen years.

Other shifts to mass surveillance continue through today. The NSA launched its Upstream program, which involved intercepting both metadata and content from key telecommunications junctures inside the U.S., soon after 9/11. It was also initially conducted under a claim of purely presidential authority. This program was brought under marginal congressional and programmatic (not targeted) Foreign Intelligence Surveillance Act (FISA) court review via Section 702 of the 2008 FISA Amendments Act. In 2017, more than15 years after its inception, the NSA ended content searches due to FISA court pressure, but the mass collection continues.

Despite the stated goal of conducting mass spying only on people outside the U.S.—which itself is problematic given international law’s requirement that surveillance be both necessary and proportionate—mass surveillance collects a tremendous amount of U.S. persons’ communications. This can happen because people communicate with people abroad, or because of overcollection—when government agencies gather far more personal data on non-targeted US persons than authorized by law. The concerns about collecting Americans’ data on U.S. soil led Congress to allow the program to officially expire in 2026, although the previously-approved mass surveillance itself continues until at least Spring of 2027.

The shift to mass surveillance would be notable enough even if it remained only a strategy of the intelligence community. It has not. Americans are awash in mass surveillance. Networks of automated license plate readers such as those offered by Flock and Vigilant Solutions blanket both public and private roadways and parking lots. These networks often allow searches by law enforcement, including across jurisdictions. They are, for example, being used to track people seeking abortions across state lines. Facial recognition tools, once the province of only the more elite parts of federal law enforcement, are increasingly used by Immigration and Customs Enforcement agents on immigrants and protesters, in airports by the Transportation Security Administration, as well as by private entities. And, of course, modern phones track users’ locations constantly—and that information is readily available to law enforcement, often with only minimal process protections.

Constitutional Costs

Regardless of the murkiness of its actual usefulness, the shift from targeted to mass surveillance has profound implications for Americans’rights. It has created risks that have become increasingly evident, especially under the Trump administration.

At a basic level, the Fourth Amendment guarantees that citizens can be secure in their “persons, houses, papers and effects” from unreasonable searches. Warrants breaching that security should be supported by probable cause and particular descriptions of the place to be searched and items to be seized. Mass surveillance turns that promise on its head, allowing access to our “papers and effects” by the government without individualized suspicion or a particularized description of what data is being seized, much less probable cause. This protection was in response to colonial British misuse of writs of assistance, which authorized indiscriminate searches rather than targeted ones.

The justifications for exempting mass surveillance from constitutional protection vary. For Section 702, the government has taken the position that U.S. persons’ communications caught up in the dragnet, either due to overcollection or because they were communicating with someone outside the United States, do not require a warrant prior to initial collection or secondary access by the FBI and several other agencies. The argument is that if the initial collection was not aimed at Americans, the information is free from constitutional protection for any later uses, even for reasons far afield from the initial rationale for collection.

Other arguments rest on the claim that metadata is outside the Fourth Amendment, despite its demonstrated ability to reveal intimate details of all of our lives. Still others rest on the Supreme Court-created Third Party Doctrine, which holds that the Fourth Amendment does not apply to data shared with companies that provide us with services. Some turn on whether analysis by machine counts, claiming that only “human eyes” matter—a particularly troubling argument with the rise of artificial intelligence. What’s more, the government has used doctrines like standing to limit the ability of those subjected to mass surveillance to seek constitutional protection. No matter the argument, the goal is the same: to place the mechanisms and fruits of mass surveillance outside the protections of the Fourth Amendment.

The overarching truth is that, due to the concerted efforts by the government since 9/11, and the rise of technologies in recent years, the slice of Americans’ lives and data that are actually protected by the Fourth Amendment has shrunk significantly in the past 25 years. Together, with the technical capabilities of mass surveillance and the increased ability for that data to be analyzed using AI tools, the “security in our papers and effects” that the constitution promises seems increasingly illusory.

In addition to the Fourth Amendment, mass surveillance creates tensions with the First Amendment. The Constitution has long recognized that the right to freedom of speech requires a zone of privacy against governmental surveillance. The right to anonymous speech as well as the right of association both recognize the chilling effect that surveillance creates for people saying unpopular things or attempting to organize for political or other societal change. Mass surveillance grants the authorities the ability to track those people, both in real time and historically, that is inconsistent with actual techniques of freedom of speech and assembly.

That is why the recently released 2026 U.S. Counterterrorism Strategy is so troubling. On page seven, the White House expressly states that it intends to target domestic activists with its heretofore foreign-targeted powers. It says that the government “will prioritize the rapid identification and neutralization of violent secular political groups whose ideology is anti-American, radically pro-transgender and anarchist” and “will use all the tools constitutionally available to us to map them at home, identify their membership, map their ties to international organizations like Antifa.” While framed as targeting “violent” groups, it’s clear that the government intends to use its national security tools, presumably including the tools of mass surveillance, against Americans in ways that will create profound tensions with the First Amendment rights of people to organize and communicate privately.

Costs Due to Mistakes and Abuse

Even assuming some utility from mass surveillance—a fact we do not dispute, even if the public record is shaky and conclusory—the history of both the national security and domestic uses of mass surveillance confirms that these tools are inevitably misused, and that mistakes have impacted huge numbers of Americans. The past twenty-five years have demonstrated that it is not possible to surveil the entire US population while staying within the bounds of even a very generous legal framework like Section 702.

As Rep. Zoe Lofgren (D-Calif.) recently stated in discussion of Section 702 in an interview with Tech Policy Press: “backdoor searches have been used improperly for protestors, 19,000 campaign donors, members of Congress, journalists, government officials, a state court judge who had complained to the FBI about police misconduct. It has been abused substantially in the past.” The NSA experienced so much abuse of its mass surveillance tools by actual or aspiring romantic partners and ex-spouses that an internal name emerged for it: “LOVEINT,” or Love Intelligence.

That same pattern of abuse is now emerging at the domestic law enforcement level. A Texas police officer misused, and then lied about, using license plate readers to track a woman suspected of seeking an abortion. Multiple law enforcement officials have been accused of tracking people they either wished to have a relationship with or who were their exes. And mass surveillance technologies have been used to track both immigration targets and citizens engaging in their First Amendment-protected right to track and record the police.

Mistakes are inevitable with collections of data of this size and scope. The history of the FISA court’s reviews of Section 702 is littered with examples of the NSA not being able to follow its own rules limiting the scope of what it collects and analyzes, even after having been given multiple chances by the court. On the local level, the technical protections that Flock, for example, put in place have repeatedly been insufficient to stop “accidental” sharing its data with out-of-state law enforcement. These mistakes have fueled growing efforts by local communities across the country to remove license plate readers. Those efforts should be the first step in a broader reconsideration of mass surveillance.

More generally, ubiquitous surveillance carries a real societal cost. The chilling effects are real and pervasive, and they tend to fall hardest on the most marginalized members of society. Moreover, social progress requires the ability to experiment in secret. It’s hard to imagine a society progressing morally to the point of accepting and legalizing things like marijuana use or gay marriage if the earliest signs of that shift are snuffed out because of overzealous surveillance.

Reversing Course

While a cost-benefit analysis is not the best frame for deciding constitutional rights, it is a place to start to evaluate government policies. If the costs are too high and the benefits too small, what should the public do? While the policy and legal frameworks can be individually complex, mass surveillance is a problem in all of its applications. So too should solutions be comprehensive rather than piecemeal.

One comprehensive strategy is to reset the promise of the Fourth Amendment and recognize that a warrant is required prior to collection, access or use of information gathered through mass surveillance. This would apply to collections that include U.S. persons, whether done for national security or domestic purposes. This protection would apply regardless of whether the information is in the form of metadata. It would apply regardless of whether the information is held in homes or by services people rely on, such as telephones, internet or social network providers, or by private entities utilizing mass surveillance for their own purposes. By passing this legislation, Congress could ensure this rejection of mass surveillance, and include real enforcement such as a private right of action and an automatic exclusionary remedy in criminal prosecutions. The courts could also recognize this protection of “papers and effects” directly as a plain language interpretation of the Fourth Amendment.

There are already a number of efforts that take on pieces of mass surveillance. Section 702 has expired and should remain so. This was due largely to efforts to block the “back door” access to Section 702-collected data without warrants. The bipartisan “Fourth Amendment is Not for Sale Act” would prevent the government from purchasing data that it would otherwise need a warrant to obtain. The Supreme Court itself has already been chipping away at the Third Party Doctrine, with a recent step in the rejection of mass geofence warrants—warrants seeking the identities of individuals based upon their proximity to a crime—in Chatrie v. United States. Now, such warrants fall, at least initially, under the Fourth Amendment.

A more comprehensive approach would also address mass surveillance carried out by private companies, and to ensure that Americans have the right to encrypt and secure their data. There are many reasons the United States would benefit from a comprehensive privacy law—and curbing mass surveillance is one of them. Addressing mass surveillance is certainly one of them. Ideas such as the banning of secondary uses of data—with roots in the Fair Information Practice Principles from the 1970s—are worth pushing forward. So are moves such as creating fiduciary duties for mass data collectors. There are many more ways to curtail private companies’ mass surveillance while staying within constitutional boundaries. But addressing the costs of mass surveillance by both companies and governments is even more important in a world where AI agents are making decisions both about the public and on their behalf based on their data and observed behavior.

Twenty-five years after the U.S. government embraced mass surveillance, it’s time to evaluate it as a whole, and consider responses that address the problem as a whole. Americans must ask: Is it consistent with a self-governing democracy to have systems that watch everyone everywhere? Is the public comfortable with governments—federal, state, local—that seek to “know it all” about its citizens? Is the public comfortable with private mass surveillance in its own right and as it’s being increasingly used to fuel government surveillance? These questions have long needed serious consideration. But as it becomes increasingly evident that the Trump administration is using mass surveillance to keep itself in power, stifle dissent, and undermine political opponents, these questions are now more urgent than ever.

  • ✇bellingcat
  • Investigating a Murder: Public Records Uncover New Clues in Chinatown Cold Case Peter Barth
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. Illustration by Oisín Mac Con Iomaire On a June night in 1996, in a basement apartment in Boston’s Chinatown, a killer put a pistol to the back of a young man’s head and pulled the trigger. The victim’s body was wrapped in quilts, lowered into the trunk of a car and driven about 20 miles southwest of the city, where it was dumped in the woods. It lay undiscovered through two New England wint
     

Investigating a Murder: Public Records Uncover New Clues in Chinatown Cold Case

31 de Agosto de 2026, 09:58

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published.

Illustration by Oisín Mac Con Iomaire

On a June night in 1996, in a basement apartment in Boston’s Chinatown, a killer put a pistol to the back of a young man’s head and pulled the trigger. The victim’s body was wrapped in quilts, lowered into the trunk of a car and driven about 20 miles southwest of the city, where it was dumped in the woods. It lay undiscovered through two New England winters until a dog walked home carrying a human bone. 

For three decades, authorities did not release the victim’s name to the public. He is listed in the National Missing and Unidentified Persons System (NamUs) as #UP12385, one of 202 unidentified people recorded in the state of Massachusetts. The circumstances of his discovery are reduced to five words: “Skeletal remains found in woods.” The case remains unsolved.

Bellingcat spent more than a year investigating in an effort to put a name to the victim. Using open sources, including freedom-of-information requests and newspaper archives, as well as interviews with investigators who worked on the case, we pieced together the story of the man’s life and death in an era before the internet kept a digital record. In total, we submitted 27 public records requests to 18 local, state and federal law enforcement agencies. We also contacted more than two dozen investigators, prosecutors, crime victims, journalists, experts and community groups connected to the case, though most said they did not recall the 30-year-old murder. 

We learned that authorities identified the victim as Fu Chun Wang, a 26-year-old undocumented Chinese immigrant who did not speak English. We also learned of a sweeping investigation into Chinese organised crime in New England in the late 1990s. Authorities suspected Wang was a member of the Fukienese Flying Dragons, a gang the FBI described at the time as a “violent offshoot” of the larger Flying Dragons crime ring.

Fu Chun Wang’s mugshot. Source: Released by Sharon Police Department

Based in New York’s Chinatown, the Fukienese Flying Dragons were active across much of the East Coast. The gang trafficked migrants, extorted and robbed businesses, kidnapped for ransom and murdered rivals. Authorities believed Wang belonged to the Boston faction of the group, which was suspected of carrying out home invasions targeting Asian and Asian-American restaurant owners and staff across New England in 1996. 

Heavily redacted FBI documents and correspondence between local and state authorities show that a federal operation investigating the home invasions uncovered information about Wang’s murder and other crimes. The operation, whose name is redacted, was led by the US Attorney for the District of New Hampshire.

Authorities suspected that the Boston-based members of the Fukienese Flying Dragons were also involved in prostitution, illegal gambling and kidnappings in multiple states. Investigators examined possible ties between the gang and a suspect in one of Boston’s deadliest mass killings: the 1991 Chinatown Massacre. 

According to these newly released files, investigators received information that Wang had been murdered by members of his own gang shortly after Boston Police arrested and charged him for using credit cards stolen in one of the home invasions. While some gang members were identified as suspects and investigated, none were ever charged and convicted for the murder.

A Dog with a Bone

On April 10, 1998, a resident in Sharon, an affluent suburb southwest of Boston, called the police. Their black Labrador retriever had returned from the woods carrying a large bone. Four days later, testing by the coroner’s office in Boston determined it was a human femur likely belonging to an adult male. Police searched the woods, where a detective uncovered more bones near a bundle of quilts. Inside, they found a decomposed skeleton. 

A coroner ruled that the victim was a man in his twenties or thirties who was possibly Asian or Native American. He had long black hair with blonde streaks. There were at least two overlapping bullet holes in the back of his skull. Local media reported at the time that the victim had been shot “execution style”. A single, corroded .380 calibre shell casing was found inside the quilt. The victim was wearing a green-and-white striped rugby shirt, denim jeans and white leather sneakers on the night he died. Loose change, a pocket knife and a tarnished set of keys were found nearby on the forest floor. 

Blurred
Crime scene photos showing remains found in the woods, near the intersection of Walpole Street and Bluff Head Road, in April 1998. Source: Sharon Police Department

A botanist from Harvard University determined the body had likely been in the woods for at least 18 months. Experts from The Smithsonian Institution told police that forensic facial reconstruction would be “of questionable value” due to the damage caused by the gunshot injuries. 

Before his murder, open source records of Wang’s life amounted to a few scattered data points: interactions with authorities, apartment rentals, a loan application and a hospitalisation following a car accident. 

Police reports after his death show investigators pieced together a patchy collection of biographical data. He was from a family of five in Dongshan, a village in the southeastern Chinese province of Fujian, and was likely born into poverty. During his autopsy, it was noted that his teeth showed signs he had been “undernourished” as a child. He moved to the US to work in the restaurant industry and eventually joined a gang. 

A January 1994 Immigration and Naturalisation Service (INS) record containing Wang’s fingerprints. Source: Released by Sharon Police Department

Wang migrated at a time when thousands of Fujianese were arriving in the US every month in search of a better life. Like many migrants at the time, he entered the country without documentation. By 1994, when Wang met with immigration authorities in Boston, he gave his address as an apartment on East Broadway in New York’s Chinatown. Two years later, he was issued an employment authorisation card. Boston Police would learn from the Immigration and Naturalisation Service (INS) that Wang had been granted political asylum.

Search results on Ancestry.com suggest that prior to living in Massachusetts, Wang had also lived in Virginia and Vermont. Records from the Sharon Police Department corroborate these findings. In Virginia, police learned that he worked in restaurants and had applied for a loan to buy a car, a 1994 green Mitsubishi Mirage. 

In Vermont, Wang worked at a Chinese restaurant called Men-at-Wok until he was injured in a car accident in May 1996. After he was reported missing a month later, his car sat unclaimed in an auto body shop in Vermont until a bank sought to reclaim it.

Wang’s INS employment authorisation card. Source: Released by Sharon Police Department

Unclaimed checking and savings accounts at Fleet National Bank are listed under Wang’s name and the Quincy address in the Massachusetts unclaimed property database. The amount of money in these accounts is not publicly available, but the presence of unclaimed funds in his name, along with his abandoned car, are some of the many indicators that he met with foul play.

‘We’ll Kill Your Family’

In the summer of 1996, Chinese restaurant owners and their staff in suburban Boston and New Hampshire were terrorised by a wave of violent home invasions and robberies. Police described the suspects as “an organised group of Asian individuals”.

Composite sketches of two suspects in a July 1996 home invasion in Merrimack, NH based on witness descriptions. Source: Merrimack Police Department

The modus operandi was often the same. In the early morning hours, young men barged into rooming houses while Chinese restaurant workers slept. Sometimes they robbed the business owners’ homes. Armed with guns and knives, the intruders tied up their victims before stealing cash and valuables. One woman who was attacked at knifepoint told police an assailant threatened: “Shut up or I will kill you”. In another case, a victim was stabbed.

The assailants were described as young men or teenagers. In several cases, victims reported that they spoke Fuzhou, also known as Foochow, a language originating from eastern Fujian Province. 

Jim Keating, a retired Sharon Police detective, told Bellingcat that many Asian gang extortions and robberies in the Boston area at the time were not reported. “They were all afraid of these guys, because they said, ‘We’ll come back and we’ll kill your family.’ And they would.”

In the span of a few months in 1996, similar robberies occurred in the Massachusetts towns of Bedford and Westborough, as well as Malden, a city about 10 kilometres north of Boston. These were followed by home invasions in the bordering state of New Hampshire, in Wolfeboro, Merrimack, Lebanon and Manchester. It is unclear if the home invasions and robberies were connected, but Bellingcat’s review of historic newspaper clippings and newly released police documents suggest that at least one other gang may have been responsible for some of the crimes. 

In response to the crime spree, local, state and federal law enforcement agencies coordinated investigative efforts. Bellingcat obtained files detailing a federal operation led by the US Attorney for the District of New Hampshire. It included agents from the FBI, INS, Drug Enforcement Administration, Bureau of Alcohol, Tobacco, Firearms and Explosives, Customs Service, Border Patrol and Royal Canadian Mounted Police, along with officers from state, county, and local law enforcement agencies in New England.

Local media coverage of the home invasions in 1996. Source: Janet Wilson, The Boston Globe

Ben Leong, a retired Boston Police detective, said Asian gangs committing robberies, extortion and home invasions against restaurant owners and their staff were common in the 1990s. He said many of these crimes went unreported for cultural reasons, over fears of gang retaliation, and because victims did not trust law enforcement.

“Back then there were many factions of gangs,” he said. “The gang members were recruited throughout the country, nationally and internationally. Law enforcement was always playing catch up to identify the prevalent groups, and the individual members and leaders committing illegal activity.” 

Leong, who is president of the International Organisation of Asian Crime Investigators and Specialists (IOACIS), said authorities had a better understanding of gang culture in Boston by around 1996 when local, state and federal agencies began sharing information.

Murdered in Chinatown

On June 9, 1996, one day after a home invasion in Wolfeboro, New Hampshire, Boston police were called to a Macy’s department store when a man tried to use credit cards stolen in the robbery to buy jewellery and electronics. He was arrested and booked on charges of receiving stolen property. The man was Fu Chun Wang.

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

In Sharon Police records, Wang was described as “very depressed” and “possibly suicidal”. He told a Boston Police detective that he urgently needed to send $1,000 to his family in China. Other records said his father was ill and needed money. Files found on Judyrecords.com, a free database that purports to include more than 770 million US court case records, show that Wang was arraigned and a court appointed a defence attorney to him. He was bailed out after pleading not guilty. 

Less than two weeks later, on June 21, Wang was reported missing to the Quincy Police Department in Massachusetts. The police report said Wang was last seen in Boston’s Chinatown on June 13, two days after he left jail. Police records indicate that a female friend of Wang’s had asked an attorney to file the missing persons report. The attorney who reported him missing told Bellingcat that he did not remember Wang. 

The Sharon Police Department’s murder case file notes that within weeks of finding the human remains in the woods, they received information from Boston Police and the FBI that “Wang was murdered in Chinatown” in June 1996 and “his body [was] never found”. 

Investigator’s handwritten notes about Wang’s case. Source: Sharon Police Department

Not all documents were released to Bellingcat, and some names were redacted. Fragmentary notes and witness statements provide the only clues to Wang’s final days. In handwritten records, Sharon Police said a man who was described as the “head of [an] Asian gang in Boston” had Wang killed over the New Hampshire breaking-and-entering incident.

A note in the Sharon Police file described a witness to Wang’s murder as a “Flying Dra” and a “NY gangster”. Retired detective Jim Keating confirmed to Bellingcat that Wang, along with his associates and killer, were suspected members of the Fukienese Flying Dragons. He said he believed Wang was murdered because he posed a risk to the gang following his arrest.

Investigator’s notes describing a witness as a gang member. Source: Sharon Police Department

Documents released by the Merrimack Police Department and Sharon Police Department said that an inmate in New York who, in 1997, was serving a 25-year sentence for attempted kidnapping, offered to share information about Wang’s murder with the FBI in return “for a reduction (sentence)”. The inmate implicated the gang in at least two 1996 New Hampshire home invasions and also gave authorities information about the murder.

The account is heavily redacted but includes a note that the US Attorney for New Hampshire was making arrangements to interview the man. Police in New Hampshire said they believed the inmate “was truthful in his statements and has knowledge about the murder of Mr Wang”. Keating confirmed to Bellingcat that he and investigators from other law enforcement agencies traveled to New York to interview the man. He said the inmate, a suspected member of the Fukienese Flying Dragons, gave a statement on Wang’s murder. 

Keating said that investigators had learned of a dispute on the night of the murder between Wang and the gang’s leader. He said Wang had planned to run an illegal gambling operation at an apartment, which the gang boss was using as a prostitution venue. This led to an argument at another location, and when Wang left for the apartment the boss followed him. 

But Keating said that based on the information gathered throughout the investigation, he believes the primary motive for Wang’s murder was his arrest over the use of credit cards stolen in the Wolfeboro home invasion. With that arrest, Wang presented a risk to the rest of the gang because investigators could tie them to the home invasions. “Wang broke the basic rules by taking something that was identifiable and using it, and that’s what the whole damn thing was about,” Keating said.

Crime scene photos of the basement unit on Oxford Place in Boston’s Chinatown where Wang was murdered. Source: Sharon Police Department

Keating told Bellingcat that the crime scene had been damaged by flooding after the murder. He said he used a power saw to cut off the bottom of a door in the unit and that lab testing revealed the presence of Wang’s blood. 

Investigators also worked to confirm Wang’s identity by testing the DNA of the remains found in the woods. In a 1999 case summary written by a Massachusetts State Police Trooper, it was noted that the FBI was attempting to locate Wang’s parents through police in China.

Three months later, the FBI’s Hong Kong office sent a communique to Boston confirming that the Chinese Ministry of Public Security and Interpol had located Wang’s parents. Wang’s mother, it said, was willing to provide a DNA sample for comparison. 

The FBI communique is the final document in the newly released files that details the efforts to confirm Wang’s identity with DNA. However, Keating told Bellingcat that a DNA sample was obtained from Wang’s mother in China. It was brought back to the US for testing and confirmed to be a match, he said. “I don’t know when the DNA was collected or who did it. But I know that they did that,” Keating said. “There is no question about who he was.” 

Bellingcat contacted both the Norfolk and the Suffolk County District Attorneys to confirm the DNA match, but did not receive a response to questions about DNA testing or the victim’s identity. Other former law enforcement officers named in the files released to Bellingcat have not responded to requests for comment. 

One document included in the murder file references discussions between homicide investigators and the FBI about charging Wang’s killer with home invasion offences. The note discusses the potential to have a witness testify against Wang’s suspected killer. It is unknown whether he was ever charged. 

The Shooter

The man authorities suspected of shooting Wang, or ordering his killing, was described in the documents as the head of the Fukienese Flying Dragons in Boston. Police said he was an undocumented immigrant from Fujian Province who ran a sex trafficking ring. 

A redacted note in the Sharon Police Department murder file references Wang’s suspected killer as “a player” in the Chinatown Massacre, an infamous 1991 case in which five men were shot dead in a basement gambling parlour in Boston. In another note in the case file, investigators wrote that he was “thought to be a federal informant”. 

The name of the gang boss was redacted in the police file released to Bellingcat.

Keating said Wang’s identity was confirmed by DNA and that blood evidence, corroborated by witness statements, placed his killing in the Boston Chinatown apartment. The retired Sharon detective said at that point, the Boston Police Department and the Suffolk County District Attorney’s Office should have, respectively, taken over the investigation and prosecution of the case. 

The Boston Police Department did not respond to questions from Bellingcat. It does not include the killing in its list of unsolved homicides. 

The Suffolk County District Attorney’s Office said it did not have records for Wang’s murder. “Unfortunately, after a thorough search with assistance from our homicide unit, no responsive records could be located,” it said.

In response to Bellingcat’s original public records request, the Norfolk County District Attorney’s office, which covers Sharon, said the files were exempt from public disclosure because they pertained to “an active and ongoing criminal investigation”.

A spokesman for the office said last week that the circumstances surrounding the remains of an adult male discovered in Sharon in April 1998 “remain under investigation” by a state police detective assigned to the Norfolk District Attorney’s Unsolved Case Unit. 

The FBI confirmed that the agency assisted state and local partners in an operation investigating home invasions in the late 1990s but did not answer questions about Wang’s murder or his suspected killer. “Given that we’re not the lead, we’ll refer you to Massachusetts State Police,” a spokeswoman said.

Massachusetts State Police referred questions to the Suffolk and Norfolk County District Attorney’s offices.

It remains a mystery why Wang’s killer was never charged and prosecuted for his murder. “They got away with so much. Blatantly got away with so much,” Keating said of the gang. “Killing people for these guys was like … these guys were all expendable.”

The Norfolk District Attorney’s Office encouraged anyone with any information about the case to contact the Massachusetts State Police Tip Line or the Sharon Police Department.


Melissa Zhu contributed research to this article.

Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. You can also subscribe to our Patreon channel here. Subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post Investigating a Murder: Public Records Uncover New Clues in Chinatown Cold Case appeared first on bellingcat.

  • ✇Krebs on Security
  • Two Alleged ‘TeamPCP’ Hackers Arrested in Australia BrianKrebs
    Authorities in Australia have arrested two men believed to be members of TeamPCP, a prolific cybercrime and data extortion group blamed for perpetrating the longest running spree of software supply chain attacks ever. In a statement released today, the Australian Federal Police (AFP) said two men from Western Australia, aged 21 and 23, were arrested in connection with a “sophisticated cybercrime syndicate that allegedly created malicious open-source software to rob thousands of global businesses
     

Two Alleged ‘TeamPCP’ Hackers Arrested in Australia

27 de Agosto de 2026, 08:04

Authorities in Australia have arrested two men believed to be members of TeamPCP, a prolific cybercrime and data extortion group blamed for perpetrating the longest running spree of software supply chain attacks ever.

In a statement released today, the Australian Federal Police (AFP) said two men from Western Australia, aged 21 and 23, were arrested in connection with a “sophisticated cybercrime syndicate that allegedly created malicious open-source software to rob thousands of global businesses.”

The AFP did not name the defendants, but KrebsOnSecurity learned the 21-year-old suspect’s real identity in June, and has been communicating with him ever since. This story includes interviews with TeamPCP’s self-described spokesperson, and examines clues left behind by the TeamPCP leader that likely led to his undoing.

TeamPCP vaulted onto the cybercrime scene in late 2025, embedding malicious code in hundreds of open source software tools and extorting victims for profit. Members of the group made headlines by compromising corporate cloud environments using a self-propagating worm dubbed Shai-Hulud, which added malicious code to open source programs maintained by developers whose credentials at public code repositories like GitHub or NPM were phished or stolen.

Writing for Wired, journalist Andy Greenberg described TeamPCP’s core tactic as a kind of cyclical exploitation of software developers.

“The hackers gain access to a network where an open source tool commonly used by coders is being developed,” Greenberg wrote in May. “The hackers plant malware in the tool that ends up on other software developers’ machines, including some who are writing other tools intended to be used by coders. The malware allows TeamPCP’s hackers to steal credentials that let them publish malicious versions of those software development tools, too. The cycle repeats, and TeamPCP’s collection of breached networks grows.”

TeamPCP also has practiced something akin to cyclical recruitment. In May, the source code for the third iteration of Shai-Hulud was published online, and TeamPCP soon after launched a contest offering $1,000 in virtual currency to whichever participant could conduct the largest supply chain operation using the worm’s code. According to the contest rules, participants were scored based on the number of weekly and monthly downloads of packages they compromised — directly incentivizing them to target the most popular code libraries.

A screenshot of a message from TeamPCP’s Telegram account, announcing the supply chain hacking contest. Image: dataminr.com.

“TeamPCP has stated the competition is a recruiting opportunity and they intend to purchase all meaningful access harvested from participants’ campaigns,” the security firm Dataminr wrote. “The $1,000 XMR (Monero) prize is a recruitment floor and has been dismissed by the actor as ‘just like participation trophy,’ adding ‘if you find something good you will be paid way more,’ confirming the contest’s true function as talent identification and malicious access acquisition at scale.”

In March, TeamPCP executed a supply chain attack targeting AI infrastructure by compromising the code for LiteLLM, an open source AI gateway that connects users to more than 100 different large language models. A recent analysis by the security firm CloudSEK found TeamPCPs attack on LiteLLM harvested cloud service keys and other secrets from more than 2,500 organizations, including many of the world’s top technology companies.

In May, TeamPCP claimed credit for compromising at least 3,800 code repositories at the Microsoft-owned GitHub, after a GitHub developer installed a code extension that was compromised by TeamPCP’s malware.

MEET THE CYBERCATS

Security experts say TeamPCP is less of a hacker group than an amalgamation of threat actors from multiple cybercriminal gangs who sometimes work together toward similar goals.

“It is not a structured criminal crew with a single operator,” said Austin Larsen, a principal threat analyst with the Google Threat Intelligence Group. “It is a peer community of individually-skilled actors, with one clear center of gravity.”

That center of gravity is George Prepakis, an accomplished security researcher and self-described exploit developer who operates the Twitter/X profile @kernelstub. Earlier this year, @kernelstub tweeted a public invite link to a Matrix chat server he created and dubbed “Cybercats,” and TeamPCP and several other cybercrime entities have been using this server to communicate daily for the past several months.

A screenshot of the Matrix chat server “Cybercats,” whose members used hacker handles associated with multiple distinct cybercrime groups that have occasionally collaborated on a series of supply chain and data ransom attacks over the past nine months.

Kernelstub, like other administrators in the Cybercats chat, has been using his Twitter/X profile name as his handle in these Matrix communications, frequently tweeting references to other members and to conversations taking place in the Cybercats chat. In a number of cases, the corresponding X accounts for members of the Cybercats chat taunted cybercrime victims publicly before the incidents were reported in the news media.

The Cybercats administrator listed at the top of the screenshot above — “Boxturtle” — is a close associate of TeamPCP who has been tweeting about the group’s conquests under the name @xpl0itrsturtle. This handle corresponds to a data breach broker active on Breachforums and Darkforums who has been selling data stolen in a wave of recent breaches at automobile manufacturers, including BMW Group, Audi, Honda, Mercedes-Benz, Volvo and Toyota, as well as data allegedly taken from Snapchat and SportRadar.

The data leak site for the extortion group or handle “xpl0itrs.”

The Cybercats administrator “SeesawSec” in the screenshot above is the alias of whoever is behind the cybercrime group known as Fulcrumsec, which recently claimed credit for data extortion attacks against the pharmaceutical giant Novo Nordisk, the data broker LexisNexis, and Avnet, a Fortune 500 distributor of electronic components.

The data leak site of Fulcrum Security, a.k.a. Fulcrumsec.

The Cybercats administrator “@pcpcasper” also has been using a similar name on X to discuss TeamPCP’s attacks and victims. This person has an extensive message history on Telegram, where their messages and shared videos show @pcpcasper is an active and vocal member of the National Socialist Network, a neo-Nazi political organization based in Australia.

At one point in these chats, @pcpcasper shared videos and images of what they claimed was their cat, and several of those videos place this user in Western Australia. One source close to the investigation told KrebsOnSecurity that @pcpcasper was one of the two arrested, a claim supported by messages that @kernelstub posted online this morning.

The Cybercats member roster pictured above also features an administrator with the username “T,” which is short for the now-banned Twitter/X profile @pcpcats, the account operated by the self-described TeamPCP spokesperson who was arrested today. As we’ll see in a moment, @pcpcats also is from Western Australia.

By the time @kernelstub tweeted a public invite link to the Cybercats Matrix server, T/@pcpcats was posting only infrequently to the group chat, with other members often inquiring as to his whereabouts and well-being. The group’s collective concern related to @pcpcats’s tendency to blame his increasingly extended absences on the use of hallucinogens and other narcotics that kept him awake for days on end, but also caused him to crash in bed for several days after the highs wore off.

WHO IS THE TEAMPCP LEADER?

The Cybercats member @pcpcats has used multiple nicknames on the cybercrime forums, including EllisD25/LSD on Darkforums, BulkDMT on Breachstars, and Express on Breachforums. These accounts are linked because they all advertised the same Tox ID and/or Session ID as instant message contact handles in their cybercrime forum posts. BulkDMT was also known on the forums as DMT Host, which was a virtual private server (VPS) hosting service that was peddled on Darkforums and Breachstars.

DMT Host/EllisD25, posting on the English-language cybercrime community DarkForums in September 2025. Image: ke-la.com.

According to the cyber intelligence firm Intel 471, Express registered on Breachforums using the email address shitstickpp@gmail.com. Intel 471 finds Express posted on Breachforums across a two-month period in 2025 using four different Internet addresses located in South Africa. On July 30, 2025, Express announced on Breachforums they were selling access to 14 gigabytes of data stolen from South Africa’s State Information Technology Agency.

The threat intelligence platform Flashpoint recorded more than a year’s worth of messages from the TeamPCP leader’s alter ego on Telegram — Persy_PCP —  who claimed they split their life living between two countries [full disclosure: Flashpoint is an advertiser on this blog]. “I have these [files] as well, problem is these are in another country,” Persy_PCP explained to another user inquiring about a stolen data set in November 2025.

Later that month, Persy_PCP complained, “My whole country is racist and they want people like me dead.” Flashpoint records show BulkDMT shared in September 2025 that “this country is going to fucking starve when they take the farmers land,” a likely reference to white landowners in South Africa who claim to be targeted by an ongoing genocide campaign.

This tracks with public reporting on TeamPCP. Cyberscoop reported in June that Google had traced TeamPCP’s residential and mobile Internet address connections to South Africa, “indicating the primary operator was located there during at least some of its attacks.”

BulkDMT also shared on the group chat at Breachforums that they were recovering from an addiction to methamphetamine. “My life is kinda fucked rn [right now], but that’s fine and there isn’t really a point in pouring so much emotional energy into that fact, my parents had money but I unfortunately got really addicted to some things so I don’t get to benefit from that. As long as I continue to survive, stay sober, and move closer towards my goals that’s enough drive and meaning.”

The identity threat protection company SpyCloud finds shitstickpp@gmail.com shows up in the registration of an account called ChristmasSnow on the cybercrime community Raidforums in 2022. Nearly all of the Internet addresses used to access that account came from ISPs in Perth, Australia, SpyCloud found.

KrebsOnSecurity looked up all of those Perth IP addresses in passive DNS records maintained by DomainTools.com, and found one of them — 211.27.196.111 — for several years was used as a private file server by a family in Perth with the last name of Thomson. Those records show at least three hosts — ithomson.direct.quickconnect.to (a remote Synology server), kthomson0061.direct.quickconnect.to, and joshuawthomson39.myqnapcloud.com (a QNAP network storage device) — persisted at that address between 2022 and 2025.

Searching on “joshuathomson39” in the breach tracking service Constella Intelligence reveals an account at the freight forwarding company kwe.com created in the name of Joshua Thomson from Perth, Australia. The open source intelligence platform Epieos finds the phone number attached to that kwe.com account was used to register a Facebook profile for Josh Thomson, which says his family includes a brother named Ruben, his father Ian, and his mom Cindy.

That Facebook profile also says Josh and his family are originally from Pietermaritzburg, in KwaZulu-Natal, South Africa, but currently living in Cottesloe, a beach-side suburb of Perth. A search in DomainTools for Ian Thomson and Australia unearthed five domains by the same registrant, including securecomputing.au, thomson.org.au, and thomsonfamily.net.au. Ian Thomson is a dentist in Cottesloe, and a biography says he graduated from The University of the Witwatersrand in Johannesburg, South Africa.

Constella finds a joshua@thomson.org.au registered a number of accounts online, but Josh doesn’t seem to have much of a connection to dodgy cybercrime forums. His brother Ruben, on the other hand, has quite the presence on these communities, dating back to at least 2018. Constella reports ruben@thomson.org.au frequently reused the password “joshuathomson1,” and Constella further finds that password was used by just a handful of accounts, including yolosolo17@gmail.com and surfinup8@gmail.com.

According to Intel 471, surfinup8@gmail.com was used to register the user Yolosolo17 on the crime forum Altenen in 2018, and that user account was registered from the Perth address 110.141.230.15. On Altenen, Yolosolo17 advertised free web proxies, as well as the domain rubenthomson.com, which was at one point used to sell steeply discounted iPhones. DomainTools says rubenthomson.com was hosted at 110.141.230.15 and registered to surfinup8@gmail.com.

A cached copy of the domain rubenthomson.com from 2017 shows a login page underneath a banded stack of money. Image: archive.org.

SpyCloud reports 10.141.230.15 was used by the email address sheepstealing@gmail.com on Raidforums and surfinup8@gmail.com on Nulled, and that the same IP was used by the email addresses ian@thomsonfamily.net.au, jasper@yakuza.cc, and rubenthomson1@gmail.com. SpyCloud also shows that sheepstealing Gmail address is tied to the accounts Sheep420, YoloSolo117 and Yakuza.cc on Raidforums, and to the account “Sheep Stealing” on Hackforums. Intel 471 says sheepstealing@gmail.com was used to register the account DingoFlour on Breachforums in October 2023, as well Sheepx on Altenen.

Epieos reports that ruben@securecomputing.au is tied to an Airbnb account for Ruben, who described himself as a Web developer who went to school at the University of Western Australia and was living outside the country. “Hey, I’m Ruben, my friends call me Ellis. I’m a Perth creative who occasionally books rooms when visiting family and for photography.”

Epieos also finds sheepstealing@gmail.com registered an upwork.com profile under the name Ruben, who said his main skills are setting up secure server hosting solutions and PHP full-stack Web development.

“I’m familiar with Linux, working with relational databases (SQL),” the Upwork profile reads. “I also script in Python mainly for writing social media bots.”

The Upwork profile for Ruben Thomson in Cottesloe, Australia.

Epieos further discovered sheepstealing@gmail.com is connected to a Microsoft account for Ruben Thomson, and to a now-defunct GitHub account called XmasSnow/XmasSnowisBack that scammed people on the forums in 2022 by claiming to sell exclusive exploits for recently-released software patches (recall that shitstickpp@gmail.com was used to register a forum account named ChristmasSnow).

This same sheepstealing email address registered a Twitter/X account in 2026 called “Gone Fishing” that lists its location as South Africa. That Gmail account also left several reviews for businesses listed on Google Maps over the past seven years, but all of those establishments are located on the west coast of Australia.

Business reviews in Western Australia left by the Google account sheepstealing at gmail.com.

The people search service Pipl finds a 21-year-old Ruben Thomson in Western Australia who has a phone number ending in 979. A lookup on that number at Epieos reveals it is connected to a TikTok account under the name Ellis, and to a PayPal account in the name of Ruben Thomson.

Finally, a search on the name Ruben Thomson from Cottesloe at the Australian government’s record of registered businesses finds he has incorporated or served as an official in multiple companies created since 2024, including Secure Computing Solutions, Tensor Industries, and another entity ironically named OPSEC Express. Recall that Express was BulkDMT’s nickname on Breachforums.

Australian companies connected to Ruben Thomson. Image: abr.business.gov.au.

It’s ironic because OPSEC is short for the term “operational security,” which refers to techniques and behaviors used to obfuscate and compartmentalize one’s real-life identity online, and using your cybercrime handle as part of your own company name is very much the antithesis of that practice.

There is at least one other major opsec failure by Ruben that exposed a link to TeamPCP. In June 2025, someone using the name Ruben Thomson registered on HackerOne, a popular “bug bounty” program that seeks to reward and recognize researchers who agree to work with affected software vendors to help fix the flaws before publishing about their findings. What was Ruben Thomson’s chosen HackerOne username? Deadcatx3, a nickname that has been flagged by multiple security firms as an alias used by TeamPCP.

The HackerOne profile for “Ruben Thomson” uses the nickname Deadcatx3, which multiple security firms have concluded is an alias used by TeamPCP. Image credit: flare.io.

INTERVIEW WITH ELLIS

In early July 2026, not long after having discovered clues about Ellis’s real life identity, KrebsOnSecurity interviewed the TeamPCP leader via Signal, where he was remarkably open about his activities and personal struggles [for the sake of simplicity, the TeamPCP spokesperson will be referred to from here on as Ellis].

Ellis claims he stopped doing cybercrime for TeamPCP in March 2026 — just before the attacks that compromised LiteLLM — and that at least one other individual has taken over the group’s leadership since then. Ellis shared that a year earlier he had just completed the latest in a series of detox and sobriety programs, and was two months sober when he reconnected with some old friends from the malware development scene.

“One year ago I needed help monetizing some [GitHub credentials], I was two months sober and needed a distraction and something to keep busy as well as people to speak to,” Ellis said. “I had largely disconnected from my old circle, they had become very toxic and I needed to get away from the substances. Previously I had done some mass exploitation campaigns and grew up doing [malware development] and [capture the flag] contests. There were some friends who were also vending but had stopped a while, and one of them introduced me to some chats where I posted access for sale.”

Prior to that, Ellis said, he was homeless and hopping between “some very unstable places.”

“Blackhatting is fun,” he said. “There are actual rewards and incentives to learn and you grow with your team. Without qualifications, no employer will even take the time to hear you out.”

Ellis claims he’s earned a grand total of about $20,000 for his activities with TeamPCP, and that it was never about the money or fame for him. Asked whether his experiences with TeamPCP might prepare him for gainful employment in a legitimate IT job, Ellis said he doubted it.

“I am nowhere close to a skill level where I am comfortable, and this would take maybe half a decade of further experience,” he said. “I no longer have to choose between rent and food for that I’m grateful and so are the team members.”

Ellis expressed no remorse over his cybercrime activities, and said he was grateful for the friendships and relationships built throughout his engagement with TeamPCP. The young hacker also seemed resigned to his fate, and told KrebsOnSecurity that he’ll accept the consequences if he’s ever arrested.

“If I’ve already been found out then its out of my control, I’ll make peace with that,” he said. “Honestly, I think someone like me needs a lot of help that prison just can’t provide. If I had the funds to study different parts of the field and closer guidance, this would have turned out differently. But that’s a pipe dream and we both know this.”

It is clear from reading Ellis’s posts to the group’s Matrix server chats that his struggles with sobriety are ongoing. On Thursday, June 25, Ellis told @kernelstub he was about to “trip” with his “homie.”

“What kind,” @kernelstub inquired.

“Ketty and some DMT,” Ellis replied, referring to the dissociative anesthetic ketamine and dimethyltryptamine (DMT), a powerful psychedelic compound that is found naturally in some plants but is also synthetically produced in underground lab environments. “There’s a little 2cb so we might throw that in the mix,” he continued, referring to another psychedelic compound by its chemical shorthand.

Roughly two weeks before his arrest, Ellis told KrebsOnSecurity he was ready to leave his life of crime behind and was prepared to turn himself in, but that in the meantime he was making plans to tie up loose ends.

Less than 24 hours later, the TeamPCP leader posted an image on Telegram showing a yellowish powdered substance in a baggie and on a scale, possibly synthetic DMT. The image shows the powder being weighed next to a series of small vape cartridges, two of which are open on the table in front of the photographer.

An image posted by the TeamPCP leader to Telegram, advertising his acquisition of some type of psychoactive substance, most likely a synthetic version of the powerful hallucinogen known as DMT.

The two defendants were arrested Wednesday morning. The AFP said the men face a combined 14 cybercrime offenses and are scheduled to appear in Perth Magistrates Court today.

Charlie Eriksen is a security researcher at Aikido Security who has closely followed TeamPCP’s cybercrime campaigns. Eriksen said TeamPCP are a good example of a new kind of threat actor that does not fit neatly into the usual categories.

“They are not a state actor, not quite organized cybercrime, and not purely ideological,” he said. “Their motivations seem to mix money, disruption, attention, and ideology.”

Eriksen said that historically there has always been a meaningful gap between reading about an attack technique and being able to reliably turn it into an operational campaign, but that large language models (LLMs) and artificial intelligence increasingly are helping threat actors to bypass that knowledge gap.

“You had to understand the research, adapt the code, troubleshoot it, build infrastructure around it, and then repeat that process across different targets,” he said. “LLMs have compressed that gap significantly.”

According to Eriksen, this creates an environment where threat actors suddenly have the ability to operate at significant scale without having developed the operational discipline that traditionally accompanies that level of capability. Put another way, it sets the stage for cybercriminals who are capable enough to cause significant damage, but not necessarily careful enough to understand or care about the consequences.

“They can be noisy, they can make mistakes,” he said. “They can leave evidence everywhere. They can take risks that a professional criminal group or intelligence service would consider completely unacceptable. But that does not necessarily make them less dangerous. In some ways, it can make them more dangerous.”

In a recent blog post, Eriksen called TeamPCP’s Shai-Hulud worm the “best thing to happen to supply chain security,” because it forced GitHub and other public coding platforms to erect new security safeguards.

In direct response to TeamPCP’s broad success at pushing poisoned versions of popular software packages, GitHub in late July introduced a three-day “cooldown” mechanism for Dependabot, the platform’s tool for auto-fetching newly shipped updates for any package dependencies. Cooldown periods are designed to help buy time for security tools and package maintainers to identify and remove any compromised versions. Other coding ecosystems like Python and various JavaScript platforms also added support for cooldown periods this year amid growing calls from security experts about the need for more widespread adoption of the safety feature.

Eriksen said TeamPCP’s legacy is that they achieved in the span of a few months what the supply chain security community has been unable to do for years.

“They managed to wake up Microsoft to the fact that they had become negligent in terms of security,” Eriksen said. “By compromising GitHub and stealing their source code, they humiliated Microsoft into action, making them finally act on what we had been asking them to do and take seriously for a while now.”

Update, 10:08 a.m. ET: A story this morning from ABC News in Australia confirms Ruben Ian Thomson of Cottesloe was one of the two arrested. The 23-year-old suspect thought to be @pcpcasper, Michael Gaebler, also was arrested in Perth. ABC News reports that Thomson was denied bail (Mr. Gaebler’s attorney reportedly did not request bail for his client), and that both men will be held in custody until their next court appearance on September 18.

  • ✇Low-level adventures
  • I pointed an agent at a bootloader. It found bugs but not useful ones 0x434b
    Now that we're past the sensational headline, let's be real. This is the first post in a series about using AI/LLMs to do security work. I know, I know. Everybody and their grandma is using AI for this nowadays. Every time I'm opening up any kind of social media, I feel like this graph still holds true up to now:AI startup growth visualized, 2026.Also, this blog series will not be about "AI will replace us" (at least not yet) nor about "prompt engineering tips" (albeit an overlap will be there).
     

I pointed an agent at a bootloader. It found bugs but not useful ones

Por:0x434b
26 de Agosto de 2026, 08:02
I pointed an agent at a bootloader. It found bugs but not useful ones

Now that we're past the sensational headline, let's be real. This is the first post in a series about using AI/LLMs to do security work. I know, I know. Everybody and their grandma is using AI for this nowadays. Every time I'm opening up any kind of social media, I feel like this graph still holds true up to now:

I pointed an agent at a bootloader. It found bugs but not useful ones
AI startup growth visualized, 2026.

Also, this blog series will not be about "AI will replace us" (at least not yet) nor about "prompt engineering tips" (albeit an overlap will be there). What this post in particular will be about is some kind of retrospective combined with what it actually looks like when you put a capable model down in front of a real target and ask it to do the whole job. I want to take that apart and rebuild it into something that isn't a party trick. So who knows, maybe the further we get along in this series, the closer you're going to get to witnessing me putting my name in the above graph as well 😎.

I started experimenting with "AI-powered" solutions around the beginning of 2023 at an earlier company (the same time Google came out of the closet with their first public findings). If I recall correctly, when I started, it was still the "GPT-3" era. Asking an LLM about automated security work often resulted in major hallucination backed by a strong sense of confidence (from the LLM). If I had to visualize using AI for security work a few years back, this would come to mind:

I pointed an agent at a bootloader. It found bugs but not useful ones
GPT-3 thinking

The above may be explained with what everybody was trying to do at the time: 0-shot prompting for a 0-day. This was largely due to the tiny context window of 2048, then 8192, and later a very much welcomed 128000 tokens. Tiny by today's standards. A lot has changed since then, and I hope we're catching up to the current developments, as the development speed at which not just AI security works but also AI advances is scarily fast in my humble opinion.

Anyhow, to do all of this properly, I have to start where everyone started. So this post is deliberately the 2023/2024 version of the idea: one agent, one (big) context window, one repository, and a prompt that basically says, "Here, go find me something." No framework, no orchestration, no pipeline. Just me giving a model a multifaceted job that would normally take a person a couple of days to weeks. I ran the experiment against a public Qualcomm source. It found bugs. The bugs are not good (as expected). That combination is the whole point, so let me walk you through it in detail before I explain why.

Note If you are here for a dramatic 0-day, this is not that post. It is the post that explains why it wasn't, and I think the "why" is worth more than a CVE would have been.

The idea

I had this blog post series idea on my pile of side projects for ages, but life kept me busy. However, recently I finished my secure-boot writeup. If you have not read it, it was about how a cryptographically flawless signature check can still leave the parsers behind it exposed. So my headspace was still kind of stuck in that whole "embedded security" world when I (finally) started writing this one. So this blog will overlap with the discussed targets from the aforementioned write-up. I figured Qualcomm's Android Boot Loader would be a good place to start because it is one of the few pieces of this stack that is actually public. It is proper C, and it is full of parsers that need to handle attacker-influenced data: sparse images, boot image headers, partition tables, and device trees. When looking at a typical Qualcomm Android boot chain it roughly looks like this:

  PBL                  on-die mask ROM
   |
   v
  XBL                  Qualcomm's UEFI core: edk2-based, but
   |                   PROPRIETARY and closed (xbl.elf)
   v
  ABL                  a UEFI application launched by XBL (abl.elf)
   |  \
   |   `--> QcomModulePkg              [ OUR TARGET ]
   |          from CodeLinaro  clo/le/abl/tianocore/edk2
   |          LinuxLoaderEntry (the app entry), BootLib,
   |          FastbootLib, AVB, boot.img / slot / DTB-DTBO
   v
  Linux / Android

So what I set out to do was fuzz the Android Boot Loader, in particular the QcomModulePkg. To the best of my knowledge, this public tree lives on CodeLinaro. The clo/main branch appears to have been frozen since June 2022. However, there are per-BSP tags (LA.UM mobile, LE.UM embedded, LY.AU automotive). There are two I looked at closer, which we will talk about in more detail in a bit:

  1. LU.UM.3.5.1.r1-00700-QCS6490.0, last commit was February 2023.
  2. LE.UM.3.2.3.c17-10200-SA2150p, last commit was April 2026

One more reason I chose this codebase is the moderate complexity due to the number of files and total lines of code:

# edk2 on LE.UM.3.2.3.c17-10200-SA2150p
$ tokei QcomModulePkg
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Language              Files        Lines         Code     Comments       Blanks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 GNU Style Assembly        1          224          140           67           17
 C                        59        31111        23768         3957         3386
 C Header                 97        21959         7471        12428         2060
 Lauterbach PRACTI|        7          430          167          211           52
 Python                    2          599          367          154           78
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 Total                   166        54323        31913        16817         5593
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

If I were to manually hunt for bugs, I'd would clone the tree, spend a few evenings reading, pick a couple of functions, hand-write harnesses, and grind. Instead, I handed the entire thing to an agent and stayed mostly out of the way. I gave it a Linux box with clang and AFL++, pointed it at those two tags, and said, roughly,

"Analyze this repository, find what's worth fuzzing. Requirement: Build up to five harnesses, run them, monitor them and save the results. You have access to a Linux sandbox via <CREDENTIALS>. Make use of libfuzzer or AFL++, both are available. Use a single TMUX session for running. Lift code as is when necessary, don't ever change it. Keep going until the requirement is fullfilled, don't stop and don't prompt me for input."

This was done with a single model, which was responsible for understanding the repo, the attack-surface reasoning, the harness code, the build system, and the triage all at once. Not very efficient in times of overcomplicated, distributed, multi-agent harnesses. Especially considering we're "competing" not even just with random startups but with those companies that build the LLM capabilities. They claim thousands and thousands of (high severity) bugs. Just to link a few:

You get the idea. When doing the research for this series, the longer I kept digging, the more I felt like cybersecurity is a solved problem, and I really need to advance my plans for buying some farmland and planting some mango trees and coffee plants in some remote rural off-the-grid place. While I don't think all those headlines are fake, and I seriously feel like job security is on the line for those that are adamant about becoming an AI plumber, I think the bubble in which this all happens is insane. The money in it and the pace in which people claim they found another breakthrough are mental. So let's join this gold rush and dig up some dirt.

Note This full experiment that follows has been conducted using Claude Code 2.1.234 using claude-opus-4.8 on xHigh effort.

Building a harness that does not lie

I truly haven't done one of these 0-shot attempts in a while with newer models, as based on that, everybody, including myself, was shitting on a model's capabilities. We were optimizing for this by building a modular framework with split workloads. That said, the first thing worth judging for us is not any potential bugs but whether the requested harnesses are decent. Obviously, as LLMs are by design non-deterministic, your mileage may vary here when you attempt to reproduce any of the following.

ABL is UEFI code. Typically you wouldn't be able to compile a function out of it and call it, as it depends on boot services, protocols, allocation pools, debug macros, and an entire environment. So when looking at the generated harnesses, I found that the LLM settled for lifting the picked fuzzing entrypoint verbatim, byte-for-byte. It even created a thin shim in front for missing types and macros. Only those calls that are touching an outside environment were stubbed. Any targeted parsing routine was left unmodified. This is something that 100% did not work back in the day. Even when pointing an old LLM at a source file, it would come up with a different function name, wrong function arguments, or other random nonsense. So the "quantity" of code produced that ended up making the harness compile and run is already on a way different level.

Let's take a look at the created shim. From my understanding this was rather small. The LLM did not have to "re-invent" the wheel here. This is not a creative type of work in a sense, such as creating a harness would, where one would need to think of what APIs to call, in which order, basically creating something from scratch. The lifting "just" requires understanding of which types were missing and where they are located in the original source. So without more rambling, here is the shim:

// file: edk2_shim.h
/*
 * Minimal EDK2 / UEFI shim: just enough for the lifted QcomModulePkg sparse
 * code to compile and run on a host libFuzzer/ASan build. Every macro/type
 * here mirrors the real EDK2 semantics the lifted code relies on.
 */
#ifndef EDK2_SHIM_H
#define EDK2_SHIM_H

#include <stdint.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

/* --- EDK2 source annotations (no-ops on host) ------------------------- */
#ifndef IN
#define IN
#endif
#ifndef OUT
#define OUT
#endif
#ifndef OPTIONAL
#define OPTIONAL
#endif
#ifndef CONST
#define CONST const
#endif
#ifndef STATIC
#define STATIC static
#endif

/* --- base types ------------------------------------------------------- */
typedef uint8_t   UINT8;
typedef uint16_t  UINT16;
typedef uint32_t  UINT32;
typedef uint64_t  UINT64;
typedef int8_t    INT8;
typedef int16_t   INT16;
typedef int32_t   INT32;
typedef int64_t   INT64;
typedef uintptr_t UINTN;
typedef intptr_t  INTN;
typedef unsigned char BOOLEAN;
typedef void      VOID;
typedef char      CHAR8;
typedef uint16_t  CHAR16;
typedef UINTN     EFI_STATUS;
typedef VOID     *EFI_HANDLE;

#ifndef TRUE
#define TRUE  ((BOOLEAN)1)
#endif
#ifndef FALSE
#define FALSE ((BOOLEAN)0)
#endif

#define MAX_UINT32 ((UINT32)0xFFFFFFFFU)
#define MAX_UINT64 ((UINT64)0xFFFFFFFFFFFFFFFFULL)

/* --- EFI_STATUS (high bit = error, matches EDK2 ENCODE_ERROR) ---------- */
#define ENCODE_ERROR(a) ((EFI_STATUS)(((UINTN)1 << (sizeof(UINTN) * 8 - 1)) | (a)))
#define EFI_ERROR(s)    (((INTN)(UINTN)(s)) < 0)
#define EFI_SUCCESS           ((EFI_STATUS)0)
#define EFI_INVALID_PARAMETER ENCODE_ERROR(2)
#define EFI_BAD_BUFFER_SIZE   ENCODE_ERROR(4)
#define EFI_OUT_OF_RESOURCES  ENCODE_ERROR(9)
#define EFI_DEVICE_ERROR      ENCODE_ERROR(7)
#define EFI_NO_MEDIA          ENCODE_ERROR(12)
#define EFI_VOLUME_CORRUPTED  ENCODE_ERROR(10)
#define EFI_VOLUME_FULL       ENCODE_ERROR(11)
#define EFI_NOT_FOUND         ENCODE_ERROR(14)
#define EFI_UNSUPPORTED       ENCODE_ERROR(3)

#define MAX_GPT_NAME_SIZE 72

/* --- DEBUG(): single-arg no-op that swallows the (LEVEL, fmt, ...) tuple */
#define EFI_D_ERROR   0
#define EFI_D_INFO    0
#define EFI_D_VERBOSE 0
#define DEBUG(Expression)

/* --- overflow guard the lifted code calls ----------------------------- */
#define CHECK_ADD64(a, b) (((UINT64)(a) + (UINT64)(b)) < (UINT64)(a))

/* --- fake BlockIo protocol -------------------------------------------- */
/* sparse/META read only Media->BlockSize (positional init { BlockSize });
 * the GPT path also needs Media->MediaId and a WriteBlocks() stub. New
 * fields are APPENDED so the existing positional initializers stay valid. */
typedef UINT64 EFI_LBA;
typedef struct { UINT32 BlockSize; UINT32 MediaId; } EFI_BLOCK_IO_MEDIA;
struct EFI_BLOCK_IO_PROTOCOL_s;
typedef EFI_STATUS (*EFI_BLOCK_WRITE_BLOCKS) (
    struct EFI_BLOCK_IO_PROTOCOL_s *This, UINT32 MediaId, EFI_LBA Lba,
    UINTN BufferSize, VOID *Buffer);
typedef struct EFI_BLOCK_IO_PROTOCOL_s {
  EFI_BLOCK_IO_MEDIA    *Media;
  EFI_BLOCK_WRITE_BLOCKS WriteBlocks;
} EFI_BLOCK_IO_PROTOCOL;

/* --- pool allocators -------------------------------------------------- */
static inline VOID *AllocateZeroPool(UINTN Size) { return calloc(1, (size_t)Size); }
static inline VOID  FreePool(VOID *P) { free(P); }

#ifndef ARRAY_SIZE
#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))
#endif

#endif /* EDK2_SHIM_H */

It is types, a DEBUG that expands to nothing and pool allocators that are just calloc/free, so the lifted code compiles and runs, but its logic is exactly as shipped. Every harness in this post includes it...

In my initial prompt I was strict about one thing. The lifted function stays exactly as shipped. Around that premise, to my surprise, a core driver was built that shapes fuzzer bytes into something the parser will accept, the device-side calls are stubbed, and an oracle is watching the write operations. Two properties make the result trustworthy-ish, and I checked both when inspecting what has been delivered:

  1. A canary proves the oracle works. Each harness gets a deliberately broken input that must crash under AddressSanitizer. If the canary does not fire, the harness is lying. I'll omit for brevity, but every harness had a macro definition that would test the API, and compilation worked with a deliberate crash to show it,
  2. Coverage proves reachability. If the fuzzer never hits the branch we wanted to hit, then basing our interpretation of "no crashes found" is wrong. "No coverage" or "wrong coverage" was the culprit. So each target got a coverage run as well.

Ultimately what I observed when I gave the LLM the task is that the agent was running a small loop without me having ever prompted it to do so.

lift verbatim -> plant canary -> build (libFuzzer or AFL++) -> seeds
        -> run -> triage -> guard-and-refuzz ---+
                    ^                           |
                    +---------------------------+

This is beyond anything that would have happened a few years ago. Again, I'm repeating myself here, but if we were lucky back in the day (gosh, that sounds weird), an LLM maybe got as far as to create a LLVMFuzzerTestOneInput-style libfuzzer harness (when explicitly prompted) that makes a single API call with hopefully correctly typed arguments and then attempts to compile it. It was often dumbfounded when any of this wouldn't have worked. So yes, seeing the progress here is actually very nice. That said, I'm not going into much detail now about why this single agent loop it produced may not be very efficient or cost-effective. We're getting to that eventually. With the method that was used repeatedly by the LLM explained, the rest of the post is a mini technical deep dive, one harness at a time.

Seeds and coverage: proving a parser was reached

Before I start throwing coverage percentages around, two things have to hold: the fuzzer has to actually reach the code (it targeted), and I have to be able to prove it did. Skipping either and a run really doesn't mean much. So looking at this from a fuzzing point of view, we could say that if we point a mutator at raw random bytes, it will burn a lot of budget just to bypass some magic constants or size constraint checks. It will likely only by chance (if even) touch the core logic we care about and could potentially break. So obviously one way to analyze this is coverage information, and the LLM decided on its own accord that analyzing coverage metrics is the way to go to determine whether a fuzzing harness is making legit progress or whether it just compiles and runs. Every target ships a small generator that hands the fuzzer a structurally valid input to start from.

random bytes           ->  [ magic + size gate ]  ->  rejected     (0% of the parser)
a seed (valid header)  ->  [ magic + size gate ]  ->  real logic   (the part that breaks)
                          ^
                          the generator writes that valid header, so run #1 lands
                          past the gate instead of grinding toward it

Obviously the specifics on how that looks like differ per target and I'll talk about them later when we discuss the harnesses itself. The point here being, raw byte mutations and coverage tracking are one half of the equation that the LLM attempted to solve. The other half are good seeds. For the LLM those were not "nice-to-have things", it went ahead and made sure every harness gets kickstarted with some. So the "thought process" if you want to call it that, of the LLM I used must have reached a state that said, "Having no crashes from a fuzzer that never arrived where it was supposed to arrive is worthless. I cannot trust a harness without a coverage number sitting next to it". So, for each harness, it self-reviewed the coverage information by building the harness target like this:

build: clang -fprofile-instr-generate -fcoverage-mapping
             |
             v   run over the corpus
          default.profraw
             |
             v   llvm-profdata merge
          app.profdata
             |
             v   llvm-cov report   over   *_extract.c
          lines / functions / branches actually reached

That in itself was again interesting, as my prompt I provided was not necessarily guiding it towards this approach. I kept it vague on purpose to see how far we've actually come. With that introduced, let's check the harnesses and their performance.

Three parsers that held up

So as stated before, I requested up to five harnesses. I was kind of pushing it with that, but I wanted to see just how much a 2026 LLM can achieve without looking at cost, tokens spent, and time taken to finish the request. Those are all metrics for another part in this series. That said, me specifically mentioning "up to" was a test from my side to see if the LLM was taking this upper limit into consideration or if it just tunnel visioned hard on the five. It did the latter. It produced five harnesses. Three out of those five found nothing. They built correctly, and they were exercising real code, not just dummies or shim sections, and they produced coverage, just no crashes. I'd argue these are still worth a section to explore what has been fuzzed.

So in good academic fashion, first some stats. The fuzzers have been running close to 44 hours (whoops, I wanted to let them run for a few, but then life happened). These three harnesses I'll quickly walk through logged like 80 billion executions in total (about 5B on sparse, 36B on META, and 42B on the boot header), each pinned to a single core of a 14-core box (laptop with Intel(R) Core(TM) Ultra 7 155U) at anywhere from ~10k to ~70k executions per second. So the bottom line here is: They were running for a considerable amount of time and at excellent speeds. However, a shallow fuzzer with nothing to exercise will always be excellent in speed...

Sparse images: guarded arithmetic, no crash

This one is interesting. Sparse image flashing (HandleSparseImgFlash, plus HandleChunkTypeRaw/HandleChunkTypeFill and ValidateChunkDataAndFlash, in FastbootCmds.c) is a textbook target: an attacker could supply a flashed image, and the loader needs to parse it before it can trust it. I assume this target was chosen for this exact reason, with the premise that a loader parsing potentially untrusted data could be worth a look.

To give some technical background on this one. An Android sparse image is a small header followed by a run of chunks. The sparse_header carries the block size, the total block count, and how many chunks follow. Each chunk_header then announces what kind of chunk it is (raw, fill, don't-care, or CRC) and how big it is. HandleSparseImgFlash walks them in order, and for every chunk, it multiplies the chunk's block count by the block size to work out how many bytes to move, accumulating an offset as it goes. That multiply-and-accumulate over attacker-controlled counts is the whole reason this is worth a look (I assume). Putting this into some structural diagram:

Android sparse image
====================
   +---------------------------------------------------------+
   | sparse_header : magic 0xed26ff3a, blk_sz, total_blks,   |
   |                 total_chunks                            |
   +---------------------------------------------------------+
   | chunk_header  : chunk_type, chunk_sz (blocks), total_sz |
   | payload       : RAW = chunk_sz*blk_sz bytes, FILL = 4,  |
   |                 DONT_CARE / CRC = 0 / 4                 |
   +---------------------------------------------------------+
   |  ... repeated total_chunks times ...                   |
   +---------------------------------------------------------+

  the walk (HandleSparseImgFlash):

     for chunk in 0 .. total_chunks:
         bytes = blk_sz * chunk_sz          <-- attacker-controlled multiply
         RAW       -> WriteToDisk(payload, bytes)
         FILL      -> WriteToDisk(fill,    bytes)
         DONT_CARE -> advance the offset, no write
         CRC32     -> checksum only

So this walking the structure and calculating offsets and the total bytes is an arithmetic. Arithmetic operations are often prone to overflows. So it definitely kind of checks out that this could be worth fuzzing. The harness built around this follows exactly that logic. A lifted HandleSparseImgFlash and its chunk handlers stay as the core logic, WriteToDisk becomes a memcpy into a 64MiB buffer, the partition lookups are stubbed, and the driver keeps the header valid on every iteration so the fuzzer stays down in the chunk loop instead of dying on the magic number. 

// file: sparse_harness.c
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>

#include "edk2_shim.h"
#include "sparse_format.h"

/* Defined in FastbootCmds_extract.c: sets up the stub partition and
 * calls the lifted HandleSparseImgFlash(). */
extern EFI_STATUS SparseFuzzEntry(VOID *Image, UINT64 sz);

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
{
    if (Size < sizeof(sparse_header_t))
        return 0;

    /* The parser writes into the buffer in place, so hand it a private,
     * exactly-sized allocation and let ASan police the bounds. */
    uint8_t *Image = (uint8_t *)malloc(Size);
    if (!Image)
        return 0;
    memcpy(Image, Data, Size);

#ifdef NORMALIZE_HEADER
    /* AFL++ build: it does not call LLVMFuzzerCustomMutator, so keep the
     * sparse header valid here so the bytes still reach the chunk loop. */
    {
        sparse_header_t *h = (sparse_header_t *)Image;
        h->magic         = SPARSE_HEADER_MAGIC;
        h->major_version = 1;
        h->file_hdr_sz   = (uint16_t)sizeof(sparse_header_t);
        h->chunk_hdr_sz  = (uint16_t)sizeof(chunk_header_t);
        h->blk_sz        = 512u * (1u + (h->blk_sz & 7u));
    }
#endif

    SparseFuzzEntry(Image, (UINT64)Size);   /* -> lifted HandleSparseImgFlash */

    free(Image);
    return 0;
}

#ifndef AFL_BUILD
/* libFuzzer build: keep the sparse header valid after each mutation so inputs
 * reach the chunk loop instead of dying at the magic / size gates. The chunk
 * stream is left free to mutate, because that is the target. */
size_t LLVMFuzzerMutate(uint8_t *Data, size_t Size, size_t MaxSize);
size_t LLVMFuzzerCustomMutator(uint8_t *Data, size_t Size, size_t MaxSize,
                               unsigned int Seed)
{
    (void)Seed;
    size_t n = LLVMFuzzerMutate(Data, Size, MaxSize);
    if (n >= sizeof(sparse_header_t)) {
        sparse_header_t *h = (sparse_header_t *)Data;
        h->magic         = SPARSE_HEADER_MAGIC;
        h->major_version = 1;
        h->file_hdr_sz   = (uint16_t)sizeof(sparse_header_t);  /* 28 */
        h->chunk_hdr_sz  = (uint16_t)sizeof(chunk_header_t);   /* 12 */
        h->blk_sz        = 512u * (1u + (h->blk_sz & 7u));
    }
    return n;
}
#endif

With the harness, the LLM created a header file as well:

// file: sparse_format.h
/*
 * Verbatim from QcomModulePkg/Library/FastbootLib/SparseFormat.h
 * (CodeLinaro tag LU.UM.3.5.1.r1-00700-QCS6490.0). Original AOSP/Qualcomm
 * license headers apply. Kept byte-identical so struct layout matches the
 * lifted parser exactly.
 */
#ifndef SPARSE_FORMAT_H
#define SPARSE_FORMAT_H

#include "edk2_shim.h"

typedef struct sparse_header {
  UINT32 magic;         /* 0xed26ff3a */
  UINT16 major_version; /* (0x1) - reject images with higher major versions */
  UINT16 minor_version; /* (0x0) - allow images with higer minor versions */
  UINT16 file_hdr_sz;   /* 28 bytes for first revision of the file format */
  UINT16 chunk_hdr_sz;  /* 12 bytes for first revision of the file format */
  UINT32 blk_sz;       /* block size in bytes, must be a multiple of 4 (4096) */
  UINT32 total_blks;   /* total blocks in the non-sparse output image */
  UINT32 total_chunks; /* total chunks in the sparse input image */
  UINT32
      image_checksum; /* CRC32 checksum of the original data, counting "don't
                         care" */
} sparse_header_t;

#define SPARSE_HEADER_MAGIC 0xed26ff3a

#define CHUNK_TYPE_RAW 0xCAC1
#define CHUNK_TYPE_FILL 0xCAC2
#define CHUNK_TYPE_DONT_CARE 0xCAC3
#define CHUNK_TYPE_CRC 0xCAC4

typedef struct chunk_header {
  UINT16 chunk_type; /* 0xCAC1 -> raw; 0xCAC2 -> fill; 0xCAC3 -> don't care */
  UINT16 reserved1;
  UINT32 chunk_sz; /* in blocks in output image */
  UINT32 total_sz; /* in bytes of chunk input file including chunk header and
                      data */
} chunk_header_t;

typedef struct SparseImgParams {
  UINT32 Chunk;
  UINT32 TotalBlocks;
  UINT64 ChunkDataSz;
  UINT64 ImageEnd;
  UINT64 WrittenBlockCount;
  UINT64 BlockCountFactor;
  UINT64 PartitionSize;
  EFI_BLOCK_IO_PROTOCOL *BlockIo;
  EFI_HANDLE *Handle;
} SparseImgParam;

#endif /* SPARSE_FORMAT_H */

There's one thing we haven't touched yet. The sparse_harness.c shows a call to SparseFuzzEntry but never defines it in the harness. That function symbol got placed in the lifted code:

// file: FastbootCmds_extract.c
EFI_STATUS
SparseFuzzEntry (VOID *Image, UINT64 sz)
{
  /* PartitionName is only touched by the (stubbed) partition lookup. */
  return HandleSparseImgFlash ((CHAR16 *)u"system", 6u, Image, sz);
}

HandleSparseImgFlash in that same file is a byte-identical copy of the repository code. It wants a real partition to flash to, so the extract fakes precisely that and nothing more: a 64 MiB heap buffer stands in for the system partition, GetPartitionSize returns its size, and WriteToDisk is replaced by a bounds-checked copy into that buffer. This is what the fuzzing harness that got created targets. The created flow looks like this:

fuzzer bytes
  -> sparse_harness.c : LLVMFuzzerTestOneInput
  -> SparseFuzzEntry             (adapter, in the harness)
  -> HandleSparseImgFlash        <- verbatim Qualcomm code
  -> HandleChunkTypeRaw / Fill   <- verbatim Qualcomm code
  -> WriteToDisk                 <- the ONLY stub in the chain (the oracle)

Everything above WriteToDisk is Qualcomm's unmodified code. That was more to discuss about the created structure than I had anticipated, so let's leave it at that, and I'll shorten it for the other examples. The takeaway is that the created setup around the harness is far from naive. The LLM tried to achieve a lot. Whether that was the correct choice is a discussion for another day.

Ultimately, what we care about is the following: Did the fuzzer reach that arithmetic we discussed, or just bounce off the header? I did analyze the coverage, which says it got all the way in: over 75% of lines and 100% of functions, and all four chunk types were covered. The fuzzer managed to run the whole chunk loop and reached the size math many, many times.

$ llvm-cov report ./sparse_fuzz -instr-profile=sparse.profdata FastbootCmds_extract.c

Filename                  Regions  Miss   Cover   Funcs  Miss    Cover    Lines  Miss   Cover   Branch  Miss   Cover
-------------------------------------------------------------------------------------------------------------------
FastbootCmds_extract.c        268    60  77.61%      8     0  100.00%      322    68  78.88%      116    32  72.41%

This resulted in zero crashes, and one thing that stands out as why that is seems to be the CHECK_ADD64 routine that guards every one of those add operations:

/* Return True if integer overflow will occur */
#define CHECK_ADD64(a, b) ((MAX_UINT64 - b < a) ? TRUE : FALSE)

This is something a human reviewer likely would have caught. Source code that's littered with safe-math checks. Even if the macro is defined in a different file, a modern IDE makes this a one-shortcut jump. It was a good effort. How good is this harness, really? Structurally, better than I went in expecting. The parser is lifted byte-for-byte, so I'm fuzzing Qualcomm's code here. Having this end-to-end harness + stub + shim + libfuzzer and AFL++ support in a single query would not have worked before. This makes this blog/research worthwhile.

This brings us to the end of harness one. For the other two that produced no crashes, I'll shorten some of the background story and focus on the what has been fuzzed, and reason about why that was the case. I'll spare you the full walkthrough as with the sparse_harness.c whenever the produced artifact(s) are nearly identical. Without further ado, let's go for the next one.

META images: offsets checked before use

META flashing (HandleMetaImgFlash, the same file and tag as the sparse one). This looks like another fastboot flash path. Instead of a single image, this takes a blob that packs several sub-images together and flashes them in one shot. The header also has some magic bytes (0xce1ad63c) and is followed by a table of entries. One entry per included image. Each entry contains a partition name and some start_offset and size values that point into the actual payload. Again, we have a loader that walks the structure, and when doing so, each section gets handed to a single image flasher: HandleRawImgFlash. This looks very similar to our sparse case. I can see why an LLM would pick this after the earlier harness.

META image
==========
   +---------------------------------------------------------+
   | meta_header : magic 0xce1ad63c, meta_hdr_sz, img_hdr_sz |
   +---------------------------------------------------------+
   | img_header_entry[0] : ptn_name[72], start_offset, size  |
   | img_header_entry[1] : ...                               |
   |  ... up to MAX_IMAGES_IN_METAIMG (32) entries ...       |
   +---------------------------------------------------------+
   | payload : sub-image bytes, addressed by each entry's    |
   |           (start_offset, size) into this region         |
   +---------------------------------------------------------+

As before, this path is interesting for fuzzing, as it would contain potentially attacker-controlled offset and size values. These are used in the loader to access the image structure and, from a naive first thought, could potentially be used to access out-of-bounds addresses. So yes, this is similar to sparse. When looking at the produced artifacts, the LLM used the same formula for this one too. As promised I will spare you with the details here. The LLM lifted the function HandleMetaImgFlash and wrote a similar-style harness with an adapter function:

// file: meta_harness.c
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>

#include "edk2_shim.h"
#include "meta_format.h"

extern EFI_STATUS MetaFuzzEntry(VOID *Image, UINT64 Size);

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
{
    if (Size < sizeof(meta_header_t))
        return 0;
    uint8_t *Image = (uint8_t *)malloc(Size);
    if (!Image)
        return 0;
    memcpy(Image, Data, Size);
    MetaFuzzEntry(Image, (UINT64)Size);
    free(Image);
    return 0;
}

Checking the coverage information shows it ran and covered what it set out to do:

$ llvm-cov report ./meta_fuzz -instr-profile=meta.profdata MetaImg_extract.c -show-functions

Name                  Regions  Miss   Cover    Lines  Miss   Cover   Branch  Miss   Cover
-----------------------------------------------------------------------------------------
HandleMetaImgFlash         80    18  77.50%       89    25  71.91%       36    10  72.22%
HandleRawImgFlash           5     0 100.00%        9     0 100.00%        2     0 100.00%
TOTAL                     100    21  79.00%      111    27  75.68%       42    10  76.19%

HandleMetaImgFlash sits at 71.9% coverage, with HandleRawImgFlash fully exercised. Again, we still found zero crashes. Yes, I know coverage doesn't guarantee crashes, but at least having it covered would have given us a chance... Doing some quick root-cause analysis on why no crashes have been spotted, it's sadly the same shape and form as with the sparse harness: Before a single byte is copied, each entry runs through our known CHECK_ADD64 on its offset arithmetic and then is followed by a hard range check, ImageEnd < Image + start_offset + size, that rejects the entry with EFI_INVALID_PARAMETER. Where the first harnesses fully relied on CHECK_ADD64 around its-size math, META adds an explicit end-of-buffer bound on top of it. Fair enough. The bottom line here is that there's not much to say about the shape and quality. It's almost an identical copy from start (why it was picked) to finish (how it was fuzzed) compared to before. Now for the third harness... sadly, it doesn't shake things up yet.

Boot image headers: overflow checks do their job

The third harness targets the boot image header validator in the function CheckImageHeader. This function is responsible for validating a boot.img header before the kernel is unpacked. Different things are getting computed, like kernel size, ramdisk, and dtb. These all go through macros like ROUND_TO_PAGE or ADD_OF. These are designed to prevent overflows:

/* ADD_OF: BootLib/LinuxLoaderLib.h 
 * ROUND_TO_PAGE: Include/Library/BootLinux.h 
 */
#define ADD_OF(a, b)         ((MAX_UINT32 - (b) > (a)) ? ((a) + (b)) : ZERO)
#define ROUND_TO_PAGE(x, y)  ((ADD_OF ((x), (y))) & (~(y)))

The fuzzed codebase has three types of header versions it checks: v0, v1, and v2. The harness exercised all three versions, plus the recovery DTBO branch. The harness is the same lift-and-stub recipe as sparse and META, so I will omit the "analysis" for brevity. Here's the generated harness:

// file: bootimg_harness.c
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>

#include "edk2_shim.h"
#include "bootimg_format.h"

extern EFI_STATUS BootImgFuzzEntry(VOID *Buf, UINT32 Sz, BOOLEAN Recovery);

#define HDRBUF 4096   /* a boot header page; >= v0(1632)+v1(16)+v2(12) */

int LLVMFuzzerTestOneInput(const uint8_t *Data, size_t Size)
{
    uint8_t *buf = (uint8_t *)calloc(1, HDRBUF);   /* zero-padded page buffer */
    if (!buf)
        return 0;
    memcpy(buf, Data, Size < HDRBUF ? Size : HDRBUF);
    memcpy(buf, BOOT_MAGIC, BOOT_MAGIC_SIZE);      /* pass the magic gate */

    BootImgFuzzEntry(buf, HDRBUF, FALSE);          /* non-recovery path */
    BootImgFuzzEntry(buf, HDRBUF, TRUE);           /* recovery (v1/v2 dtbo) path */

    free(buf);
    return 0;
}

Checking the coverage, if that were our only metric to go by, we'd be happy:

$ llvm-cov report ./bootimg_fuzz -instr-profile=bootimg.profdata BootImg_extract.c

Filename            Regions  Miss  Cover    Funcs  Miss   Cover    Lines  Miss  Cover   Branch  Miss  Cover
---------------------------------------------------------------------------------------------------------
BootImg_extract.c       152     7  95.39%       3     0  100.00%      140     7  95.00%      62     3  95.16%

95% of lines and every function were entered, with a handful of missed lines sitting in a branch, which was gated behind something the harness did not model: DTBO_MAX_SIZE_ALLOWED. Again, we have seen no crashes. The constant use of ADD_OF returns ZERO instead of wrapping, and CheckImageHeader reads a zero result as "integer overflow" and bails out with EFI_BAD_BUFFER_SIZE. As soon as the fuzzer triggers a 32-bit wraparound, the fuzzed code exits early.

This brings me to the end of the third harness. This harness again re-used the same formula of lifting, shimming, and targeting a single API. What the LLM failed to grasp, for a third time in a row now, is that the function is "gated" behind overflow-safe math macros. The LLM targeted the function for the right reasons: attacker-controlled input data and potentially unsafe size and offset math, but it stopped there with the "analysis" of "is this worth fuzzing". Let's take a look at the remaining two. They at least bring something new to the table.

GUID Partition Tables: the first crash

This is the first harness that, when looking at the results, surprised me. The GPT writer (PatchGpt, WriteGpt, ParseGptHeader) could lead to a partition table being rewritten from an attacker-supplied image, using header-driven pointer arithmetic.

GPT flash image (attacker-supplied, in the download buffer)
==========================================================

   LBA 0   +-------------------------------------------------+
           | Protective MBR                                  |
   LBA 1   +-------------------------------------------------+
           | Primary GPT header : "EFI PART", HeaderCRC,     |  <- ParseGptHeader
           |   PartEntrySz = 128, MaxPtCnt (<= 128)          |     validates (CRC-32)
   LBA 2+  +-------------------------------------------------+
           | Partition entry array : MaxPtCnt slots x 128 B  |  <- PatchGpt walks it,
           |   [entry 0][entry 1] ... [entry MaxPtCnt-1]     |     counting populated
           +-------------------------------------------------+
           |  ... data ...  backup array  ...  backup header |
           +-------------------------------------------------+

At first glance it looks like the LLM used the same "winning recipe" (for creating the harness, not for finding 0-days) once more. It found a parser. The parser is doing some arithmetic operations. Those, based on historic knowledge, have a tendency to be prone to over- or underflows. A quick read shows a partition-entry walk that computes (count - 1) * entry_size with no guard on count being zero. I assume this was as well yet another reason a harness was built around this section of the code. The harness itself follows the same recipe once more:PatchGpt, WriteGpt, and ParseGptHeader are lifted verbatim. The on-storage device I/O is stubbed.

// file: harness_gpt.c
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>

#include "edk2_shim.h"
#include "gpt_format.h"

extern EFI_STATUS GptFuzzEntry (VOID *Buf, UINT32 Sz);

static void put_u32 (uint8_t *p, uint32_t v)
{
  p[0] = v; p[1] = v >> 8; p[2] = v >> 16; p[3] = v >> 24;
}
static void put_u64 (uint8_t *p, uint64_t v)
{
  for (int i = 0; i < 8; i++) p[i] = (v >> (8 * i)) & 0xff;
}
static uint32_t get_u32 (const uint8_t *p)
{
  return (uint32_t)p[0] | ((uint32_t)p[1] << 8) |
         ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
}

/* Make one GPT header pass ParseGptHeader while leaving MaxPtCnt and the LBAs
 * fuzzer-derived (clamped into the accepted range). Primary headers must carry
 * CurrentLba == GPT_LBA; secondary headers skip that check. */
static void repair_header (uint8_t *h, int primary)
{
  put_u32 (h + 0, GPT_SIGNATURE_2);
  put_u32 (h + 4, GPT_SIGNATURE_1);
  put_u32 (h + HEADER_SIZE_OFFSET, GPT_HEADER_SIZE);      /* 92 */
  put_u32 (h + PENTRY_SIZE_OFFSET, GPT_PART_ENTRY_SIZE);  /* 128 */
  if (primary)
    put_u64 (h + PRIMARY_HEADER_OFFSET, GPT_LBA);         /* CurrentLba == 1 */
  /* keep LBAs within DeviceDensity/BlkSz so the capacity checks pass */
  put_u64 (h + FIRST_USABLE_LBA_OFFSET, get_u32 (h + FIRST_USABLE_LBA_OFFSET) & 0xffff);
  put_u64 (h + LAST_USABLE_LBA_OFFSET,  get_u32 (h + LAST_USABLE_LBA_OFFSET)  & 0xffff);
  /* clamp MaxPtCnt into [0,128] (0 is accepted by the real validation) */
  put_u32 (h + PARTITION_COUNT_OFFSET,
           get_u32 (h + PARTITION_COUNT_OFFSET) % (MAX_NUM_PARTITIONS + 1));
  /* recompute header CRC over HeaderSz bytes with the CRC field zeroed */
  put_u32 (h + HEADER_CRC_OFFSET, 0);
  uint32_t crc = 0;
  ShimCalculateCrc32 (h, GPT_HEADER_SIZE, &crc);
  put_u32 (h + HEADER_CRC_OFFSET, crc);
}

int LLVMFuzzerTestOneInput (const uint8_t *Data, size_t Size)
{
  uint8_t *buf = (uint8_t *)calloc (1, GPT_SCRATCH);
  if (!buf)
    return 0;
  memcpy (buf, Data, Size < GPT_SCRATCH ? Size : GPT_SCRATCH);

  /* protective MBR at LBA0 -> route PartitionGetType to the GPT branch */
  buf[MBR_SIGNATURE]     = MBR_SIGNATURE_BYTE_0;
  buf[MBR_SIGNATURE + 1] = MBR_SIGNATURE_BYTE_1;
  buf[MBR_PARTITION_RECORD + OS_TYPE] = GPT_PROTECTIVE;

  /* PartEntrySz==128 & MaxPtCnt<=128 pin PartEntryArrSz to MIN_PARTITION_ARRAY_SIZE,
   * so WriteGpt places the backup header at:
   *   SecondaryGptHdr = Gpt + 2*BlkSz + 2*PartEntryArrSz
   * (PrimaryGptHdr = Gpt + BlkSz, then Offset=2*PartEntryArrSz + BlkSz on top). */
  repair_header (buf + GPT_BLKSZ, 1);                                       /* primary */
  repair_header (buf + 2 * GPT_BLKSZ + 2 * MIN_PARTITION_ARRAY_SIZE, 0);    /* backup  */

  /* Sz models the download size; keep the trailing SetMem(PrimaryGptHdr, Sz)
   * inside the scratch region (buf + BlkSz + Sz <= GPT_SCRATCH). */
  GptFuzzEntry (buf, GPT_SCRATCH - GPT_BLKSZ);

  free (buf);
  return 0;
}

The GptFuzzEntry stub looks like this:

EFI_STATUS
GptFuzzEntry (VOID *Buf, UINT32 Sz)
{
  FlashingGpt = FALSE;
  ParseSecondaryGpt = FALSE;
  return UpdatePartitionTable ((UINT8 *)Buf, Sz, 0, (struct StoragePartInfo *)0);
}

Two choices the LLM made here are specific to this target:

  1. The driver keeps a real CRC-32 in the loop instead of stubbing it to always pass. The harness also repairs the header each iteration so the fuzzer reaches the arithmetic through a valid checksum rather than around a disabled one. ShimCalculateCrc32 is a real CRC calculation. It's not just a stub that returns "OK". This in turn should mean the GPT image has a valid shape.
  2. The driver backs the parser with a 512 KiB scratch buffer that models the fastboot download region.

To reach the aforementioned arithmetic at all, ParseGptHeader has to accept the image twice, once for the primary header and once for the backup that sits after the entry array. Additionally, it requires a valid EFI PART signature, a header size between 92 and the block size, a correct CRC32, first and last usable LBAs inside the device capacity, a partition-entry size of exactly 128, and a partition count no larger than 128. These constraints were fully identified by the LLM and put inside the harness in the repair_header function. The discussed double parsing can be seen in the WriteGpt function:

// file: PartitionTableUpdate.c
STATIC UINT32 
WriteGpt (INT32 Lun, UINT32 Sz, UINT8 *Gpt) 
{
  // <SNIP>

  /* Verity that passed block has valid GPT primary header */
  PrimaryGptHdr = (Gpt + BlkSz);
  Ret = ParseGptHeader (&GptHeader, PrimaryGptHdr, DeviceDensity, BlkSz);
  if (Ret) {
    DEBUG ((EFI_D_ERROR, "GPT: Error processing primary GPT header\n"));
    return Ret;
  }

  /* Check if a valid back up GPT is present */
  PartEntryArrSz = GptHeader.PartEntrySz * GptHeader.MaxPtCnt;
  if (PartEntryArrSz < MIN_PARTITION_ARRAY_SIZE)
    PartEntryArrSz = MIN_PARTITION_ARRAY_SIZE;

  /* Back up partition is stored in the reverse order with back GPT, followed by
   * part entries, find the offset to back up GPT */
  Offset = (2 * PartEntryArrSz);
  SecondaryGptHdr = Offset + BlkSz + PrimaryGptHdr;
  Ret = ParseGptHeader (&GptHeader, SecondaryGptHdr, DeviceDensity, BlkSz);
  if (Ret) {
    DEBUG ((EFI_D_ERROR, "GPT: Error processing backup GPT header\n"));
    return Ret;
  }

  Ret = PatchGpt (Gpt, DeviceDensity, PartEntryArrSz, &GptHeader, BlkSz);

  // <SNIP>
}

The backup header is located 2 * PartEntryArrSz + BlkSz past the primary, so it sits after the entry array, and both calls have to return zero before control ever reaches PatchGpt. The LLM seems to kind of understood the difficulty for a fuzzer to reach deep here and added the header repairs accordingly. The fuzzer was left on autopilot for two things specifically: the partition count and the entry-array bytes. And surprisingly, the fuzzer found something:

$ ./gpt_fuzz findings/gpt_underflow_repro_maxptcnt0.bin
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3122206987
INFO: Loaded 1 modules   (959 inline 8-bit counters): 959 [0x55d047914318, 0x55d0479146d7),
INFO: Loaded 1 PC tables (959 PCs): 959 [0x55d0479146d8,0x55d0479182c8),
./gpt_fuzz: Running 1 inputs 1 time(s) each.
Running: findings/gpt_underflow_repro_maxptcnt0.bin
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1553839==ERROR: AddressSanitizer: SEGV on unknown address 0x7f0c83679ba8 (pc 0x55d047892639 bp 0x7ffe36738380 sp 0x7ffe36738160 T0)
==1553839==The signal is caused by a WRITE memory access.
    #0 0x55d047892639 in PatchGpt /home/pwn/abl-sparse-fuzz/PartitionTable_extract.c:290:3
    #1 0x55d047892639 in WriteGpt /home/pwn/abl-sparse-fuzz/PartitionTable_extract.c:396:9
    #2 0x55d0478913e7 in UpdatePartitionTable /home/pwn/abl-sparse-fuzz/PartitionTable_extract.c:489:11
    #3 0x55d04788f9d4 in LLVMFuzzerTestOneInput /home/pwn/abl-sparse-fuzz/harness_gpt.c:89:3
    #4 0x55d0475fcefb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) fuzzer.o
    #5 0x55d0475e2338 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) fuzzer.o
    #6 0x55d0475eb644 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) fuzzer.o
    #7 0x55d0475d0bd7 in main (/home/pwn/abl-sparse-fuzz/gpt_fuzz+0x46bd7) (BuildId: 0129ff498bbb865459d0fc684e4591092aafebb0)
    #8 0x7f0b83427c8d  (/usr/lib/libc.so.6+0x27c8d) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #9 0x7f0b83427dca in __libc_start_main (/usr/lib/libc.so.6+0x27dca) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #10 0x55d0475d0ca4 in _start (/home/pwn/abl-sparse-fuzz/gpt_fuzz+0x46ca4) (BuildId: 0129ff498bbb865459d0fc684e4591092aafebb0)

==1553839==Register values:
rax = 0x0000000000000000  rbx = 0x00007ffe36738160  rcx = 0x0000000000000200  rdx = 0x0000000007ffffde
rdi = 0x0000000000000002  rsi = 0x00007f0b83679a00  rbp = 0x00007ffe36738380  rsp = 0x00007ffe36738160
 r8 = 0x0000000000000021   r9 = 0x0000000000000000  r10 = 0x0000000000000000  r11 = 0x0000000000000000
r12 = 0x00000000ffffffa8  r13 = 0x00007f0c83679ba8  r14 = 0x00007f0b83679c00  r15 = 0x0000000000004000
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV /home/pwn/abl-sparse-fuzz/PartitionTable_extract.c:290:3 in PatchGpt
==1553839==ABORTING

The bug sits in PatchGpt, here is the relevant section:

while ((TotalPart < GptHeader->MaxPtCnt) &&
       ((*LastPartitionEntry != 0) || (*(LastPartitionEntry + 1) != 0))) {
  TotalPart++;
  LastPartitionEntry = (UINT64 *)
    (PrimaryGptHeader + BlkSz + TotalPart * PARTITION_ENTRY_SIZE);
}
LastPartOffset = (TotalPart - 1) * PARTITION_ENTRY_SIZE + PARTITION_ENTRY_LAST_LBA;
PUT_LONG_LONG (PrimaryGptHeader + BlkSz + LastPartOffset, (UINT64)(NumSectors - 34));

If the entry array is empty, the loop never runs, TotalPart stays zero, and (TotalPart - 1) underflows the UINT32. LastPartOffset resolves to 0xFFFFFFA8, so PUT_LONG_LONG writes eight bytes about four gigabytes past the buffer. The saved reproducer literally contains nothing but zeros:

$ xxd findings/gpt_underflow_repro_maxptcnt0.bin
00000000: 0000 0000 0000 0000 0000 0000 0000 0000  ................
<...>
000085f0: 0000 0000 0000 0000 0000 0000 0000 0000  ................

The thing that makes this an awkward bug to talk about is the fact that the required input to trigger this bug in particular is just the empty input as well as minimized/found by libfuzzer:

$ xxd crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
ls -lh ./crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
-rw-r--r-- 1 pwn pwn 0 Aug 22 17:58 ./crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
$ ./gpt_fuzz ./crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 4152867095
INFO: Loaded 1 modules   (959 inline 8-bit counters): 959 [0x5618b5018318, 0x5618b50186d7),
INFO: Loaded 1 PC tables (959 PCs): 959 [0x5618b50186d8,0x5618b501c2c8),
./gpt_fuzz: Running 1 inputs 1 time(s) each.
Running: ./crash-da39a3ee5e6b4b0d3255bfef95601890afd80709
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1556384==ERROR: AddressSanitizer: SEGV on unknown address 0x7fdbe83f1ba8 (pc 0x5618b4f96639 bp 0x7ffde6f712e0 sp 0x7ffde6f710c0 T0)
==1556384==The signal is caused by a WRITE memory access.
    #0 0x5618b4f96639 in PatchGpt /home/pwn/abl-sparse-fuzz/PartitionTable_extract.c:290:3
<SNIP>

It's definitely not a useful bug. Whether it is a bug worth anyone's time is out of scope for now. I did take a look at this when triaging , and it seems to be at best a low severity one:

  • Reachability - CmdFlash -> UpdatePartitionTable -> WriteGpt -> ParseGptHeader (primary) -> ParseGptHeader (backup) -> PatchGpt. The input could be a downloaded flash image that needs to be fully attacker-controlled. This seems like it could be somehow pulled off, but the path to trigger the bug itself is more than gated.
  • Preconditions - From a quick look, CmdFlash seems to refuse flashing at all unless the device is unlocked and refuses critical partitions unless unlock-critical is also set. Also, as seen above, the maliciously crafted GPT image needs primary and backup headers, both of which need to pass through ParseGptHeader, with either a declared partition count of zero or a zeroed first entry so the walk ends at TotalPart == 0.
  • Impact - Meh

So this is not even worth reporting, so I did not. It's just a bug, not a vulnerability as far as I'm concerned. I did not have high hopes to find anything to begin with, so having this at all at this stage is surprising to me as I picked that repo at random. But on the bright side of things, we still got our last harness and, actually, a second bug. One interesting thing with this one is that the LLM picked up on all the conditions that needed to be satisfied. It built the repair_header for that. This is a significantly better understanding about the environment compared to the three earlier harnesses that were "only" gates by some arithmetic-safe math.

Device-tree glue: valid trees, unsafe strings

Okay, close to the end, last harness, last bugs. Let's get into it right away. This one is by far the most interesting one for multiple reasons. The device-tree glue (UpdateDeviceTree.c) that this resolves around is not a hand-rolled parser as in all cases before. It really is just glue on top of libfdt. libfdt is the standard flattened device tree library. While the library itself has not been fuzzed to death in OSS-FUZZ (as far as I could tell), it's definitely being pulled in by U-Boot and QEMU. Maybe fuzzing libfdt itself could be a nice endeavor, but this here is all about fuzzing the Qualcomm code sitting on top: the parts that take a property libfdt hands back and treat it like a trusted, null-terminated C string with a sane length.

Appended device tree (inside the AVB-verified boot image)
=========================================================

  boot.img
    +-- kernel
    +-- ramdisk
    +-- dtb  -->  flattened device tree, parsed by libfdt
                    |
                    +-- /firmware/android/fstab/<x>/dev = "...,/soc/..."  <- UpdateFstabNode
                    +-- /firmware/android/vbmeta         parts = "odm,..." <- UpdateVbmetaNode

The harness that was being built is mostly re-using the same recipe as all others as well. The functions of interest that are the bridge between the Qualcomm code and the libfdt side are lifted verbatim (UpdateFstabNode, UpdateVbmetaNode, QueryMemoryCellSize, and UpdateGranuleInfo). Instead of stubbing the device-tree library, the LLM decided to link the real one that was present on the sandbox I provided (libfdt 1.7.2 dynamically linked via -lfdt, and uninstrumented).

So before we jump into the findings, I noticed that the LLM made a particular decision for the harness that seemed to have made the whole thing work in the first place. The core problem with a byte mutator from a fuzzer is that no amount of random mutations (without guidance) will likely yield a valid device tree (as this is a complex structure). On the other hand, if we provide a semi-malformed blob to the Qualcomm glue code, it gets handed straight to the libfdt side of things. This likely would cause libfdt to crash or, more likely, discard such an input for further processing due to its own internal checks. The goal of this harness was not to fuzz libfdt itself but the Qualcomm-written glue. So what the LLM did now was that every fuzzer-generated input goes through fdt_check_full, a libfdt internal function that checks for malformations. Only those inputs that pass this check are structurally valid and "deemed" good enough to be passed to the lifted Qualcomm code. The harness itself is following the same shape and form as highlighted in the first half of the article, so I'll just dump the DtbFuzzEntry function, which is the actual entry point the LLVMFuzzerTestOneInput harness calls. I'm doing so because this time around it's not a single API but a linear execution of these also-aforementioned multiple API calls.

// file: UpdateDeviceTree_lifted.c
EFI_STATUS
DtbFuzzEntry (VOID *FdtBuf, UINTN Cap)
{
  UINT32 CellLen = 0;

  /* gate: only structurally valid device trees get past here */
  if (fdt_check_full (FdtBuf, (size_t)Cap) != 0)
    return EFI_NOT_FOUND;
  if (fdt_open_into (FdtBuf, FdtBuf, (int)Cap) != 0)
    return EFI_NOT_FOUND;

  /* everything below is lifted QcomModulePkg glue, run on a tree libfdt called valid */
  fdt_check_header_ext (FdtBuf);
  QueryMemoryCellSize (FdtBuf, &CellLen);
  UpdateGranuleInfo (FdtBuf);
  UpdateVbmetaNode (FdtBuf, (CHAR8 *)"odm", NULL);
  UpdateFstabNode (FdtBuf);

  return EFI_SUCCESS;
}

To summarize: libfdt vouches for the structure, which I think was a smart move by the LLM, and the glue then trusts whatever content sits inside that structure. So the harness ends up testing the exact thing I care about: does the Qualcomm code hold up when a device tree is well-formed but its property values are hostile?

Limitation The device tree these functions rewrite is not a loose file an attacker can drop on the device. It is baked inside a boot image itself, and on Android the boot image is checked by AVB (Android Verified Boot) before anything in it is used. Tamper with the tree on a locked device and verification fails. The phone stops booting and the modified tree never reaches the bug site.

Therefore, read everything below as post-unlock. Any of the following bugs will only be reached if the device is unlocked or if there's already a separate AVB bypass. Now let's dive into the findings!

fstab: a missing slash becomes a null dereference

UpdateFstabNode has a small job. The device tree ships with an fstab entry, the table that tells Android which storage partition to mount as root, and this function rewrites the boot-device path in that entry before the kernel reads it. To do the rewrite, it takes the existing dev string, finds the /soc/ marker inside it, and then searches for the next / after that marker to find where the old path ends. That linked search is where the bug sits:

// file: UpdateDeviceTree_lifted.c

// <SNIP>

ReplaceStr += AsciiStrLen (Table.DevicePathId);
NextStr = AsciiStrStr ((ReplaceStr + 1), "/");
DevNodeBootDevLen = NextStr - ReplaceStr;  // NextStr may be NULL
if (DevNodeBootDevLen >= AsciiStrLen (BootDevBuf)) {
  gBS->CopyMem (ReplaceStr, BootDevBuf, AsciiStrLen (BootDevBuf));
  PaddingEnd = DevNodeBootDevLen - AsciiStrLen (BootDevBuf);
  if (PaddingEnd) {
    gBS->CopyMem (ReplaceStr + AsciiStrLen (BootDevBuf), NextStr,
                  AsciiStrLen (NextStr));  // reads through NULL
    for (Index = 0; Index < PaddingEnd; Index++) {  // wild write, never reached
      ReplaceStr[AsciiStrLen (BootDevBuf) + AsciiStrLen (NextStr) + Index] = ' ';
    }
  }
}

In the above snippet, we can see the relevant code. NextStr is being used in the calculation of DevNodeBootDevLen without ever verifying whether the / was actually found.

Limitation After some investigation I found that this whole branch that was fuzzed only runs on builds where IsDynamicPartitionSupport() is false, so a modern device (e.g. Android 10+) using dynamic partitions never reaches it this bug at all.

Given a dev value that has the marker but no trailing /  in it, the search returns NULL. NextStr - ReplaceStr is then NULL minus a valid pointer (0 - ReplaceStr), which first underflows into a gigantic length and straight after runs into a call to AsciiStrLen(NextStr) which will cause a NULL-ptr dereference:

$ ./dtb_fuzz findings/dtb_fstab_nullderef_repro.dtb
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 3038691496
INFO: Loaded 1 modules   (243 inline 8-bit counters): 243 [0x55b073317a00, 0x55b073317af3),
INFO: Loaded 1 PC tables (243 PCs): 243 [0x55b073317af8,0x55b073318a28),
./dtb_fuzz: Running 1 inputs 1 time(s) each.
Running: findings/dtb_fstab_nullderef_repro.dtb
dtb_format.h:47:67: runtime error: null pointer passed as argument 1, which is declared to never be null
/usr/include/string.h:440:33: note: nonnull attribute specified here
SUMMARY: UndefinedBehaviorSanitizer: undefined-behavior dtb_format.h:47:67
AddressSanitizer:DEADLYSIGNAL
=================================================================
==1553786==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000000 (pc 0x7f5dda3aeddd bp 0x7ffce9ec7110 sp 0x7ffce9ec68b8 T0)
==1553786==The signal is caused by a READ memory access.
==1553786==Hint: address points to the zero page.
    #0 0x7f5dda3aeddd  (/usr/lib/libc.so.6+0x1aeddd) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #1 0x55b07318a419 in strlen.part.0 asan_interceptors.cpp.o
    #2 0x55b07329e753 in AsciiStrLen /home/pwn/abl-sparse-fuzz/./dtb_format.h:47:59
    #3 0x55b07329e753 in UpdateFstabNode /home/pwn/abl-sparse-fuzz/UpdateDeviceTree_extract.c:348:25
    #4 0x55b07329ee41 in DtbFuzzEntry /home/pwn/abl-sparse-fuzz/UpdateDeviceTree_extract.c:389:3
    #5 0x55b07329b99a in LLVMFuzzerTestOneInput /home/pwn/abl-sparse-fuzz/harness_dtb.c:30:3
    #6 0x55b073008fbb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) fuzzer.o
    #7 0x55b072fee3f8 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) fuzzer.o
    #8 0x55b072ff7704 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) fuzzer.o
    #9 0x55b072fdcc97 in main (/home/pwn/abl-sparse-fuzz/dtb_fuzz+0x40c97) (BuildId: e8bb876687682f2b52c55a9bc654d4d309e8d47a)
    #10 0x7f5dda227c8d  (/usr/lib/libc.so.6+0x27c8d) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #11 0x7f5dda227dca in __libc_start_main (/usr/lib/libc.so.6+0x27dca) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #12 0x55b072fdcd64 in _start (/home/pwn/abl-sparse-fuzz/dtb_fuzz+0x40d64) (BuildId: e8bb876687682f2b52c55a9bc654d4d309e8d47a)

==1553786==Register values:
rax = 0x0000000000000000  rbx = 0x0000000000000000  rcx = 0x0000000000000000  rdx = 0x0000000000000000
rdi = 0x0000000000000000  rsi = 0x0000000000000000  rbp = 0x00007ffce9ec7110  rsp = 0x00007ffce9ec68b8
 r8 = 0x00007b5dd7e003d0   r9 = 0x000055b073374b00  r10 = 0x00007ffce9ec7140  r11 = 0x0000000000000202
r12 = 0x00007f5dd9d318b6  r13 = 0xffff80a2262ce74a  r14 = 0x00000000e102dbd3  r15 = 0x0000000000000000
AddressSanitizer can not provide additional info.
SUMMARY: AddressSanitizer: SEGV (/usr/lib/libc.so.6+0x1aeddd) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
==1553786==ABORTING

We can take a closer look at the reproducer, and we will see at offset 0xa0 the fstab device that got thrown into the parser: /soc/x:

$ xxd findings/dtb_fstab_nullderef_repro.dtb
00000000: d00d feed 0000 0105 0000 0038 0000 00e0  ...........8....
00000010: 0000 0028 0000 0011 0000 0010 0000 0000  ...(............
00000020: 0000 0025 0000 00a8 0000 0000 0000 0000  ...%............
00000030: 0000 0000 0000 0000 0000 0001 0000 0000  ................
00000040: 0000 0003 0000 0004 0000 0000 0000 0002  ................
00000050: 0000 0003 0000 0004 0000 000f 0000 0002  ................
00000060: 0000 0001 6669 726d 7761 7265 0000 0000  ....firmware....
00000070: 0000 0001 616e 6472 6f69 6400 0000 0001  ....android.....
00000080: 6673 7461 6200 0000 0000 0001 7665 6e64  fstab.......vend
00000090: 6f72 0000 0000 0003 0000 0007 0000 001b  or..............
000000a0: 2f73 6f63 2f78 0000 0000 0002 0000 0002  /soc/x..........
000000b0: 0000 0001 7662 6d65 7461 0000 0000 0003  ....vbmeta......
000000c0: 0000 0001 0000 001f 0000 0000 0000 0002  ................
000000d0: 0000 0002 0000 0002 0000 0002 0000 0009  ................
000000e0: 2361 6464 7265 7373 2d63 656c 6c73 0023  #address-cells.#
000000f0: 7369 7a65 2d63 656c 6c73 0064 6576 0070  size-cells.dev.p
00000100: 6172 7473 00                             arts.

A real entry would look something like /dev/block/platform/soc/1d84000.ufshc/by-name/system, where /soc/ is followed by a device node and then another /.

  • Preconditions - To trigger this, we need a build that has dynamic partitions disabled, which, for example, is pre-Android 10 era. Older embedded devices may still reach here by default.
  • Impact - More Meh

Sadly, yet another boring bug, but let's continue ... we have more!

vbmeta: a focused harness finds two memory-safety bugs

The next bug is in the same file, in UpdateVbmetaNode, and its job is the mirror image of the last one. Instead of splicing a string in, it takes the vbmeta node's parts property (a comma-separated list of partition names) and removes one entry, odm, from it. To accomplish that, it first copies the entire parts string into a fixed 12800-byte scratch buffer, using the string's own length as the copy size with no upper bound. So if one hands this a parts string longer than 12800 bytes, it will cause a heap buffer overflow.

That said, there's a catch. The harness created by the LLM mutates the whole DTB. So where's the issue? We recall that a device-tree object is a complex structure. Each property in it records its own length right before its data, and the header records the total size of the tree. To make parts bigger, a fuzzer would have to make the underlying data larger, increase the size field accordingly, and update the header. All in a single mutation pass. That's too complex of a job for a basic mutation strategy. Random byte-flipping never lands that combination, so any mutation large enough to overflow leaves the tree structurally broken, and libfdt's own fdt_check_full discards such a broken tree before any parsing happens. Somehow the LLM caught this and built another second harness (technically we're sitting at six harnesses now) around this specific issue. So it not only disregarded my "build up to 5 harnesses", it even went beyond that. This, let's call it "optimized" harness focuses solely on the parts value and then wraps a minimal valid device tree around by using libfdt:

// file: harness_dtb_vbmeta.c
#include <stdint.h>
#include <stddef.h>
#include <string.h>
#include <stdlib.h>

#include "edk2_shim.h"
#include <libfdt.h>

extern EFI_STATUS UpdateVbmetaNode (VOID *fdt, CHAR8 *OldPartStr, CHAR8 *NewPartStr);

#define VB_CAP (256u * 1024u)

/* Build /firmware/android/vbmeta with parts = data[0..len) (NUL-terminated so
 * AsciiStrLen == the fuzzer-controlled length), then run the glue. */
static void run_parts (const uint8_t *data, size_t len)
{
  if (len > VB_CAP / 3)               /* keep the DTB build well inside VB_CAP */
    return;
  uint8_t *buf = (uint8_t *)calloc (1, VB_CAP);
  char    *parts = (char *)malloc (len + 1);
  if (!buf || !parts) { free (buf); free (parts); return; }
  if (len)
    memcpy (parts, data, len);
  parts[len] = '\0';                  /* AsciiStrLen(parts) == first NUL, else len */

  if (fdt_create_empty_tree (buf, VB_CAP) == 0) {
    int fw = fdt_add_subnode (buf, 0, "firmware");
    int an = fw >= 0 ? fdt_add_subnode (buf, fw, "android") : fw;
    int vb = an >= 0 ? fdt_add_subnode (buf, an, "vbmeta") : an;
    if (vb >= 0 &&
        fdt_setprop (buf, vb, "parts", parts, (int)len + 1) == 0) {
      UpdateVbmetaNode (buf, (CHAR8 *)"odm", NULL);
    }
  }
  free (parts);
  free (buf);
}

int LLVMFuzzerTestOneInput (const uint8_t *Data, size_t Size)
{
  run_parts (Data, Size);
  return 0;
}

With the fuzzer input going straight in as the parts value. It triggers the CopyMem overflow immediately.

$ ./dtb_vbmeta_fuzz findings/vbmeta_copymem_overflow_repro.bin
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 1724910399
INFO: Loaded 1 modules   (254 inline 8-bit counters): 254 [0x55a8e4fe0a80, 0x55a8e4fe0b7e),
INFO: Loaded 1 PC tables (254 PCs): 254 [0x55a8e4fe0b80,0x55a8e4fe1b60),
./dtb_vbmeta_fuzz: Running 1 inputs 1 time(s) each.
Running: findings/vbmeta_copymem_overflow_repro.bin
=================================================================
==1554415==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7d6d3c5edb00 at pc 0x55a8e4f03988 bp 0x7ffc8b2cf8f0 sp 0x7ffc8b2cf0b0
WRITE of size 13000 at 0x7d6d3c5edb00 thread T0
    #0 0x55a8e4f03987 in __asan_memmove (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x29e987) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)
    #1 0x55a8e4f681f6 in ShimCopyMem /home/pwn/abl-sparse-fuzz/./dtb_format.h:68:55
    #2 0x55a8e4f66630 in UpdateVbmetaNode /home/pwn/abl-sparse-fuzz/UpdateDeviceTree_extract.c:177:5
    #3 0x55a8e4f64b5b in run_parts /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:50:7
    #4 0x55a8e4f64b5b in LLVMFuzzerTestOneInput /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:59:3
    #5 0x55a8e4cd1fbb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) fuzzer.o
    #6 0x55a8e4cb73f8 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) fuzzer.o
    #7 0x55a8e4cc0704 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) fuzzer.o
    #8 0x55a8e4ca5c97 in main (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x40c97) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)
    #9 0x7efd3d427c8d  (/usr/lib/libc.so.6+0x27c8d) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #10 0x7efd3d427dca in __libc_start_main (/usr/lib/libc.so.6+0x27dca) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #11 0x55a8e4ca5d64 in _start (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x40d64) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)

0x7d6d3c5edb00 is located 0 bytes after 12800-byte region [0x7d6d3c5ea900,0x7d6d3c5edb00)
allocated by thread T0 here:
    #0 0x55a8e4f07869 in calloc (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x2a2869) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)
    #1 0x55a8e4f664bf in AllocateZeroPool /home/pwn/abl-sparse-fuzz/./edk2_shim.h:100:59
    #2 0x55a8e4f664bf in UpdateVbmetaNode /home/pwn/abl-sparse-fuzz/UpdateDeviceTree_extract.c:150:21
    #3 0x55a8e4f64b5b in run_parts /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:50:7
    #4 0x55a8e4f64b5b in LLVMFuzzerTestOneInput /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:59:3
    #5 0x55a8e4cd1fbb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) fuzzer.o
    #6 0x55a8e4cb73f8 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) fuzzer.o
    #7 0x55a8e4cc0704 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) fuzzer.o
    #8 0x55a8e4ca5c97 in main (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x40c97) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)
    #9 0x7efd3d427c8d  (/usr/lib/libc.so.6+0x27c8d) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #10 0x7ffc8b2d1c02  (<unknown module>)

SUMMARY: AddressSanitizer: heap-buffer-overflow (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x29e987) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4) in __asan_memmove
Shadow bytes around the buggy address:
  0x7d6d3c5ed880: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7d6d3c5ed900: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7d6d3c5ed980: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7d6d3c5eda00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7d6d3c5eda80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
=>0x7d6d3c5edb00:[fa]fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7d6d3c5edb80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7d6d3c5edc00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7d6d3c5edc80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7d6d3c5edd00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x7d6d3c5edd80: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==1554415==ABORTING

Reading that trace top to bottom had me questioning the result at first, as frame 0 is in the harness itself. Frame 1 is inside the shim for ShimCopyMem:

// file: dtb_format.h

// <SNIP>
/* --- gBS subset the glue calls --------------------------------------- */
static VOID ShimCopyMem (VOID *d, VOID *s, UINTN n) { memmove (d, s, (size_t)n); }
static VOID ShimSetMem  (VOID *b, UINTN n, UINT8 v) { memset (b, v, (size_t)n); }
typedef struct {
  VOID (*CopyMem) (VOID *Dst, VOID *Src, UINTN Len);
  VOID (*SetMem)  (VOID *Buf, UINTN Len, UINT8 Val);
} SHIM_BOOT_SERVICES;
static SHIM_BOOT_SERVICES ShimBS = { ShimCopyMem, ShimSetMem };
static SHIM_BOOT_SERVICES *gBS = &ShimBS;

// <SNIP>

ShimCopyMem is just a memmove, standing in for the real gBS->CopyMem (a length-bounded, overlap-safe copy, which is exactly what memmove is), so it is mostly a truthful stub and not the source of the bug. The part that matters is frame 2: UpdateVbmetaNode calling that copy with AsciiStrLen(Prop->data) as the length and nothing bounding it against the 12800-byte destination (see earlier). Interestingly enough, in the same function, right next to the above bug is a string operation that removes a partition from the list ends with a decrement and a write:

// file: UpdateDeviceTree_extract.c  
if (!NewPartStr && !RestParts)
  ReplaceStr = ReplaceStr - 1;
*ReplaceStr = '\0';            // one byte before PartitionString[0]

UpdateVbmetaNode is called with "odm" as the partition to strip. If parts begins with odm and has no comma after it, RestParts will turn into NULL and ReplaceStr still points at the first byte of the buffer, so ReplaceStr - 1 walks one byte before it and the null-terminator write lands out of bounds:

$ ./dtb_vbmeta_fuzz findings/vbmeta_replacestr_underflow_repro.bin
INFO: Running with entropic power schedule (0xFF, 100).
INFO: Seed: 1978897154
INFO: Loaded 1 modules   (254 inline 8-bit counters): 254 [0x56216c878a80, 0x56216c878b7e),
INFO: Loaded 1 PC tables (254 PCs): 254 [0x56216c878b80,0x56216c879b60),
./dtb_vbmeta_fuzz: Running 1 inputs 1 time(s) each.
Running: findings/vbmeta_replacestr_underflow_repro.bin
=================================================================
==1554499==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x7e00f75e00ff at pc 0x56216c7feb41 bp 0x7ffcaaa0cdd0 sp 0x7ffcaaa0cdc8
WRITE of size 1 at 0x7e00f75e00ff thread T0
    #0 0x56216c7feb40 in UpdateVbmetaNode /home/pwn/abl-sparse-fuzz/UpdateDeviceTree_extract.c:207:17
    #1 0x56216c7fcb5b in run_parts /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:50:7
    #2 0x56216c7fcb5b in LLVMFuzzerTestOneInput /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:59:3
    #3 0x56216c569fbb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) fuzzer.o
    #4 0x56216c54f3f8 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) fuzzer.o
    #5 0x56216c558704 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) fuzzer.o
    #6 0x56216c53dc97 in main (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x40c97) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)
    #7 0x7f90f8427c8d  (/usr/lib/libc.so.6+0x27c8d) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #8 0x7f90f8427dca in __libc_start_main (/usr/lib/libc.so.6+0x27dca) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #9 0x56216c53dd64 in _start (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x40d64) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)

0x7e00f75e00ff is located 1 bytes before 12800-byte region [0x7e00f75e0100,0x7e00f75e3300)
allocated by thread T0 here:
    #0 0x56216c79f869 in calloc (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x2a2869) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)
    #1 0x56216c7fe4bf in AllocateZeroPool /home/pwn/abl-sparse-fuzz/./edk2_shim.h:100:59
    #2 0x56216c7fe4bf in UpdateVbmetaNode /home/pwn/abl-sparse-fuzz/UpdateDeviceTree_extract.c:150:21
    #3 0x56216c7fcb5b in run_parts /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:50:7
    #4 0x56216c7fcb5b in LLVMFuzzerTestOneInput /home/pwn/abl-sparse-fuzz/harness_dtb_vbmeta.c:59:3
    #5 0x56216c569fbb in fuzzer::Fuzzer::ExecuteCallback(unsigned char const*, unsigned long) fuzzer.o
    #6 0x56216c54f3f8 in fuzzer::RunOneTest(fuzzer::Fuzzer*, char const*, unsigned long) fuzzer.o
    #7 0x56216c558704 in fuzzer::FuzzerDriver(int*, char***, int (*)(unsigned char const*, unsigned long)) fuzzer.o
    #8 0x56216c53dc97 in main (/home/pwn/abl-sparse-fuzz/dtb_vbmeta_fuzz+0x40c97) (BuildId: 5d41d9df1ba440e50b0522eabcadbec513d43aa4)
    #9 0x7f90f8427c8d  (/usr/lib/libc.so.6+0x27c8d) (BuildId: da90c940060d13f3bc8a337f9c591b40ca12815e)
    #10 0x7ffcaaa0dbfe  (<unknown module>)

SUMMARY: AddressSanitizer: heap-buffer-overflow /home/pwn/abl-sparse-fuzz/UpdateDeviceTree_extract.c:207:17 in UpdateVbmetaNode
Shadow bytes around the buggy address:
  0x7e00f75dfe00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75dfe80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75dff00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75dff80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75e0000: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x7e00f75e0080: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa[fa]
  0x7e00f75e0100: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75e0180: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75e0200: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75e0280: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x7e00f75e0300: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==1554499==ABORTING

This one needs no oversized property, just a parts value starting with odm and no comma, so it is reachable through the ordinary full-DTB flow as well, not only through this focused harness.

  • Preconditions - A build for Android below version 10, because the in-tree UpdateVbmetaNode(fdt, "odm", NULL) call is compiled only under ANDROID_PLATFORM_VERSION < 10. Moreover, the overflow needs a parts property longer than 12800 bytes, and the one-byte underflow needs parts to start with odm and carry no comma after it.
  • Impact - Again, still kinda meh

The overflow is the only somewhat interesting primitive of the four bugs. It's a linear heap overflow with attacker-controlled length and contents, which can corrupt adjacent ABL heap allocations rather than only fault. The underflow writes a single fixed 0x00 one byte before the buffer, enough to clobber the preceding chunk's metadata. That said, all of them are still post-unlock. So exploitability is abysmal. Impact is negligible.

Conclusion

This brings me to the end of the quick and dirty triage and, at the same time, to the end of this first article. I could have gotten in more depth about the Qualcomm codebase itself, but this was not the point. The point was to understand and see what a modern-day LLM (as of the time of writing) is capable of when throwing a multi-step task at it. Can it keep context? How does it handle context switches? What's the quality of the output like? And so forth. Before anyone comes at me for "this was not a very academic benchmark". I fully get that. It was not the point. This was a baseline: one current model, one repository, one broad prompt, and no framework around the run. It was all about getting a feel for what the ceiling is presently (for this particular LLM) and where and how we could improve.

The bottom line is that what I encountered is still a very 2023/2024 era result. On a more serious note, I have to acknowledge that the model did more than I expected. It selected targets, lifted real code, built working harnesses and shims, checked coverage, and found four reproducible bugs without asking me to steer it.

One of the weak points back then is still one of the weak points today: prioritization of tasks and foresight. The targets that were fuzzed were all behind either some checks or conditions that some dataflow/code review should have spotted. Code generation itself was already quite neat a few years ago, just more limited in quantity.

That said, in my initial prompt I did not explicitly ask for the chaining of the identification and ranking of fuzzing candidates before making an educated guess. I just told the LLM to "analyze". Again, this showed me that precisely prompting your intent matters, not that this is any news in 2026. The same applies for splitting a huge workload into isolated subtasks. That's where LLMs excel right now, and we will get to that.

In the next post I'll start building this in public, benchmarked and reproducible so the results can actually be checked. The aim is an orchestrator that focuses on fuzzing and works from source as a first-class citizen. The goal will be to weigh severity and reachability as it goes. One major precondition will be that it's working with a "production-grade" and large codebase without a human holding its hand the whole way. I don't intend to publish yet another "autonomous AI hacking tool that solved JuiceShop".

References

jetx at 1Win Moldova

  • ✇bellingcat
  • Tracking a Sanctioned Russian Vessel’s West African Odyssey Bellingcat Investigation Team
    Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published. A sanctioned vessel that was previously reported to have transported weapons destined for Russian mercenaries has been traversing ports on the west coast of Africa since March, exhibiting what experts told Bellingcat  was an unusual set of movements and behaviours. Patria (IMO: 9159921) has been sanctioned by the US, Ukraine and Canada. Support Bellingcat Your donations directly c
     

Tracking a Sanctioned Russian Vessel’s West African Odyssey

25 de Agosto de 2026, 05:52

Sign up here to receive Bellingcat’s biggest investigations by email as soon as they are published.

A sanctioned vessel that was previously reported to have transported weapons destined for Russian mercenaries has been traversing ports on the west coast of Africa since March, exhibiting what experts told Bellingcat  was an unusual set of movements and behaviours.

Patria (IMO: 9159921) has been sanctioned by the US, Ukraine and Canada.

Support Bellingcat

Your donations directly contribute to our ability to publish groundbreaking investigations and uncover wrongdoing around the world.

Radio France International (RFI) reported last year that it was one of two ships to deliver weapons to Conakry in Guinea that were intended for the Kremlin-controlled Africa Corps and their operations in Mali.

Satellite imagery and Automated Identification System (AIS) data from Lloyd’s List Intelligence shows Patria has shuttled between the Port of Douala in Cameroon and the Port of Owendo in Libreville, Gabon four times since March. 

It has also twice stopped in anchorage off the coast of Lagos, Nigeria: first in March and then again at the time of publication. Analysis shows the vessel also spent time in anchorage off the coast of Equatorial Guinea. 

The online news site, Modern Ghana, first reported Patria’s presence off the coast of Lagos in July after X-users @SONNAROW_OSINT and @RFNOSBlog picked up on Patria’s position.

It is not clear what Patria has delivered or picked up at these ports. Nor is it clear why it has spent so long going back and forth between them. But experts Bellingcat spoke to said the unusual patterns of behaviour raised numerous questions.

Charlie Brown, a former US Naval Officer and Senior Advisor at United Against Nuclear Iran said the combination of Patria’s repeated regional port calls, extended periods at anchor, and an apparent absence of a normal point-to-point trading cycle warranted scrutiny, especially as the ship is under sanction and is previously reported to have shipped arms. 

Tracking the Patria

Patria is a cargo vessel that has a distinct shape and features. Its bridge is located on the bow and it has a bright red deck that contrasts with its blue hull and two yellow cranes. 

At the end of the deck, the ship has a built-in ramp for vehicles (the Patria is a so-called roll on/roll off, or RoRo, vessel that is designed to transport wheeled vehicles). Its chimney is located next to the ramp.

Footage of the Patria, posted on Youtube on Jan 22, 2024. Credit: Hanro Shipping – Sakhalin Projects LLC / YouTube Channel @hanroship

This, in combination with the length of the ship (101 m), allowed Bellingcat to pick the vessel out in satellite imagery. AIS data helped us further track its long journey which began in the Sea of Japan, in Russia’s far-east, in January.

For the most part, we were able to match Patria’s AIS position with corresponding satellite imagery. We found no evidence of obvious spoofing incidents (where a ship intentionally broadcasts misleading AIS data) by the vessel during its months-long voyage, however, there were some instances where satellite images were not available and thus spoofing by the vessel cannot be completely ruled out.

MapLibre | Protomaps© OpenStreetMap contributors

Port of Olga, Russia

AIS data indicates that Patria loaded at the Port of Olga in the Sea of Japan between Jan. 21 and 23. Patria can also be seen on satellite imagery on these dates.

Credit: Planet Labs PBC.

Port of Douala, Cameroon

AIS data indicates that Patria unloaded some cargo in the Port of Douala between Mar. 11 and 12. Again, the ship can also be seen in satellite imagery on these dates.

Credit: Planet Labs PBC.

Lagos Anchorage, Nigeria

AIS data indicates Patria anchored off the coast of Lagos from Mar. 14 to 15.

A Sentinel-2 image from the 15th appears to show another ship next to Patria. AIS data indicates that this is JS Gratitude, a bunkering tanker. This close proximity suggests that Patria was refuelling.

Credit: Contains modified Copernicus Sentinel data 2026.

Bata Anchorage, Equatorial Guinea

AIS data and satellite imagery indicate Patria stayed off the coast of Equatorial Guinea for several days.

Credit: Planet Labs PBC.

Port of Owendo, Libreville, Gabon

AIS data and satellite imagery indicate Patria loaded at the Port of Owendo in Libreville after spending a few days off the coast.

Credit: Planet Labs PBC.

Port of Douala, Cameroon

AIS data and satellite imagery indicate Patria stayed at the Douala Anchorage from Apr. 6 to 14, before unloading at the Port of Douala between Apr. 14 and 18.

Credit: Planet Labs PBC.

Port of Owendo, Libreville, Gabon

AIS data suggests Patria loaded in Libreville again between Apr. 22 and 26.

Port of Douala, Cameroon

AIS data, supported by satellite imagery, indicates Patria stayed at the Douala Anchorage for nearly a month from Apr. 27 to May 21 before unloading in Douala from May 21 to 27.

Credit: Planet Labs PBC.

A third trip between the Port of Owendo, Libreville to Douala, Cameroon

AIS data indicates, after nearly a month’s wait in Douala anchorage, Patria again loaded at Owendo before returning to Douala to unload.

A fourth trip between the Port of Owendo, Libreville to Douala, Cameroon

AIS data indicates Patria again loaded at Owendo before returning to Douala to unload.

Lagos Anchorage, Nigeria

AIS data indicates, after a short visit to the Libreville anchorage, Patria anchored off the coast of Lagos where it remained at the time of publication.

Credit: Planet Labs PBC.

Examining the Patria’s Draught

We reviewed the draught of the ship at each port visit and found that the ship’s draught always dropped after a stay at the Port of Douala, suggesting it was unloading there.

A ship’s “draught” is the distance from the bottom of the hull (the keel) to the waterline. When loaded, a ship is heavier and sits lower in the water (e.g. a draught of six metres) than when it is unloaded (e.g. a draught of four metres).

Draught is the depth of a ship below the waterline. 

In the period from March to July, the Patria made five port calls to Douala and each time the draught decreased. Conversely, it called four times at the Port of Owendo in Libreville, each time the draught increased, meaning the ship became heavier, suggesting it was loading.

The draught is self-reported by ships but usually when it arrives at ports this kind of data is checked – reporting accurate draught is also a safety issue for ships arriving and departing at ports. 

Bellingcat asked the ship’s owners, managers and both ports if items were being transferred from Libreville to Douala but did not receive a response at time of publication.

Brown, the former US Naval Officer and now a Senior Advisor at United Against Nuclear Iran, said Patria’s movements were unusual.

“A sanctioned vessel linked to a prior military logistics shipment spending nearly six months operating between a small cluster of West African ports, Douala, and Owendo, without returning to a clear commercial trading pattern warrants scrutiny,” Brown told us.  


“While innocent explanations such as mechanical issues, commercial disputes, lack of cargo, chartering delays, or prolonged maintenance are possible, the combination of repeated regional port calls, extended periods at anchor, and an apparent absence of a normal point-to-point trading cycle is atypical for a merchant vessel.” 

He added that the current period of more than 30 days at the Lagos Anchorage, in particular, is noteworthy. 

David Soud, Head of Research and Analysis at I.R Consilium also told Bellingcat that Patria’s prolonged Lagos Anchorage could have innocent explanations such as its need for ongoing repairs, or that its operators were out of money, but added that there could also be more calculated reasons and it was laying low for a while.

Bellingcat analysed AIS data from Lagos Anchorage and found that while there has been high congestion, no other RoRo or container vessel waited longer than 10 days to enter the port in the period that Patria has been at Lagos Anchorage. At time of writing, Patria has been in anchorage for more than 30 days.

Regarding the Patria’s apparent deliveries of cargo between Libreville in Gabon, and Douala in Cameroon, Soud told Bellingcat:

“Given the vessel’s history of transporting military equipment to African seaports for overland delivery to Russian and allied forces in the Sahel, it’s not out of the question that some form of supplies for Russian or other forces could be picked up in Gabon, whose government has developed a closer relationship with Moscow, to be discharged in Douala, which is the main entry point for goods going to Central African Republic.”

Bellingcat asked the Nigerian Ports Authority why Patria had been in anchorage for so long, whether it had applied to dock and whether the port was aware of its sanctioned status but did not receive a response at time of publication.

The ports of Douala in Cameroon and Owendo in Libreville, Gabon did not respond to Bellingcat’s requests for comment about the Patria’s visits and the cargo it was carrying.

Bellingcat also contacted the two companies connected to the vessel – Hanro Shipping and Sakhalin Shipping Company which are listed as the vessel’s owner and manager respectively in sanctions documents. We also contacted the company connected to JS Gratitude. We did not receive a response at time of publication.


Youri van der Weide, Galen Reich, Yörük Işık contributed to this report.

Cover image: Planet Lab image shows the Patria at the Port of Douala, Cameroon, on April 17, 2026. Credit: Planet Labs PBC.

Bellingcat is a non-profit and the ability to carry out our work is dependent on the kind support of individual donors. If you would like to support our work, you can do so here. Subscribe to our Newsletter and follow us on Bluesky here, Instagram here, Reddit here and YouTube here.

The post Tracking a Sanctioned Russian Vessel’s West African Odyssey appeared first on bellingcat.

Detailed Timeline of OpenAI’s Cyberattack on Hugging Face

20 de Agosto de 2026, 14:44

OpenAI presented details of its AI’s model’s cyberattack on Hugging Face at Black Hat last week. Simon Willison details the timeline. It’s really interesting to read through—and really impressive cyberoffense work.

  • ✇Low-level adventures
  • Breaking secure boot without breaking the crypto 0x434b
    This will be mostly rambling disguised as a technical walkthrough. I will touch on certain topics such as hardware roots of trust, signed image formats, Qualcomm boot stages, Android Verified Boot, UEFI, measured boot, and remote evidence (remote attestation). My idea is that I will briefly introduce the concepts and then introduce a seemingly "perfect-world"-example that can still break under certain assumptions. So, expect a technical post leaning towards offensive security. obkjgevs Note I
     

Breaking secure boot without breaking the crypto

Por:0x434b
19 de Agosto de 2026, 05:26
Breaking secure boot without breaking the crypto

This will be mostly rambling disguised as a technical walkthrough. I will touch on certain topics such as hardware roots of trust, signed image formats, Qualcomm boot stages, Android Verified Boot, UEFI, measured boot, and remote evidence (remote attestation). My idea is that I will briefly introduce the concepts and then introduce a seemingly "perfect-world"-example that can still break under certain assumptions. So, expect a technical post leaning towards offensive security.

obkjgevs
Note I will not claim completeness, each section is to be viewed with caution. This is a massive field spanning multiple vendors, OEMs, concepts and varying implementations. That said, if I messed up completely somewhere please let me know.

Primer: Trust before the operating system exists

Before any embedded device can start, let's say Linux, mount a root filesystem, enforce SELinux, or contact an update server, something has already selected bytes from mutable storage and transferred control to them. That first transition happens with almost none of the defenses we normally rely on, so the platform has to manufacture trust from a much smaller base: usually an immutable read-only memory (ROM) paired with a one-time-programmable policy (OTP). This forms what's commonly referred to as a "hardware Root of Trust (RoT)". The fixed ROM provides unalterable execution code (like a primary bootloader), while OTP components, such as eFuses, permanently store device-unique cryptographic keys, hashes, and security configurations. This minimal base is enough loader code to authenticate the next, bigger stage.

Secure boot, measured boot, and remote attestation are three different consumers of that base I just introduced. Secure boot decides whether a state transition is locally authorized. Measured boot records the transition. Remote attestation packages protected claims into signed evidence so a verifier can compare the device with reference values and policies. A representative embedded system therefore looks less like one signature check and more like two connected protocols:

                                      local authorization
                                      -------------------
power-on
   |
   v
+-------------------+    authenticate + validate    +------------------+
| mask ROM / PBL    |------------------------------>| mutable boot code|
| OTP root-key hash |                               | BL2 / XBL / TME  |
| lifecycle policy  |                               +--------+---------+
+-------------------+                                        |
                                                             | repeat for
                                                             | TEE, kernel,
                                                             | DTB, firmware
                                                             v
                                                     +---------------+
                                                     | runtime state |
                                                     +-------+-------+
                                                             |
                                                             | measurements
                                      remote appraisal       v
                                      ----------------  +---------------+
challenge / nonce ------------------------------------>| Attester      |
                                                       | protected key |
                                                       +-------+-------+
                                                               |
                                                               | Evidence
                                                               v
                                                        +--------------+
                                                        | Verifier     |
                                                        | refs + policy|
                                                        +------+-------+
                                                               |
                                                               | result
                                                               v
                                                        Relying Party

Secure boot and remote attestation, end to end

The above diagram is very rough and mixes terminology across different vendors (e.g., BL2, XBL, SBL). So we'll look at these elements more closely later in the post. The goal is more that it serves as a general, loose introduction.

Secure boot and remote attestation, from reset to remote policy

Secure boot is a hardware-rooted authorization process in which an already trusted stage authenticates and validates the next security-relevant transition before allowing it to execute. "Authenticates" covers signatures and certificate chains. "Validates" covers what the signature alone cannot decide: image type, hardware target, load range, lifecycle, debug policy, version, rollback floor, and whether this path is permitted in production. In short, a boot stage n decides whether the next in line boot stage n+1 is allowed to proceed based on a pre-defined set of checks.

Now, there's still the distinction between authentication and validation, and between signatures and certificates. A valid signature only proves one narrow thing: that a holder of an accepted private key authorized some specific byte string at some point. That is a much smaller claim than people tend to read into it. Almost everything that makes secure boot actually work lives in the gap between "these bytes were signed" and "this device may run this code right now." So, concretely, a valid signature does not prove that:

  • Every byte the loader later consumes was inside that signed string. A signature covers one specific range, so any header field, metadata, or second payload that sits outside it was never authorized.
  • The signer meant this image for this chip and this lifecycle. The same valid signature could belong to a different product or to a development build that should never run on a shipped device.
  • The image is current. An old build stays bit-for-bit authentic forever, even after a newer version patches a hole in it.
  • The caller actually acted on the result. A verifier can compute "reject" and the surrounding code can still hand control to the image anyway.
  • The verified buffer stayed put until execution. Bytes can be checked in one place and quietly swapped before the CPU runs them.
  • The signed code is safe. "We authorized this" is not the same as "this is memory-safe, correctly configured, or benign."
  • The firmware is secret. Signatures authenticate. They do not encrypt, so anyone can still read the image straight off the flash.
  • The device stays in that state afterward. "The image on storage was authentic at boot" and "the device is uncompromised right now" are two different claims.

If you squint, that list is a table of contents for the second half of this post: each line is one way a mathematically perfect signature check still lets unsigned code run. It is also where the split between authentication and validation earns its keep. Authentication answers a narrow question: is this signature valid, and does the keychain back to a root I trust? A certificate chain only helps with that half. It vouches that a key descends from a trusted root, not that the key was allowed to sign this particular image. Validation is everything else: the right image type, the right product, the right lifecycle, the right version, and whether this path is even permitted here. The signature math can be flawless, and every one of those validation questions can still be answered wrong.

If we wanted to go a little deeper and consult a framework regarding embedded security, we could take a look at NIST SP 800-193. This separates firmware resilience into three categories: protection, detection, and recovery. Secure boot is mostly a protection mechanism. Measured boot and attestation contribute to detection. Neither automatically provides recovery.

NOTE None of the three categories introduced by NIST mean much if an alternate path can bypass the mechanism entirely.

Authenticated, verified, measured, and attested boot

We've juggled a lot of these buzzwords, so here is the short version of what each one does, what it produces, and who consumes it:

Boot type What it does What it produces Who consumes it
Authenticated boot Checks the next image's signature against an accepted key. A yes/no answer to: "Is this signed by someone we trust?" The current stage's own decision logic.
Verified boot Authenticates, then validates policy such as type, target, version, and lifecycle, and enforces it. A boot that proceeds only with a fully authorized image, or otherwise halts. The device itself, at every handoff.
Measured boot Hashes each stage and records the digest into protected, append-only state. A tamper-evident record of what actually ran. Later stages, and ultimately attestation.
Attested boot Signs those measurements together with device identity and a fresh nonce. Evidence that a remote party can check against reference values. A remote verifier or relying party.

A measurement here just means a cryptographic hash of the bytes a stage is about to run, written into a protected state a later stage can add to but never rewrite. Depending on the device class, these are kept in TPM PCRs. On a more resource-constrained device, they may be used straight in key derivation. We will touch upon that a little in the later following "DICE" section. With those pinned down, let's condense the earlier diagram using them:

                    LOCAL DECISION (DEVICE)

+-------------------------------------------+
|                                           |
|  storage                                  |
|     |                                     |
|     v                                     |
|   load                                    |
|     |                                     |
|     v                                     |
|  hash / authenticate                      |
|     |                                     |
|     +--------------------+                |
|     |                    |                |
|     v                    v                |
|  secure boot policy    measure            |
|     |                    |                |
|     |                    v                |
|     |              protected claim set    |
|     |                    |                |
|     |                    v                |
|     |              sign(nonce, claims)    |
|     |                    |                |
|     +--> execute         |                |
|     |      |             |                |
|     |      v             |                |
|     |     run            |                |
|     |                    |                |
|     +--> reject /        |                |
|          recovery / halt |                |
|                          |                |
+--------------------------|----------------+
                           |
                           | attestation evidence
                           v

================================================
           DEVICE / VERIFIER BOUNDARY
================================================

                           |
                           v

                 REMOTE DECISION (VERIFIER)

                 verify signature
                        |
                        v
                   verify nonce
                        |
                        v
                evaluate trust policy
                        |
                  +-----+-----+
                  |           |
                  v           v
             grant / trust   deny /
                             quarantine

Local device decision vs. remote verifier decision

The mechanisms complement one another. Verified boot refuses a locally unauthorized image, while measured boot records what happened. Remote attestation lets another system decide whether to provision a key, accept telemetry, admit the device to a network, or quarantine it. Recording an unexpected digest does not stop it from running, and enforcing a signature does not tell a remote service what actually happened. So having one over the other is not a security decision to make. It's more about how many layers a product should deploy based on a given threat model.

What does secure boot protect?

To discuss this topic, we can formulate a hypothesis to build on.

Hypothesis Given an attacker targeting a secure device, let's assume: A) They control external flash, an update package, or a recovery transport. B) They can repeatedly reset and physically access a deployed device. C) They may later gain code execution in one mutable boot stage or bus master. D) They cannot forge the production signature or recover the offline root private key. E) They cannot change mask ROM or correctly programmed OTP/eFuses, although ROM code may still contain exploitable bugs.

The aforementioned scenario defines a handful of "rules" but they're basically two buckets: 1) An attacker has physical access, some control over some input data, and hunts for vulnerabilities, and 2) they cannot change the fact that embedded secrets or ROM code are unchangeable/not (easily) extractable.

Now against such an attacker, a properly implemented secure boot chain should stop persistent replacement of boot firmware, kernel, TEE components, coprocessor images, device trees, and/or root filesystems, given those components are covered by the platform's chain of trust. Adding (monotonic) version state tracking can stop version rollback as the minimum flashable version is directly embedded in, for example, eFuses. Similarly, this mechanism can be used to embed debug and other policies to, for example, disable access over JTAG on a production unit. This foundation can be expanded for a hardware-backed remote attestation as well. But given that this is where secure boot shines, there are ways to work around it. Secure boot does not stop a network exploit against genuine firmware, a ROP chain assembled from signed code, a data-only attack, an authorized old image, or runtime injection through an RWX mapping. Those attacks may later undermine the chain or establish persistence, but "the image on storage was authentic at boot" and "the device is currently uncompromised" remain different claims.

The minimum implementation

At reset the ROM code needs to initialize a bare minimum so boot (stages) later on can progress. It is not aware of what "the firmware" is. It only knows and "has access" to a bare minimum of immutable facts, like a hash of an embedded root key, some lifecycle or debug state information, and whether things like secure boot are enabled. The lifecycle state is where the device sits in its own timeline: fresh silicon starts in a development or test state (debug open, dev-signed images allowed), and burning a one-time fuse moves it permanently to locked production, which only accepts production-signed images and closes the debug interfaces. That transition is one-way, which is why a production device must reject a dev-signed image even when its signature is perfectly valid.

NOTE Everything read from flash is untrusted. That includes certificate chains, image headers, segment tables, load addresses, entry points, other signatures and generally all executable bytes

The ROM loader has to turn untrusted inputs into one precise authorization decision:

  1. Who authorized this image?
  2. Which bytes and execution parameters did they authorize?
  3. Is that current (-ly requested) state allowed on this device?

Most formats answer those questions indirectly. Instead of signing every large segment independently, the signer authenticates a smaller description of said segment. Another name for this description is metadata and policy information. Often this is also referred to as a manifest. When we look at a specific implementation, Qualcomm's signed ELF images use signed image metadata and a hash segment as highlighted in their public information material.

A signature therefore authenticates the description. The digests inside it bind the description to the underlying data. The loader's role is to reconstruct the claimed image based on that and either accept the proposed data or refuse a handoff to the next boot stage.

fused root hash
      |
      v
[certificate chain] --> leaf key --> verify [signed manifest]
                                              |
                         +--------------------+--------------------+
                         |                    |                    |
                    expected H(A)        expected H(B)       target/version
                         |                    |              ranges/entry
                         v                    v                    |
                   hash [segment A]     hash [segment B]           |
                         |                    |                    |
                         +--------------------+--------------------+
                                              v
                                      lock memory --> jump

Signature anchors the manifest & digests bind each segment

In general, the exact encoding is vendor-specific, but if we were to come up with some pseudo-code, it may look like this:

#define MAX_SEGMENTS 16

struct segment_desc {
    uint64_t file_offset;       /* bytes read from the container */
    uint64_t file_size;
    uint64_t load_address;      /* physical destination */
    uint64_t memory_size;       /* includes zero-filled tail */
    uint32_t permissions;       /* R/W/X policy after loading */
    uint8_t  sha384[48];        /* digest of this segment */
};

struct signed_manifest {
    uint32_t format_version;
    uint32_t image_id;          /* XBL, TEE, ABL, modem, kernel... */
    uint32_t hardware_id;
    uint32_t oem_id;
    uint32_t lifecycle_mask;    /* dev, test, production */
    uint64_t security_version;  /* anti-rollback value */
    uint64_t entry_point;
    uint32_t segment_count;
    struct segment_desc segment[MAX_SEGMENTS];
};

struct authentication_block {
    struct signed_manifest manifest;
    uint8_t manifest_signature[SIG_MAX];
    uint8_t certificate_chain[CERT_CHAIN_MAX];
};

A signed image: manifest plus segment descriptors

Based on this code, we can argue that signing segment[i].sha384 would be insufficient. An attacker who can alter the load address or change the entry point can change what a genuine segment does without changing its bytes. Therefore, things like an image ID, target, lifecycle state, version, addresses, sizes, permissions, and entry point belong inside the same signed statement. Generally at this stage it needs to be studied which metadata information bits need to be ensured are valid and should never be tampered with as they may cause side effects.

That said, there's one catch. Regardless of anything, the loader must always parse something before it knows where the manifest and signature are. You cannot check a signature/hash/digest if you don't know where it's located. That code responsible is in the pre-authentication parser, and every byte this function reads needs to be treated as hostile. So, this parser should be built in a way that it's as minimal as possible: overflow-safe arithmetic, bounded counts, no attacker-directed writes, etc. We would want to make sure that any interpretation of complex image semantics only happens AFTER an initial authentication.

Rollback state is part of authorization

A signature is timeless unless policy gives it a time dimension. As long as a signed image matches the expected signature, it passes. To further tighten a time domain, there exists this notion of rollback state. Rollback state in embedded security is typically represented as an integer security version in a signed manifest and a monotonic floor (never decreasing) stored in an OTP, eFuses, a replay protected memory block (RPMB), or any other similar replay-resistant storage. This version information, while in reality just being an integer comparison, is tied to time as each version has a specific release date. Imagine the following pseudo-code:

uint64_t floor = rollback_read(image_id);

if (manifest.security_version < floor)
    reject(AUTH_ROLLBACK);

boot_verified_candidate();

/* Commit only at the platform's defined successful-update point. */
if (candidate_confirmed_bootable() && manifest.security_version > floor)
    rollback_advance(image_id, manifest.security_version);

Anti-rollback version check and floor advance

While such an integer check for the allowed version is seemingly simple on paper, this check carries a lot more side effects that need to be considered. For example, having a single global floor counter for a full firmware stack is very problematic. Nowadays, firmware is a complex structure of different components. Each component may need different updates, or, as a matter of fact, certain components may be updated over-the-air while others require an OEM/factory treatment. So let's assume we have three components: a bootloader (e.g., an Android one - ABL), a TEE, and a modem. If I update the modem firmware, should I advance the global counter and, in a following update for the bootloader, do the same again? Who keeps track of that, especially if we consider updates may be produced by different teams, vendors, and so on? At the same time, if we consider multiple counters, one for each component, such as defined in a pseudo-manifest:

{
 "components": [
  {
    "image_id": "ABL",
    "security_version": 12
  },
  {
    "image_id": "TEE",
    "security_version": 8 
  },
    {
    "image_id": "MODEM",
    "security_version": 31
  }
 ]
}

Per-component rollback counters

We need to make sure that a counter is securely bound to a component's identity. Suppose an old ABL image carries security version 8, while the established minimum floor has already advanced to 12. The loader must reject that image. If a bug can be abused to make the check compare the old image's ABL version against the TEE's lower floor of 7, the image will get accepted. The loader must therefore establish that it is loading an ABL image and use the correct rollback counter. In the manifest, image_id identifies the component and security_version carries its version. Both must be authenticated. The loader must also compare image_id with the component expected at that point in the boot chain before selecting the corresponding rollback floor. Lastly, let's highlight legitimate firmware updates. When is a counter advanced? Advancing the floor before an image is known to boot can brick the device. Advancing it too late leaves a rollback window. So this requires some semantics like:

Update to new version B from A
  -> verify B
    -> trial-boot B
      -> confirm B is healthy
        -> atomically mark B successful and advance floor to n+1

When the rollback floor is safely advanced

Failure must be a terminal state

The authentication routine does not stop the processor by itself. It returns a result to its caller. Secure boot exists only if that caller turns every failure into a refusal to execute the requested image. Logging the error would not be enforcement. Neither would be retrying through a weaker boot mode or continuing with whatever remains in the load buffer (skipping the failed check section). A failed check must always reach a terminal state for that boot attempt. Whether that results in a halt, reset, or entering a recovery mode is highly implementation specific. What remains true for all three options is that in no case is there a fall-through where booting continues.

int rc = authenticate_and_validate(&image, &policy);

if (rc != AUTH_OK)
    fail_closed(rc);  /* authenticated recovery, reset, or halt; never returns */

if (lock_image_memory(&image) != 0)
    fail_closed(AUTH_MEMORY);

jump_to_verified_entry(image.entry);  /* successful handoff; never returns */

__builtin_unreachable();

Every failure path ends closed before handoff

This creates a one-way control flow. Every route until normal execution passes through authentication. Any error ends outside that route.

One concrete chain: Qualcomm from reset to Android

So far, we have looked at one authenticated transition: establishing an authority, validating a signed description, reconstructing the image, locking it, and handing it off. A real SoC repeats that transition across several processors and boot modes. Qualcomm's public architecture gives us one concrete graph on which to place the earlier abstractions. What's following is one current design, not a universal Qualcomm specification.

OTP/eFuse: secure-boot configuration, OEM root-certificate hash, lifecycle
                                  |
                                  v
                           PBL / Boot ROM
                    loads and authenticates both
                         /                 \
                        v                   v
              TME final runtime          XBL-SC
              mutable SoC RoT       loads later images
              authentication and           |
              resource services <----------+
                        |       authenticate, then assign resources
                        v
          +-------------+------------------+
          |             |                  |
          v             v                  v
   TEE / hypervisor   peripheral and    OS bootloader
                      management fw     (for example UEFI)
                                             |
                                      Android: libavb
                                      verifies VBMeta
                                             |
                          +------------------+------------------+
                          |                                     |
                          v                                     v
                  hash descriptors                     hashtree roots
                 boot / vendor_boot / ...          system / vendor / ...
                          |                                     |
                          +------------------+------------------+
                                             v
                                     Linux + dm-verity

PBL / Boot ROM -- product-dependent trigger --> USB EDL
                                                  |
                                          Sahara + programmer
                                                  |
                                              Firehose
                                      storage/memory operations

A Qualcomm boot chain, reset to Android

Don't try to memorize TME versus XBL-SC versus Sahara. The thing that matters for the remainder of this article is that every arrow is the same authenticate-then-hand-off decision that we introduced in the earlier section(s). They're just different names for the same concept. Anyhow, looking at the diagram above, we start at the primary bootloader (PBL), the immutable ROM bootloader. Its behavior is constrained by a one-time programmable configuration, including the secure-boot one and the OEM root-certificate hash. During this stage the "Trust Management Engine" (TME) provides authentication services while the PBL loads and authenticates both the final TME runtime and the eXtended (secure) bootloader (XBL-SC). Qualcomm describes those two images together as the second stage bootloader functionality. Afterward their roles split, XBL-SC initializes hardware and loads later images from storage. It calls the TME to authenticate those and assigns expected resources with them as well. This covers the application-processor images, such as the TEE, hypervisor, and operating system bootloader, in addition to the peripheral and management controller firmware. Further down the boot process, TME hands resources to their final owners.

While the above is way more complex than what we discussed in the minimal implementation, above we can kind of say that every edge is what the minimal implementation describes. Each Qualcomm signed-ELF transition instantiates the contract from the previous section. XBL-SC parses and loads from untrusted storage, TME authenticates signed metadata and segment hashes, policy binds the image to its intended hardware and role, and destination addresses are checked against allowlisted memory. Just as a brief introduction, since it recurs in the failure cases later: Android Verified Boot (AVB) is Android's secure boot, libavb is its reference verification library, and VBMeta is the signed manifest that library checks. Android Verified Boot follows the same abstract contract at the OS boundary, but with VBMeta and libavb rather than Qualcomm's signed-ELF format.

So overall, the labels change, but the contract/functionality does not. Additionally, the graph also exposes what a straight ROM-to-kernel diagram hides. A modern co-processor usually has its own executable images and, depending on the product's access-control policy, may own or access shared resources before Linux starts. Those images, their signing authorities, rollback counters, and memory permissions are part of the same security domain. So is every component capable of modifying memory that another stage will execute.

Recovery adds another branch. Some products expose a boot-ROM-initiated Emergency Download Mode. One such documented example provides a hardware bootstrap into USB EDL. In that mode, a host uses Sahara and Firehose with a product-specific device programmer to provision or rewrite storage, and some programmers expose memory operations as well. On a secure-boot-enabled production device, that programmer is another executable image whose authority and capabilities must be part of the review.

At the Android boundary, the OS bootloader integrates libavb and verifies the top-level VBMeta structure against the device's root of trust. Signed hash descriptors typically carry expected digests for small, read-once partitions, such as boot. Hashtree descriptors authenticate the roots used to verify large filesystems, such as system as blocks are read. Chain-partition descriptors delegate another signed VBMeta structure to a named key and rollback-index location. VBMeta therefore performs the same manifest role at a different layer. It extends the authorization decision from individual firmware containers to a graph of Android partitions. It is not a Qualcomm hash segment and does not use the same concrete format. That block-by-block check is dm-verity, the Linux device-mapper target that validates each filesystem block against a signed hash tree at read time, so a tampered block fails when it is used rather than needing the whole partition hashed up front.

This was a quick real-world example that likely could be extended with some more knowledge, but it may also warrant its own article when we want a full walkthrough of Android secure boot. That said, it highlights the notion of "verifying every image" in practice. We need to keep track of every executable component, every path that can load another, and every actor that can alter it before execution. Secure boot makes those decisions locally. The next problem we're going to take a look at is how a remote service learns which authorities, images, and configurations a device accepted and whether the configuration stored remotely matches what's running on a device.

From a local measurement to a remote decision

Now that we have established that secure boot answers a local policy question about whether a device may boot a specified image, we could claim that's the end. The device is secured. But if we consider a device/service operator, the equation changes. There are different decisions to be made: Should the device be provisioned, accept telemetry, expose customer data, or should the device be admitted to a network? These cannot be answered with secure boot alone. Secure boot answers whether an image is authentic and locally allowed based on, e.g., a minimum version, a signing authority, etc.

Additionally, a remote party cannot observe what actually has happened during boot and which boot decisions have been made. It may (if at all) only see the outcome (device booted, device reset/halted). This almost binary outcome proves little. Now we could argue that a device during boot could emit something like a measurement digest for a remote party to observe. But this is not enough. At any time during the boot process, the software could invent such a digest, replay a captured one, or copy a result from another device. A trustworthy report needs a protected origin, a device identity, and proof that this result was produced as a result of a specific request.

This is where remote attestation comes into play, which acts as a bridge. A protected component reports the preserved measurements and boot state, while the remote service compares those claims with its own reference values and policy. This specific separation of concerns is important. A device reports what it observed. It at no time has any say about whether an observer should trust it. RFC 9334 gives us precise naming for the involved roles, inputs, and the exchange itself:

                  endorsements      reference values      appraisal policy
                       |                    |                     |
                       +--------------------+---------------------+
                                            |
                                            v
challenge / nonce --> Attester --Evidence--> Verifier --Attestation Result--> Relying Party
                          |                                                   |
                          +-- measurements, lifecycle, debug, version,        +-- provision key
                              device identity, boot state...                  +-- admit to network
                                                                              +-- quarantine / deny

RATS roles (RFC 9334)

We have an "attester" who produces the "evidence". We have a "verifier" who's appraising it using what's referred to as "manufacturer endorsement." A more established (at least in this article) terminology for endorsements would be "reference values" or "policies." We also have a "relying party" who consumes the resulting verdict and acts upon it. All those roles may collapse into one backend, e.g., in a small IoT/embedded deployment. That said, the separation of concerns here is what prevents a signature is valid silently becoming device is trustworthy.

Before requesting evidence, a verifier will typically generate a random nonce. The attester includes that challenge in the signed evidence, and the verifier rejects a response with a different value. This makes replaying an older response impractical. The nonce ensures a response is fresh. It does not guarantee the content inside the response is, though. For example, a boot digest may have been collected hours ago and preserved in a protected state. It's the job of the evidence to identify what was measured and which environment preserved it. Now this back-and-forth needs a standardized format so both parties can understand what's happening. Furthermore, it needs to be ensured that the integrity of the exchanged data holds true at any given time. This is where something called an attestation token comes into play. A set of claims is protected by a signature or MAC under an attestation key. The Entity Attestation Token (EAT) defines a general framework and common vocabulary for such tokens, with CBOR/CWT or JSON/JWT encodings. The Arm PSA Attestation Token narrows it down in the context of ARM and their platform security. Anyhow, this is where a nonce connects to the token. Neither the nonce nor the EAT stands alone. They become one protected claim. This was a lot of buzzwords and claims. But if we were to use a simplified pseudo-attestation token with a minimal CBOR syntax, it could look like this:

{
  nonce: h'8e5f...',
  ue-id: h'01b4...',
  implementation-id: h'7a9c...',
  security-lifecycle: "secured",
  debug-state: "disabled",
  boot-seed: h'4cc1...',
  software-components: [
    {
      measurement-type: "BL2_SHA384",
      measurement-value: h'2fd8...',
      signer-id: h'a011...',
      version: "7.4.2"
    },
    {
      measurement-type: "TEE_SHA384",
      measurement-value: h'91ca...',
      signer-id: h'a011...',
      version: "3.20.0"
    }
  ]
}

COSE_Sign1(
  protected = { alg: ES256, kid: h'alias-key-id' },
  payload   = <claims above>,
  signature = Sign(attestation_private_key, Sig_structure)
)

A simplified EAT, signed with COSE

The signature authenticates the token. Appraisal (verification) is a second algorithm with its own inputs and failure modes, something like:

def appraise(evidence, challenge, endorsements, reference_values, policy):
    token = cose_sign1_verify(evidence, endorsements.attestation_roots)
    require(constant_time_equal(token.nonce, challenge))
    require(token.ue_id in endorsements.issued_devices)
    require(token.security_lifecycle == "secured")
    require(token.debug_state == "disabled")

    for component in token.software_components:
        expected = reference_values.lookup(
            token.implementation_id,
            component.measurement_type,
            component.version,
        )
        require(component.signer_id in policy.allowed_firmware_signers)
        require(component.measurement_value in expected.accepted_digests)
        require(component.version >= policy.minimum_version(component))

    return AttestationResult(trusted=True,
                             device_id=token.ue_id,
                             policy_version=policy.version)

Appraisal: verify token, bind nonce, check components

That closes the loop opened by secure boot. The device enforces its local policy, the attester reports the resulting state, the verifier applies the remote policy, and the relying party acts on that appraisal. The pseudocode hides one critical dependency: why should the verifier trust the key that signed the evidence, and what binds that key to the measured device state? A discrete TPM can provide that root of trust. That said, many, especially cheaper embedded devices, do not have one.

DICE when a TPM is too large

Device Identifier Composition Engine (DICE) is one answer to the key question above. It was originally designed for resource-constrained devices. Essentially, it reuses a boundary we already know. A hardware-controlled first stage reads the next image, hashes its bytes, and then hands over control. Secure boot asks whether that digest is authorized. DICE does not slap a binary label like "it's good" or "it's bad" on it. It uses the digest to derive some key material. Different measured codes, therefore, receive a different cryptographic identity.

Let's quickly go over some DICE-specific terminology and connect the dots with the mental model we already introduced in this article. The first two DICE terms describe familiar pieces. The DICE Root of Trust plays the same early, privileged role as Boot ROM in our earlier chain: it performs the sensitive operation before ordinary firmware runs. A Trusted Computing Base (TCB) Component Identifier (TCI) describes the next stage. It includes its code measurement and may also cover security-relevant configuration, version, or operating mode. Then there is something new, a Unique Device Secret (UDS), which, unlike a fused OEM root hash used by secure boot, is secret key material and at all times unique only to a single device. Only a DICE hardware-controlled step can read this. Then afterward, DICE combines both UDS and TCI with a cryptographic one-way function (OWF). The resulting secret is called the Compound Device Identifier (CDI). This secret depends on both the physical hardware and the measured firmware state. All further actions will solely depend on CDI. The measured stage can repeat the operation for the next stage, using its CDI where the hardware used the UDS. The DICE Layering Architecture reduces to this dependency:

TCI[n]   = H(layer[n] code || security-relevant configuration)

CDI[0]   = OWF(seed = UDS,        data = TCI[0])
CDI[n]   = OWF(seed = CDI[n - 1], data = TCI[n])

AliasKey = AsymKDF(CDI[last], "attestation key")

DICE key-derivation chain

Now, the OWF mixes a parent secret with the next measurement without exposing the parent. Repeating that action means the final "alias key" depends on the device's UDS and every measured layer in the path. In turn, that also means that if we were to change an early TCI, every downstream CDI would change as well (including the alias key). The verifier never receives those secrets. In a certificate-based deployment, a manufacturer-backed chain connects the alias public key to measurement claims about the device state. The alias private key is used to sign the challenge-bound evidence discussed earlier. The certificate provides information about what state the key represents. Similarly, the signature proves that whoever the sender is has access to the key.

That said, DICE provides this skeleton, but it's not the owner of deciding. A forged image can theoretically still derive a valid but different identity. Secure boot decides whether it is allowed to execute. The remote verifier only decides whether that identity is acceptable. Anyhow, only a state embedded in a TCI affects a decision. Therefore, the DICE root of trust needs to measure correctly, and the implementation must at all costs protect UDS, each CDI and every derived private key. One thing we will take a look at later on in more detail: Assume that a firmware somehow steals an old alias private key. Could this still be used to sign fresh evidence for a boot state that no longer exists?

So, bottom line, this was quite a bit of new jargon again only to get a single point across: DICE's role is clear. It attempts to turn the boot measurements we already had into an identity used by remote attestation.

Android: Exporting the AVB result

Let's briefly return to the Android chain from earlier. The OS bootloader verified a VBMeta graph locally. Hardware-backed key attestation exports claims about that decision to a remote service. It binds the caller's challenge into an attestation certificate extension. Inside that extension, RootOfTrust describes the AVB authority, enforcement state, and accepted VBMeta graph. Something similar to:

RootOfTrust ::= SEQUENCE {
    verifiedBootKey    OCTET_STRING,
    deviceLocked       BOOLEAN,
    verifiedBootState  ENUMERATED {
        Verified(0), SelfSigned(1), Unverified(2), Failed(3)
    },
    verifiedBootHash   OCTET_STRING
}

Android key attestation's RootOfTrust fields

deviceLocked says whether the bootloader is locked, while verifiedBootState reports the result of "Verified Boot". verifiedBootKey identifies the root public key accepted by the bootloader. In the reference implementation verifiedBootHash is the digest of the top-level VBMeta structure. Together, the key and hash identify the authority and graph behind the Verified result. The remote service must still validate the certificate chain, challenge, and hardware security level, then apply its own policy to those fields, as required by the AOSP attestation guidance.Verified therefore means that the bootloader accepted a particular root and VBMeta graph. It does not mean that the remote service approves that root or that the running device is uncompromised. That distinction will matter later, when a production device truthfully reports Verified after booting a graph signed with a public test key.

The invariant beneath the implementation

We now have both halves of the intended secure system covered. Secure boot makes a local decision about what may execute. Remote attestation exports authenticated evidence so a service can make its own decision. The Qualcomm and AVB chains showed the first half. DICE and Android key attestation showed the second. Their formats and roots differ, but if we strip away those details, they all depend on one security claim:

Finding The device may transfer control only to an authorized state, and the evidence it exports must describe that same state.

For that claim to be true, four conditions must survive the entire path from untrusted storage to execution and remote appraisal:

             ENFORCEMENT        COVERAGE         AUTHORITY          CONTINUITY
             Did it run?        What was signed? Who may sign now?  What ran later?

untrusted --> [ decision ] ----> [ exact bytes ] --> [ policy ] ----> [ immutable use ]
storage          |                    |                 |                  |
                 +--------------------+-----------------+------------------+
                                      one authorization claim

The four invariants, storage to execution

This brings me to the end of this first part of the article. We discussed secure boot, remote attestation, boot chains, DICE, and a few things in between. The next section will drift away from a plain technical primer on these topics and tackle, IMHO, a more interesting aspect. If we assume we want to break a sound implementation on an arbitrary device, what options do we even have?


Failure classes around a sound signature verifier

Earlier we discussed the technical background. Now let's shift gears. Bear with me here a little longer before we get into it. Let's define the following scenario:

Environment A locked production device. Secure boot verifies every image in the chain and the signature checks are cryptographically sound. There is no bug in the verifier. A correct key is being used. A correct algorithm being applied. Despite all of this we can demonstrate that unsigned code still runs. How?

That is the complete premise. "Correct key" means the verifier uses the key provisioned as its trust anchor and performs the intended signature algorithm correctly. It does not assume that provisioning, version policy, coverage, control flow, or the later handoff is correct. The attacker may control boot media or a recovery/update input, repeatedly reset the target, inject physical faults, and exploit a parser or later privileged component. Signature forgery, hash collisions, algorithm confusion, and implementation bugs inside the mathematical signature verifier remain out of scope for this thought experiment. Holding the verifier constant leaves four questions about what the rest of the system actually proved:

  1. Did the check run, and did failure actually stop the boot?
  2. Did it cover the right bytes and the metadata that gives those bytes meaning?
  3. Was it the right authority for this device, image, lifecycle, and point in time?
  4. Are those still the bytes that execute?

Not everything we're going to discuss below will start from a reset. Some are based on getting access to the first instruction pointer. Others walk us through a flaw in signed code.

NOTE This is not an argument against secure boot. It is an argument for treating it as an end-to-end authorization protocol rather than a cryptographic helper function.

To answer the four questions above, I'm going to split these into four distinct questions. I'll discuss each and give a known example of a failure class where someone used this question to circumvent existing mitigations. Let's start with the cheapest failure case, where the verifier returns the right answer, but the boot path never enforces it.

Bucket 1 - Did the check run?

The first invariant is control flow. Every path to execution must reach the check. We need to treat every non-success value as a failure and stop before handing control to the next stage. This sounds too simple to deserve a section. It deserves one precisely because it is simple enough to be assumed.

When the caller destroys the result

/* Correct API: 0 success, any negative value failure. */
int rc = verify_sig(image, key);

/* Bug 1: computed, logged, ignored. */
verify_sig(image, key);
boot(image);

/* Bug 2: one error value was mistaken for the error domain. */
if (rc != -1)
    boot(image);       /* -2, -ENOMEM, -EKEYREJECTED all boot */

/* Bug 3: failure to read policy becomes "secure boot disabled". */
if (read_secure_boot_state(&enabled) != OK)
    enabled = false;

Three ways a caller discards a correct verdict

These are three different bugs, but the outcome is identical! Failure is converted into permission to boot. The verifier did its job. It did it correctly. However, the caller just ignored the result. They are deliberately obvious. A mature bootloader is unlikely to place verify_sig() and boot() next to each other where the mistake can be spotted at first glance. The decision is usually done in different sections. In between, there's likely at least one wrapper, return-type conversion, monitor call, some IPC boundary, or other policy reads. You get the idea. The above example is naive. However, to my surprise, public disclosures show that this is not just a toy example:

CVE-2019-2278 describes that a user-keystore signature was ignored during boot on several Snapdragon product families. The archived CodeAurora patch exposes the control flow:

/* Simplified from the vulnerable code removed by the patch. */
if (verify_keystore(user_addr, ks) == false) {
    boot_verify_send_event(KEYSTORE_VERIFICATION_FAIL);
} else
    dprintf(CRITICAL, "Keystore verification success!\n");

user_keystore = ks;  /* also runs after verification failure */

CVE-2019-2278: the keystore assignment runs on both branches

This shows that user_keystore = ks was right after the conditional statement, so it ran on both paths... Later on, boot_verify_image() passed that keystore into verify_image_with_sig(). The failure changed the recorded boot state, but it did not stop the rejected keystore from becoming an authority used to verify the boot image. Another example is CVE-2019-14560, it reached the same outcome through policy state. EDK II failed to check the result of GetEfiGlobalVariable2()while determining whether secure boot was enabled. Those disclosures are small enough to read as isolated coding mistakes. On a retail Chromecast, the same family of errors became one link in a complete secure-boot bypass.

Case study: CVE-2023-48425 turns AVB failure into success

The system we're going to take a look at here is a retail unit of a Google Chromecast, which was released in 2022. It's built around an Amlogic SoC. In Google's December 2023 security bulletin they mention CVE-2023-48425. A flaw with high severity in the U-boot component. There's a corresponding write-up from the researcher who discovered it.

Environment Production units disabled interruption of autoboot, so the chain first needed a way into the U-Boot shell, which came from a physical fault (CVE-2023-48424). It was not a remote bypass by itself.

This is where another CVE comes in. CVE-2023-48424 allowed for using a physical fault on the eMMC interface to stall boot at the right point, creating enough delay to interrupt U-Boot over UART. That fault did not itself make AVB accept an unsigned image. It only gave the researchers a privileged place from which to inspect and influence the next decision. The device still considered itself a production unit. Its board variant came from an eFuse-backed decision, and is_device_unlocked() continued to report false. The useful exception was elsewhere: Amlogic's upgrade flow used the U-Boot environment variable upgrade_step to mark a maintenance stage. The relevant code, shortened from the device source quoted in the researchers' write-up, was:

upgradestep = env_get("upgrade_step");

if (is_device_unlocked() || !strcmp(upgradestep, "3"))
    flags |= AVB_SLOT_VERIFY_FLAGS_ALLOW_VERIFICATION_ERROR;

result = avb_slot_verify(&avb_ops_, requested_partitions,
                         ab_suffix, flags, error_mode, out_data);

if (!strcmp(upgradestep, "3"))
    result = AVB_SLOT_VERIFY_RESULT_OK;

return result;

CVE-2023-48425: upgrade_step=3 overwrites AVB's result with OK

Recall that strcmp() returns zero when the strings match. In the above snippet, when using upgrade_step=3 both if-conditions activate. The first one lets libavb return verification data despite an error. It does not turn that error into success. The second is the actual downfall of the security decision here. Whatever avb_slot_verify() returned is replaced with AVB_SLOT_VERIFY_RESULT_OK.

attacker-controlled upgrade_step = 3
                 |
                 +-> allow verification errors
                 |
modified image --+-> avb_slot_verify() --> VERIFICATION_ERROR
                                                |
                                                +- overwritten with OK
                                                         |
                                                         v
                                              boot while still "locked"

Chromecast: a valid rejection rewritten to success

So Android Verified Boot (AVB) ran. It cryptographically did not need to fail. The attacker also did not need a signing key. U-boot simply turned a valid rejection into success under an attacker-influenced maintenance state. That distinction is why CVE-2023-48425 belongs in this bucket. CVE-2023-48424 made the state reachable. It was not the code that destroyed AVB's answer. Initially, the bypass lasted for only one boot (tethered). U-Boot cleared upgrade_step=3 during the next startup, restoring normal AVB enforcement. Without another bug, the attacker would have to repeat the physical fault and UART interaction after every reboot. Now to walk through the full chain for good measure, let's briefly talk about what happened after. Persistence came from CVE-2023-6181. U-Boot treated insufficiently restricted Bootloader Control Block data from the misc partition as commands during preboot. After gaining code execution, the researchers used that path to restore upgrade_step=3 on later boots. In the end, Google shipped a fix that made sure that maintenance state must be authenticated, narrowly scoped, and unable to rewrite rejection as success.

Honorable mentions

I felt like just giving a single example for each bucket would not be enough to call them a bucket in the first place. These sections at the end of each bucket will give some honorable mentions about related/similar bugs.

One thing I stumbled upon was CVE-2024-7344. It stood out not because it's a similar style bug but because it ignores the fact it's not related to the embedded world at all. CVE-2024-7344 shows the same failure on the mature UEFI platform used by laptops, desktops, and servers. In this vulnerability, the first verifier did its job. The gap appeared shortly after. Let's do a two-minute primer on UEFI before taking a look at the vulnerability itself. UEFI firmware checks a boot application against its allow and revocation database before executing it. Many systems trust Microsoft's third-party UEFI CA so vendor recovery tools, Linux shims, and other boot software work out of the box. This is the part where Microsoft enters the story now. It signed a binary called reloader.efi under that aforementioned CA. The signature authorized this UEFI application to run. It did not authorize every image the application may load afterward to run. Normally, a UEFI application asks the firmware to load a child with LoadImage() and then later start it via StartImage(). Using this defined API order gives secure boot a chance to apply its policy to the child. Now back to the vulnerability. The excellent ESET analysis found that there's a different path that can be taken instead of those two APIs. In particular, reloader.efi read cloak.dat from the EFI system partition, XOR-decoded the PE image inside it, manually mapped that image into memory, and executed it with its own loader. Now here's the problem: the custom loader never called LoadImage(), the unsigned child image never reached the platform's secure boot check at all.

expected: firmware --verify--> reloader.efi --LoadImage--> verify child --> execute
actual:   firmware --verify--> reloader.efi --custom PE loader-----------> execute

CVE-2024-7344: a custom PE loader skips the check

An attacker with administrator or equivalent physical access could place the signed loader and a malicious cloak.dat on a EFI system partition, then reboot.

This is why this case belongs in this bucket. The Chromecast case earlier was calling AVB and changed its rejection into success. reloader.efi created a new execution "edge" on which the verifier was never called. Both paths ended in unauthorized code execution. The fix here was twofold. Microsoft had to add the old signed binaries into the UEFI's dbx revocation database so firmware would reject them despite valid signatures while the vendor needed to fix the loader. This brings us back to the embedded boot chain from earlier in the article: signing one stage is safe only if every execution edge it creates returns to the platform verifier or enforces an equivalent policy correctly.

There are more examples for this fault bucket, but I'll just briefly mention three others that you could look into yourself.

  1. OP-TEE-2022-0001. On a Raspberry Pi 3, an EM pulse cleared a non-zero signature error in a CPU register to TEE_SUCCESS, causing OP-TEE to accept an invalid signature. OP-TEE 3.20 added fault-mitigation wrappers that check both the called path and the result. Hardware glitch sensors and physical hardening can add another layer against the same fault model.
  2. Nokia 6 Research. A custom USB cable could force the MSM8937 (Qualcomm Snapdragon 430) phone into Qualcomm EDL, where signed Firehose programmers exposed peek and poke memory primitives. The researchers used those primitives to keep a debugger alive, patch boot stages after verification, and eventually start a modified kernel and ramdisk with unrestricted root access. Production devices should tightly gate EDL, reject obsolete or vulnerable programmers, and prevent writes to verified code before handoff.
  3. CVE-2018-20785. On the Neato Botvac Connected (and the related Vorwerk robot vacuum), the AM335x secure path correctly decrypts and runs the normal IPL and QNX IFS. The catch is that sending the right sequence to the USB serial port at startup drops the device into a hidden boot menu that XMODEM-uploads and executes an unsigned QNX IFS. The normal path never fails a check. A sanctioned recovery/download mode simply never calls the verifier at all (as demonstrated in the WOOT '19 paper). This is the recovery-path variant of the bucket, where a maintenance or download mode must not become an unauthenticated boot path.

Together, these four cases close this bucket's scope.

Finding Every path that can execute code, regardless of normal boot, child loaders, faulted checks, or recovery modes, they always need to turn rejection into a terminal state before handing execution off to the next stage.

Would remote attestation have caught Bucket 1?

It would not have stopped the local bypass. At best, it could change the remote decision: reject the device, withhold a fleet key, or deny access to a service. On the Chromecast, an independent measurement of the modified boot image could have produced a digest the backend did not recognize, even though U-Boot changed AVB's rejection into success. Independence is doing all the work. If evidence reused U-Boot's final OK, it would repeat the local mistake. If UEFI measured only reloader.efi, the unsigned child would remain invisible.

Attestation can expose a missing or overwritten check only when it describes the object that actually executes. That leads to the next question: did verification and measurement cover every byte that gave the object its meaning? This is what we're going to cover in Bucket 2.

Bucket 2 - Did it cover the right bytes?

The second invariant is coverage. This time, let's assume the verifier ran, used the correct key, and returned success for a genuinely signed image. That still leaves us with a surprisingly awkward question: what exactly did it authenticate? Looking at this from a broader lens. What is a cryptographic function even doing? On a high level, they are used to authenticate a sequence of bytes in our scenarios. It typically has no idea about the shape. Is this an ELF image, a device tree, a kernel command line, or a "firmware"? None of this really matters to the cryptographic function. The bootloader is the entity that creates that meaning when it parses and then uses authenticated data for further actions. This gives us three broad ways to get the coverage wrong:

  1. An attacker-controlled boundary decides which bytes are hashed
  2. An unsigned selector and destination change what the verified code consumes or where data is placed
  3. The verifier and the loader interpret the same container differently

The common mistake is to think only executable code needs protection. In reality, every byte that changes what is loaded, where it is placed, or how it is interpreted belongs to the same authorization statement.

storage

  [ signed kernel ] [ unsigned selector ] [ unsigned payload ]
          |                   |                   |
          |                   |                   +--> bytes used later
          |                   +--> chooses what the kernel consumes
          +--> signature verification succeeds
                              |
                              v
                    effective program differs
                    from the signed program

The coverage gap: signed code, unsigned selectors

The first failure shape is the most circular one. The artifact is allowed to describe its own verification boundary. This sounds silly, but there are cases for this, which almost always had the shape of:

header = parse_untrusted_header(file);

digest = sha256(file + header->signed_offset,
                header->signed_length);

return verify(digest, header->signature, root_key);

The artifact describing its own signed range

If signed_offset and signed_length are not fixed by the format or authenticated by an earlier stage, an attacker can select them and force what is being checked here. A valid signature over a small genuine region can approve a larger underlying malicious object. The signature is still correct. The verifier was simply asked the wrong question at that time.

The second shape is less obvious. The authenticated code can remain completely genuine while unsigned metadata changes what the code executes. A recent Sonos vulnerability turns that abstract statement into a complete exploit chain!

Case study: CVE-2023-50810 booting a signed kernel with an unsigned initramfs

The target is the Sonos Era 100, a production smart speaker released in 2023. CVE-2023-50810 tracks what we're about to get into. The speakers boot process uses U-Boot and a custom command called sonosboot. At a high level, sonosboot loads and authenticates the kernel image, prepares the kernel command line, and finally hands execution to U-Boot's bootm command.

Environment The standalone attack required physical access to modify eMMC or a separate runtime vulnerability that provided equivalent flash-write access. It was not a remote secure-boot bypass by itself.

The first problem was an unused but still active U-Boot feature. The device attempted to load a persistent U-Boot environment from the eMMC offset 0x500000, even though the factory image did not contain a valid environment there. This normally produced the following warning:

*** Warning - bad CRC, using default environment

That CRC only detects accidental corruption. It does not authenticate who created the environment. An attacker able to modify eMMC could place a correctly formatted environment at that offset and control variables used later in the boot. Controlling the environment was useful, but not sufficient yet. The available U-Boot commands on this device were restricted, and sonosboot attempted to replicate the environment's bootargs with a known kernel command line beore calling bootm:

/* Simplified from NCC Group's reverse engineering. */
setenv("bootargs", kernel_cmdline);
bootm(...);

The Sonos bug: setenv()'s return is ignored

The return value from setenv() was ignored. U-Boot also supports environment flags, including a flag that makes a variable read-only. An attacker could therefore store both a malicious value and an instruction that prevents sonosbootfrom replacing them:

bootargs=... initrd=ADDR,SIZE
.flags=bootargs:sr

In a case such as above, setenv() would fail because bootargs was read-only. Since sonosboot ignored that failure, execution continued with an attacker's old command line. While this attack looks like it would belong to Bucket 1, I'd argue real exploit chains (from the past especially :D) do not care about what taxonomy I'm introducing here. Furthermore, a single exploit chain can, quite frankly, cross more than one bucket as well. So what makes this a nice showcase for Bucket 2 is what comes next. Controlling initrd=ADDR,SIZE only helps if attacker-controlled bytes are present at that address when Linux starts. With this particular Era 100 device, the image format had an attacker-controlled kernel_offset. The normal value was 0x40, but U-Boot did not enforce it. Increasing the offset created space before the genuine kernel for arbitrary data. An attacker was able to use this gap to fill it with a malicious cpio archive while leaving the genuine signed kernel at the offset where sonosboot expected to find it. The overall image could therefore be shaped like this while the real kernel still passed its signature checks:

custom image on eMMC

+----------------------+----------------------+-----------------------+
| image header         | malicious cpio       | genuine signed kernel |
| kernel_offset = N    | archive              | begins at offset N    |
+----------------------+----------------------+-----------------------+
           |                       |                       |
           |                       |                       +--> verifies
           |                       +--> attacker controls these bytes
           +--> tells sonosboot where the genuine kernel begins

after U-Boot loads the image at a predictable base address

BASE                   ADDR                    BASE + N
 |                      |                         |
 v                      v                         v
+----------------------+----------------------+-----------------------+
| image header         | malicious cpio       | genuine signed kernel |
+----------------------+----------------------+-----------------------+
                         <------ SIZE ------>
                         ^
                         |
                         +-- bootargs contains initrd=ADDR,SIZE

Sonos image layout: unsigned prefix, signed kernel

U-Boot would load the complete image at BASE. It found a genuine kernel at BASE + N and verified it. The earlier archive bytes remained in RAM, so ADDR could point into that prefix and SIZE could describe its length. The signed kernel was still the vendor's kernel. The signature covered neither the offset that staged the prefix nor the command line argument that gave those bytes meaning as an initramfs. In other words, kernel_offset was used as a placement primitive. initrd=ADDR,SIZE then was used as a selection primitive. The exploit relies on both. Linux processes initrd=XXX early in the boot process. In a simplified form, the relevant handler turns the command line string back into a physical address and size:

static int __init early_initrd(char *p)
{
    unsigned long start = memparse(p, &p);

    if (*p == ',') {
        phys_initrd_start = start;
        phys_initrd_size = memparse(p + 1, NULL);
    }

    return 0;
}

Linux parsing the attacker-controlled initrd=

This handler only records the physical start and size. Later initramfs code reads that memory range, decompresses the cpio archive, and creates its files in the temporary root filesystem. Because an attacker could supply an /init, the kernel selected it as the first userspace process. It ran as PID 1 with root privileges. From there, the researchers were able to load a custom kernel module and obtain kernel execution. The full data flow looked something like this:

attacker-controlled eMMC
        |
        +-- U-Boot environment
        |      |
        |      +-- bootargs="... initrd=ADDR,SIZE"
        |      `-- .flags=bootargs:sr
        |
        `-- custom boot image
               |
               +-- kernel_offset=N
               +-- unsigned initramfs at ADDR
               `-- genuine signed kernel
                              |
                              v
                    sonosboot verifies kernel
                              |
                              +-- setenv("bootargs", safe_value)
                              |       |
                              |       `-- fails; result ignored
                              |
                              v
                         bootm starts
                      genuine signed kernel
                              |
                              `-- parses initrd=ADDR,SIZE
                                           |
                                           v
                                  reads archive from RAM
                                           |
                                           v
                                  unpacks cpio into rootfs
                                           |
                                           v
                                  runs attacker /init as PID 1

The full Era 100 chain to root

This is why, in my opinion, this exploit chain is a great fit for Bucket 2 despite also having an overlap with Bucket 1 due to an ignored return value. The signature covered the kernel bytes. It did not cover all metadata that determined the kernel's effective input.

The Era 100 was not the first device to get this wrong. In 2017, CVE-2016-10277 let a locked Nexus 6 inject its own initrd=ADDR,SIZE through Motorola boot configuration. The payload came from a stale fastboot download buffer rather than an image prefix, but the authorization failure was the same: ABOOT verified one initramfs while an unsigned command-line selector made the signed kernel consume another.

Honorable mentions

The Sonos chain combined a coverage bug with a failure to stop. The next four cases take different routes through the same coverage invariant.

  1. CVE-2023-39902 affected U-Boot SPL on several NXP i.MX 8M families. SPL parsed an unauthenticated FIT/FDT description before authenticating the FIT payload. A crafted description could steer writes over SPL memory and lead to the execution of unauthenticated software. NXP mitigated the problem by binding the FIT description to the ROM-authenticated SPL, so SPL authenticates the structure before trusting it to find and place the remaining payload.
  2. U-Boot's FIT format has twice allowed artifact metadata to influence the verifier's own coverage. CVE-2018-1000205 trusted a hashed-strings offset supplied by the FIT itself. The fix was simple, they removed trust in FIT-supplied hashed-strings offsets. Another related disclosure in 2026 (tracked under CVE-2026-46728) showed that hashed-nodes was not protected and failed to bind full paths. The fix here was to derive mandatory full paths from the selected configuration instead of trusting the artifact's list.
  3. CVE-2024-32883 brings the same problem into MCUboot and remote attestation. The project's advisory shows that MCUboot separated protected and unprotected TLVs, but it did not enforce that every security-relevant TLV type appear in the protected region. An attacker could inject a boot record that later fed unauthenticated properties into attestation data. The immediate workaround was to disable boot-record functionality when it was not needed. The fix here restricted which TLV types may remain unprotected.
  4. CVE-2023-20696 is the third shape from the top of this bucket. The verifier and the loader read the same container differently. MediaTek's preloader parsed the boot certificate chain with an ASN.1 routine whose permissive mode steps into an object without first checking its type. An attacker prepends the genuine cert2 with a DER BIT STRING that wraps a valid copy of it. The signature check follows the wrapper and verifies the embedded genuine certificate, while a later parse skips the wrapper and reads the firmware hashes from the attacker-controlled outer object. Malicious firmware whose hashes match those forged values then passes as authentic. Same signed bytes, two parses, two different objects.

These examples cover four different inputs: an image description, the verifier's coverage list, a later attestation claim, and a single container the verifier and loader read two different ways. In every case, attacker-controlled bytes (or an attacker-chosen reading of them) stayed outside the statement that was supposed to authorize it.

Finding The authorization boundary must include every byte that changes what is loaded, where it is placed, and how it is interpreted. Signing the code is insufficient when unsigned metadata can redirect it.

Would remote attestation have caught Bucket 2?

Again, remote attestation would not have stopped the local bypass. It could only help a remote service if its measurement described the state that actually executed. Suppose the Era 100 measured only the signed kernel. The digest would remain expected even while that kernel consumed an unsigned initramfs. Freshness and a valid attestation signature would merely authenticate an incomplete claim. An independent measurement of the final command line and initramfs could expose the difference. However, MCUboot shows the awkward failure mode (as seen in CVE-2024-32883): attacker-controlled metadata may also contaminate the evidence used to report that state. Every attestation claim, therefore, needs a trace back to authenticated measurement input. Otherwise, remote attestation transports the local coverage mistake across the network.

Complete coverage tells us what a signature authorized. It still does not tell us whether that signer was allowed to authorize this image for this device, lifecycle, and point in time. That is Bucket 3.

Bucket 3 - Was it the right authority?

Bucket 2 gave us a complete authorization boundary. Let's continue with an optimistic assumption of every security-relevant byte being covered, the verifier running, and failure stopping the boot from progressing. This sounds like we're golden, but we can still "lose" here because a valid signature only proves that the owner of a private key approved those bytes. What it does not answer is whether that signer should have been trusted in the first place. The device still has to decide what this key may sign and whether that permission is still valid. This is the difference between authentication and authorization. The first asks, "Does this signature match this key and message?" The second asks, "Is that relationship acceptable here?"

                    cryptographic check

image + signature + candidate public key
                    |
                    v
             signature is valid
                    |
                    v
                    authorization check

        +---------------------------------------+
        | Is this signer allowed for:           |
        |                                       |
        | - this component                      |
        | - this product                        |
        | - this hardware lifecycle             |
        | - this security version               |
        | - the current revocation policy       |
        +---------------------------------------+
                    |                 |
                   yes                no
                    |                 |
                    v                 v
                   boot             reject

Authentication vs. authorization

For example, a key trusted only for recovery must not authorize a TEE image, and a development key must not authorize software on a production device. That separation only works if the bootloader has a trustworthy answer to a basic question: What am I about to load? Usually, the answer comes from the bootloader's own control flow. If it opened the TEE partition because its next job is to start the TEE, then it already knows the intended role before it parses the candidate image. It must carry that role into the authorization decision. The candidate cannot be allowed to call itself a recovery image just so its signer is checked against the recovery-key policy. If a manifest supplies that identity, an earlier trusted stage must already have authenticated the binding. This is the coverage rule from Bucket 2, now applied to the metadata that selects an authority. That fixes the component-scope problem, but it does not make authorization permanent. Even an image signed by the correct release key becomes unacceptable once the rollback floor moves past its security version. In the following pseudocode, expected_component is simply the name given to that role already known by the bootloader. A stripped-down policy check could look similar to this:

bool image_is_authorized(const struct image *image,
                         enum component_id expected_component,
                         const struct device_state *device,
                         const struct policy *policy)
{
    if (!verify_signature(image, &image->signing_key))
        return false;

    if (!key_allowed(policy, image->signing_key.id,
                     expected_component,
                     device->product,
                     device->lifecycle))
        return false;

    if (image->security_version < device->floor[expected_component])
        return false;

    if (key_revoked(policy, image->signing_key.id) ||
        image_revoked(policy, image->digest))
        return false;

    return true;
}

A full authorization check, not just a signature

The signature verification in the first if can be flawless while any later check is missing or fed the wrong state. This is the first question from above that was asking about which component and device state a key may authorize. The next question will be about whether a requested image n is still allowed on date m. Typically the rollback floor answers that question. An old image remains authentic. Its hash matches, its signature verifies, and a vendor really did release it. The problem is that it contains a vulnerability fixed by a new version. We've been through this in the very beginning of this blog. That said, from an offensive perspective, the interesting part is that anti-rollback does not strengthen the signature. It withdraws permission from an otherwise valid image. Android Verified Boot (AVB) implements the core comparison quite literally. libavb reads a protected floor for the selected rollback-index location and compares it with the signed value in VBMeta:

io_ret = ops->read_rollback_index(
    ops, rollback_index_location_to_use, &stored_rollback_index);

if (io_ret != AVB_IO_RESULT_OK)
    return AVB_SLOT_VERIFY_RESULT_ERROR_IO;

if (vbmeta_header.rollback_index < stored_rollback_index) {
    ret = AVB_SLOT_VERIFY_RESULT_ERROR_ROLLBACK_INDEX;
    if (!allow_verification_error)
        goto out;
}

libavb's rollback-index comparison

The comparison is only half of what's being done. libavb returns candidate indexes to the platform. The bootloader then must persist them in tamperproof storage without preventing an A/B device from falling back before a new slot is known to boot. The AVB integration guide therefore recommends advancing floors only from a slot marked successful. Never advancing them leaves every older signed image authorized forever!

Then we have the notion of revocation. It solves a related but different problem. A rollback floor rejects an outdated version. Revocation rejects a signer or a specific object that must no longer be trusted, even when its version is high enough. With this out of the way, we are ready to look at a case where the verifier, the image graph, and the lock-state report all behaved consistently. The authority provisioned underneath them was the actual vulnerability.

Case study: AVBTestKeyInTheWild turns a test key into a production root

The 2025 AVBTestKeyInTheWild paper found AOSP AVB test keys in first-party production firmware. The main result of that research is crystal clear: the attacker does not break RSA. They used a private key that the product effectively told AVB to trust.

Environment The publicly available AOSP test signing key only lets an attacker sign images, not write them. Each demonstrated device also needed physical access and its own flashing or lock-state path to place them. It was not a remote bypass by itself.

As with the UEFI example from an earlier bucket, this is also not just a fault of the base, which in this case is Android (compared to Microsoft Windows before). It's about how integration can open holes. Anyhow, in this specific vulnerability, there's one detail that is important. A public key is supposed to be public, so finding one inside firmware is normal. In a production key pair, the private key must remain secret because it creates new valid signatures. A test key deliberately drops that assumption so anyone can use it during development. AOSP deliberately ships AVB test private keys so developers can build and test a complete verified-boot graph. There is nothing wrong with that either. They are test material and are public by design. The production failure happens when an OEM does not replace them. The AVB build documentation defaults to a test key and shows test-key paths in example configurations. A release build has to override them with product-controlled authority. At runtime, libavb verifies the signature using the public key carried by the VBMeta authentication block. It then asks the platform whether that key is trusted through validate_vbmeta_public_key(). The platform side is conceptually doing this:

AvbIOResult validate_vbmeta_public_key(
    const uint8_t *candidate_key,
    size_t candidate_key_len,
    bool *out_is_trusted)
{
    const uint8_t *root = read_provisioned_avb_root();

    *out_is_trusted = constant_time_equal(
        candidate_key, candidate_key_len,
        root, provisioned_root_len());

    return AVB_IO_RESULT_OK;
}

AVB's key-trust callback

This callback returns the correct answer for the state provisioned into the device. If root is the AOSP test public key, an attacker with the publicly available matching private key can satisfy both checks exactly as intended. The researchers first walked the chained VBMeta graph in official firmware and compared every signing key with known AOSP test keys. Their mass scan found 69 potentially vulnerable device models, not 69 devices on which exploitation was individually confirmed. They then demonstrated the complete attack on three production devices using Qualcomm, MediaTek, and Unisoc SoCs. The cryptographic part of the modification followed the normal AVB release flow:

official firmware
      |
      +-- unpack boot.img
      |
      +-- modify ramdisk and install Magisk
      |
      +-- rebuild boot.img
      |
      +-- create a new AVB footer for the modified image
      |
      +-- rebuild the affected VBMeta descriptors
      |
      +-- sign each changed AVB node with its known test private key
      |
      `-- verify the completed graph with avbtool
                         |
                         v
              mathematically valid graph

Re-signing a modified image with the public test key

Nothing in that flow asks the attacker to find a hash collision or forge a signature. They recalculate the hashes, rebuild the descriptors, and sign the result with a private key that should never have represented a production authority!

publicly available AOSP test private key
                     |
                     +-- signs modified boot.img metadata
                     |
                     `-- signs rebuilt VBMeta graph
                                      |
                                      v
production bootloader trusts matching test public key
                                      |
                     +----------------+----------------+
                     |                                 |
                     v                                 v
              signature valid                 key is "trusted"
                     |                                 |
                     +----------------+----------------+
                                      |
                                      v
                            locked, green boot

Test key accepted as a production root

That gets us a bootable signed image, but not yet a way to place it on a locked phone. This distinction is easy to skip and would leave the exploit chain with a fairly large hole. For example, on the Fairphone 3, the researchers combined the bad authority with an older firmware's weak lock-state handling. A devinfo partition represented the lock state, and the locked and unlocked contents differed by only two bits. The researchers could unlock, flash the modified boot.img and vbmeta.img, then restore the locked devinfo contents.

save locked devinfo
        |
        v
unlock without the expected automatic wipe
        |
        v
flash attacker-built boot.img and vbmeta.img
        |
        v
restore locked devinfo contents
        |
        v
bootloader reports locked
        |
        v
AVB verifies signatures under the provisioned test key
        |
        v
modified firmware boots green

Fairphone 3: faking locked state via devinfo

Just like the Sonos chain in Bucket 2, the complete exploit crosses bucket boundaries. The devinfo weakness provides the write and relock primitive. The test key makes the modified graph an authorized production image after that state is restored. The result is why this makes such a strong example. The Fairphone 3 booted a Magisk-modified image in the green Verified Boot state and passed MEETS_STRONG_INTEGRITY. The local report was internally consistent. Bottom line for this exploit chain: The device was locked. AVB had verified the graph. The reported boot key and boot hash described the graph that had just been accepted. The word "verified" was not a lie. The policy underneath it trusted a key available to every attacker.

field reported remotely          what it means in this attack
------------------------------   --------------------------------------
deviceLocked = true              devinfo currently says "locked"
verifiedBootState = Verified     AVB accepted the configured root
verifiedBootKey = H(test key)    attacker has the matching private key
verifiedBootHash = H(new graph)  graph describes the modified firmware
fresh challenge                  evidence is fresh, but the image is bad

Truthful evidence, wrong authority

So, how to fix or, more importantly, prevent these kinds of problems? While I'm not proposing a universal fix, in my book it's all about the fact that production devices need a product-controlled root, and the release pipeline must reject any VBMeta graph that still contains a known test key. The more interesting problem is recovery. If the bad root is immutable, the device cannot distinguish the vendor's repair update from an attacker's update. Both signatures verify under the same trusted key. Without an independent hardware-backed rotation or revocation path, field repair may be impossible. The mechanism that should deliver the fix now authorizes the attacker as well.

Honorable mentions

The earlier discussed "AVBTestKeyInTheWild" is a provisioning failure. The same authority invariant also breaks when a version never reaches its rollback database or when an old signer remains trusted after it should have been retired. There are a few additional examples for such cases:

  1. CVE-2026-44362 affects OP-TEE 3.20 through 4.10 when subkey-based TA signing chains are used. shdr_load_pub_key() parsed subkey_version but did not copy it into the runtime key, so check_update_version() received zero and the rollback database never advanced. OP-TEE 4.11 added the missing assignment: key->version = subkey->subkey_version;.
  2. The UEFI detour from Bucket 1 pays off again here as well: Earlier in 2026, ESET found 11 old Microsoft-signed shims at version 0.9 or earlier. An attacker could bring one to systems trusting Microsoft's third-party UEFI CA. One bypass made revocation and signature verification use different PE signature-length fields. The mismatch could hide a revoked second-stage certificate and lead to bootkit execution.
  3. PKfail, CVE-2024-8105, found non-production AMI Platform Keys in hundreds of UEFI product models and one matching private key in a public leak. That private platform key can replace the KEK, which can update db and enroll a key for malicious boot code while Secure Boot remains enabled.

These are three different policy failures. OP-TEE loses the version, the old shims outlive their intended trust, and PKfail gives a reference key production-wide authority. In every case, a valid signature becomes a poor reason to boot.

Finding A signature proves that one key approved one byte sequence. Secure boot must also prove that the key may authorize this component, product, lifecycle, and version. It must also prove that neither the key nor the image has since been revoked.

Would remote attestation have caught Bucket 3?

This bucket is where remote attestation gets uncomfortable. It can faithfully export the wrong local authority decision. The Android RootOfTrust claims we introduced earlier include verifiedBootKey and verifiedBootHash. A verifier that pins both fields to an approved key and release graph for the exact product could have rejected the modified Fairphone firmware. That is stronger than accepting deviceLocked = true, verifiedBootState = Verified, or an aggregate integrity label. The "AVBTestKeyInTheWild" demonstration passed MEETS_STRONG_INTEGRITY, so those higher-level facts were not sufficient. The backend also needs a current policy. A fresh nonce only proves that the device created this evidence now. It does not prove that the signing key, firmware version, or reference digest is still acceptable now. Attestation therefore needs its own rollback floors, key revocations, product bindings, and release allowlists. Otherwise, it simply transports the local authority mistake across the network and gives it a fresh signature.

At this point our image has reached every check, every meaningful byte was covered, and the signer was authorized for the exact product and version. One question remains: are those still the bytes used when control is transferred? That is Bucket 4.

Bucket 4 - Are those still the bytes that execute?

Bucket 3 gave us the correct signer for the correct component and version. Even that decision only says that a particular sequence of bytes was acceptable at the exact moment it was checked. The final invariant is continuity between that check and the later handoff. The CPU does not execute AUTH_OK. It executes whatever bytes the final pointer references when the bootloader jumps. When looking at a very trivial, stripped-down bootloader, this becomes clear:

ctx->image = download_buffer;

if (authenticate(ctx->image, ctx->image_len) != AUTH_OK)
    halt();

prepare_handoff(ctx);
BootLinux(ctx->image);

Continuity: the bytes must not change after the check

This is safe only if prepare_handoff() cannot change ctx->image, its length, or the bytes behind that pointer. The same must be true for every other CPU, peripheral, and debug interface that can write to the region. There are two basic ways to break that assumption. One is pointer replacement, which is basically:

verify(ptr -> signed image A) -> AUTH_OK -> ptr = image B -> boot(ptr)

The other one is in-place mutation, which has the shape of:

verify(buffer A) -> AUTH_OK -> overwrite buffer -> boot(buffer A')

Both are, mostly, time-of-check to time-of-use (TOCTOU) problems. What was true when the image was checked is no longer true when it is used. The pointer-replacement form gives us the cleanest technical deep dive because the two objects can be named: image A passes authentication, but image B reaches the CPU.

Case study: CVE-2021-1931 lets image A authorize image B

Wade's Snapdragon 660 research targeted two production phones from different manufacturers. Their models were not disclosed.

Environment The pointer swap is a second-stage technique, not the foothold. It assumes the attacker already controls the bootloader, which here came from CVE-2021-1931, a Fastboot overflow reachable over USB. So the prerequisite was physical USB access, not a remote position.

The initial vulnerability was CVE-2021-1931. It was a length-validation bug while processing Fastboot commands. The CVE allowed control of the bootloader. The signed-image/unsigned-image swap came afterward, so it is important not to confuse the foothold with the technique that broke our continuity invariant. Fastboot runs inside the Android Boot Loader (ABL) and accepts commands and downloads over USB. On these phones, the download area and the loaded bootloader code shared one writable memory layout. A large payload sent where a Fastboot command was expected ran past its buffer and into the bootloader itself. The research found that the overwrite began after0x101000 bytes on one phone and 0x403000 on the other. That is an unusual exploitation primitive. Instead of redirecting execution to a separate payload, the input rewrote instructions in the program that was already running. The basic idea with this primitive was to reconstruct enough of the overwritten ARM64 code to keep the bootloader alive, then change selected instructions. At that point, this ultimately led to patching the locked-device checks and the later boot flow in RAM. The interesting obstacle came next. An internal Qualcomm service controlled access associated with encrypted user data. It checked both the bootloader's lock state and the Android image's signature. The compromised bootloader could not simply patch that more privileged service. However, authentication and execution were separate calls. The service checked an image through LoaderImageAndAuth(), while ABL later passed a pointer to BootLinux(). The used trick here was to change the Fastboot client to upload one composite buffer:

Fastboot download buffer

payload start                     payload + b_offset
      |                                  |
      v                                  v
+------------+--------------------------+--------------------------+
| b_offset   | signed Android image A   | unsigned Android image B |
| 4 bytes    | verifier reads this      | attacker modified this   |
+------------+--------------------------+--------------------------+
             ^                          ^
             |                          |
             +-- first image pointer    +-- later image pointer

CVE-2021-1931: one buffer, signed A and unsigned B

First, the patched Fastboot path moved past the four-byte offset and placed a pointer to signed image A in Info->ImageBuffer. LoaderImageAndAuth() therefore saw a legitimate image and a locked bootloader. After authentication returned success, four replaced ARM64 instructions moved the pointer again:

sub x19, x19, #4          // move back to the start of the payload
ldr w22, [x19]            // read b_offset
add x19, x19, x22         // point x19 at unsigned image B
str x19, [x21, #0xa0]     // replace Info->ImageBuffer

Four instructions retarget the image after authentication

At this point, x21 referenced the Info structure and offset 0xa0 held its image pointer. The final store did not modify image A. It changed which image the next routine would see. If we were to map this entire second stage into pseudo-code, it would reduce to something like this:

uint8_t *payload = fastboot_download_buffer;
uint32_t b_offset = load_le32(payload);

Info->ImageBuffer = payload + 4;           /* signed image A */
status = LoaderImageAndAuth(Info);         /* returns success */

Info->ImageBuffer = payload + b_offset;    /* unsigned image B */
BootLinux(Info);                           /* parses and boots B */

Authenticate A, boot B

The privileged service did exactly what it was asked to do. It authenticated A. BootLinux() also did exactly what it was asked to do. It booted B. The missing property was a binding between both calls. Laid out over time, the checked object and the used object are never the same one:

time ------------------------------------------------------------->

  t0: authentication                 t1: boot handoff
  ------------------                 ----------------
  Info->ImageBuffer = payload + 4    Info->ImageBuffer = payload + b_offset
           |                                  ^
           |                                  | str x19, [x21, #0xa0]
           v                                  |  (runs after AUTH_OK)
     signed image A                     unsigned image B
           |                                  |
           v                                  v
  LoaderImageAndAuth(Info)            BootLinux(Info)
     returns AUTH_OK                    parses and boots B

              checked object   !=   used object

Checked object ≠ used object (TOCTOU)

This led to booting an unsigned Android image without changing the locked state. The same separation also let the signed image satisfy the check protecting Qualcomm's user-data path before the unsigned image ran. The primary fix was, of course, to remove the Fastboot overflow. The bucket-level lesson is smaller and outlives the overflow: the final consumer must use the same immutable byte range that produced the authentication result. LoaderImageAndAuth() was never wrong. It just described an Info->ImageBuffer the caller could still rewrite. Binding the two calls closes the gap. Authentication should return an opaque handle over an exact, now-immutable range instead of a Boolean over a caller-owned pointer, and the handoff should consume that handle:

/* Authentication seals the exact byte range it verified and returns a
 * handle. No raw Info->ImageBuffer survives for a compromised caller to
 * retarget between the check and the jump. */
verified_handle h;

if (loader_authenticate_and_seal(payload + 4, image_len, &h) != AUTH_OK)
    halt();

boot_verified_image(h);   /* resolves h internally; takes no pointer */

The fix: seal the verified range behind a handle

The same can be reached without handles by verifying only after the image lands in protected final memory. The caller can no longer write. Either way, compromised caller state must not be able to retarget the handoff!

Honorable mentions

The Snapdragon case replaced a software pointer. The same invariant fails when an attacker changes external storage between two reads or when another hardware agent can modify data after it was checked.

  1. CVE-2024-28183 affected the anti-rollback path in the ESP-IDF bootloader. A physical attacker could change the flash after the version check but before loading, causing an older valid passive application partition to boot even with flash encryption enabled. The vulnerable check sat in bootloader_utility_load_boot_image(). Here, check_anti_rollback() ran before try_load_partition() re-fetched the image from flash, so the checked bytes and the loaded bytes could differ. elttam's write-up has all the details. Espressif fixed the gap by reading the version while hashing, checking it again after loading, and adding a final application-side version check before the handoff.
  2. CVE-2023-20521 applied the same pattern to SPI storage on several AMD embedded processor families. A physical attacker could tamper with SPI ROM records after the Secure Processor bootloader verified their memory content. AMD addressed it through updated Platform Initialization firmware. The smaller impact still exposes a writer that reviews often miss: the storage device itself.

These examples differ in impact and in the writer used. The security failure is the same: the later consumer relied on an authentication statement about an earlier state of the object. Pointer replacement and storage TOCTOU are the best examples for this bucket, but the checked object and the executed object can drift apart in other ways too, and each example I give you here is backed by real research, even when the bug is not strictly a secure-boot handoff break:

  1. A DMA-capable master or debug agent can write the verified region after the check, or the core can re-fetch execute-in-place code that was swapped after verification: ONEKEY's X(R)IP research does this on an ESP32, flipping the chip-select line so the SoC runs XiP from a second, unverified flash
  2. Post-verification relocation or decompression can act on bytes that sat outside the signed range: on the ESP32, CVE-2018-18558 left application-section load addresses uncovered, letting an attacker steer where a signed image's sections landed.
  3. Runtime W^X failure can let unsigned code run long after a clean boot: Gal Beniamini's War of the Worlds used Qualcomm's QSEE, which could map normal-world memory as both writable and executable, to inject code into the running Linux kernel.

The last two are coverage and runtime failures more than handoff breaks, but they sit in the same family: something was trustworthy when it was checked and different when it was used. The Nokia 6 EDL chain from Bucket 1 is the purest handoff version. The researchers halted the SoC after PBL authenticated SBL but before control reached it, patched the authenticated SBL in memory, then repeated the pattern on ABOOT and the kernel after each check. Every object was genuine when verified and attacker-controlled when used.

Finding Authentication must remain bound to the exact bytes, length, and entry point until control is transferred. If a pointer or writer can change that object after the check, secure boot has authenticated its history, not what the CPU executes.

Would remote attestation have caught Bucket 4?

Only if its measurement has the same continuity. If measured boot hashes image A and the boot path later switches to B, the backend receives honest evidence about the wrong object. The DICE section gave us a related promise: changed firmware should produce a different alias key. A 2022 DICE TOCTOU paper showed how that promise can fail after the key is derived. The researchers used an nRF52840 evaluation platform. Runtime malware copied the valid DICE alias private key and its certificate from RAM into flash before modifying the firmware. After reboot, DICE derived new credentials for the changed firmware as expected. The malware then replaced those credentials in RAM with the saved key and certificate from the former good state. Their backend sent a fresh nonce, and the compromised device signed it with the old private key. The response was new, its signature was valid, and the backend still accepted it despite the modified firmware. So a nonce prevents replay of an old response. It does not prevent reuse of an exposed old key to sign a new response. This is Bucket 4 moved across the network. The system measured one state, but a mutable credential reference allowed another state to speak for it. Remote attestation therefore cannot repair a local continuity gap by itself. The measurement, derived identity, and final use of that identity must remain tied to the same protected state.

That completes the four failure classes. In every bucket, the signature verifier can return the mathematically correct answer while the surrounding system asks the wrong question, ignores the answer, or applies it to something else. The next, and last, part attempts to turn those four buckets into a method for reviewing an actual boot chain.

Applying the four questions to a real boot chain

None of this needs a new tool or a grand methodology. When I'm dropped in front of an unfamiliar boot chain, the four questions are simply the order I work in. At every transition, and again at every stage of the attestation path that reports on it, I tackle the same four:

  1. Trace every route that can reach execution and confirm that failure terminates each one.
  2. Map the authenticated byte ranges against everything later parsed, copied, decompressed, relocated, or executed.
  3. Record which root, signer, product, lifecycle, version, and revocation state authorize the transition.
  4. Follow the verified object through memory ownership and mutation until the final handoff.

The "would remote attestation have caught this?" aside, that closed every bucket was not filler. The same four questions apply to the evidence a device exports: did evidence generation run, did it cover the bytes that matter, was the reported authority the right one, and does the token still describe the device by the time a relying party acts on it? Every real chain I have looked at trips over one of these long before the signature math is ever in danger.

Conclusion: the signature was never the whole claim

The cryptography is allowed to be the strongest part of the system. In mature secure-boot failures, it often is. The useful attacker question is not "How do I forge this signature?" It is, "What proposition did the signature prove, who decided that was sufficient, and how long did it remain true?" Secure boot needs an immutable root, a non-bypassable path, coverage of code and meaning-bearing metadata, a current authority and rollback policy, and continuity from verification to execution. Remote attestation exports claims about that state. It does not repair a broken local chain. It introduces its own evidence, key protection, freshness, and appraisal boundaries. Four questions are enough to keep the review honest:

  1. Did the check run?
  2. Did it cover the right bytes?
  3. Was it the right authority?
  4. Are those still the bytes that execute?

If any answer is "I assume so," that is where I would start reversing!

Note This research makes use of excellent public research. I highly encourage reading them and showing those original researchers some love!

References

❌
❌