Created
January 27, 2022 18:25
-
-
Save SeanPesce/de3838d9becc53420199e17be273adf6 to your computer and use it in GitHub Desktop.
Linux kernel module duplicator
This file contains hidden or 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
#!/usr/bin/env python3 | |
# Author: Sean Pesce | |
# | |
# This script can be used to duplicate a loadable Linux kernel module file (*.ko). | |
# The newly-created file will have unique export and module name strings to facilitate | |
# patching and loading onto a system when normal module development isn't feasible | |
# (e.g., when creating a PoC exploit for a proprietary system). | |
# | |
# Install prerequisites: | |
# sudo apt install -y python3 python3-pip | |
# sudo pip3 install argparse pyelftools | |
import argparse | |
import os | |
from elftools.elf.elffile import ELFFile | |
from random import randrange | |
# Characters that can be used for the first character in a symbol string | |
SYM_ALPHA_START = '_abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' | |
# Characters that can be used after the first character in a symbol string | |
SYM_ALPHA = SYM_ALPHA_START + '0123456789' | |
def random_character(alphabet): | |
return alphabet[randrange(len(alphabet))] | |
def random_symbol_name(length=5): | |
if length < 1: | |
return '' | |
symbol = random_character(SYM_ALPHA_START) | |
while len(symbol) < length: | |
symbol += random_character(SYM_ALPHA) | |
return symbol | |
def check_module_name(name): | |
if len(name) < 1: | |
raise argparse.ArgumentTypeError('Module name can\'t be empty') | |
if name[0] not in SYM_ALPHA_START: | |
raise argparse.ArgumentTypeError(f'Invalid character in module name at position 0: "{name[0]}"') | |
for c in name[1:]: | |
if c not in SYM_ALPHA: | |
raise argparse.ArgumentTypeError(f'Invalid character in module name: "{c}"') | |
return name | |
def get_module_name_offset(kmod): | |
for s in kmod.iter_sections(): | |
if s.name == '.gnu.linkonce.this_module': | |
return s.header['sh_offset'] + 24 | |
raise NameError('Invalid kernel module file: missing .gnu.linkonce.this_module section') | |
def get_ksymtab_strings_offset_and_size(kmod): | |
for s in kmod.iter_sections(): | |
if s.name == '__ksymtab_strings': | |
return s.header['sh_offset'], s.header['sh_size'] | |
return None, None | |
def patch_kernel_module(in_fpath, out_fpath, name, name_offset, ksymtab_str_offset=None, ksymtab_str_sz=None): | |
data = b'' | |
with open(in_fpath, 'rb') as f: | |
data = f.read() | |
# Patch module name | |
print(f'\nPatching .gnu.linkonce.this_module:\n {data[name_offset:name_offset+128].decode("ascii")} -> {name}') | |
name = name.encode('ascii') + b'\x00' | |
data = data[:name_offset] + name + data[name_offset+len(name):] | |
# Patch kernel symbol strings | |
if ksymtab_str_offset is not None and ksymtab_str_sz is not None: | |
print('\nPatching __ksymtab_strings:') | |
syms_orig = data[ksymtab_str_offset:ksymtab_str_offset+ksymtab_str_sz].split(b'\x00')[:-1] | |
syms = [] | |
for s in syms_orig: | |
new_sym = random_symbol_name(len(s)).encode('ascii') | |
while new_sym in syms: | |
new_sym = random_symbol_name(len(s)).encode('ascii') | |
print(f' {s.decode("ascii")} -> {new_sym.decode("ascii")}') | |
syms.append(new_sym) | |
ksyms_new = b'' | |
for s in syms: | |
ksyms_new += s + b'\x00' | |
assert len(ksyms_new) == ksymtab_str_sz, f'Size mismatch in __ksymtab_strings: Original was {ksymtab_str_sz} but new is {len(ksyms_new)}' | |
data = data[:ksymtab_str_offset] + ksyms_new + data[ksymtab_str_offset+ksymtab_str_sz:] | |
print(f'\nSaving to {out_fpath}') | |
with open(out_fpath, 'wb') as f: | |
f.write(data) | |
return | |
if __name__ == '__main__': | |
arg_parser = argparse.ArgumentParser() | |
arg_parser.add_argument('in_fpath', help='Original Linux kernel module file (*.ko)') | |
new_ko_name = random_symbol_name(5) | |
arg_parser.add_argument('-n', '--name', | |
help='Module name as it appears in lsmod output (must be unique to the system)', | |
default=new_ko_name, | |
type=check_module_name) | |
arg_parser.add_argument('-o', '--out', | |
help='Output file path', | |
default=None) | |
args = arg_parser.parse_args() | |
if args.out is None: | |
args.out = os.path.join('.', args.name+'.ko') | |
if new_ko_name == args.name: | |
print(f'Automatically generated new module name: {args.name}.ko') | |
with open(args.in_fpath, 'rb') as f: | |
kmod = ELFFile(f) | |
name_offset = get_module_name_offset(kmod) | |
ksymtab_str_offset, ksymtab_str_sz = get_ksymtab_strings_offset_and_size(kmod) | |
patch_kernel_module(args.in_fpath, args.out, args.name, name_offset, ksymtab_str_offset, ksymtab_str_sz) | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Depending on the kernel configuration, this script might fail to correctly modify the
name
field of thethis_module
structure.