Skip to content

Commit

Permalink
Hook gethosbyname to resolve DNS for erlang/elixir. (metalbear-co#2071)
Browse files Browse the repository at this point in the history
* Hook gethosbyname for elixir outgoing requests (it uses a deprecated getaddrinfo-like function).

* pointer slice trickery

* convert the right address to h_addr_list

* just return global reference

* deal with the crazy h_aliases

* always v4

* null end of list

* length at 4

* redoxify

* less unwrapping

* more logs

* woo

* ..

* logs

* integration test

* remove unused

* refactor + docs

* fix changelog

* remove unused features

* fix logs

* more log changes

* force pointer cast

* explicit type

* node -> name // cast

---------

Co-authored-by: Aviram Hassan <[email protected]>
  • Loading branch information
meowjesty and aviramha authored Nov 13, 2023
1 parent 896d85b commit e12963e
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 4 deletions.
1 change: 1 addition & 0 deletions changelog.d/2055.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a hook for [gethostbyname](https://www.man7.org/linux/man-pages/man3/gethostbyname.3.html) to allow erlang/elixir to resolve DNS.
8 changes: 7 additions & 1 deletion mirrord/layer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::{env::VarError, net::SocketAddr, ptr, str::ParseBoolError};

use errno::set_errno;
use ignore_codes::*;
use libc::{c_char, DIR, FILE};
use libc::{c_char, hostent, DIR, FILE};
use mirrord_config::{config::ConfigError, feature::network::outgoing::OutgoingFilterError};
use mirrord_protocol::{ResponseError, SerializationError};
#[cfg(target_os = "macos")]
Expand Down Expand Up @@ -311,3 +311,9 @@ impl From<frida_gum::Error> for LayerError {
LayerError::Frida(err)
}
}

impl From<HookError> for *mut hostent {
fn from(_fail: HookError) -> Self {
ptr::null_mut()
}
}
22 changes: 21 additions & 1 deletion mirrord/layer/src/socket/hooks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use std::{os::unix::io::RawFd, sync::LazyLock};

use dashmap::DashSet;
use errno::{set_errno, Errno};
use libc::{c_char, c_int, c_void, size_t, sockaddr, socklen_t, ssize_t, EINVAL};
use libc::{c_char, c_int, c_void, hostent, size_t, sockaddr, socklen_t, ssize_t, EINVAL};
use mirrord_layer_macro::{hook_fn, hook_guard_fn};

use super::ops::*;
Expand Down Expand Up @@ -108,6 +108,18 @@ pub(crate) unsafe extern "C" fn gethostname_detour(
.unwrap_or_bypass_with(|_| FN_GETHOSTNAME(raw_name, name_length))
}

/// Hook for `libc::gethostbyname` (you won't find this in rust's `libc` as it's been deprecated and
/// removed).
///
/// Resolves DNS `raw_name` and allocates a `static` [`libc::hostent`] that we change the inner
/// values whenever this function is called. The address itself of `*mut hostent` has to remain the
/// same (thus why it's a `static`).
#[hook_guard_fn]
unsafe extern "C" fn gethostbyname_detour(raw_name: *const c_char) -> *mut hostent {
let rawish_name = (!raw_name.is_null()).then(|| CStr::from_ptr(raw_name));
gethostbyname(rawish_name).unwrap_or_bypass_with(|_| FN_GETHOSTBYNAME(raw_name))
}

#[hook_guard_fn]
pub(crate) unsafe extern "C" fn accept_detour(
sockfd: c_int,
Expand Down Expand Up @@ -503,6 +515,14 @@ pub(crate) unsafe fn enable_socket_hooks(hook_manager: &mut HookManager, enabled
FN_GETHOSTNAME
);

replace!(
hook_manager,
"gethostbyname",
gethostbyname_detour,
FnGethostbyname,
FN_GETHOSTBYNAME
);

#[cfg(target_os = "linux")]
{
// Here we replace a function of libuv and not libc, so we pass None as the .
Expand Down
110 changes: 109 additions & 1 deletion mirrord/layer/src/socket/ops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ use std::{
sync::{Arc, OnceLock},
};

use libc::{c_int, c_void, sockaddr, socklen_t};
use errno::set_errno;
use libc::{c_int, c_void, hostent, sockaddr, socklen_t};
use mirrord_config::feature::network::incoming::IncomingMode;
use mirrord_intproxy_protocol::{
ConnMetadataRequest, ConnMetadataResponse, NetProtocol, OutgoingConnectRequest,
Expand Down Expand Up @@ -43,6 +44,31 @@ pub(super) static REMOTE_DNS_REVERSE_MAPPING: LazyLock<DashMap<IpAddr, String>>
/// Hostname initialized from the agent with [`gethostname`].
pub(crate) static HOSTNAME: OnceLock<CString> = OnceLock::new();

/// Globals used by `gethostbyname`.
static mut GETHOSTBYNAME_HOSTNAME: Option<CString> = None;
static mut GETHOSTBYNAME_ALIASES_STR: Option<Vec<CString>> = None;

/// **Safety**:
/// There is a potential UB trigger here, as [`libc::hostent`] uses this as an `*mut _`, while we
/// have `*const _`. As this is being filled to fulfill the contract of a deprecated function, I
/// (alex) don't think we're going to hit this issue ever.
static mut GETHOSTBYNAME_ALIASES_PTR: Option<Vec<*const i8>> = None;
static mut GETHOSTBYNAME_ADDRESSES_VAL: Option<Vec<[u8; 4]>> = None;
static mut GETHOSTBYNAME_ADDRESSES_PTR: Option<Vec<*mut u8>> = None;

/// Global static that the user will receive when calling [`gethostbyname`].
///
/// **Safety**:
/// Even though we fill it with some `*const _` while it expects `*mut _`, it shouldn't be a problem
/// as the user will most likely be doing a deep copy if they want to mess around with this struct.
static mut GETHOSTBYNAME_HOSTENT: hostent = hostent {
h_name: ptr::null_mut(),
h_aliases: ptr::null_mut(),
h_addrtype: 0,
h_length: 0,
h_addr_list: ptr::null_mut(),
};

/// Helper struct for connect results where we want to hold the original errno
/// when result is -1 (error) because sometimes it's not a real error (EINPROGRESS/EINTR)
/// and the caller should have the original value.
Expand Down Expand Up @@ -869,6 +895,88 @@ fn remote_hostname_string() -> Detour<CString> {
.map(Detour::Success)?
}

/// Resolves a hostname and set result to static global like the original `gethostbyname` does.
///
/// Used by erlang/elixir to resolve DNS.
///
/// **Safety**:
/// See the [`GETHOSTBYNAME_ALIASES_PTR`] docs. If you see this function being called and some weird
/// issue is going on, assume that you might've triggered the UB.
#[tracing::instrument(level = "trace", ret)]
pub(super) fn gethostbyname(raw_name: Option<&CStr>) -> Detour<*mut hostent> {
let name: String = raw_name
.bypass(Bypass::NullNode)?
.to_str()
.map_err(|fail| {
warn!("Failed converting `name` from `CStr` with {:#?}", fail);

Bypass::CStrConversion
})?
.into();

let hosts_and_ips = remote_getaddrinfo(name.clone())?;

// We could `unwrap` here, as this would have failed on the previous conversion.
let host_name = CString::new(name)?;

if hosts_and_ips.is_empty() {
set_errno(errno::Errno(libc::EAI_NODATA));
return Detour::Success(ptr::null_mut());
}

// We need `*mut _` at the end, so `ips` has to be `mut`.
let (aliases, mut ips) = hosts_and_ips
.into_iter()
.filter_map(|(host, ip)| match ip {
// Only care about ipv4s and hosts that exist.
IpAddr::V4(ip) => {
let c_host = CString::new(host).ok()?;
Some((c_host, ip.octets()))
}
IpAddr::V6(ip) => {
trace!("ipv6 received - ignoring - {ip:?}");
None
}
})
.fold(
(Vec::default(), Vec::default()),
|(mut aliases, mut ips), (host, octets)| {
aliases.push(host);
ips.push(octets);
(aliases, ips)
},
);

let mut aliases_ptrs: Vec<*const i8> = aliases
.iter()
.map(|alias| alias.as_ptr().cast())
.collect::<Vec<_>>();
let mut ips_ptrs = ips.iter_mut().map(|ip| ip.as_mut_ptr()).collect::<Vec<_>>();

// Put a null ptr to signal end of the list.
aliases_ptrs.push(ptr::null());
ips_ptrs.push(ptr::null_mut());

// Need long-lived values so we can take pointers to them.
unsafe {
GETHOSTBYNAME_HOSTNAME.replace(host_name);
GETHOSTBYNAME_ALIASES_STR.replace(aliases);
GETHOSTBYNAME_ALIASES_PTR.replace(aliases_ptrs);
GETHOSTBYNAME_ADDRESSES_VAL.replace(ips);
GETHOSTBYNAME_ADDRESSES_PTR.replace(ips_ptrs);

// Fill the `*mut hostent` that the user will interact with.
GETHOSTBYNAME_HOSTENT.h_name = GETHOSTBYNAME_HOSTNAME.as_ref().unwrap().as_ptr() as _;
GETHOSTBYNAME_HOSTENT.h_length = 4;
GETHOSTBYNAME_HOSTENT.h_addrtype = libc::AF_INET;
GETHOSTBYNAME_HOSTENT.h_aliases = GETHOSTBYNAME_ALIASES_PTR.as_ref().unwrap().as_ptr() as _;
GETHOSTBYNAME_HOSTENT.h_addr_list =
GETHOSTBYNAME_ADDRESSES_PTR.as_ref().unwrap().as_ptr() as *mut *mut libc::c_char;
}

Detour::Success(unsafe { std::ptr::addr_of!(GETHOSTBYNAME_HOSTENT) as _ })
}

/// Resolve hostname from remote host with caching for the result
#[tracing::instrument(level = "trace")]
pub(super) fn gethostname() -> Detour<&'static CString> {
Expand Down
34 changes: 34 additions & 0 deletions mirrord/layer/tests/apps/gethostbyname/gethostbyname.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#include <stdio.h>
#include <netdb.h>
#include <strings.h>
#include <arpa/inet.h>

void try_gethostbyname(const char name[]) {
struct hostent *result = gethostbyname(name);

if (result) {
printf("result %p\n\t", result);
printf("h_name %s\n\t", result->h_name);
printf("h_length %i\n\t", result->h_length);
printf("h_addrtype %i\n\t", result->h_addrtype);

for (int i = 0; result->h_addr_list[i]; i++) {
char str[INET6_ADDRSTRLEN];
struct in_addr address = {};
bcopy(result->h_addr_list[i], (char *)&address, sizeof(address));
printf("h_addresses[%i] %s\n\t", i, inet_ntoa(address));
}

for (int i = 0; result->h_aliases[i]; i++) {
printf("h_aliases[%i] %s\n\t", i, result->h_aliases[i]);
}
}
}

int main(int argc, char *argv[]) {
printf("test issue 2055: START\n");
try_gethostbyname("www.mirrord.dev");
try_gethostbyname("www.invalid.dev");
printf("test issue 2055: SUCCESS\n");
printf("\n");
}
10 changes: 9 additions & 1 deletion mirrord/layer/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,6 +651,7 @@ pub enum Application {
RustListenPorts,
Fork,
OpenFile,
CIssue2055,
RustIssue2058,
// For running applications with the executable and arguments determined at runtime.
DynamicApp(String, Vec<String>),
Expand Down Expand Up @@ -787,6 +788,11 @@ impl Application {
env!("CARGO_MANIFEST_DIR"),
"tests/apps/open_file/out.c_test_app",
),
Application::CIssue2055 => format!(
"{}/{}",
env!("CARGO_MANIFEST_DIR"),
"tests/apps/gethostbyname/out.c_test_app",
),
Application::RustIssue2058 => String::from("tests/apps/issue2058/target/issue2058"),
Application::DynamicApp(exe, _) => exe.clone(),
}
Expand Down Expand Up @@ -877,7 +883,8 @@ impl Application {
| Application::Go19DirBypass
| Application::Go20DirBypass
| Application::RustIssue2058
| Application::OpenFile => vec![],
| Application::OpenFile
| Application::CIssue2055 => vec![],
Application::RustOutgoingUdp => ["--udp", RUST_OUTGOING_LOCAL, RUST_OUTGOING_PEERS]
.into_iter()
.map(Into::into)
Expand Down Expand Up @@ -943,6 +950,7 @@ impl Application {
| Application::RustListenPorts
| Application::RustRecvFrom
| Application::OpenFile
| Application::CIssue2055
| Application::DynamicApp(..) => unimplemented!("shouldn't get here"),
Application::PythonSelfConnect => 1337,
Application::RustIssue2058 => 1234,
Expand Down
62 changes: 62 additions & 0 deletions mirrord/layer/tests/issue2055.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#![feature(assert_matches)]
use std::{net::IpAddr, path::PathBuf, time::Duration};

use mirrord_protocol::{
dns::{DnsLookup, GetAddrInfoRequest, GetAddrInfoResponse, LookupRecord},
ClientMessage, DaemonMessage, DnsLookupError,
ResolveErrorKindInternal::NoRecordsFound,
ResponseError,
};
use rstest::rstest;

mod common;
pub use common::*;

/// Verify that issue [#2055](https://github.com/metalbear-co/mirrord/issues/2055) is fixed.
/// "DNS Issue on Elixir macOS"
#[rstest]
#[tokio::test]
#[timeout(Duration::from_secs(60))]
async fn issue_2055(dylib_path: &PathBuf) {
let application = Application::CIssue2055;
let (mut test_process, mut intproxy) = application
.start_process_with_layer(dylib_path, Default::default(), None)
.await;

println!("Application started, waiting for `GetAddrInfoRequest`.");

let msg = intproxy.recv().await;
let ClientMessage::GetAddrInfoRequest(GetAddrInfoRequest { node }) = msg else {
panic!("Invalid message received from layer: {msg:?}");
};

intproxy
.send(DaemonMessage::GetAddrInfoResponse(GetAddrInfoResponse(Ok(
DnsLookup(vec![LookupRecord {
name: node,
ip: "93.184.216.34".parse::<IpAddr>().unwrap(),
}]),
))))
.await;

let msg = intproxy.recv().await;
let ClientMessage::GetAddrInfoRequest(GetAddrInfoRequest { node: _ }) = msg else {
panic!("Invalid message received from layer: {msg:?}");
};

intproxy
.send(DaemonMessage::GetAddrInfoResponse(GetAddrInfoResponse(
Err(ResponseError::DnsLookup(DnsLookupError {
kind: NoRecordsFound(3),
})),
)))
.await;

test_process.wait_assert_success().await;
test_process
.assert_stdout_contains("test issue 2055: START")
.await;
test_process
.assert_stdout_contains("test issue 2055: SUCCESS")
.await;
}

0 comments on commit e12963e

Please sign in to comment.