From 2b9a95b651069c40921e6893471d5826ce5626cc Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 1 Oct 2024 20:34:28 +0200 Subject: [PATCH 1/5] feat: extract libraries from Docker image --- pwnlib/commandline/template.py | 69 ++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/pwnlib/commandline/template.py b/pwnlib/commandline/template.py index 5cd6c7341..fea680e55 100644 --- a/pwnlib/commandline/template.py +++ b/pwnlib/commandline/template.py @@ -32,10 +32,74 @@ os.path.join(printable_data_path, "templates", "pwnup.mako")) parser.add_argument('--no-auto', help='Do not automatically detect missing binaries', action='store_false', dest='auto') +def get_docker_image_libraries(): + """Tries to retrieve challenge libraries from a Docker image built from the Dockerfile in the current working directory. + + The libraries are retrieved by parsing the output of running ldd on /bin/sh. + If the Dockerfile does not have an ENTRYPOINT, assumes that the image is based on a jail. + In that case, assumes that the jail is pwn.red/jail which requires "--privileged" and places the child image in /srv, + therefore chroots into /srv before running ldd. + """ + log.info("Extracting challenge libraries from Docker image...") + dockerfile = open("Dockerfile", "r").read() + is_jailed = re.search(r"^ENTRYPOINT", dockerfile, re.MULTILINE) is None + try: + image_sha = subprocess.check_output(["docker", "build", "-q", "."], shell=False).decode().strip() + + if is_jailed: + ldd_command = ["/bin/sh", "-c", "chroot /srv /bin/sh -c 'ldd /bin/sh'"] + else: + ldd_command = ["-c", "ldd /bin/sh"] + ldd_output = subprocess.check_output([ + "docker", + "run", + "--rm", + *(["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]), + image_sha, + *ldd_command, + ], + shell=False + ).decode() + + libc, ld = None, None + libc_basename, ld_basename = None, None + for line in ldd_output.splitlines(): + if "libc." in line: + libc = line.split("=> ", 1)[1].split(" ", 1)[0].strip() + libc_basename = libc.rsplit("/", 1)[1] + if "ld-" in line: + ld = line.split(" ", 1)[0].strip() + ld_basename = ld.rsplit("/", 1)[1] + + if not (libc and ld): + return None, None + + for filename, basename in zip([libc, ld], [libc_basename, ld_basename]): + if is_jailed: + cat_command = ["/bin/sh", "-c", "chroot /srv /bin/sh -c '/bin/cat %s'" % filename] + else: + cat_command = ["-c", "cat %s" % filename] + contents = subprocess.check_output([ + "docker", + "run", + "--rm", + *(["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]), + image_sha, + *cat_command, + ], + shell=False + ) + open(basename, "wb").write(contents) + + except subprocess.CalledProcessError as e: + log.error("docker failed with status: %d" % e.returncode) + return libc_basename, ld_basename + def detect_missing_binaries(args): log.info("Automatically detecting challenge binaries...") # look for challenge binary, libc, and ld in current directory exe, libc, ld = args.exe, args.libc, None + has_dockerfile = False other_files = [] for filename in os.listdir(): if not os.path.isfile(filename): @@ -44,6 +108,8 @@ def detect_missing_binaries(args): libc = filename elif not ld and 'ld-' in filename: ld = filename + elif filename == "Dockerfile": + has_dockerfile = True else: if os.access(filename, os.X_OK): other_files.append(filename) @@ -52,6 +118,9 @@ def detect_missing_binaries(args): exe = other_files[0] elif len(other_files) > 1: log.warning("Failed to find challenge binary. There are multiple binaries in the current directory: %s", other_files) + + if has_dockerfile and exe and not (libc or ld): + libc, ld = get_docker_image_libraries() if exe != args.exe: log.success("Found challenge binary %r", exe) From 1d3a804049d851bb7e18b6bd2e5bcc7d39789857 Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 1 Oct 2024 20:48:25 +0200 Subject: [PATCH 2/5] docs: update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a80edfb05..195187b7d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -86,6 +86,7 @@ The table below shows which release corresponds to each branch, and what date th [2444]: https://github.com/Gallopsled/pwntools/pull/2444 [2413]: https://github.com/Gallopsled/pwntools/pull/2413 [2470]: https://github.com/Gallopsled/pwntools/pull/2470 +[2479]: https://github.com/Gallopsled/pwntools/pull/2479 ## 4.14.0 (`beta`) From 030c8254f35403773aaaea4e894e807849603ead Mon Sep 17 00:00:00 2001 From: lcian Date: Tue, 1 Oct 2024 20:58:45 +0200 Subject: [PATCH 3/5] fix: python2.7 compatibility --- pwnlib/commandline/template.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/pwnlib/commandline/template.py b/pwnlib/commandline/template.py index fea680e55..b8fcd7120 100644 --- a/pwnlib/commandline/template.py +++ b/pwnlib/commandline/template.py @@ -53,11 +53,10 @@ def get_docker_image_libraries(): ldd_output = subprocess.check_output([ "docker", "run", - "--rm", - *(["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]), - image_sha, - *ldd_command, - ], + "--rm" + ] + (["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]) + [ + image_sha, + ] + ldd_command, shell=False ).decode() @@ -83,10 +82,9 @@ def get_docker_image_libraries(): "docker", "run", "--rm", - *(["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]), - image_sha, - *cat_command, - ], + ] + (["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]) + [ + image_sha + ] + cat_command, shell=False ) open(basename, "wb").write(contents) From 70866161586036bdd36f3a2d324cd65699d0eb28 Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 3 Oct 2024 21:39:41 +0200 Subject: [PATCH 4/5] address comments --- CHANGELOG.md | 1 + pwnlib/commandline/template.py | 120 +++++++++++++++++++-------------- 2 files changed, 71 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 195187b7d..f833daf9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -79,6 +79,7 @@ The table below shows which release corresponds to each branch, and what date th - [#2444][2444] Add `ELF.close()` to release resources - [#2413][2413] libcdb: improve the search speed of `search_by_symbol_offsets` in local libc-database - [#2470][2470] Fix waiting for gdb under WSL2 +- [#2479][2479] Support extracting libraries from Docker image in `pwn template` [2471]: https://github.com/Gallopsled/pwntools/pull/2471 [2358]: https://github.com/Gallopsled/pwntools/pull/2358 diff --git a/pwnlib/commandline/template.py b/pwnlib/commandline/template.py index b8fcd7120..782def410 100644 --- a/pwnlib/commandline/template.py +++ b/pwnlib/commandline/template.py @@ -1,9 +1,11 @@ from __future__ import absolute_import from __future__ import division +from __future__ import print_function from pwn import * from pwnlib.commandline import common +from sys import stderr from mako.lookup import TemplateLookup, Template parser = common.parser_commands.add_parser( @@ -36,61 +38,79 @@ def get_docker_image_libraries(): """Tries to retrieve challenge libraries from a Docker image built from the Dockerfile in the current working directory. The libraries are retrieved by parsing the output of running ldd on /bin/sh. - If the Dockerfile does not have an ENTRYPOINT, assumes that the image is based on a jail. - In that case, assumes that the jail is pwn.red/jail which requires "--privileged" and places the child image in /srv, - therefore chroots into /srv before running ldd. + Supports regular Docker images as well as jail images. """ - log.info("Extracting challenge libraries from Docker image...") - dockerfile = open("Dockerfile", "r").read() - is_jailed = re.search(r"^ENTRYPOINT", dockerfile, re.MULTILINE) is None - try: - image_sha = subprocess.check_output(["docker", "build", "-q", "."], shell=False).decode().strip() - - if is_jailed: - ldd_command = ["/bin/sh", "-c", "chroot /srv /bin/sh -c 'ldd /bin/sh'"] - else: - ldd_command = ["-c", "ldd /bin/sh"] - ldd_output = subprocess.check_output([ - "docker", - "run", - "--rm" - ] + (["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]) + [ - image_sha, - ] + ldd_command, - shell=False - ).decode() - - libc, ld = None, None - libc_basename, ld_basename = None, None - for line in ldd_output.splitlines(): - if "libc." in line: - libc = line.split("=> ", 1)[1].split(" ", 1)[0].strip() - libc_basename = libc.rsplit("/", 1)[1] - if "ld-" in line: - ld = line.split(" ", 1)[0].strip() - ld_basename = ld.rsplit("/", 1)[1] - - if not (libc and ld): + with log.progress("Extracting challenge libraries from Docker image") as progress: + if not util.misc.which("docker"): + progress.failure("docker command not found") return None, None - - for filename, basename in zip([libc, ld], [libc_basename, ld_basename]): - if is_jailed: - cat_command = ["/bin/sh", "-c", "chroot /srv /bin/sh -c '/bin/cat %s'" % filename] - else: - cat_command = ["-c", "cat %s" % filename] - contents = subprocess.check_output([ + # maps jail image name to the root directory of the child image + jail_image_to_chroot_dir = { + "pwn.red/jail": "/srv", + } + dockerfile = open("Dockerfile", "r").read() + jail = None + chroot_dir = "/" + for jail_image in jail_image_to_chroot_dir: + if re.search(r"^FROM %s" % jail_image, dockerfile, re.MULTILINE): + jail = jail_image + chroot_dir = jail_image_to_chroot_dir[jail_image] + break + try: + progress.status("Building image") + image_sha = subprocess.check_output(["docker", "build", "-q", "."], stderr=subprocess.PIPE, shell=False).decode().strip() + + progress.status("Retrieving library paths") + ldd_command = ["-c", "chroot %s /bin/sh -c 'ldd /bin/sh'" % chroot_dir] + ldd_output = subprocess.check_output([ "docker", "run", "--rm", - ] + (["--privileged"] if is_jailed else ["--entrypoint", "/bin/sh"]) + [ - image_sha - ] + cat_command, + "--entrypoint", + "/bin/sh", + ] + (["--privileged"] if jail else []) + [ + image_sha, + ] + ldd_command, + stderr=subprocess.PIPE, shell=False - ) - open(basename, "wb").write(contents) - - except subprocess.CalledProcessError as e: - log.error("docker failed with status: %d" % e.returncode) + ).decode() + + libc, ld = None, None + libc_basename, ld_basename = None, None + for lib_path in parse_ldd_output(ldd_output): + if "libc." in lib_path: + libc = lib_path + libc_basename = os.path.basename(lib_path) + if "ld-" in lib_path: + ld = lib_path + ld_basename = os.path.basename(lib_path) + + if not (libc and ld): + progress.failure("Could not find libraries") + return None, None + + progress.status("Copying libraries to current directory") + for filename, basename in zip((libc, ld), (libc_basename, ld_basename)): + cat_command = ["-c", "chroot %s /bin/sh -c '/bin/cat %s'" % (chroot_dir, filename)] + contents = subprocess.check_output([ + "docker", + "run", + "--rm", + "--entrypoint", + "/bin/sh", + ] + (["--privileged"] if jail else []) + [ + image_sha + ] + cat_command, + stderr=subprocess.PIPE, + shell=False + ) + util.misc.write(basename, contents) + + except subprocess.CalledProcessError as e: + print(e.stderr.decode()) + log.error("docker failed with status: %d" % e.returncode) + + progress.success("Retrieved libraries from Docker image") return libc_basename, ld_basename def detect_missing_binaries(args): @@ -99,7 +119,7 @@ def detect_missing_binaries(args): exe, libc, ld = args.exe, args.libc, None has_dockerfile = False other_files = [] - for filename in os.listdir(): + for filename in os.listdir("."): if not os.path.isfile(filename): continue if not libc and ('libc-' in filename or 'libc.' in filename): From 7b462e1514390abd2b74903095ea2d18f4faca5a Mon Sep 17 00:00:00 2001 From: lcian Date: Thu, 3 Oct 2024 21:48:58 +0200 Subject: [PATCH 5/5] address linter --- pwnlib/commandline/template.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pwnlib/commandline/template.py b/pwnlib/commandline/template.py index 782def410..c82ac5bd4 100644 --- a/pwnlib/commandline/template.py +++ b/pwnlib/commandline/template.py @@ -4,6 +4,7 @@ from pwn import * from pwnlib.commandline import common +from pwnlib.util.misc import which, parse_ldd_output, write from sys import stderr from mako.lookup import TemplateLookup, Template @@ -41,7 +42,7 @@ def get_docker_image_libraries(): Supports regular Docker images as well as jail images. """ with log.progress("Extracting challenge libraries from Docker image") as progress: - if not util.misc.which("docker"): + if not which("docker"): progress.failure("docker command not found") return None, None # maps jail image name to the root directory of the child image @@ -104,7 +105,7 @@ def get_docker_image_libraries(): stderr=subprocess.PIPE, shell=False ) - util.misc.write(basename, contents) + write(basename, contents) except subprocess.CalledProcessError as e: print(e.stderr.decode())