My Very Personal Guidance and Strategies to Protect Network Edge Devices

Published: 2025-02-06. Last Updated: 2025-02-17 12:51:04 UTC
by Johannes Ullrich (Version: 1)
0 comment(s)

Last week, CISA and other national cyber security organizations published an extensive document outlining "Guidance and Strategies to Protect Network Edge Devices." [1] The document is good but also very corporate and "bland." It summarizes good, well-intended advice that will help you secure edge devices. But reading it also made me think, "That's it?" Not that I expected earth-shattering advice eliminating vulnerabilities brought on by accumulating deceased worth of abandoned ware still peddled at often relatively high costs. But I don't know; maybe something more actionable would be helpful. 

So here is my advice from the small network that is maintained, like so many, "on the side" and not by a team of dedicated edge device experts.

0 - Limit Access to Admin Functions

Before you do anything, limit access to admin interfaces, and do not forget access via SSH or APIs. At the bare minimum, access should only be available via the LAN interface. Carefully select any exposed services. SSH may be an option if you must have remote access or a VPN solution (many devices support Wireguard, OpenVPN, or other protocols). But the fewer options you enable, the better. Avoid exposing web-based APIs, admin interfaces, or SSL VPN gateways at all costs. HTTPs will not protect you in this case. The issue is numerous web application vulnerabilities that keep popping up in these devices. Move any exposed admin services (HTTPS or SSH) to non-default ports.

Removing internet access to admin features will eliminate the vast majority of threats. If you must expose a VPN or SSH, continue reading.

1 - Change Credentials and use MFA

Maybe this should be 0. Either way, Change your credentials. Your device doesn’t support MFA? Get a different one! Even open-source solutions usually support some kind of MFA these days. OPNSense makes it a bit painful, but it works. Maybe even use a different username instead of "admin" and disable "admin"/"root" if that is an option. It helps a bit. Harden protocols like SSH by using keys.

2 - Define a monthly "Router Update Day."

I don't care when. Third Thursday of the month, mornings, 10 am? Check if there is an update waiting; check the release notes. Is it important? Apply now. Can it wait? Apply later. Having redundant perimeter devices should be doable even for a smaller network. Add a monthly "perimeter update" reminder to your calendar. Some device manufacturers have mailing lists to notify you of updates or release updates on a specific schedule.

3 - Stay Open Source

Some may not agree with this advice. Commercial permitter security devices are often counterproductive. You can find well-respected, preconfigured, open-source perimeter security devices. For smaller networks, OpenWRT will work great. Use PFSense or OPNSense if you operate a more complex network or need additional features. Open-source packages provide much longer support timelines for existing hardware. Updates are usually painless, support is easy to come by, and, most importantly, You will have more insight into how the device works. Commercial systems make it sound like it is worth spending a lot of money to get less: A black box with a lot of magic packet dust. However, you will not be able to understand how the device works and how to debug it effectively. Some people say that you "pay with your time". The opposite is true in this case: Understanding and maintaining a closed-source solution for perimeter security devices is often more time-consuming or MUCH MORE expensive. 

If it makes the boss happy, Buy a support contract. Multiple companies or developers behind the particular solution will offer commercial support contracts at competitive prices for all the open-source solutions mentioned above.

4 - Mark the Devices Expiration Date

Every device has an expiration date. As you purchase it, define how long the device will be in your network. Keep yourself honest. Apply a sticker to the device noting when and where it was purchased (nice for warranty, too) and when it is supposed to be replaced.

5 - Create Automated Configuration Backups

Once a day? The important part is to automate it. It will not happen unless you automate it. These configuration backups are essential for configuration downgrades to recover from a lousy upgrade quickly or if you need to replace a device. Sadly, this often requires a distinct process.

6 - Edge Devices are Endpoints too

So install endpoint protection software! OPNsense supports Wazuh (I think PFSense, too). Find out what works for your device. At the very least, monitor unapproved changes to the configuration backups. This may be an area where you want to spend some money on commercial software if needed and if it is applicable.

[1] https://www.cisa.gov/resources-tools/resources/guidance-and-strategies-protect-network-edge-devices

---
Johannes B. Ullrich, Ph.D. , Dean of Research, SANS.edu
Twitter|

0 comment(s)

The Unbreakable Multi-Layer Anti-Debugging System

Published: 2025-02-06. Last Updated: 2025-02-06 08:08:26 UTC
by Xavier Mertens (Version: 1)
0 comment(s)

The title of this diary is based on the string I found in a malicious Python script that implements many anti-debugging techniques. If some were common, others were interesting and demonstrated how low-level high-level languages like Python can access operating system information. Let’s review some of them!

Anti-debugging techniques are like a cat-and-mouse game. If you’re interested in malware analysis, this will show you how your task can be much more challenging if you’re prepared to face them. The file was found on VT with a low score of 2/62[1] (SHA256: 3a216b238bae042312ab810c0d07fdc49e8eddc97a2dec3958fb6b1f4ecd4612). The file just contains only anti-debugging stuff and not real malware. I suspect the file to be a proof-of-concept.

The script is multi-threaded and launches all the techniques in parallel:

def anti_debug_check():
    """ ? The Unbreakable Multi-Layer Anti-Debugging System """
    threads = [
        threading.Thread(target=detect_debugger),
        threading.Thread(target=detect_debugger_processes),
        threading.Thread(target=detect_vm),
        threading.Thread(target=detect_api_hooks),
        threading.Thread(target=detect_breakpoints),
        threading.Thread(target=detect_sandbox),
        threading.Thread(target=detect_cpu_usage),
        threading.Thread(target=detect_memory_tampering),
        threading.Thread(target=detect_mouse_movements),
        threading.Thread(target=detect_execution_speed),
        threading.Thread(target=detect_registry_keys),
        threading.Thread(target=detect_screenshot),
        threading.Thread(target=infinite_loop_debugger_trap),
        threading.Thread(target=inject_fake_code),
        threading.Thread(target=polymorphic_self_mutation)
    ]
    for t in threads:
        t.daemon = True
        t.start()
    for t in threads:
        t.join()

Let’s focus on the interesting ones. « polymorphic_self_mutation » will change the Python script file. In a Python program, the variable "__file__" contains the path of the currently executed script. This variable is used to read the content of the script, randomize the lines, and overwrite it:

def polymorphic_self_mutation():
    """ ? Self-Mutating Code to Avoid Static Analysis """
    with open(__file__, "r", encoding="utf-8") as f:
        lines = f.readlines()
    with open(__file__, "w", encoding="utf-8") as f:
        random.shuffle(lines)
        f.writelines(lines)

The new file will have, for example, a different hash and will be more difficult to hunt.

The next technique is a typical Python trick provided by sys.gettrace[2]. If a debugger is attached to the Python process, this function will return a trace function. The purpose of this technique is to loop forever if a debugger is attached to the Python script. 

def infinite_loop_debugger_trap():
    """ ? If Debugger is Attached, Trap it in an Infinite Loop """
    while sys.gettrace():
        pass  # Debugger is stuck here forever

I like the « memory tampering » technique: The script computes its hash and recheck it at regular intervals:

def detect_memory_tampering():
    original_hash = hashlib.md5(open(sys.argv[0], "rb").read()).hexdigest()
    while True:
        time.sleep(2)
        current_hash = hashlib.md5(open(sys.argv[0], "rb").read()).hexdigest()
        if current_hash != original_hash:
            kill_system()

The next one relies on the API call IsDebuggerPresent(). This one is often hooked to prevent the simple detection of a debugger. The value 0xE9 is the op-code for a long jump… This hooking technique is called « trampoline ». If the very first byte of the API call loaded in memory is 0xE9, it has been hooked!

def detect_api_hooks():
    kernel32 = ctypes.windll.kernel32
    original_bytes = ctypes.create_string_buffer(5)
    kernel32.ReadProcessMemory(kernel32.GetCurrentProcess(), kernel32.IsDebuggerPresent, original_bytes, 5, None)
    if original_bytes.raw[0] == 0xE9:  # Hook detected
        kill_system()

When you debug, you probably use breakpoints, right? The following code helps to detect hardware breakpoints:

def detect_breakpoints():
    context = ctypes.create_string_buffer(0x4C)
    context_ptr = ctypes.byref(context)
    context_offset = struct.calcsize("Q") * 6
    ctypes.windll.kernel32.RtlCaptureContext(context_ptr)
    dr0, dr1, dr2, dr3 = struct.unpack_from("4Q", context.raw, context_offset)
    if dr0 or dr1 or dr2 or dr3:
        kill_system()

Hardware breakpoints are used to avoid patching the program. They contain the address where to pause the execution. Hardware breakpoints are CPU registers: DRO to DR3 (on Intel CPU’s). RtlCaptureContext()[3] is used to get the current threat’s execution state which includes the registers. With the help of unpack, the script fills the variable corresponding to the registers, if one of them is not empty, there is a hardware breakpoint defined!

Other checks are really common: detection of suspicious process names, and specific registry keys, … I'll not cover them.

You can see that all functions will call kill_system() if tests are successful. This function will just annoy the malware analysts by crashing (or trying to crash) the system:

def kill_system():
    """ ? THE ULTIMATE KILL-SWITCH ? """
    try:
        ctypes.windll.ntdll.NtRaiseHardError(0xDEADDEAD, 0, 0, 0, 6, ctypes.byref(ctypes.c_ulong()))
    except:
        os.system("shutdown /s /t 0")  # Force shutdown

The purpose of the function is easy to understand but when NtRaiseHardError[4] is invoked, it does not automatically cause a kernel panic or system-wide crash. Instead, the system can handle the error in various ways, including logging the event, presenting an error dialog, or terminating the application that called the function. I tried in a VM:

C:\Users\REM>python
Python 3.5.2 (v3.5.2:4def2a2901a5, Jun 25 2016, 22:18:55) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>> import ctypes
>>> ctypes.windll.ntdll.NtRaiseHardError(0xDEADDEAD, 0, 0, 0, 6, ctypes.byref(ctypes.c_ulong()))
-1073741727
>>>

When you convert the value -1073741727 to hexadecimal, you get 0xC000001F, which is a Windows NTSTATUS code. Specifically, this error code indicates a STATUS_INVALID_PARAMETER error...

The Python script is a great example of multiple techniques that can be implemented in malware!

[1] https://www.virustotal.com/gui/file/3a216b238bae042312ab810c0d07fdc49e8eddc97a2dec3958fb6b1f4ecd4612/detection
[2] https://docs.python.org/3/library/sys.html#sys.gettrace
[3] https://learn.microsoft.com/en-us/windows/win32/api/winnt/nf-winnt-rtlcapturecontext
[4] https://github.com/AgnivaMaity/NtRaiseHardError-Example

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

0 comment(s)
ISC Stormcast For Thursday, February 6th, 2025 https://isc.sans.edu/podcastdetail/9312

Comments


Diary Archives