Sinkholing Suspicious Scripts or Executables on Linux

    Published: 2025-07-25. Last Updated: 2025-07-25 04:54:48 UTC
    by Xavier Mertens (Version: 1)
    0 comment(s)

    When you need to analyze some suspicious pieces of code, it's interesting to detonate them in a sandbox. If you don't have a complete sandbox environment available or you just want to avoid generatin noise on your network, why not route the traffic to a sinkhole or NULL-route (read: packets won't be sent across the normal network and default gateway).

    When you inspect a process using the /proc[1] virtual filesystem, there is a "route" file:

    remnux@remnux:~$ cat /proc/1180/net/route
    Iface    Destination    Gateway     Flags    RefCnt    Use    Metric    Mask        MTU    Window    IRTT                                                       
    ens19    00000000    01FEA8C0    0003    0    0    100    00000000    0    0    0                                                                            
    ens18    004A10AC    00000000    0001    0    0    0    00FFFFFF    0    0    0                                                                              
    ens19    00FEA8C0    00000000    0001    0    0    0    00FFFFFF    0    0    0                                                                              
    ens19    01FEA8C0    00000000    0005    0    0    100    FFFFFFFF    0    0    0

    It displays the IP routing table assigned to this process. Typically, IP addresses are encoded in little-endian hexadecimal values. They can be easily decoded using a few lines of Python:

    gw = "01FEA8C0"
    octets = [gw[i:i+2] for i in range(0, len(gw), 2)]
    ip = '.'.join(str(int(o, 16)) for o in octets)
    print(ip)  # Will return: 1.254.168.192

    Does it mean that we could apply a specific routing table to a process? Yes and no... In /proc, the "route" file is read-only.

    But, Linux is full of features that many people aren't aware of. One of them are namespaces[2]. It's a kernel feature (introduced around 2016 if I remember well) that provides isolation of system resources between processes (a bit like containers). Each namespace type—such as PID, mount, UTS, network, IPC, and user—isolates a specific aspect of the operating system environment. For example, the network namespace gives processes their own network stack, including interfaces and routing tables. Very interesting!

    Let's try this and run our suscipious script in a dedicated namespace. My suspicious script will be super simple:

    remnux@remnux:~$ cat sample.sh 
    #!/bin/bash
    echo "Am I bad?"
    curl https://isc.sans.edu

    First example, no network connectivity at all!

    remnux@remnux:~$ sudo unshare --net bash
    root@remnux:/home/remnux# ./sample.sh 
    Am I bad?
    curl: (6) Could not resolve host: isc.sans.edu
    root@remnux:/home/remnux# ip a
    1: lo: <LOOPBACK> mtu 65536 qdisc noop state DOWN group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    root@remnux:/home/remnux# ip r
    Error: ipv4: FIB table does not exist.
    Dump terminated
    root@remnux:/home/remnux# exit
    remnux@remnux:~$ 

    The unshare command (executed as root) will create a new shell in a new namespace with dropped network settings. When curl is executed, it can't resolve isc.sans.edu nor connect to it. We have a complete network isolation.

    Second example, let's build a dedicated IP stack that will route packets to another IP address, our synchole. A pair of virtial Ethernet interfaces must be added. In this case, 10.0.0.1 will be the new namespace and 10.0.0.2 the main one.

    (Note: I'll change the bash prompt to make it clearer)

    remnux@remnux:~$ sudo unshare --net bash
    root@remnux:/home/remnux# export PS1="namespace> "
    namespace> ip link set lo up
    namespace> ip link add veth0 type veth peer name veth1
    namespace> ip link set veth0 up
    namespace> ip addr add 10.0.0.1/24 dev veth0
    namespace> ip a
    1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
        link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
        inet 127.0.0.1/8 scope host lo
           valid_lft forever preferred_lft forever
        inet6 ::1/128 scope host 
           valid_lft forever preferred_lft forever
    2: veth1@veth0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN group default qlen 1000
        link/ether b6:5c:6e:ed:c3:62 brd ff:ff:ff:ff:ff:ff
    3: veth0@veth1: <NO-CARRIER,BROADCAST,MULTICAST,UP,M-DOWN> mtu 1500 qdisc noqueue state LOWERLAYERDOWN group default qlen 1000
        link/ether 66:72:35:1f:9f:9e brd ff:ff:ff:ff:ff:ff
        inet 10.0.0.1/24 scope global veth0
           valid_lft forever preferred_lft forever
    namespace> ip link set veth1 netns 1

    On the main namespace (your original shell), create the virtual NIC:

    root@remnux:/home/remnux# ip addr add 10.0.0.2/24 dev veth1
    root@remnux:/home/remnux# ip link set veth1 up

    Back in the new namespace:

    namespace> ping 10.0.0.2
    PING 10.0.0.2 (10.0.0.2) 56(84) bytes of data.
    64 bytes from 10.0.0.2: icmp_seq=1 ttl=64 time=0.020 ms
    64 bytes from 10.0.0.2: icmp_seq=2 ttl=64 time=0.034 ms
    ^C
    --- 10.0.0.2 ping statistics ---
    2 packets transmitted, 2 received, 0% packet loss, time 1023ms
    rtt min/avg/max/mdev = 0.020/0.027/0.034/0.007 ms

    Let's add a default route to the IP in the main namespace:

    namespace> ip route add default via 10.0.0.2
    namespace> ping 8.8.8.8
    PING 8.8.8.8 (8.8.8.8) 56(84) bytes of data.
    ^C
    --- 8.8.8.8 ping statistics ---
    13 packets transmitted, 0 received, 100% packet loss, time 12293ms

    If we run a tcpdump on veth1, we can now capture all the network connection attempts from the namespace:

    root@remnux:/home/remnux# tcpdump -i veth1 -n
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on veth1, link-type EN10MB (Ethernet), capture size 262144 bytes
    11:02:32.122380 ARP, Request who-has 10.0.0.2 tell 10.0.0.1, length 28
    11:02:32.122408 ARP, Reply 10.0.0.2 is-at b6:5c:6e:ed:c3:62, length 28
    11:02:32.154271 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 18547, seq 6, length 64
    11:02:33.178401 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 18547, seq 7, length 64
    11:02:34.202411 IP 10.0.0.1 > 8.8.8.8: ICMP echo request, id 18547, seq 8, length 64
    ^C
    5 packets captured
    5 packets received by filter
    0 packets dropped by kernel

    Finally, let's verify the routing table of the shell running in the new namespace:

    namespace> echo $$
    149522

    On the main namespace:

    root@remnux:/home/remnux# cat /proc/149522/net/route 
    Iface    Destination    Gateway     Flags    RefCnt    Use    Metric    Mask        MTU    Window    IRTT                                                       
    veth0    00000000    0200000A    0003    0    0    0    00000000    0    0    0                                                                              
    veth0    0000000A    00000000    0001    0    0    0    00FFFFFF    0    0    0  

    (0x0200000A = 10.0.0.2)

    Done! The current configuration is very basic and does not provide, amongst others, a DNS. Your sinkholed sample won't be able to resolve FQDN. Also, you could really route the packets by enabling ip_forward and NAT the traffic. 

    WARNING: This is not a bullet-proof solution to perform malware analysis: Only the network traffic was isolated!

    [1] https://docs.kernel.org/filesystems/proc.html
    [2] https://en.wikipedia.org/wiki/Linux_namespaces

    Xavier Mertens (@xme)
    Xameco
    Senior ISC Handler - Freelance Cyber Security Consultant
    PGP Key

    0 comment(s)
    ISC Stormcast For Friday, July 25th, 2025 https://isc.sans.edu/podcastdetail/9542

      Comments


      Diary Archives