diff --git a/.github/workflows/mikrotik_patch.yml b/.github/workflows/mikrotik_patch.yml index cf7d827..8e895dc 100644 --- a/.github/workflows/mikrotik_patch.yml +++ b/.github/workflows/mikrotik_patch.yml @@ -83,10 +83,16 @@ jobs: echo Latest Stabel Version:$LATEST_VERSION echo "LATEST_VERSION=${LATEST_VERSION}" >> $GITHUB_ENV - - name: Create keygen + - name: Get netinstall-${{ env.LATEST_VERSION }}.zip run: | - zip keygen.zip ./keygen.exe - + sudo wget -nv -O netinstall-$LATEST_VERSION.zip https://download.mikrotik.com/routeros/$LATEST_VERSION/netinstall-$LATEST_VERSION.zip + sudo unzip netinstall-$LATEST_VERSION.zip + - name: Patch netinstall-${{ env.LATEST_VERSION }}.exe + run: | + sudo -E python3 patch.py netinstall netinstall-$LATEST_VERSION.exe + sudo zip netinstall-$LATEST_VERSION.zip ./netinstall-$LATEST_VERSION.exe + + - name: Get mikrotik-${{ env.LATEST_VERSION }}.iso run: | sudo wget -nv -O mikrotik-$LATEST_VERSION.iso https://download.mikrotik.com/routeros/$LATEST_VERSION/mikrotik-$LATEST_VERSION.iso @@ -103,7 +109,7 @@ jobs: sudo rm -rf ./iso sudo rm -f mikrotik-$LATEST_VERSION.iso sudo mv ./new_iso/routeros-$LATEST_VERSION.npk ./ - sudo -E python3 patch.py routeros-$LATEST_VERSION.npk + sudo -E python3 patch.py npk routeros-$LATEST_VERSION.npk sudo cp keygen.exe ./new_iso/ NPK_FILES=$(find ./new_iso/*.npk) for file in $NPK_FILES; do @@ -163,16 +169,16 @@ jobs: - name: Delete Release tag ${{ env.LATEST_VERSION }} run: | HEADER="Authorization: token ${{ secrets.GITHUB_TOKEN }}" - RELEASE_INFO=$(curl -s -H $HEADER https://api.github.com/repos/${{ github.repository }}/releases/tags/${{ env.LATEST_VERSION }}) + RELEASE_INFO=$(curl -s -H $HEADER https://api.github.com/repos/${{ github.repository }}/releases/tags/$LATEST_VERSION) RELEASE_ID=$(echo $RELEASE_INFO | jq -r '.id') echo "Release ID: $RELEASE_ID" if [ "$RELEASE_ID" != "null" ]; then - curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/git/refs/tags/${{ env.LATEST_VERSION }} - echo "Tag ${{ env.LATEST_VERSION }} deleted successfully." + curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/git/refs/tags/$LATEST_VERSION + echo "Tag $LATEST_VERSION deleted successfully." curl -X DELETE -H "$HEADER" https://api.github.com/repos/${{ github.repository }}/releases/$RELEASE_ID - echo "Release with tag ${{ env.LATEST_VERSION }} deleted successfully." + echo "Release with tag $LATEST_VERSION deleted successfully." else - echo "Release not found for tag: ${{ env.LATEST_VERSION }})" + echo "Release not found for tag: $LATEST_VERSION)" fi - name: Create Release tag ${{ env.LATEST_VERSION }} @@ -185,4 +191,5 @@ jobs: files: | mikrotik-${{ env.LATEST_VERSION }}.iso install-image-${{ env.LATEST_VERSION }}.zip + netinstall-${{ env.LATEST_VERSION }}.zip diff --git a/keygen.exe b/keygen.exe deleted file mode 100644 index c911fa0..0000000 Binary files a/keygen.exe and /dev/null differ diff --git a/netinstall.py b/netinstall.py new file mode 100644 index 0000000..27c0d9c --- /dev/null +++ b/netinstall.py @@ -0,0 +1,108 @@ +import struct,lzma +ROUTEROS_BOOT = { + 129:{'arch':'power','name':'Powerboot','filter':lzma.FILTER_POWERPC}, + 130:{'arch':'e500','name':'e500_boot'}, + 131:{'arch':'mips','name':'Mips_boot'}, + 135:{'arch':'400','name':'440__boot'}, + 136:{'arch':'tile','name':'tile_boot'}, + 137:{'arch':'arm','name':'ARM__boot','filter':lzma.FILTER_ARMTHUMB}, + 138:{'arch':'mmips','name':'MMipsBoot'}, + 139:{'arch':'arm64','name':'ARM64__boot','filter':lzma.FILTER_ARMTHUMB}, + 143:{'arch':'x86_64','name':'x86_64boot'} +} +def find_7zXZ_data(data:bytes): + offset1 = 0 + _data = data + while b'\xFD7zXZ\x00\x00\x01' in _data: + offset1 = offset1 + _data.index(b'\xFD7zXZ\x00\x00\x01') + 8 + _data = _data[offset1:] + offset1 -= 8 + offset2 = 0 + _data = data + while b'\x00\x01\x59\x5A' in _data: + offset2 = offset2 + _data.index(b'\x00\x01\x59\x5A') + 4 + _data = _data[offset2:] + offset2 + return data[offset1:offset2] + +def patch_elf(data: bytes,key_dict:dict,filter=None): + initrd_xz = find_7zXZ_data(data) + initrd = lzma.decompress(initrd_xz) + new_initrd = initrd + for old_public_key,new_public_key in key_dict.items(): + if old_public_key in new_initrd: + print(f'initramfs public key patched {old_public_key[:16].hex().upper()}...') + new_initrd = new_initrd.replace(old_public_key,new_public_key) + + filters=[{"id":filter},{"id": lzma.FILTER_LZMA2, "preset": 9,}] if filter else [{"id": lzma.FILTER_LZMA2, "preset": 9,}] + new_initrd_xz = lzma.compress(new_initrd,check=lzma.CHECK_CRC32,filters=filters) + assert len(new_initrd_xz) <= len(initrd_xz),'new initrd xz size is too big' + + new_initrd_xz = new_initrd_xz.ljust(len(initrd_xz),b'\0') + new_data = data.replace(initrd_xz,new_initrd_xz) + return new_data + +def patch_pe(data: bytes,key_dict:dict,filter=None): + vmlinux_xz_offset = data.index(b'\xFD7zXZ\x00\x00\x01') + vmlinux_xz_size = data.index(b'\x00\x01\x59\x5A') + 4 - vmlinux_xz_offset + vmlinux_xz = data[vmlinux_xz_offset:vmlinux_xz_offset+vmlinux_xz_size] + vmlinux = lzma.decompress(vmlinux_xz) + initrd_xz_offset = vmlinux.index(b'\xFD7zXZ\x00\x00\x01') + initrd_xz_size = vmlinux.index(b'\x00\x01\x59\x5A') + 4 - initrd_xz_offset + initrd_xz = vmlinux[initrd_xz_offset:initrd_xz_offset+initrd_xz_size] + initrd = lzma.decompress(initrd_xz) + new_initrd = initrd + for old_public_key,new_public_key in key_dict.items(): + if old_public_key in new_initrd: + print(f'initrd public key patched {old_public_key[:16].hex().upper()}...') + new_initrd = new_initrd.replace(old_public_key,new_public_key) + + filters=[{"id":filter},{"id": lzma.FILTER_LZMA2, "preset": 9,}] if filter else [{"id": lzma.FILTER_LZMA2, "preset": 9,}] + new_initrd_xz = lzma.compress(new_initrd,check=lzma.CHECK_CRC32,filters=filters) + assert len(new_initrd_xz) <= len(initrd_xz),'new initrd xz size is too big' + + new_initrd_xz = new_initrd_xz.ljust(len(initrd_xz),b'\0') + new_vmlinux = vmlinux.replace(initrd_xz,new_initrd_xz) + + filters=[{"id":filter},{"id": lzma.FILTER_LZMA2, "preset": 9,}] if filter else [{"id": lzma.FILTER_LZMA2, "preset": 9,}] + new_vmlinux_xz = lzma.compress(new_vmlinux,check=lzma.CHECK_CRC32,filters=filters) + assert len(new_vmlinux_xz) <= len(vmlinux_xz),'new vmlinux xz size is too big' + + new_vmlinux_xz = new_vmlinux_xz.ljust(len(vmlinux_xz),b'\0') + new_data = data.replace(vmlinux_xz,new_vmlinux_xz) + return new_data + + + +def patch_netinstall(key_dict: dict,input_file,output_file=None): + import pefile + with pefile.PE(input_file) as pe: + for resource in pe.DIRECTORY_ENTRY_RESOURCE.entries: + if resource.id == pefile.RESOURCE_TYPE["RT_RCDATA"]: + for sub_resource in resource.directory.entries: + if sub_resource.id in ROUTEROS_BOOT: + bootloader = ROUTEROS_BOOT[sub_resource.id] + filter = bootloader.get("filter") + print(f'found {bootloader["arch"]}({sub_resource.id}) bootloader') + rva = sub_resource.directory.entries[0].data.struct.OffsetToData + size = sub_resource.directory.entries[0].data.struct.Size + data = pe.get_data(rva,size) + assert len(data) -4 >= struct.unpack_from('=" not in version else "" + result = check_call([executable, "-m", "pip", "install", package + installation_sign + version, "-i", index_url]) + except Exception as e: + print(e) + result = -1 + return result +def check_package(package): + from importlib import import_module + try: + import_module(package) + return True + except ImportError: + return False +def check_install_package(packages): + for package in packages: + if not check_package(package): + install_package(package) \ No newline at end of file diff --git a/patch.py b/patch.py index eed1b7e..1fdd6ed 100644 --- a/patch.py +++ b/patch.py @@ -64,8 +64,8 @@ def patch_squashfs(path,key_dict): data = data.replace(old_public_key,new_public_key) open(file,'wb').write(data) -def patch_system_npk(npk_file,key_dict): - npk = NovaPackage.load(npk_file) +def patch_system_npk(key_dict,input_file,output_file=None): + npk = NovaPackage.load(input_file) file_container = NpkFileContainer.unserialize_from(npk[NpkPartID.FILE_CONTAINER].data) for item in file_container: if item.name == b'boot/EFI/BOOT/BOOTX64.EFI': @@ -95,17 +95,35 @@ def patch_system_npk(npk_file,key_dict): kcdsa_private_key = bytes.fromhex(os.environ['CUSTOM_LICENSE_PRIVATE_KEY']) eddsa_private_key = bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PRIVATE_KEY']) npk.sign(kcdsa_private_key,eddsa_private_key) - npk.save(npk_file) + npk.save(output_file or input_file) if __name__ == '__main__': - import os,sys + + import argparse,os + parser = argparse.ArgumentParser(description='MikroTik patcher') + subparsers = parser.add_subparsers(dest="command") + npk_parser = subparsers.add_parser('npk',help='patch routeros.npk file') + npk_parser.add_argument('input',type=str, help='Input file') + npk_parser.add_argument('-o','--output',type=str,help='Output file') + + netinstall_parser = subparsers.add_parser('netinstall',help='patch netinstall file') + netinstall_parser.add_argument('input',type=str, help='Input file') + netinstall_parser.add_argument('-o','--output',type=str,help='Output file') + args = parser.parse_args() + key_dict = { bytes.fromhex(os.environ['MIKRO_LICENSE_PUBLIC_KEY']):bytes.fromhex(os.environ['CUSTOM_LICENSE_PUBLIC_KEY']), bytes.fromhex(os.environ['MIKRO_NPK_SIGN_PUBLIC_LKEY']):bytes.fromhex(os.environ['CUSTOM_NPK_SIGN_PUBLIC_KEY']) } - if len(sys.argv) == 2: - print(f'patching {sys.argv[1]} ...') - patch_system_npk(sys.argv[1],key_dict) + if args.command =='npk': + print(f'patching {args.input} ...') + patch_system_npk(key_dict,args.input) + elif args.command == 'netinstall': + from netinstall import patch_netinstall + print(f'patching {args.input} ...') + patch_netinstall(key_dict,args.input) else: - print('usage: python patch.py npk_file') + parser.print_help() + + diff --git a/upgrade.py b/upgrade.py new file mode 100644 index 0000000..9fc998d --- /dev/null +++ b/upgrade.py @@ -0,0 +1,48 @@ +from mitmproxy import http +import os +class MwrAddon: + def request(self,flow: http.HTTPFlow) -> None: + if len(flow.request.path_components)==3 and flow.request.path_components[0] == 'routeros': + version = flow.request.path_components[1] + file = os.path.join(version,flow.request.path_components[2]) + if flow.request.method == 'HEAD': + if os.path.exists(version) and os.path.isfile(file): + flow.response = http.Response.make( + status_code=200, + headers={ + 'Content-Type': 'application/octet-stream', + 'Accept-Ranges':'bytes', + 'Content-Length': str(os.stat(file).st_size), + } + ) + else: + flow.response = http.Response.make(status_code=404) + elif flow.request.method == 'GET' and flow.request.path_components[2].endswith('.npk'): + if os.path.exists(version) and os.path.isfile(file): + flow.response = http.Response.make( + status_code=200, + content=open(file,'rb').read(), + headers={'Content-Type': 'application/octet-stream',}, + ) + else: + flow.response = http.Response.make(status_code=404) +addons = [MwrAddon()] +async def start_listen(port): + from mitmproxy.tools.dump import DumpMaster + from mitmproxy import options + opts = options.Options(listen_host='0.0.0.0',listen_port=port,mode=['reverse:https://upgrade.mikrotik.com/']) + print(f'listening at *:{port}') + print(f'open http://127.0.0.1:{port}') + master = DumpMaster(opts) + master.addons.add(*addons) + try: + await master.run() + except KeyboardInterrupt: + master.shutdown() +if __name__ == "__main__": + import asyncio + from package import check_install_package + check_install_package(['mitmproxy']) + print(f'ip dns static add name=upgrade.mikrotik.com address=') + print(f'ip dns cache flush') + asyncio.run(start_listen(80)) \ No newline at end of file