Skip to content

Commit

Permalink
applied changes to the ramnit dga implementation provided by @steodor…
Browse files Browse the repository at this point in the history
  • Loading branch information
baderj committed Oct 12, 2022
1 parent 54088cf commit 907dbe4
Showing 1 changed file with 197 additions and 68 deletions.
265 changes: 197 additions & 68 deletions ramnit/dga.py
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)

0 comments on commit 907dbe4

Please sign in to comment.