Skip to content

Instantly share code, notes, and snippets.

@SeanPesce
Created January 27, 2022 18:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save SeanPesce/de3838d9becc53420199e17be273adf6 to your computer and use it in GitHub Desktop.
Save SeanPesce/de3838d9becc53420199e17be273adf6 to your computer and use it in GitHub Desktop.
Linux kernel module duplicator
#!/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)
@SeanPesce
Copy link
Author

Depending on the kernel configuration, this script might fail to correctly modify the name field of the this_module structure.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment