

Sylvain Kerkour - A fast port scanner in 100 lines of Rust
source link: https://kerkour.com/blog/rust-fast-port-scanner/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

A fast port scanner in 100 lines of Rust
Wed, Aug 11, 2021
To write a fast port scanner, a programming language requires:
- A Good I/O model, not to eat all the resources of the system.
- High-level abstractions and a good packaging system to isolate low-level code and reuse it easily.
- To be type and memory safe, because who wants offensive tools with vulnerabilities?
- And, ideally, to be compiled, because most of the time, it’s worth trading a little bit of compile time for extreme runtime speed.
Guess what? These are precisely Rust’s selling points. So let see how to build a high-speed port scanner in 100 lines of Rust.
A port scanner is basically composed of 3 parts:
- A list of ports to scan
- A port scanning algorithm (see this list on nmap’s website)
- A concurrency primitive, to scan port concurrently
A Generic port list
Scanning all the 65535 ports is often wasteful and useless. Thus, we first need to extract the list of the most common open ports in the wild. Fortunately, the nmap project already has such a list:
// from awk '$2~/tcp$/' /usr/share/nmap/nmap-services | sort -r -k3 | head -n 1000 | tr -s ' ' | cut -d '/' -f1 | sed 's/\S*\s*\(\S*\).*/\1,/'
pub const MOST_COMMON_PORTS_1002: &[u16] = &[
5601, 9300, 80, 23, 443, 21, 22, 25, 3389, 110, 445, 139, 143, 53, 135, 3306, 8080, 1723, 111,
995, 993, 5900, 1025, 587, 8888, 199, 1720, 465, 548, 113, 81, 6001, 10000, 514, 5060, 179,
1026, 2000, 8443, 8000, 32768, 554, 26, 1433, 49152, 2001, 515, 8008, 49154, 1027, 5666, 646,
5000, 5631, 631, 49153, 8081, 2049, 88, 79, 5800, 106, 2121, 1110, 49155, 6000, 513, 990, 5357,
427, 49156, 543, 544, 5101, 144, 7, 389, 8009, 3128, 444, 9999, 5009, 7070, 5190, 3000, 5432,
1900, 3986, 13, 1029, 9, 5051, 6646, 49157, 1028, 873, 1755, 2717, 4899, 9100, 119, 37, 1000,
3001, 5001, 82, 10010, 1030, 9090, 2107, 1024, 2103, 6004, 1801, 5050, 19, 8031, 1041, 255,
// ...
];
Then, we need to be able to return either this list or all the 65535 ports. In Rust, the way to achieve this is with an Iterator.
But, as Iterator is a trait, we need to use a Trait Object to be able to return an Iterator of 2 different types.
fn get_ports(full: bool) -> Box<dyn Iterator<Item = u16>> {
if full {
Box::new((1..=u16::MAX).into_iter())
} else {
Box::new(ports::MOST_COMMON_PORTS_1002.to_owned().into_iter())
}
}
Scanning a single port
To scan a port, we will use the TCP connect technique, as it’s the easiest one to implement and requires no special privilege or raw socket. The 20% which brings us 80% of the results.
async fn scan_port(target: IpAddr, port: u16, timeout: u64) {
let timeout = Duration::from_secs(timeout);
let socket_address = SocketAddr::new(target.clone(), port);
if tokio::time::timeout(timeout, TcpStream::connect(&socket_address))
.await
.is_ok()
{
println!("{}", port);
}
}
Extreme concurrency
If you are a recurrent reader of this blog you should have guessed the perfect concurrency primitive for the task (if not, you can subscribe here): A Stream :)
async fn scan(target: IpAddr, full: bool, concurrency: usize, timeout: u64) {
let ports = stream::iter(get_ports(full));
ports
.for_each_concurrent(concurrency, |port| scan_port(target, port, timeout))
.await;
}
Parsing CLI arguments
And finally, we need to parse the CLI arguments and all the configuration boilerplate to run our scanner:
use clap::{App, Arg};
use futures::{stream, StreamExt};
use std::{
net::{IpAddr, SocketAddr, ToSocketAddrs},
time::Duration,
};
use tokio::net::TcpStream;
mod ports;
#[tokio::main]
async fn main() -> Result<(), anyhow::Error> {
let cli_matches = App::new(clap::crate_name!())
.version(clap::crate_version!())
.about(clap::crate_description!())
.arg(
Arg::with_name("target")
.help("The target to scan")
.required(true)
.index(1),
)
.arg(
Arg::with_name("concurrency")
.help("Concurrency")
.long("concurrency")
.short("c")
.default_value("1002"),
)
.arg(
Arg::with_name("verbose")
.help("Display detailed information")
.long("verbose")
.short("v"),
)
.arg(
Arg::with_name("full")
.help("Scan all 65535 ports")
.long("full"),
)
.arg(
Arg::with_name("timeout")
.help("Connection timeout")
.long("timeout")
.short("t")
.default_value("3"),
)
.setting(clap::AppSettings::ArgRequiredElseHelp)
.setting(clap::AppSettings::VersionlessSubcommands)
.get_matches();
let full = cli_matches.is_present("full");
let verbose = cli_matches.is_present("verbose");
let concurrency = cli_matches
.value_of("concurrency")
.unwrap()
.parse::<usize>()
.unwrap_or(1002);
let timeout = cli_matches
.value_of("timeout")
.unwrap()
.parse::<u64>()
.unwrap_or(3);
let target = cli_matches.value_of("target").unwrap();
if verbose {
let ports = if full {
String::from("all the 65535 ports")
} else {
String::from("the most common 1002 ports")
};
println!(
"Scanning {} of {}. Concurrency: {:?}. Timeout: {:?}",
&ports, target, concurrency, timeout
);
}
let socket_addresses: Vec<SocketAddr> = format!("{}:0", target).to_socket_addrs()?.collect();
if socket_addresses.is_empty() {
return Err(anyhow::anyhow!("Socket_addresses list is empty"));
}
scan(socket_addresses[0].ip(), full, concurrency, timeout).await;
Ok(())
}
$ cargo run --release -- kerkour.com
80
8080
8443
443
Conclusion
Even if Rust is a strongly typed and compiled language, we just built a complex program as easily as if it’s were in a scripting language, but way faster and way safer.
The next steps? Implementing more port scanning strategies.
As usual, you can find the code on GitHub: github.com/skerkour/kerkour.com
Want to learn Rust and offensive security?
Take a look at my book Black Hat Rust.
All early-access supporters get a special discount and awesome bonuses:
https://academy.kerkour.com/black-hat-rust?coupon=BLOG.
Warning: this offer is limited in time!
Recommend
-
11
How to send emails with Rust Tue, Mar 16, 2021 Sending emails in Rust can be achieved in two ways: either by using an SMTP server or by using a third-party service with an API such as AWS SES...
-
5
The biggest threat to Rust's sustainability Thu, Mar 18, 2021 Its fast-paced development cycles. For more data points, please
-
6
How to execute shellcodes from memory in Rust Tue, Mar 23, 2021 Executing code from memory in Rust is very dependant of the platform as all modern Operating Systems implement security measures to avoid...
-
15
How to create small Docker images for Rust Tue, Apr 6, 2021 Building minimal Docker images to deploy Rust brings up a lot of benefits: it’s not only good for security (reduced attack surface) but also...
-
9
42 Companies using Rust in production Thu, Apr 8, 2021 A lot of people want to learn Rust but...
-
9
Botpress: Natural Language Processing with Sylvain Perron By SE Daily Podcast Friday, May 7 2021
-
4
How to implement worker pools in Rust Tue, Jul 20, 2021 Don’t. Worker pools are not a great fit for Rust due to its ownership model. Instead, embrace functional programming and immutable data. Rust pro...
-
4
How to deal with large Cargo workspaces in Rust Tue, Aug 17, 2021 I’m a big fan of monoliths, but when Rust projects become larger and larger, we have to use
-
6
2022年选择哪个Rust Web框架 2022年可选择的三个Rust Web框架:actix-web、
-
11
什么情况下不要用Rust语言? - kerkour Rust 在软件可靠性和性能方面向前迈出了一大步,这直接转化为 $$ 和节省的时间。它解决了开发人员每天面临的许多问题,例如不变性和良好的抽象。但与所有技术一样,它也有一些缺点,可能不会使其成为...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK