-
Notifications
You must be signed in to change notification settings - Fork 161
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
applied changes to the ramnit dga implementation provided by @steodor…
- Loading branch information
Showing
1 changed file
with
197 additions
and
68 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,79 +1,208 @@ | ||
#!/usr/bin/env /usr/local/bin/python3 | ||
""" | ||
dga_ramnit | ||
---------- | ||
Generate DNS queries using the ramnit DGA | ||
See `The DGA of ramnit | ||
<https://bin.re/blog/the-dga-of-ramnit/>`__ for a detailed explanation of the | ||
algorithm. | ||
The code below is almost fully based on `<https://github.com/baderj/domain_generation_algorithms/blob/master/ramnit/dga.py>`__ by `Johannes Bader | ||
<https://github.com/baderj>`__. | ||
""" | ||
from secrets import token_hex, choice | ||
import argparse | ||
import sys | ||
|
||
TLDS = ['click', 'com', 'eu', 'bid'] | ||
KNOWN_SEEDS = [ | ||
'16647BB4', 'E7392D18', 'C129388E', 'E706B455', 'DC485593', 'EF214BBF', | ||
'28488EEA', '4BFCBC6A', '79159C10', '92F4BE35', '4302C04A', '52278648', | ||
'9753029A', 'A6EAB21A', '46CF1B28', '1CCEC41C', '0C5787AE2', '0FCFFD9E9', | ||
'75EA95C2', '8A0AEC7D', '1DF640A8', '14DF29DD', '8222270B', '55536A85', | ||
'5C39E467', 'D2B3C361', 'F318D47D', '231D9480', '13317EAC', '89547381', | ||
'6C36D41D' | ||
] | ||
SLD_MIN_LENGTH = 9 | ||
SLD_MAX_LENGTH = 25 | ||
NUMBER_DOMAINS = 10 | ||
|
||
|
||
class RandInt: | ||
""" | ||
Generate random integers using a `Linear congruential generator | ||
<https://en.wikipedia.org/wiki/Linear_congruential_generator>`__ | ||
""" | ||
|
||
def __init__(self, seed): | ||
def __init__(self, seed=None): | ||
""" | ||
:arg str seed: | ||
the seed for the LCG pseudo-random numbers generator | ||
if a seed is not provided, a random 8-hex digits seed is generated using the :meth:`secrets.token_hex` method | ||
""" | ||
if seed is None: | ||
seed = int(token_hex(8), 16) | ||
self.value = seed | ||
|
||
def rand_int_modulus(self, modulus): | ||
ix = self.value | ||
ix = 16807*(ix % 127773) - 2836*(ix // 127773) & 0xFFFFFFFF | ||
self.value = ix | ||
return ix % modulus | ||
|
||
def get_domains(seed, nr, tlds): | ||
if not tlds: | ||
tlds = [".com"] | ||
|
||
r = RandInt(seed) | ||
|
||
for i in range(nr): | ||
seed_a = r.value | ||
domain_len = r.rand_int_modulus(12) + 8 | ||
seed_b = r.value | ||
domain = "" | ||
for j in range(domain_len): | ||
char = chr(ord('a') + r.rand_int_modulus(25)) | ||
domain += char | ||
tld = tlds[i % len(tlds)] | ||
domain += '.' if tld[0] != '.' else '' | ||
domain += tld | ||
m = seed_a*seed_b | ||
r.value = (m + m//(2**32)) % 2**32 | ||
""" | ||
generate a random integer using a classic LGC generator, update the seed, and return the result of the modulo operation between the random integer and the `modulus` argument | ||
for example, if the `modulus` value is `12`, the return is a random | ||
integer from `0` to `11` (the possible results of a modulo `12` operation). the randomization is provided by the random integer | ||
subjected to the modulo operation. | ||
""" | ||
lcg_random_int = 16807*(self.value % 127773) \ | ||
- 2836*(self.value // 127773) & 0xFFFFFFFF | ||
self.value = lcg_random_int | ||
|
||
return lcg_random_int % modulus | ||
|
||
|
||
def get_domains( | ||
seed=None, number_domains=NUMBER_DOMAINS, tlds=None, | ||
sld_min_length=SLD_MIN_LENGTH, sld_max_length=SLD_MAX_LENGTH): | ||
""" | ||
generate a list of Ramnit domains | ||
:arg str seed: | ||
:arg int number_domains: | ||
:arg lst tlds: | ||
:arg int sld_min_length: | ||
:arg int sld_maxx_length: | ||
""" | ||
if tlds is None: | ||
tlds = TLDS | ||
|
||
if not isinstance(tlds, (list, tuple)): | ||
tlds = [tlds] | ||
|
||
# validate the seed value if provide by the user; it must be a HEX digit | ||
if seed: | ||
try: | ||
seed = int(seed, 16) | ||
except ValueError as error: | ||
raise ValueError( | ||
f'invalid seed value:{seed} is not a HEX number' | ||
) from error | ||
|
||
# clean up the TLDs if provided by the user; we don't want any [.] dots | ||
tlds = [tld.replace('.', '') for tld in tlds] | ||
|
||
random_int = RandInt(seed) | ||
|
||
for idx in range(number_domains): | ||
first_seed = random_int.value | ||
|
||
""" | ||
the LCG generator returns a value between 0 and `modulus` - `1`. assuming it returns 0, the minimum length of the SLD is given by | ||
the value after the `+` sign in the code line below. | ||
the maximum length of the domain is determined by the largest value | ||
returned by the modulo operation in the LCG code. using the defaults from this code: | ||
- 9 characters are provided by the second term of the sum below | ||
- the rest of the characters up to the maximum length of 25 must come | ||
the random component. result of the modulus operation must pad the | ||
domain name with another 25 - 9 = 16 characters. the modulus operand | ||
that returns from 0 to 16 is 1 + (25 -9) = 17 | ||
""" | ||
domain_len = random_int.rand_int_modulus( | ||
1 + (sld_max_length - sld_min_length) | ||
) + sld_min_length | ||
|
||
second_seed = random_int.value | ||
|
||
domain = ''.join( | ||
[ | ||
chr(ord('a') + random_int.rand_int_modulus(25)) | ||
for _ in range(domain_len) | ||
] | ||
) | ||
|
||
domain = f'{domain}.{tlds[idx % len(tlds)]}' | ||
|
||
# we need to reseed the LCG for the next domain in the sequence | ||
reseed = first_seed * second_seed | ||
random_int.value = (reseed + reseed//(2**32)) % 2**32 | ||
|
||
yield domain | ||
|
||
if __name__=="__main__": | ||
""" | ||
example seeds: | ||
16647BB4 | ||
E7392D18 | ||
C129388E | ||
E706B455 | ||
DC485593 | ||
EF214BBF | ||
28488EEA | ||
4BFCBC6A | ||
79159C10 | ||
92F4BE35 | ||
4302C04A 10 -t "click bid eu" | ||
52278648 | ||
9753029A 100 -t .eu | ||
A6EAB21A 500 | ||
46CF1B28 500 | ||
1CCEC41C | ||
0C5787AE2 | ||
0FCFFD9E9 | ||
75EA95C2 | ||
8A0AEC7D | ||
1DF640A8 | ||
14DF29DD | ||
8222270B | ||
55536A85 | ||
5C39E467 | ||
D2B3C361 | ||
F318D47D | ||
231D9480 | ||
13317EAC | ||
89547381 | ||
6C36D41D | ||
|
||
def parse_args(): | ||
""" | ||
parse the command line | ||
""" | ||
parser = argparse.ArgumentParser(description="generate Ramnit domains") | ||
parser.add_argument("seed", help="seed as hex") | ||
parser.add_argument("nr", help="nr of domains", type=int) | ||
parser.add_argument("-t", "--tlds", help="list of tlds", default=None) | ||
args = parser.parse_args() | ||
tlds = None | ||
if args.tlds: | ||
tlds = [x.strip() for x in args.tlds.split(" ")] | ||
for domain in get_domains(int(args.seed, 16), args.nr, tlds): | ||
print(domain) | ||
|
||
choose_seed = parser.add_mutually_exclusive_group(required=True) | ||
choose_seed.add_argument( | ||
'-s', '--seed', dest='seed', action='store', | ||
help='seed as hex; a value is mandatory') | ||
choose_seed.add_argument( | ||
'-r', '--generate-random-seed', dest='random_seed', action='store_true', | ||
help='generate a random seed' | ||
) | ||
choose_seed.add_argument( | ||
'-k', '--random-known-seed', dest='known_seed', action='store_true', | ||
help='choose a random seed from the list of known seeds' | ||
) | ||
|
||
parser.add_argument( | ||
'-x', '-number-domains', dest='number_domains', type=int, | ||
default=NUMBER_DOMAINS, | ||
help='number of domains, default: %(default)s domains') | ||
parser.add_argument( | ||
'-t', '--tlds', dest='tlds', action='store', default=','.join(TLDS), | ||
help=( | ||
'list of tlds, comma or space separated string,' | ||
' default: %(default)s') | ||
) | ||
parser.add_argument( | ||
'-m', '--min-length', dest='min_length', type=int, | ||
default=SLD_MIN_LENGTH, | ||
help='minimum length of the SLD, default: %(default)s' | ||
) | ||
parser.add_argument( | ||
'-M', '--max-length', dest='max_length', type=int, | ||
default=SLD_MAX_LENGTH, | ||
help='maximum length of the SLD, default: %(default)s' | ||
) | ||
|
||
return parser | ||
|
||
|
||
def main(): | ||
"""main""" | ||
args = parse_args().parse_args() | ||
|
||
if args.random_seed: | ||
seed = None | ||
elif args.known_seed: | ||
seed = choice(KNOWN_SEEDS) | ||
else: | ||
seed = args.seed | ||
|
||
tlds = args.tlds.replace(' ', ',').split(',') | ||
|
||
print(*[ | ||
domain for domain in get_domains( | ||
seed=seed, number_domains=args.number_domains, tlds=tlds, | ||
sld_min_length=args.min_length, sld_max_length=args.max_length | ||
) | ||
], sep='\n', end='\n') | ||
|
||
|
||
if __name__ == "__main__": | ||
|
||
try: | ||
main() | ||
except Exception as err: # pylint disable=broad-except | ||
print(err) | ||
sys.exit(2) | ||
sys.exit(0) |