Skip to content

Instantly share code, notes, and snippets.

@KasparNagu
Last active February 12, 2026 11:54
Show Gist options
  • Select an option

  • Save KasparNagu/9ee02cb62d81d9e4c7a833518a710d6e to your computer and use it in GitHub Desktop.

Select an option

Save KasparNagu/9ee02cb62d81d9e4c7a833518a710d6e to your computer and use it in GitHub Desktop.
Script to extract Advanced Installer Exes
#!/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)
@Siradankullanici
Copy link

@cw2k
Copy link

cw2k commented Feb 10, 2026

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:

  for i in range(0,10000)

10000 (0x2710) is not sufficient.
It should be increased to 0x4000 to reliably detect the footer even in signed binaries.

  1. 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:

self.search_back = 10000

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':

(
    offset,
    self.nfiles,
    dummy1,
    offset1,
	
    self.info_off,
    file_off,
    hexhash,
	
    dummy2,
    name,
) = struct.unpack( "<"
        "I I I I "
        "I I 32s "
        "I 12s"
    ,
    footer
)

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:

                                            while True:
                                                     blk = inf.read(1<<16)
                                                     if len(blk) == 0:
                                                             break
                                                     out.write(blk)

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).

def extract_files(reader, files, buffer_size=0x20000):
...
                while (block := inf.read(buffer_size)):
                    out.write(block)

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:

                self.filehandle = filehandle
                self.size = size
                self.xorLength = xorLength
                self.pos = 0
                self.keepOpen = keepOpen

after:

                self.filehandle = filehandle
                self.size       = size
                self.xorLength  = xorLength
                self.pos        = 0
                self.keepOpen   = keepOpen   

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:

	self.debug.write(
		  f"offset={ offset			:08X} \t"
		   f"files={ self.nfile		:5d } \t"
		 f"offset1={ offset1		:08X} \t"
		f"info_off={ self.info_off	:08X} \t"
		f"file_off={ file_off		:08X} \t"
		 f"hexhash={ hexhash			} \t"
		    f"name={ name				} \t"
		f"\n"
	)

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

@Siradankullanici
Copy link

can you create pull request for that?

@Siradankullanici
Copy link

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.

@cw2k
Copy link

cw2k commented Feb 12, 2026

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. ;^)

@Siradankullanici
Copy link

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