Skip to content

Commit

Permalink
serialising public key to string, adding docs, shifting stuff about
Browse files Browse the repository at this point in the history
  • Loading branch information
Sarsoo committed Feb 9, 2024
1 parent dcb257a commit 328c011
Show file tree
Hide file tree
Showing 23 changed files with 417 additions and 282 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,6 @@ I remember I was listening to, I think, [Security This Week with Carl Franklin](

I also wanted to play with a big rust project for standard targets with threading. Although I had a lot of fun with my browser-based checkers game, [Draught](https://draught.sarsoo.xyz), working against WASM has some restrictions.

[Read the Docs](https://github.com/Sarsoo/dnstp/settings/pages)
[Read the Docs](https://github.com/Sarsoo/dnstp/settings/pages)

One of my aims was to see whether arbitrary data could be transmitted using more or less compliant DNS messages, i.e.not just sending junk over UDP to port 53. The closer to compliant DNS that the messages are, the more subtle the process is. In my own network, I have NAT rules that will redirect any DNS messages that are destined for external DNS servers to my own internal ones first. If the packets are crap and malformed, they could be rejected before they even reach my subtle server.
19 changes: 14 additions & 5 deletions dnstp-client/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
use std::fs::{File, OpenOptions};
//! # Client Side
//!

use std::fs::OpenOptions;
use std::net::SocketAddr;
use std::thread;
use std::time::Duration;
use clap::Parser;
use log::{info, LevelFilter};
use rand::RngCore;
use simplelog::*;
use dnstplib::DomainConfig;

use dnstplib::message::DNSMessage;
use dnstplib::net::{DNSSocket, NetworkMessage};
Expand Down Expand Up @@ -53,19 +57,24 @@ fn main() {

socket.run_rx(processor.get_message_channel().expect("couldn't get message processing channel"));

let domain = vec![args.key_endpoint, args.base_domain].join(".");
let domain_config = DomainConfig {
base_domain: args.base_domain,
key_endpoint: args.key_endpoint
};

let domain = domain_config.get_fq_key_endpoint();

let mut rng = rand::thread_rng();
loop {

info!("sending...");

let message = DNSMessage::from_hostname(address, rng.next_u32() as u16, domain.clone());
let message = DNSMessage::req_from_hostname(address, rng.next_u32() as u16, domain.clone());

let bytes = message.to_bytes();

tx_channel.send(Box::from(NetworkMessage {
buffer: Box::from(bytes),
tx_channel.send(Box::new(NetworkMessage {
buffer: Box::new(bytes),
peer: args.address.parse().unwrap()
}));

Expand Down
7 changes: 5 additions & 2 deletions dnstp-server/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
//! DNS server component for processing requests and replying with DNS records
//! # Server Side
//! DNS server component for processing requests and replying with DNS records.
//!
//! The aim is to have clients exfil to this server and to allow pulling down data from the server.

use clap::Parser;
use std::{thread};

use log::info;
use simplelog::*;
use std::fs::{File, OpenOptions};
use std::fs::OpenOptions;
use std::net::SocketAddr;
use dnstplib::DomainConfig;

Expand Down
7 changes: 7 additions & 0 deletions dnstp/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,11 @@
pub struct DomainConfig {
pub base_domain: String,
pub key_endpoint: String,
}

impl DomainConfig {
pub fn get_fq_key_endpoint(&self) -> String
{
vec![self.key_endpoint.clone(), self.base_domain.clone()].join(".")
}
}
46 changes: 42 additions & 4 deletions dnstp/src/crypto/mod.rs
Original file line number Diff line number Diff line change
@@ -1,25 +1,45 @@
//! Method for handling cryptography including ECDH shared secret derivation and symmetric key encryption
//!
//! **Step 1**: [`get_random_asym_pair`] Generate a public/private key pair on each side
//!
//! **Step 2**: Swap the [`p256::EncodedPoint`]s from step 1 between parties
//!
//! **Step 3**: [`get_shared_asym_secret`] Combine one private key with the other public key to end up at the same shared secret
//!
//! **Step 4**: [`asym_to_sym_key`] Take the [`p256::NistP256`] shared asymmetric secret key and use it as a symmetric key ready for encryption decryption
//!
//! **Step 5**: [`generate_aes_nonce`] Get a nonce to use when encrypting
//!
//! **Step 6**: [`encrypt`] Use the key from step 4 with the nonce from step 5 to encrypt arbitrary data
//!
//! **Step 7**: [`decrypt`] Use the same key from step 6 and ***the same nonce from step 6*** to decrypt the outputted ciphertext from step 6.

#[cfg(test)]
mod tests;

use std::str::FromStr;
use p256::{EncodedPoint, PublicKey, ecdh::EphemeralSecret, NistP256};

Check warning on line 21 in dnstp/src/crypto/mod.rs

View workflow job for this annotation

GitHub Actions / Build & Test

unused import: `EncodedPoint`

Check warning on line 21 in dnstp/src/crypto/mod.rs

View workflow job for this annotation

GitHub Actions / Build & Test

unused import: `EncodedPoint`
use p256::elliptic_curve::ecdh::SharedSecret;
use aes_gcm_siv::{aead::{Aead, KeyInit}, AeadCore, Aes256GcmSiv, Nonce};

use rand_core::OsRng;

pub fn get_random_asym_pair() -> (EphemeralSecret, EncodedPoint)
pub const PUBLIC_KEY_OPENING: &str = "-----BEGIN PUBLIC KEY-----\n";
pub const PUBLIC_KEY_CLOSING: &str = "\n-----END PUBLIC KEY-----\n";

/// Generate a public/private key pair
pub fn get_random_asym_pair() -> (EphemeralSecret, String)
{
let secret = EphemeralSecret::random(&mut OsRng);
let public_point = EncodedPoint::from(secret.public_key());
let public_point = secret.public_key().to_string();

(secret, public_point)
}

pub fn get_shared_asym_secret(secret: EphemeralSecret, opposing_public_key: EncodedPoint) -> Result<SharedSecret<NistP256>, ()> {
/// Use one private key and an opposing public key to arrive at the same shared secret
pub fn get_shared_asym_secret(secret: EphemeralSecret, opposing_public_key: String) -> Result<SharedSecret<NistP256>, ()> {

match PublicKey::from_sec1_bytes(opposing_public_key.as_ref()) {
match PublicKey::from_str(&opposing_public_key) {
Ok(other_public) => {
Ok(secret.diffie_hellman(&other_public))
}
Expand All @@ -37,16 +57,19 @@ pub fn get_shared_asym_secret(secret: EphemeralSecret, opposing_public_key: Enco
// Nonce::from(nonce_buffer)
// }

/// Generate a safe nonce to use in symmetric encryption
pub fn generate_aes_nonce() -> Nonce
{
Aes256GcmSiv::generate_nonce(OsRng)
}

/// Turn the asymmetric shared secret into a symmetric encryption key
pub fn asym_to_sym_key(secret: &SharedSecret<NistP256>) -> Aes256GcmSiv
{
Aes256GcmSiv::new(secret.raw_secret_bytes())
}

/// Symmetrically encrypt data using a key derived from ECDH
pub fn encrypt(key: &Aes256GcmSiv, nonce: &Nonce, bytes: &Vec<u8>) -> Result<Vec<u8>, ()>
{
match key.encrypt(nonce, bytes.as_ref()) {
Expand All @@ -59,6 +82,7 @@ pub fn encrypt(key: &Aes256GcmSiv, nonce: &Nonce, bytes: &Vec<u8>) -> Result<Vec
}
}

/// Symmetrically decrypt data using a key derived from ECDH
pub fn decrypt(key: &Aes256GcmSiv, nonce: &Nonce, bytes: &Vec<u8>) -> Result<Vec<u8>, ()>
{
match key.decrypt(nonce, bytes.as_ref()) {
Expand All @@ -69,4 +93,18 @@ pub fn decrypt(key: &Aes256GcmSiv, nonce: &Nonce, bytes: &Vec<u8>) -> Result<Vec
Err(())
}
}
}

pub fn trim_public_key(public: &String) -> String
{
public[27.. 125 + 27].to_string().replace('\n', ".")
}

pub fn fatten_public_key(public: &String) -> String
{
let mut fattened = public.clone();
fattened.insert_str(0, PUBLIC_KEY_OPENING);
fattened.push_str(PUBLIC_KEY_CLOSING);

fattened.replace('.', "\n")
}
11 changes: 11 additions & 0 deletions dnstp/src/crypto/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,15 @@ fn arbitrary_string_back_and_forth() {
let result = String::from_utf8(plain_text).unwrap();

assert_eq!(data, result);
}

#[test]
fn public_key_trimming()
{
let (_, public) = get_random_asym_pair();

let trimmed = trim_public_key(&public);
let fattened = fatten_public_key(&trimmed.to_string());

assert_eq!(public, fattened);
}
5 changes: 4 additions & 1 deletion dnstp/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//! # Common Functionality
//! The vast majority of functionality is in this library crate. The client and server executable crates are really just wiring up bits and pieces from this library.

pub mod message_parser;

mod byte;
Expand All @@ -6,6 +9,6 @@ pub mod message;
pub mod net;
mod string;
pub mod config;
mod crypto;
pub mod crypto;

pub use config::DomainConfig;
16 changes: 12 additions & 4 deletions dnstp/src/message/message.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
use std::net::{Ipv4Addr, SocketAddr};
use crate::message::{DNSQuestion, DNSHeader, questions_to_bytes, Direction, ResponseCode, QType, QClass, ResourceRecord, records_to_bytes, ARdata};

/// A DNS message which can be used as either a request or response based on its direction and composition
#[derive(Debug)]
pub struct DNSMessage {
/// Status/request codes, counts for other collections
pub header: DNSHeader,
/// Hostname queries, should be the same in both requests and responses
pub questions: Vec<DNSQuestion>,
/// Responses for [`DNSMessage::questions`], has similar structure with varying data field based on query type
pub answer_records: Vec<ResourceRecord>,
pub authority_records: Vec<ResourceRecord>,
pub additional_records: Vec<ResourceRecord>,
/// IP and socket address of the client which sent this message/client to send message to
pub peer: SocketAddr
}

impl DNSMessage {

/// Transform a message into a network transmissable byte sequence
pub fn to_bytes(& self) -> Vec<u8>
{
let mut header_bytes = self.header.to_bytes().to_vec();
Expand All @@ -29,7 +35,8 @@ impl DNSMessage {
return header_bytes
}

pub fn from_hostname(peer: SocketAddr, id: u16, hostname: String) -> DNSMessage
/// Helper function for getting a DNS request for the IPv4 of a single hostname
pub fn req_from_hostname(peer: SocketAddr, id: u16, hostname: String) -> DNSMessage
{
DNSMessage {
header: DNSHeader::new_request(id, None),
Expand All @@ -47,7 +54,8 @@ impl DNSMessage {
}
}

pub fn from_hostnames(peer: SocketAddr, id: u16, hostnames: Vec<String>) -> DNSMessage
/// Helper function to get a DNS request for the IPv4s of multiple hostnames
pub fn reqs_from_hostnames(peer: SocketAddr, id: u16, hostnames: Vec<String>) -> DNSMessage
{
DNSMessage {
header: DNSHeader::new_request(id, Some(hostnames.len() as u16)),
Expand All @@ -67,7 +75,7 @@ impl DNSMessage {
}
}

pub fn a_from_request(request: &DNSMessage, ip: impl Fn(&DNSQuestion) -> Ipv4Addr) -> DNSMessage
pub fn a_resp_from_request(request: &DNSMessage, ip: impl Fn(&DNSQuestion) -> Ipv4Addr) -> DNSMessage
{
let mut response = DNSMessage{
header: request.header.clone(),
Expand All @@ -83,7 +91,7 @@ impl DNSMessage {
.map(|x|
ResourceRecord::from_query(x,
12,
Box::from(ARdata::from(ip(x))),
Box::new(ARdata::from(ip(x))),
None))
.collect();

Expand Down
35 changes: 14 additions & 21 deletions dnstp/src/message/question/tests.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
use super::*;

macro_rules! assert_questions_eq {
($left:expr, $right:expr) => {
assert_eq!($left.qname, $right.qname);
assert_eq!($left.qclass, $right.qclass);
assert_eq!($left.qtype, $right.qtype);
};
}

#[test]
fn one_question_back_and_forth() {
let q = DNSQuestion {
Expand All @@ -13,9 +21,7 @@ fn one_question_back_and_forth() {

let (q_reconstructed, q_remaining) = questions_from_bytes(q_bytes, 1).unwrap();

assert_eq!(q.qname, q_reconstructed[0].qname);
assert_eq!(q.qclass, q_reconstructed[0].qclass);
assert_eq!(q.qtype, q_reconstructed[0].qtype);
assert_questions_eq!(q, q_reconstructed[0]);
}

#[test]
Expand All @@ -39,13 +45,8 @@ fn two_questions_back_and_forth() {

let (q_reconstructed, q_remaining) = questions_from_bytes(q_bytes, 2).unwrap();

assert_eq!(q.qname, q_reconstructed[0].qname);
assert_eq!(q.qclass, q_reconstructed[0].qclass);
assert_eq!(q.qtype, q_reconstructed[0].qtype);

assert_eq!(q2.qname, q_reconstructed[1].qname);
assert_eq!(q2.qclass, q_reconstructed[1].qclass);
assert_eq!(q2.qtype, q_reconstructed[1].qtype);
assert_questions_eq!(q, q_reconstructed[0]);
assert_questions_eq!(q2, q_reconstructed[1]);
}

#[test]
Expand Down Expand Up @@ -77,15 +78,7 @@ fn three_questions_back_and_forth() {

let (q_reconstructed, q_remaining) = questions_from_bytes(q_bytes, 3).unwrap();

assert_eq!(q.qname, q_reconstructed[0].qname);
assert_eq!(q.qclass, q_reconstructed[0].qclass);
assert_eq!(q.qtype, q_reconstructed[0].qtype);

assert_eq!(q2.qname, q_reconstructed[1].qname);
assert_eq!(q2.qclass, q_reconstructed[1].qclass);
assert_eq!(q2.qtype, q_reconstructed[1].qtype);

assert_eq!(q3.qname, q_reconstructed[2].qname);
assert_eq!(q3.qclass, q_reconstructed[2].qclass);
assert_eq!(q3.qtype, q_reconstructed[2].qtype);
assert_questions_eq!(q, q_reconstructed[0]);
assert_questions_eq!(q2, q_reconstructed[1]);
assert_questions_eq!(q3, q_reconstructed[2]);
}
4 changes: 2 additions & 2 deletions dnstp/src/message/record/a_rdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pub struct ARdata {

impl Debug for ARdata {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IP")
.field("data", &self.rdata)
f.debug_struct("A")
.field("IP", &self.rdata)
.finish()
}
}
Expand Down
4 changes: 2 additions & 2 deletions dnstp/src/message/record/aaaa_rdata.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ pub struct AAAARdata {

impl Debug for AAAARdata {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.debug_struct("IP")
.field("data", &self.rdata)
f.debug_struct("AAAA")
.field("IP", &self.rdata)
.finish()
}
}
Expand Down
2 changes: 1 addition & 1 deletion dnstp/src/message/record/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ pub fn records_from_bytes(bytes: Vec<u8>, total_answers: u16) -> Result<(Vec<Res
class: qclass_formed,
ttl: four_byte_combine(ttl.0.unwrap(), ttl.1.unwrap(), ttl.2.unwrap(), ttl.3.unwrap()),
rd_length: data_length_combined.unwrap(),
r_data: Box::from(RawRData::from(data.clone()))
r_data: Box::new(RawRData::from(data.clone()))
});

name_offset = (None, None);
Expand Down
Loading

0 comments on commit 328c011

Please sign in to comment.