Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor yacat example #403

Merged
merged 20 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion examples/utils.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
"""Utilities for yapapi example scripts."""
import argparse
from datetime import datetime, timezone
from pathlib import Path
import tempfile


import colorama # type: ignore

Expand All @@ -18,13 +22,18 @@


def build_parser(description: str):
current_time_str = datetime.now(tz=timezone.utc).strftime("%Y%m%d_%H%M%S%z")
default_log_path = Path(tempfile.gettempdir()) / f"yapapi_{current_time_str}.log"

parser = argparse.ArgumentParser(description=description)
parser.add_argument("--driver", help="Payment driver name, for example `zksync`")
parser.add_argument("--network", help="Network name, for example `rinkeby`")
parser.add_argument(
"--subnet-tag", default="devnet-beta.1", help="Subnet name; default: %(default)s"
)
parser.add_argument(
"--log-file", default=None, help="Log file for YAPAPI; default: %(default)s"
"--log-file",
default=str(default_log_path),
help="Log file for YAPAPI; default: %(default)s",
)
return parser
54 changes: 3 additions & 51 deletions examples/yacat/yacat.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,52 +1,4 @@
FROM golemfactory/base:1.5
FROM dizcza/docker-hashcat:intel-cpu

MAINTAINER Radek Tereszczuk <[email protected]>


RUN apt-get update && apt-get install -y alien clinfo

# Install Intel OpenCL driver

#ENV INTEL_OPENCL_URL=http://registrationcenter-download.intel.com/akdlm/irc_nas/vcp/13793/l_opencl_p_18.1.0.013.tgz
ENV INTEL_OPENCL_URL=http://registrationcenter-download.intel.com/akdlm/irc_nas/9019/opencl_runtime_16.1.1_x64_ubuntu_6.4.0.25.tgz

RUN mkdir -p /tmp/opencl-driver-intel
WORKDIR /tmp/opencl-driver-intel
RUN curl -O $INTEL_OPENCL_URL; \
tar -xzf $(basename $INTEL_OPENCL_URL); \
for i in $(basename $INTEL_OPENCL_URL .tgz)/rpm/*.rpm; do alien --to-deb $i; done; \
dpkg -i *.deb; \
mkdir -p /etc/OpenCL/vendors; \
echo /opt/intel/*/lib64/libintelocl.so > /etc/OpenCL/vendors/intel.icd; \
rm -rf *

ENV HASHCAT_VERSION hashcat-5.1.0
ENV HASHCAT_UTILS_VERSION 1.9

# Update & install packages for installing hashcat
RUN apt-get update && \
apt-get install -y wget p7zip make build-essential git libcurl4-openssl-dev libssl-dev zlib1g-dev

RUN mkdir /golem/yacat

WORKDIR /golem/yacat
RUN wget --no-check-certificate https://hashcat.net/files/${HASHCAT_VERSION}.7z && \
7zr x ${HASHCAT_VERSION}.7z && \
rm ${HASHCAT_VERSION}.7z

RUN wget --no-check-certificate https://github.com/hashcat/hashcat-utils/releases/download/v${HASHCAT_UTILS_VERSION}/hashcat-utils-${HASHCAT_UTILS_VERSION}.7z && \
7zr x hashcat-utils-${HASHCAT_UTILS_VERSION}.7z && \
rm hashcat-utils-${HASHCAT_UTILS_VERSION}.7z

#Add link for binary
RUN ln -s /golem/yacat/${HASHCAT_VERSION}/hashcat64.bin /usr/bin/hashcat
RUN ln -s /golem/yacat/hashcat-utils-${HASHCAT_UTILS_VERSION}/bin/cap2hccapx.bin /usr/bin/cap2hccapx

RUN cp /golem/yacat/${HASHCAT_VERSION}/hashcat.hcstat2 /golem/yacat
RUN chmod -R 777 /golem/yacat

RUN apt clean

WORKDIR /golem/work

VOLUME /golem/work /golem/output /golem/resource
VOLUME /golem/input /golem/output
WORKDIR /golem/entrypoint
224 changes: 133 additions & 91 deletions examples/yacat/yacat.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
#!/usr/bin/env python3
import argparse
import asyncio
from datetime import datetime, timedelta
import pathlib
from pathlib import Path
import sys
from tempfile import NamedTemporaryFile
from typing import AsyncIterable, Optional

from yapapi import Executor, NoPaymentAccountError, Task, WorkContext, windows_event_loop_fix
from yapapi import Golem, NoPaymentAccountError, Task, WorkContext, windows_event_loop_fix
from yapapi.executor.events import CommandExecuted
from yapapi.log import enable_default_logger, log_summary, log_event_repr # noqa
from yapapi.payload import vm

examples_dir = pathlib.Path(__file__).resolve().parent.parent
examples_dir = Path(__file__).resolve().parent.parent
sys.path.append(str(examples_dir))

from utils import (
Expand All @@ -20,146 +24,184 @@
TEXT_COLOR_YELLOW,
)

HASHCAT_ATTACK_MODE = 3 # stands for mask attack, hashcat -a option
KEYSPACE_OUTPUT_PATH = Path("/golem/output/keyspace")

def write_hash(hash):
with open("in.hash", "w") as f:
f.write(hash)
# Ideally, this value should depend on the chunk size
ATTACK_TIMEOUT: timedelta = timedelta(minutes=30)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe DICTIONARY_ATTACK_TIMEOUT ? ... just "attack" seems more serious than it is to a casual (non-security-terminology-aware) user ...

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense, though using the word dictionary in this case may be confusing (i.e. in hashcat dictionary and mask are two different types of attacks). How about MASK_ATTACK_TIMEOUT and renaming the worker funcion to perform_mask_attack?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated in daad9bc

KEYSPACE_TIMEOUT: timedelta = timedelta(minutes=10)

arg_parser = build_parser("Run a hashcat attack (mask mode) on Golem network")
arg_parser.add_argument("--hash", type=str, help="Target hash to be cracked", required=True)
arg_parser.add_argument(
"--mask", type=str, help="Hashcat mask to be used for the attack", required=True
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd provide an example value for --mask somewhere, preferably in help strings in the parser, to make this example runnable more easily without referring to external docs (apart from general golem/yapapi docs)

Copy link
Contributor

@azawlocki azawlocki May 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or maybe provide an example invocation, with --mask and --hash, in the string passed to build_parser()?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good one! Actually I might just do both. 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added in a0b544c

)
arg_parser.add_argument(
"--chunk-size", # affects skip and limit hashcat parameters
type=int,
help="Limit for the number of words to be checked as part of a single activity",
default=4096,
)
arg_parser.add_argument(
"--hash-type",
type=int,
help="Type of hashing algorithm to use (hashcat -m option)",
default=400,
)
arg_parser.add_argument(
"--max-workers",
type=int,
help="The maximum number of nodes we want to perform the attack on (default is dynamic)",
default=None,
)

# Container object for parsed arguments
args = argparse.Namespace()


async def compute_keyspace(context: WorkContext, tasks: AsyncIterable[Task]):
"""Worker script which computes the size of the keyspace for the mask attack.

This function is used as the `worker` parameter to `Golem#execute_tasks`.
It represents a sequence of commands to be executed on a remote provider node.
"""
async for task in tasks:
cmd = f"hashcat --keyspace " f"-a {HASHCAT_ATTACK_MODE} -m {args.hash_type} {args.mask}"
context.run("/bin/bash", "-c", cmd)

future_result = yield context.commit(timeout=KEYSPACE_TIMEOUT)
# each item is the result of a single command on the provider (including setup commands)
result: List[CommandExecuted] = await future_result
# we take the last item since it's the last command that was executed on the provider
cmd_result: CommandExecuted = result[-1]
kmazurek marked this conversation as resolved.
Show resolved Hide resolved

if cmd_result.success:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to check if cmd_result.success == True here, if it weren't then the line future_result = yield context.commit(timeout=KEYSPACE_TIMEOUT) would raise yapapi.executor.CommandExecutionError.

Copy link
Contributor

@azawlocki azawlocki May 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd rewrite the body of compute_keyspace() as follows:

    async for task in tasks:
        cmd = f"xhashcat --keyspace " f"-a {HASHCAT_ATTACK_MODE} -m {args.hash_type} {args.mask}"
        context.run("/bin/bash", "-c", cmd)

        try:
            future_result = yield context.commit(timeout=KEYSPACE_TIMEOUT)
            # each item is the result of a single command on the provider (including setup commands)
            result: List[CommandExecuted] = await future_result
            # we take the last item since it's the last command that was executed on the provider
            cmd_result: CommandExecuted = result[-1]
            keyspace = int(cmd_result.stdout)
            task.accept_result(result=keyspace)
        except CommandExecutionError as e:
            raise RuntimeError(f"Failed to compute attack keyspace: {e}")

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That would print for example:

[2021-05-27 13:07:04,963 WARNING yapapi.summary] Worker for provider 'provider-2' failed; reason: Failed to compute attack keyspace: Command '{'run': {'entry_point': '/bin/bash', 'args': ('-c', 'xhashcat --keyspace -a 3 -m 400 ?a?a?a'), 'capture': {'stdout': {'stream': {}}, 'stderr': {'stream': {}}}}}' failed on provider; message: 'ExeScript command exited with code 127'; stderr: 'bash: xhashcat: command not found

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, I'll switch to a try-except in compute_keyspace.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed in 2ab9620

keyspace = int(cmd_result.stdout)
else:
raise RuntimeError("Failed to compute attack keyspace")

task.accept_result(result=keyspace)


async def perform_attack(ctx: WorkContext, tasks: AsyncIterable[Task]):
"""Worker script which performs a hashcat mask attack against a target hash.

This function is used as the `worker` parameter to `Golem#execute_tasks`.
It represents a sequence of commands to be executed on a remote provider node.
"""
async for task in tasks:
skip = task.data
limit = skip + args.chunk_size
worker_output_path = f"/golem/output/hashcat_{skip}.potfile"

ctx.run(f"/bin/sh", "-c", _make_attack_command(skip, limit, worker_output_path))
output_file = NamedTemporaryFile()
ctx.download_file(worker_output_path, output_file.name)

yield ctx.commit(timeout=ATTACK_TIMEOUT)

def write_keyspace_check_script(mask):
with open("keyspace.sh", "w") as f:
f.write(f"hashcat --keyspace -a 3 {mask} -m 400 > /golem/work/keyspace.txt")
result = output_file.file.readline()
task.accept_result(result)
output_file.close()


def read_keyspace():
with open("keyspace.txt", "r") as f:
return int(f.readline())
def _make_attack_command(skip: int, limit: int, output_path: str) -> str:
return (
f"touch {output_path}; "
f"hashcat -a {HASHCAT_ATTACK_MODE} -m {args.hash_type} "
f"--self-test-disable --potfile-disable "
f"--skip={skip} --limit={limit} -o {output_path} "
f"'{args.hash}' '{args.mask}' || true"
)


def _parse_result(potfile_line: bytes) -> Optional[str]:
"""Helper function which parses a single .potfile line and returns the password part.

def read_password(ranges):
for r in ranges:
path = pathlib.Path(f"hashcat_{r}.potfile")
if not path.is_file():
continue
with open(path, "r") as f:
line = f.readline()
split_list = line.split(":")
if len(split_list) >= 2:
return split_list[1]
Hashcat uses its .potfile format to report results. In this format, each line consists of the
hash and its matching word, separated with a colon (e.g. `asdf1234:password`).
"""
potfile_line = potfile_line.decode("utf-8")
if potfile_line:
return potfile_line.split(":")[-1].strip()
return None


async def main(args):
package = await vm.repo(
image_hash="2c17589f1651baff9b82aa431850e296455777be265c2c5446c902e9",
image_hash="055911c811e56da4d75ffc928361a78ed13077933ffa8320fb1ec2db",
min_mem_gib=0.5,
min_storage_gib=2.0,
)

async def worker_check_keyspace(ctx: WorkContext, tasks):
async for task in tasks:
keyspace_sh_filename = "keyspace.sh"
ctx.send_file(keyspace_sh_filename, "/golem/work/keyspace.sh")
ctx.run("/bin/sh", "/golem/work/keyspace.sh")
output_file = "keyspace.txt"
ctx.download_file("/golem/work/keyspace.txt", output_file)
yield ctx.commit(timeout=timedelta(minutes=10))
task.accept_result()

async def worker_find_password(ctx: WorkContext, tasks):
ctx.send_file("in.hash", "/golem/work/in.hash")

async for task in tasks:
skip = task.data
limit = skip + step

# Commands to be run on the provider
commands = (
"rm -f /golem/work/*.potfile ~/.hashcat/hashcat.potfile; "
f"touch /golem/work/hashcat_{skip}.potfile; "
f"hashcat -a 3 -m 400 /golem/work/in.hash {args.mask} --skip={skip} --limit={limit} --self-test-disable -o /golem/work/hashcat_{skip}.potfile || true"
)
ctx.run(f"/bin/sh", "-c", commands)

output_file = f"hashcat_{skip}.potfile"
ctx.download_file(f"/golem/work/hashcat_{skip}.potfile", output_file)
yield ctx.commit(timeout=timedelta(minutes=10))
task.accept_result(result=output_file)

# beginning of the main flow

write_hash(args.hash)
write_keyspace_check_script(args.mask)

# By passing `event_consumer=log_summary()` we enable summary logging.
# See the documentation of the `yapapi.log` module on how to set
# the level of detail and format of the logged information.
async with Executor(
package=package,
max_workers=args.number_of_providers,
async with Golem(
budget=10.0,
# timeout should be keyspace / number of providers dependent
timeout=timedelta(minutes=30),
subnet_tag=args.subnet_tag,
driver=args.driver,
network=args.network,
event_consumer=log_summary(log_event_repr),
) as executor:
) as golem:

print(
f"Using subnet: {TEXT_COLOR_YELLOW}{args.subnet_tag}{TEXT_COLOR_DEFAULT}, "
f"payment driver: {TEXT_COLOR_YELLOW}{executor.driver}{TEXT_COLOR_DEFAULT}, "
f"and network: {TEXT_COLOR_YELLOW}{executor.network}{TEXT_COLOR_DEFAULT}\n"
f"payment driver: {TEXT_COLOR_YELLOW}{golem.driver}{TEXT_COLOR_DEFAULT}, "
f"and network: {TEXT_COLOR_YELLOW}{golem.network}{TEXT_COLOR_DEFAULT}\n"
)

keyspace_computed = False
start_time = datetime.now()

# This is not a typical use of executor.submit as there is only one task, with no data:
async for _task in executor.submit(worker_check_keyspace, [Task(data=None)]):
keyspace_computed = True

if not keyspace_computed:
# Assume the errors have been already reported and we may return quietly.
return
completed = golem.execute_tasks(
compute_keyspace,
[Task(data="compute_keyspace")],
payload=package,
max_workers=1,
timeout=KEYSPACE_TIMEOUT,
)

keyspace = read_keyspace()
async for task in completed:
keyspace = task.result

print(
f"{TEXT_COLOR_CYAN}"
f"Task computed: keyspace size count. The keyspace size is {keyspace}"
f"{TEXT_COLOR_DEFAULT}"
)

step = int(keyspace / args.number_of_providers) + 1
data = [Task(data=c) for c in range(0, keyspace, args.chunk_size)]
max_workers = args.max_workers or (keyspace // args.chunk_size) // 2

ranges = range(0, keyspace, step)
completed = golem.execute_tasks(
perform_attack,
data,
payload=package,
max_workers=max_workers,
timeout=ATTACK_TIMEOUT,
)

password = None

async for task in executor.submit(
worker_find_password, [Task(data=range) for range in ranges]
):
async for task in completed:
print(
f"{TEXT_COLOR_CYAN}Task computed: {task}, result: {task.result}{TEXT_COLOR_DEFAULT}"
)

password = read_password(ranges)
result = _parse_result(task.result)
if result:
password = result

if password is None:
print(f"{TEXT_COLOR_RED}No password found{TEXT_COLOR_DEFAULT}")
else:
if password:
print(f"{TEXT_COLOR_GREEN}Password found: {password}{TEXT_COLOR_DEFAULT}")
else:
print(f"{TEXT_COLOR_RED}No password found{TEXT_COLOR_DEFAULT}")

print(f"{TEXT_COLOR_CYAN}Total time: {datetime.now() - start_time}{TEXT_COLOR_DEFAULT}")


if __name__ == "__main__":
parser = build_parser("yacat")

parser.add_argument("--number-of-providers", dest="number_of_providers", type=int, default=3)
parser.add_argument("mask")
parser.add_argument("hash")

args = parser.parse_args()
args = arg_parser.parse_args()

# This is only required when running on Windows with Python prior to 3.8:
windows_event_loop_fix()
Expand Down