#[cfg(feature = "server")]
use crate::offsets::offset_server::OffsetServer;
use crate::{
db::{Offsets, OffsetsDatabase},
offsets::{offset_server::Offset, OffsetsRequest, OffsetsResponse},
};
use pdb::{FallibleIterator, Source, SymbolData, SymbolTable, PDB};
use reqwest::StatusCode;
#[cfg(feature = "server")]
use std::net::SocketAddr;
use std::{borrow::Cow, collections::HashMap, fs::read_to_string, io::Cursor, path::PathBuf};
use tokio::fs::write;
use tokio::sync::Mutex;
#[cfg(feature = "server")]
use tonic::transport;
#[cfg(feature = "server")]
use tonic::transport::{Identity, ServerTlsConfig};
use tonic::{Request, Response, Status};
pub struct OffsetHandler {
pub database: Mutex<OffsetsDatabase>,
pub cache_path: PathBuf,
}
#[cfg(feature = "server")]
pub struct Server {
handler: OffsetHandler,
endpoint: SocketAddr,
#[allow(dead_code)]
tls_identity: Option<Identity>,
}
#[cfg(feature = "server")]
impl Server {
pub fn new<S: AsRef<str>>(
endpoint: SocketAddr,
cache_path: S,
tls_identity: Option<Identity>,
) -> Result<Server, anyhow::Error> {
Ok(Server {
handler: OffsetHandler::new(cache_path.as_ref())?,
endpoint,
tls_identity,
})
}
pub async fn run(self) -> Result<(), anyhow::Error> {
let endpoint = self.endpoint;
let mut server = transport::Server::builder();
if let Some(tls_identity) = self.tls_identity {
server = server.tls_config(ServerTlsConfig::new().identity(tls_identity))?;
}
server
.add_service(OffsetServer::new(self.handler))
.serve(endpoint)
.await?;
Ok(())
}
}
impl OffsetHandler {
pub fn new<S: AsRef<str>>(cache_path: S) -> anyhow::Result<OffsetHandler> {
let database = Mutex::new(match read_to_string(cache_path.as_ref()) {
Ok(s) => serde_json::from_str(&s)?,
_ => OffsetsDatabase::default(),
});
Ok(OffsetHandler {
database,
cache_path: cache_path.as_ref().into(),
})
}
}
macro_rules! get_offset {
($map:ident, $name:literal) => {
match $map.get($name) {
Some(offset) => offset,
None => {
eprintln!("Failed to find offset for {}", $name);
return Ok(None);
}
}
};
}
fn get_offsets_from_pdb_bytes<'a, S: 'a + Source<'a>>(s: S) -> pdb::Result<Option<Offsets>> {
let mut pdb: PDB<'a, S> = pdb::PDB::open(s)?;
let symbol_table: SymbolTable<'a> = pdb.global_symbols()?;
let address_map = pdb.address_map()?;
let map: HashMap<Cow<str>, u32> = symbol_table
.iter()
.map(|sym| sym.parse())
.filter_map(|data| match data {
SymbolData::Public(proc) => Ok(Some(proc)),
_ => Ok(None),
})
.filter_map(|proc| match proc.offset.to_rva(&address_map) {
Some(rva) => Ok(Some((proc.name.to_string(), rva.0))),
_ => Ok(None),
})
.collect()?;
let ldrp_hash_table = *get_offset!(map, "LdrpHashTable");
let ldrp_module_datatable_lock = *get_offset!(map, "LdrpModuleDatatableLock");
let ldrp_handle_tls_data = *get_offset!(map, "LdrpHandleTlsData");
let ldrp_release_tls_entry = *get_offset!(map, "LdrpReleaseTlsEntry");
let ldrp_mapping_info_index = *get_offset!(map, "LdrpMappingInfoIndex");
let ldrp_module_base_address_index = *get_offset!(map, "LdrpModuleBaseAddressIndex");
let rtl_initialize_history_table = *get_offset!(map, "RtlInitializeHistoryTable");
let rtl_insert_inverted_function_table = *get_offset!(map, "RtlInsertInvertedFunctionTable");
Ok(Some(Offsets {
ldrp_hash_table,
ldrp_module_datatable_lock,
ldrp_handle_tls_data,
ldrp_release_tls_entry,
ldrp_mapping_info_index,
ldrp_module_base_address_index,
rtl_initialize_history_table,
rtl_insert_inverted_function_table
}))
}
#[tonic::async_trait]
impl Offset for OffsetHandler {
async fn get_offsets(
&self,
request: Request<OffsetsRequest>,
) -> Result<Response<OffsetsResponse>, Status> {
let hash = request.into_inner().ntdll_hash;
if hash.len() != 33 {
return Err(Status::invalid_argument("Bad hash length"));
}
if !hash.chars().all(|x| char::is_ascii_hexdigit(&x)) {
return Err(Status::invalid_argument("Bad hex digit"));
}
let database = &mut self.database.lock().await;
let offsets_map = &mut database.offsets;
if let Some(offsets) = offsets_map.get(&hash) {
return Ok(Response::new(offsets.into()));
}
let pdb = match reqwest::get(format!(
"http://msdl.microsoft.com/download/symbols/ntdll.pdb/{}/ntdll.pdb",
&hash
))
.await
{
Ok(response) => response,
Err(e) => return Err(Status::not_found(format!("Error on fetch: {}", e))),
};
let status = pdb.status();
match status {
StatusCode::OK => {}
StatusCode::NOT_FOUND => {
return Err(Status::not_found("PDB hash not found"));
}
c => {
return Err(Status::internal(format!("Internal error: {}", c)));
}
};
let pdb = match pdb.bytes().await {
Ok(bytes) => bytes,
Err(e) => {
return Err(Status::internal(format!("Error on bytes: {}", e)));
}
};
let offsets = match get_offsets_from_pdb_bytes(Cursor::new(&pdb)) {
Ok(offsets) => offsets,
Err(e) => {
return Err(Status::internal(format!(
"Processing error: {}. Bytes: {:?}",
e, pdb
)));
}
};
let offsets = match offsets {
Some(offsets) => offsets,
None => {
return Err(Status::internal("Failed to find some functions"));
}
};
offsets_map.insert(hash, offsets);
let s = match serde_json::to_string::<OffsetsDatabase>(&*database) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to serialize database: {}. DB: {:?}", e, database);
return Ok(Response::new(offsets.into()));
}
};
match write(&self.cache_path, &s).await {
Ok(_) => {}
Err(e) => {
eprintln!(
"Failed to write database to cache file {}: {}. Payload: {}",
&self.cache_path.as_path().to_string_lossy(),
e,
&s
)
}
}
Ok(Response::new(offsets.into()))
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::offsets::{offset_server::Offset, OffsetsRequest};
use tonic::Request;
#[tokio::test]
async fn hash_length() {
let handler = OffsetHandler::new("test/cache.json").unwrap();
let request = Request::new(OffsetsRequest {
ntdll_hash: "123".into(),
});
let err = handler.get_offsets(request).await.unwrap_err();
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert_eq!(err.message(), "Bad hash length");
}
#[tokio::test]
async fn hash_digits() {
let handler = OffsetHandler::new("test/cache.json").unwrap();
let request = Request::new(OffsetsRequest {
ntdll_hash: "46F6F5C30E7147E46F2A953A5DAF201AG".into(),
});
let err = handler.get_offsets(request).await.unwrap_err();
assert_eq!(err.code(), tonic::Code::InvalidArgument);
assert_eq!(err.message(), "Bad hex digit");
}
#[tokio::test]
async fn not_found() {
let handler = OffsetHandler::new("test/cache.json").unwrap();
let request = Request::new(OffsetsRequest {
ntdll_hash: "46F6F5C30E7147E46F2A953A5DAF201A2".into(),
});
let err = handler.get_offsets(request).await.unwrap_err();
assert_eq!(err.message(), "PDB hash not found");
}
#[tokio::test]
async fn good_fetch() {
let handler = OffsetHandler::new("test/cache.json").unwrap();
let request = Request::new(OffsetsRequest {
ntdll_hash: "46F6F5C30E7147E46F2A953A5DAF201A1".into(),
});
let response = handler.get_offsets(request).await.unwrap().into_inner();
assert!(handler
.database
.lock()
.await
.offsets
.contains_key("46F6F5C30E7147E46F2A953A5DAF201A1"));
assert_eq!(response.ldrp_hash_table, 0x16A140);
assert_eq!(response.ldrp_module_datatable_lock, 0x16B240);
assert_eq!(response.ldrp_handle_tls_data, 0x47C14);
}
}