Compare commits

...
Author SHA1 Message Date
Ole André Vadla Ravnås e0ada4a9ff [WIP] Toy around with a potential Softwall feature 2026-02-12 23:13:19 +01:00
7 changed files with 820 additions and 49 deletions

No files matched your search

+1
View File
@@ -34,6 +34,7 @@ native_outdir := $(artifacts_dir)/native/$(host_arch)
bpf_outdir := $(artifacts_dir)/bpf
bpf_noarch_progs := \
activity-sampler \
softwall \
$(NULL)
bpf_arch_progs := \
syscall-tracer \
+275
View File
@@ -0,0 +1,275 @@
#include <stdbool.h>
#include <stdint.h>
#include <linux/bpf.h>
#include <bpf/bpf_core_read.h>
#include <bpf/bpf_helpers.h>
#include <bpf/bpf_tracing.h>
#define MAX_TARGET_TGIDS 4096
#define MAX_TARGET_UIDS 256
#define MAX_RULES 4096
#define MAX_INODE_INDEX 8192
#define MAX_AUDIT_EVENTS (1 << 20)
typedef struct _Event Event;
typedef __u16 EventType;
typedef struct _AuditEvent AuditEvent;
typedef struct _InodeKey InodeKey;
typedef struct _FileOpenRule FileOpenRule;
typedef struct _Stats Stats;
struct _Event
{
__u64 time_ns;
__u32 tgid;
__u32 tid;
EventType type;
__u16 _pad0;
};
enum _EventType
{
EVENT_TYPE_AUDIT
};
struct _AuditEvent
{
Event parent;
__u32 rule_id;
__u32 _pad0;
};
struct _InodeKey
{
__u64 dev;
__u64 ino;
};
struct _FileOpenRule
{
__u64 dev;
__u64 ino;
__s32 errno_neg;
__u32 _pad0;
};
struct _Stats
{
__u64 emitted_events;
__u64 emitted_bytes;
__u64 dropped_events;
__u64 dropped_bytes;
};
struct
{
__uint (type, BPF_MAP_TYPE_HASH);
__uint (max_entries, MAX_TARGET_TGIDS);
__type (key, __u32);
__type (value, __u8);
}
target_tgids SEC (".maps");
struct
{
__uint (type, BPF_MAP_TYPE_HASH);
__uint (max_entries, MAX_TARGET_UIDS);
__type (key, __u32);
__type (value, __u8);
}
target_uids SEC (".maps");
struct
{
__uint (type, BPF_MAP_TYPE_HASH);
__uint (max_entries, MAX_RULES);
__type (key, __u32);
__type (value, FileOpenRule);
}
rules_by_id SEC (".maps");
struct
{
__uint (type, BPF_MAP_TYPE_HASH);
__uint (max_entries, MAX_INODE_INDEX);
__type (key, InodeKey);
__type (value, __u32);
}
inode_index SEC (".maps");
struct
{
__uint (type, BPF_MAP_TYPE_RINGBUF);
__uint (max_entries, MAX_AUDIT_EVENTS);
}
audit_events SEC (".maps");
struct
{
__uint (type, BPF_MAP_TYPE_PERCPU_ARRAY);
__uint (max_entries, 1);
__type (key, __u32);
__type (value, Stats);
}
stats SEC (".maps");
typedef __u32 dev_t;
struct super_block
{
dev_t s_dev;
} __attribute__ ((preserve_access_index));
struct inode
{
struct super_block * i_sb;
unsigned long i_ino;
} __attribute__ ((preserve_access_index));
struct file
{
struct inode * f_inode;
} __attribute__ ((preserve_access_index));
static bool should_enforce_current (__u32 * out_tgid, __u32 * out_tid);
static Stats * get_ringbuf_stats (void);
static void note_emit (Stats * stats, __u64 bytes);
static void note_drop (Stats * stats, __u64 wanted_bytes);
static AuditEvent * reserve_audit_event (void);
static bool get_file_inode_key (struct file * file, InodeKey * out_key);
SEC ("lsm/file_open")
int
BPF_PROG (on_file_open, struct file * file)
{
__u32 tgid, tid;
if (!should_enforce_current (&tgid, &tid))
return 0;
InodeKey key;
if (!get_file_inode_key (file, &key))
return 0;
__u32 * rule_id = bpf_map_lookup_elem (&inode_index, &key);
if (rule_id == NULL)
return 0;
FileOpenRule * rule = bpf_map_lookup_elem (&rules_by_id, rule_id);
if (rule == NULL)
return 0;
AuditEvent * ev = reserve_audit_event ();
if (ev != NULL)
{
ev->parent.time_ns = bpf_ktime_get_ns ();
ev->parent.tgid = tgid;
ev->parent.tid = tid;
ev->parent.type = EVENT_TYPE_AUDIT;
ev->rule_id = *rule_id;
ev->_pad0 = 0;
bpf_ringbuf_submit (ev, 0);
}
return rule->errno_neg;
}
static bool
should_enforce_current (__u32 * out_tgid, __u32 * out_tid)
{
__u64 pid_tgid = bpf_get_current_pid_tgid ();
__u32 tgid = pid_tgid >> 32;
__u32 tid = (__u32) pid_tgid;
*out_tgid = tgid;
*out_tid = tid;
__u8 * tgid_enabled = bpf_map_lookup_elem (&target_tgids, &tgid);
if (tgid_enabled != NULL)
return true;
__u64 uid_gid = bpf_get_current_uid_gid ();
__u32 uid = (__u32) uid_gid;
__u8 * uid_enabled = bpf_map_lookup_elem (&target_uids, &uid);
if (uid_enabled != NULL)
return true;
return false;
}
static bool
get_file_inode_key (struct file * file, InodeKey * out_key)
{
if (file == NULL)
return false;
struct inode * inode = BPF_CORE_READ (file, f_inode);
if (inode == NULL)
return false;
out_key->dev = BPF_CORE_READ (inode, i_sb, s_dev);
out_key->ino = BPF_CORE_READ (inode, i_ino);
return true;
}
static AuditEvent *
reserve_audit_event (void)
{
void * event = bpf_ringbuf_reserve (&audit_events, sizeof (AuditEvent), 0);
Stats * stats = get_ringbuf_stats ();
if (event != NULL)
note_emit (stats, sizeof (AuditEvent));
else
note_drop (stats, sizeof (AuditEvent));
return (AuditEvent *) event;
}
static Stats *
get_ringbuf_stats (void)
{
__u32 key = 0;
return bpf_map_lookup_elem (&stats, &key);
}
static void
note_emit (Stats * stats, __u64 bytes)
{
if (stats == NULL)
return;
stats->emitted_events++;
stats->emitted_bytes += bytes;
}
static void
note_drop (Stats * stats, __u64 wanted_bytes)
{
if (stats == NULL)
return;
stats->dropped_events++;
stats->dropped_bytes += wanted_bytes;
}
char LICENSE[] SEC ("license") = "Dual BSD/GPL";
+242 -48
View File
@@ -405,6 +405,9 @@ namespace Frida {
if (protocol == "syscall-trace")
return new SyscallTraceServiceSession ();
if (protocol == "softwall")
return new SoftwallServiceSession ();
throw new Error.NOT_SUPPORTED ("Unsupported service address");
}
@@ -2244,76 +2247,267 @@ namespace Frida {
return null;
}
}
private static string inet_ntop_to_string (int af, uint8 * src, size_t srclen) {
uint8 buf[46];
unowned string? res = Posix.inet_ntop (af, src, buf);
return (res != null) ? res : "?";
private sealed class SoftwallServiceSession : Object, ServiceSession {
private Softwall? wall = new Softwall ();
private const size_t MAX_BATCH_BYTES = 4U * 1024U * 1024U;
construct {
wall.events_available.connect (on_events_available);
}
private static uint32 resolve_uid_from_username (string name) throws Error {
unowned Posix.Passwd? pw = Posix.getpwnam (name);
if (pw == null)
throw new Error.INVALID_ARGUMENT ("Unknown user: %s", name);
return (uint32) pw.pw_uid;
public async void activate (Cancellable? cancellable) throws Error, IOError {
ensure_active ();
}
private delegate void UInt32Handler (uint32 v) throws Error;
private delegate void StringHandler (string s) throws Error;
private void ensure_active () throws Error {
if (wall == null)
throw new Error.INVALID_OPERATION ("Service is closed");
private static uint32[] read_uint32_array (VariantReader reader) throws Error {
uint n = reader.count_elements ();
var arr = new uint32[n];
if (wall.state == Softwall.State.STOPPED)
wall.start ();
}
for (uint i = 0; i != n; i++) {
int64 v = reader.read_element (i).get_int64_value ();
if (v < 0 || v > uint32.MAX)
throw new Error.INVALID_ARGUMENT ("Value is out of range");
public async void cancel (Cancellable? cancellable) throws IOError {
if (wall == null)
return;
arr[i] = (uint32) v;
reader.end_element ();
wall.stop ();
wall = null;
close ();
}
public async Variant request (Variant parameters, Cancellable? cancellable = null) throws Error, IOError {
ensure_active ();
var reader = new VariantReader (parameters);
string type = reader.read_member ("type").get_string_value ();
reader.end_member ();
var reply = new VariantBuilder (VariantType.VARDICT);
if (type == "read-events") {
var events = new VariantBuilder (new VariantType ("av"));
size_t total = 0;
var drain_status = wall.drain_events (ev => {
Variant item = new Variant.tuple ({
"audit",
ev->parent.time_ns,
ev->parent.tgid,
ev->parent.tid,
ev->rule_id
});
size_t item_size = (size_t) item.get_size ();
if (total != 0 && total + item_size > MAX_BATCH_BYTES)
return Softwall.EventFlow.STOP;
events.add_value (new Variant.variant (item));
total += item_size;
return Softwall.EventFlow.CONTINUE;
});
vardict_add (reply, "events", events.end ());
vardict_add (reply, "status",
(drain_status == Softwall.DrainStatus.DRAINED) ? "drained" : "more");
return reply.end ();
}
return arr;
}
if (type == "read-stats") {
var stats = wall.read_stats ();
private static uint64[] read_uint64_array (VariantReader reader) throws Error {
uint n = reader.count_elements ();
var arr = new uint64[n];
vardict_add (reply, "emitted-events", stats.emitted_events);
vardict_add (reply, "emitted-bytes", stats.emitted_bytes);
for (uint i = 0; i != n; i++) {
arr[i] = reader.read_element (i).get_uint64_value ();
reader.end_element ();
vardict_add (reply, "dropped-events", stats.dropped_events);
vardict_add (reply, "dropped-bytes", stats.dropped_bytes);
return reply.end ();
}
return arr;
}
if (type == "add-targets") {
if (reader.has_member ("pids")) {
reader.read_member ("pids");
foreach_uint32 (reader, pid => wall.add_target_tgid (pid));
reader.end_member ();
}
private static void foreach_uint32 (VariantReader reader, UInt32Handler cb) throws Error {
uint n = reader.count_elements ();
for (uint i = 0; i != n; i++) {
int64 v = reader.read_element (i).get_int64_value ();
if (v < 0 || v > uint32.MAX)
throw new Error.INVALID_ARGUMENT ("Value is out of range");
if (reader.has_member ("uids")) {
reader.read_member ("uids");
foreach_uint32 (reader, uid => wall.add_target_uid (uid));
reader.end_member ();
}
cb ((uint32) v);
if (reader.has_member ("users")) {
reader.read_member ("users");
foreach_string (reader, username => {
var uid = resolve_uid_from_username (username);
wall.add_target_uid (uid);
});
reader.end_member ();
}
reader.end_element ();
return reply.end ();
}
}
private static void foreach_string (VariantReader reader, StringHandler cb) throws Error {
uint n = reader.count_elements ();
for (uint i = 0; i != n; i++) {
unowned string s = reader.read_element (i).get_string_value ();
cb (s);
reader.end_element ();
if (type == "remove-targets") {
if (reader.has_member ("pids")) {
reader.read_member ("pids");
foreach_uint32 (reader, pid => wall.remove_target_tgid (pid));
reader.end_member ();
}
if (reader.has_member ("uids")) {
reader.read_member ("uids");
foreach_uint32 (reader, uid => wall.remove_target_uid (uid));
reader.end_member ();
}
if (reader.has_member ("users")) {
reader.read_member ("users");
foreach_string (reader, username => {
var uid = resolve_uid_from_username (username);
wall.remove_target_uid (uid);
});
reader.end_member ();
}
return reply.end ();
}
if (type == "add-rule") {
reader.read_member ("rule");
reader.read_member ("when");
if (!reader.has_member ("file_open"))
throw new Error.INVALID_ARGUMENT ("Unsupported rule.when (expected file_open)");
reader.read_member ("file_open");
string path = reader.read_member ("path").get_string_value ();
reader.end_member (); /* path */
reader.end_member (); /* file_open */
reader.end_member (); /* when */
string action_str = reader.read_member ("action").get_string_value ();
reader.end_member (); /* action */
reader.end_member (); /* rule */
var action = parse_action (action_str);
var id = wall.add_file_open_rule (path, action);
vardict_add (reply, "id", id);
return reply.end ();
}
if (type == "remove-rule") {
uint32 id = (uint32) reader.read_member ("id").get_int64_value ();
reader.end_member ();
wall.remove_rule (id);
return reply.end ();
}
throw new Error.INVALID_ARGUMENT ("Unsupported request type: %s", type);
}
private static void vardict_add (VariantBuilder b, string key, Variant val) {
b.add_value (new Variant.dict_entry (new Variant.string (key), new Variant.variant (val)));
private void on_events_available () {
var b = new VariantBuilder (VariantType.VARDICT);
vardict_add (b, "type", "events-available");
message (b.end ());
}
private static Softwall.Action parse_action (string s) throws Error {
switch (s) {
case "deny":
return Softwall.Action.DENY;
case "not-found":
return Softwall.Action.NOT_FOUND;
default:
throw new Error.INVALID_ARGUMENT ("Unsupported action: %s", s);
}
}
}
private string inet_ntop_to_string (int af, uint8 * src, size_t srclen) {
uint8 buf[46];
unowned string? res = Posix.inet_ntop (af, src, buf);
return (res != null) ? res : "?";
}
private uint32 resolve_uid_from_username (string name) throws Error {
unowned Posix.Passwd? pw = Posix.getpwnam (name);
if (pw == null)
throw new Error.INVALID_ARGUMENT ("Unknown user: %s", name);
return (uint32) pw.pw_uid;
}
private delegate void UInt32Handler (uint32 v) throws Error;
private delegate void StringHandler (string s) throws Error;
private uint32[] read_uint32_array (VariantReader reader) throws Error {
uint n = reader.count_elements ();
var arr = new uint32[n];
for (uint i = 0; i != n; i++) {
int64 v = reader.read_element (i).get_int64_value ();
if (v < 0 || v > uint32.MAX)
throw new Error.INVALID_ARGUMENT ("Value is out of range");
arr[i] = (uint32) v;
reader.end_element ();
}
return arr;
}
private uint64[] read_uint64_array (VariantReader reader) throws Error {
uint n = reader.count_elements ();
var arr = new uint64[n];
for (uint i = 0; i != n; i++) {
arr[i] = reader.read_element (i).get_uint64_value ();
reader.end_element ();
}
return arr;
}
private void foreach_uint32 (VariantReader reader, UInt32Handler cb) throws Error {
uint n = reader.count_elements ();
for (uint i = 0; i != n; i++) {
int64 v = reader.read_element (i).get_int64_value ();
if (v < 0 || v > uint32.MAX)
throw new Error.INVALID_ARGUMENT ("Value is out of range");
cb ((uint32) v);
reader.end_element ();
}
}
private void foreach_string (VariantReader reader, StringHandler cb) throws Error {
uint n = reader.count_elements ();
for (uint i = 0; i != n; i++) {
unowned string s = reader.read_element (i).get_string_value ();
cb (s);
reader.end_element ();
}
}
private void vardict_add (VariantBuilder b, string key, Variant val) {
b.add_value (new Variant.dict_entry (new Variant.string (key), new Variant.variant (val)));
}
}
+298
View File
@@ -0,0 +1,298 @@
public sealed class Frida.Softwall : Object {
public signal void events_available ();
public State state {
get {
return _state;
}
}
public enum State {
STOPPED,
STARTED,
}
private State _state = STOPPED;
private BpfMap? target_tgids;
private BpfMap? target_uids;
private BpfMap? rules_by_id;
private BpfMap? inode_index;
private BpfMap? stats;
private BpfRingbufReader? events_reader;
private IOChannel? events_channel;
private Source? events_source;
private Gee.Collection<BpfLink> links = new Gee.ArrayList<BpfLink> ();
private uint32 next_rule_id = 1;
protected override void dispose () {
stop ();
base.dispose ();
}
public void start () throws Error {
assert (state == STOPPED);
var obj = BpfObject.open ("softwall.elf", Frida.Data.HelperBackend.get_softwall_elf_blob ().data);
target_tgids = obj.maps.get_by_name ("target_tgids");
target_uids = obj.maps.get_by_name ("target_uids");
rules_by_id = obj.maps.get_by_name ("rules_by_id");
inode_index = obj.maps.get_by_name ("inode_index");
var events = obj.maps.get_by_name ("audit_events");
stats = obj.maps.get_by_name ("stats");
obj.prepare ();
events_reader = new BpfRingbufReader (events);
obj.load ();
foreach (var program in obj.programs) {
var link = program.attach ();
links.add (link);
}
events_channel = new IOChannel.unix_new (events.fd);
arm_events_watch ();
_state = STARTED;
}
public void stop () {
events_source?.destroy ();
events_source = null;
events_channel = null;
events_reader = null;
links.clear ();
stats = null;
inode_index = null;
rules_by_id = null;
target_uids = null;
target_tgids = null;
next_rule_id = 1;
_state = STOPPED;
}
public void add_target_tgid (uint tgid) throws Error {
target_tgids.update_u32_u8 (tgid, 1);
}
public void remove_target_tgid (uint tgid) throws Error {
target_tgids.remove_u32 (tgid);
}
public void add_target_uid (uint uid) throws Error {
target_uids.update_u32_u8 (uid, 1);
}
public void remove_target_uid (uint uid) throws Error {
target_uids.remove_u32 (uid);
}
public enum Action {
DENY,
NOT_FOUND,
}
public uint32 add_file_open_rule (string path, Action action) throws Error {
assert (state == STARTED);
uint64 dev, ino;
resolve_path_to_inode (path, out dev, out ino);
var id = next_rule_id++;
var errno_neg = map_action_to_errno (action);
var rule = FileOpenRule ();
rule.dev = dev;
rule.ino = ino;
rule.errno_neg = errno_neg;
rule._pad0 = 0;
rules_by_id.update_raw ((uint8[]) &id, (uint8[]) &rule);
var key = InodeKey ();
key.dev = dev;
key.ino = ino;
inode_index.update_raw ((uint8[]) &key, (uint8[]) &id);
return id;
}
public void remove_rule (uint32 id) throws Error {
assert (state == STARTED);
var rule = FileOpenRule ();
try {
rules_by_id.lookup_raw ((uint8[]) &id, (uint8[]) &rule);
} catch (Error e) {
throw new Error.INVALID_ARGUMENT ("Unknown rule id: %u".printf (id));
}
var key = InodeKey ();
key.dev = rule.dev;
key.ino = rule.ino;
try {
inode_index.remove_raw ((uint8[]) &key);
} catch (Error e) {
}
rules_by_id.remove_u32 (id);
}
public DrainStatus drain_events (AuditEventHandler on_event) {
var status = events_reader.drain (payload => {
assert (payload.length >= sizeof (AuditEvent));
var e = (AuditEvent *) payload;
return (on_event (e) == CONTINUE)
? BpfRingbufReader.RecordAction.CONTINUE
: BpfRingbufReader.RecordAction.STOP;
});
if (status == DRAINED)
arm_events_watch ();
return (status == DRAINED)
? DrainStatus.DRAINED
: DrainStatus.STOPPED;
}
public enum DrainStatus {
DRAINED,
STOPPED,
}
public delegate EventFlow AuditEventHandler (AuditEvent * ev);
public enum EventFlow {
CONTINUE,
STOP,
}
private void arm_events_watch () {
if (events_source != null)
return;
var src = new IOSource (events_channel, IOCondition.IN);
var state = new AuditEventsWatchState (this);
src.set_callback (state.on_ready);
src.attach (MainContext.get_thread_default ());
events_source = src;
}
private class AuditEventsWatchState : Object {
public unowned Softwall wall;
public AuditEventsWatchState (Softwall wall) {
this.wall = wall;
}
public bool on_ready (IOChannel ch, IOCondition cond) {
wall.on_events_readable ();
return Source.REMOVE;
}
}
private void on_events_readable () {
events_source?.destroy ();
events_source = null;
events_available ();
}
public Stats read_stats () throws Error {
Stats total = Stats ();
uint32 key = 0;
stats.foreach_percpu_value<Stats?> ((uint8[]) &key, (cpu, s) => {
total.emitted_events += s.emitted_events;
total.emitted_bytes += s.emitted_bytes;
total.dropped_events += s.dropped_events;
total.dropped_bytes += s.dropped_bytes;
});
return total;
}
private static int32 map_action_to_errno (Action action) {
switch (action) {
case Action.NOT_FOUND:
return -Posix.ENOENT;
case Action.DENY:
default:
return -Posix.EACCES;
}
}
private static void resolve_path_to_inode (string path, out uint64 dev, out uint64 ino) throws Error {
Posix.Stat st;
if (Posix.stat (path, out st) != 0)
throw new Error.INVALID_ARGUMENT ("Unable to stat path: %s", path);
dev = (uint64) st.st_dev;
ino = (uint64) st.st_ino;
}
public struct Event {
public uint64 time_ns;
public uint32 tgid;
public uint32 tid;
public uint16 type;
public uint16 _pad0;
}
public enum EventType {
AUDIT
}
public struct AuditEvent {
public Event parent;
public uint32 rule_id;
public uint32 _pad0;
}
public struct InodeKey {
public uint64 dev;
public uint64 ino;
}
private struct FileOpenRule {
public uint64 dev;
public uint64 ino;
public int32 errno_neg;
public uint32 _pad0;
}
public struct Stats {
public uint64 emitted_events;
public uint64 emitted_bytes;
public uint64 dropped_events;
public uint64 dropped_bytes;
}
}
+4 -1
View File
@@ -333,6 +333,7 @@ if have_local_backend
'linux' / 'frida-helper-types.vala',
'linux' / 'activity-sampler.vala',
'linux' / 'syscall-tracer.vala',
'linux' / 'softwall.vala',
'linux' / 'symbol-resolver.vala',
'linux' / 'proc-maps.vala',
]
@@ -362,8 +363,9 @@ if have_local_backend
'linux' / 'frida-helper-backend.resources',
helpers_native_dir / 'bootstrapper.bin',
helpers_native_dir / 'loader.bin',
helpers_bpf_noarch_dir / 'activity-sampler.elf',
helpers_bpf_noarch_dir / 'activity-sampler.elf',
helpers_bpf_arch_dir / 'syscall-tracer.elf',
helpers_bpf_noarch_dir / 'softwall.elf',
],
output: [
'frida-data-helper-backend.vapi',
@@ -379,6 +381,7 @@ if have_local_backend
'@INPUT2@',
'@INPUT3@',
'@INPUT4@',
'@INPUT5@',
],
)
helper_backend_sources += [helper_backend_data]