Last active
February 12, 2026 11:54
-
-
Save KasparNagu/9ee02cb62d81d9e4c7a833518a710d6e to your computer and use it in GitHub Desktop.
Script to extract Advanced Installer Exes
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 python | |
| import sys | |
| import struct | |
| import os | |
| #inspired by https://aluigi.altervista.org/bms/advanced_installer.bms | |
| #with some additionaly reverse engeneering, quite heursitic (footer search, xor guessing etc) | |
| #licence: public domain | |
| class AdvancedInstallerFileInfo: | |
| def __init__(self, name, size, offset, xorSize): | |
| self.name = name | |
| self.size = size | |
| self.offset = offset | |
| self.xorSize = xorSize | |
| def __repr__(self): | |
| return "[%s size=%d offset=%d]" % (self.name, self.size, self.offset) | |
| class AdvancedInstallerFileReader: | |
| def __init__(self,filehandle,size,keepOpen,xorLength): | |
| self.filehandle = filehandle | |
| self.size = size | |
| self.xorLength = xorLength | |
| self.pos = 0 | |
| self.keepOpen = keepOpen | |
| def xorFF(self,block): | |
| if isinstance(block,str): | |
| return "".join([chr(ord(i)^0xff) for i in block]) | |
| else: | |
| return bytes([i^0xff for i in block]) | |
| def read(self,size = None): | |
| if size is None: | |
| return self.read(self.size - self.pos) | |
| if self.pos < self.xorLength: | |
| xorLen = min(self.xorLength - self.pos, size) | |
| xorBlock = self.filehandle.read(xorLen) | |
| xorLenEffective = len(xorBlock) | |
| self.pos += xorLenEffective | |
| xorBlock = self.xorFF(xorBlock) | |
| if xorLenEffective < size: | |
| return xorBlock + self.read(size - xorLenEffective) | |
| return xorBlock | |
| blk = self.filehandle.read(min(size,self.size - self.pos)) | |
| self.pos += len(blk) | |
| return blk | |
| def close(self): | |
| if not self.keepOpen: | |
| self.filehandle.close() | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, type, value, traceback): | |
| self.close() | |
| class AdvancedInstallerReader: | |
| def __init__(self,filename,debug=None): | |
| self.filename = filename | |
| self.filehandle = open(filename,"rb") | |
| self.search_back = 10000 | |
| self.xorSize = 0x200 | |
| self.footer_position = None | |
| self.debug = debug | |
| self.threadsafeReopen = False | |
| self.files = [] | |
| def close(self): | |
| self.filehandle.close() | |
| def search_footer(self): | |
| for i in range(0,10000): | |
| self.filehandle.seek(-i,os.SEEK_END) | |
| magic = self.filehandle.read(10) | |
| if magic == b"ADVINSTSFX": | |
| self.footer_position = i + 0x48 - 12 | |
| break | |
| if self.footer_position is None: | |
| raise Exception("ADVINSTSFX not found") | |
| def read_footer(self): | |
| if self.footer_position is None: | |
| self.search_footer() | |
| self.filehandle.seek(-self.footer_position,os.SEEK_END) | |
| footer = self.filehandle.read(0x48) | |
| offset, self.nfiles, dummy1, offset1, self.info_off, file_off, hexhash, dummy2, name, = struct.unpack("<llllll32sl12s", footer) | |
| if self.debug: | |
| self.debug.write("offset=%d files=%d offset1=%d info_off=%d file_off=%d hexhash=%s name=%s\n" % (offset,self.nfiles,offset1,self.info_off,file_off,hexhash,name)) | |
| def read_info(self): | |
| self.read_footer() | |
| self.files = [] | |
| self.filehandle.seek(self.info_off,os.SEEK_SET) | |
| for i in range(0,self.nfiles): | |
| info = self.filehandle.read(24) | |
| dummy1, dummy2, dummy3, size, offset, namesize = struct.unpack("<llllll",info) | |
| if self.debug: | |
| self.debug.write(" size=%d offset=%d namesize=%d dummy1=0x%x dummy2=0x%x dummy3=0x%x\n" % (size,offset,namesize,dummy1,dummy2,dummy3)) | |
| if namesize < 0xFFFF: | |
| name = self.filehandle.read(namesize*2) | |
| name = name.decode("UTF-16") | |
| if self.debug: | |
| self.debug.write(" name=%s\n" % name) | |
| self.files.append(AdvancedInstallerFileInfo(name,size,offset,self.xorSize if dummy3 == 2 else 0)) | |
| else: | |
| raise Exception("Invalid name size %d" % namesize) | |
| def open(self,infoFile): | |
| if isinstance(infoFile,AdvancedInstallerFileInfo): | |
| if self.threadsafeReopen: | |
| fh = open(self.filename,"rb") | |
| else: | |
| fh = self.filehandle | |
| fh.seek(infoFile.offset,os.SEEK_SET) | |
| return AdvancedInstallerFileReader(fh,infoFile.size,not self.threadsafeReopen,infoFile.xorSize) | |
| else: | |
| if not self.files: | |
| self.read_info() | |
| for f in files: | |
| if f.name == infoFile: | |
| return self.open(f) | |
| return None | |
| def infolist(self): | |
| if not self.files: | |
| self.read_info() | |
| return self.files | |
| def __enter__(self): | |
| return self | |
| def __exit__(self, type, value, traceback): | |
| self.close() | |
| def __repr__(self): | |
| return "[path=%s footer=%s nFiles=%d]" % (self.filename,self.footer_position,len(self.files)) | |
| if __name__ == "__main__": | |
| import argparse | |
| parser = argparse.ArgumentParser(description="Advanced Installer Extractor") | |
| parser.add_argument('file', type=str, help="Advanced Installer to open") | |
| parser.add_argument('files', type=str,nargs="*", help="Files to consider") | |
| parser.add_argument('-x','--extract', default=False, action="store_true",help="Extract to current directory") | |
| parser.add_argument('-l','--list', default=False, action="store_true",help="List files") | |
| parser.add_argument('-v','--verbose', default=False, action="store_true",help="Debug output") | |
| args = parser.parse_args(); | |
| considerFiles = set(args.files) | |
| with AdvancedInstallerReader(args.file,sys.stdout if args.verbose else None) as ar: | |
| for f in ar.infolist(): | |
| if not considerFiles or f.name in considerFiles: | |
| if args.list: | |
| print(f) | |
| if args.extract: | |
| path = f.name.replace("\\","/") | |
| dirname = os.path.dirname(path) | |
| if dirname: | |
| if not os.path.exists(dirname): | |
| os.makedirs(dirname) | |
| with ar.open(f) as inf, open(path,"wb") as out: | |
| while True: | |
| blk = inf.read(1<<16) | |
| if len(blk) == 0: | |
| break | |
| out.write(blk) | |
| if args.verbose: | |
| print(ar) |
can you create pull request for that?
okay it seems like it's semi bad ai but hard to see is it's really AI. Even the AI detector can't find that correctly.
okay it seems like it's semi bad ai but hard to see is it's really AI. Even the AI detector can't find that correctly.
'semi bad ai' ? - sorry I don't get the point.
Btw please consider use of
"Edit" of your comments to specify or correct things and
"Delete" to clean things up
Instead of creating a new one.
Like maybe edit your old comment
..and I'll just delete my reply. ;^)
your code looks like ai generated because ai suggests whitespace handleded by python but flake8 gives error due to that
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I reworked the code and the output:
https://gist.github.com/cw2k/2b2163c422183b884b7405bc0e09dfb2
…cleaned it up a bit along the way. ;^)
Note:
Well beside extraction I also got the idea to add an option to
'undo' the Xor encryption in the source file.
If applied It'll be still runnable as (Advanced) Installer, but now you may also open it in 7-Zip to just extract it's content.
Plain and simple and without making a great 'Installation ritual' as AI does.
Creating space blocker file, extracting to temp, copying from temp to install location.
These all just takes time, need twice the space for installation and these pointless writeoperations just wears down your valuable SD-harddisk.
BugFixing:
1 – Search window too small
The search window used to locate the ADVINSTSFX footer is currently too small.
When an executable is digitally signed, Windows appends the signature data after the ADVINSTSFX footer.
In my test case, the signature block was roughly 0x3000 bytes, so a backward‑search window of 0x4000 bytes is a much safer value.
The current code in AdvancedInstallerReader::search_footer() uses:
10000(0x2710) is not sufficient.It should be increased to 0x4000 to reliably detect the footer even in signed binaries.
Change the loop to:
for i in range(0, self.search_back)
and
2. make use of the member variable that was already introduced for this purpose here:
A more precise solution would be to use the PE header fields optionalHeader.securityOffset and optionalHeader.securitySize to skip over the signature data explicitly. That approach avoids guessing the window size, but it also requires additional PE parsing logic, which may not be desirable if the goal is to keep the implementation simple.
2. struct.unpack with long
That essential part, the AI file format, is 'hiding' at line 81
struct.unpack("<llllll32sl12s", footer...That line is taking up to 180 characters in the horizontal source code plain.
Nice to respect some 80-char per line limit and not overshot it for no good reason.
Why so sparse?
Code lines rules !
;^)
And we are really getting a gain from that.
Use some 'vertical space' to tame that 'beast':
Notice you use "l" that gets signed (L)ong.
I suggest using "I" that gets unsigned (I)ntegers, which fits better to our needs.
Okay,
I vs l
and again side by side: Il
Hard to spot the difference, isn't it? But there is one:
Why "I" is preferable to "L"
"L" is an unsigned long, but the C standard allows unsigned long to be 32 or 64 bits depending on the platform.
Python’s struct normalizes this to 32 bits on all mainstream platforms, but "I" is the format that explicitly means unsigned 32-bit integer.
So "I" communicates intent more clearly and avoids ambiguity.
Like this thing are getting manageable, a little bit better
StyleBugs
This is actually not an actual bug.
But when maintaining code, these are boomers.
S1. Convoluted code in main.
This is actually not a bug.
However, it's a 'beautiful' example to learn about bad style.
Look at that fragment in main:
It is deeply nested and somewhat about level 8.
Normally if nestlevel is bigger than 4, it is a good sign that you should refactor your code.
Split it up into more subfunctions (methods).
beside maturing the code ( Eliminating stuff like "while True" ).
Expressions like "1<<16" vs "0x10000"
For what reason that bit shifting?
Keeping up such "code monsters" - you, or the people that are maintaining that code, will pay the "contribution" to this decision in the form of more "scratching their heads" and trying to get all aligned correctly in the code editor.
S2. Use more line breaks and white spaces
At least every new method ( def ... ) deserves a line break.
Also in the code it's nice to have a line break before and after if.
Like when writing a text, use paragraphs to phrase certain thoughts.
You also do when writing code.
A line break at the right place can do much to clearly stop the idea and purpose.
Align by equals
before:
after:
S3. Use f-strings
turn that 'mess'
self.debug.write("offset=%d files=%d offset1=%d info_off=%d file_off=%d hexhash=%s name=%s\n" % (offset,self.nfiles,offset1,self.info_off,file_off,hexhash,name))into this:
So much nicer to the eye.
Whitespaces are your friends. Don't be afraid to use them. The Python parser is made to handle them right, and they do much good in understanding code on a visual level.
I added output formatting at the end aligned with tabulators in the source code.
... and added tab's as separators for the output values
Inside parentheses, you are free from Python restrictions on indentations.
Used that freedom wisely.
...and now you can easy swap the lines with the editor to change the order