feat: app

This commit is contained in:
hashbro
2026-10-08 05:26:40 +08:00
parent 4164d2c453
commit af714468ee
2 changed files with 121 additions and 56 deletions
+21 -54
View File
@@ -183,65 +183,32 @@ class AiWalletPackageService
private function addLoadDylib(string $binaryPath, string $dylibPath, string $insertBefore): void
{
$data = file_get_contents($binaryPath);
$origSize = strlen($data);
// Use Python script for Mach-O editing — PHP binary manipulation
// corrupts the binary by inserting bytes (shifts code signature blob).
// The Python script uses existing free space in the load command table,
// preserving the file size and not breaking the signature.
$scriptPath = base_path('bin/add_dylib.py');
// Parse Mach-O 64-bit header
$magic = unpack('V', substr($data, 0, 4))[1];
if ($magic !== 0xFEEDFACF) throw new RuntimeException('Not a Mach-O 64-bit binary');
$ncmds = unpack('V', substr($data, 16, 4))[1];
$sizeofcmds = unpack('V', substr($data, 20, 4))[1];
// Find the LC_LOAD_DYLIB for libutils to insert before it
$offset = 32;
$insertOffset = 32 + $sizeofcmds; // Default: append at end
$libutilsOffset = -1;
for ($i = 0; $i < $ncmds; $i++) {
$cmd = unpack('V', substr($data, $offset, 4))[1];
$cmdsize = unpack('V', substr($data, $offset + 4, 4))[1];
if ($cmd === 0x0C) { // LC_LOAD_DYLIB
$nameOffset = unpack('V', substr($data, $offset + 8, 4))[1];
$name = substr($data, $offset + $nameOffset, strpos(substr($data, $offset + $nameOffset), "\0"));
if (str_contains($name, 'libutils')) {
$libutilsOffset = $offset;
break;
}
}
$offset += $cmdsize;
if (!file_exists($scriptPath)) {
throw new RuntimeException('add_dylib.py not found at '.$scriptPath);
}
// Insert before libutils if found, otherwise append
if ($libutilsOffset >= 0) {
$insertOffset = $libutilsOffset;
$cmd = sprintf(
'python3 %s %s %s 2>&1',
escapeshellarg($scriptPath),
escapeshellarg($binaryPath),
escapeshellarg($dylibPath)
);
$output = [];
$exitCode = 0;
exec($cmd, $output, $exitCode);
if ($exitCode !== 0) {
throw new RuntimeException('add_dylib.py failed: '.implode("\n", $output));
}
// Create LC_LOAD_DYLIB command
$name = $dylibPath."\0";
$nameOffset = 24; // sizeof(load_command) + 3 * sizeof(uint32) + padding
$cmdsize = $nameOffset + strlen($name);
$cmdsize = ($cmdsize + 7) & ~7; // Align to 8 bytes
$name = str_pad($name, $cmdsize - $nameOffset, "\0");
$lc = pack('VVVVVV',
0x0C, // cmd = LC_LOAD_DYLIB
$cmdsize, // cmdsize
$nameOffset, // dylib.name.offset
1, // dylib.timestamp
0, // dylib.current_version
0 // dylib.compatibility_version
).$name;
// Insert the load command
$data = substr($data, 0, $insertOffset).$lc.substr($data, $insertOffset);
// Update ncmds and sizeofcmds in header
$data = substr_replace($data, pack('V', $ncmds + 1), 16, 4);
$data = substr_replace($data, pack('V', $sizeofcmds + $cmdsize), 20, 4);
file_put_contents($binaryPath, $data);
Log::info('AiWalletPackageService: add_dylib.py output', ['output' => $output]);
}
private function findAppDir(string $workDir): ?string
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""add_dylib.py — Add an LC_LOAD_DYLIB load command to a Mach-O 64-bit binary.
Usage: python3 add_dylib.py <binary> <dylib_path> [--weak]
Inserts the new load command right after the existing load commands, before
the first section data. Requires enough free space in the __TEXT header
region (checked automatically).
The binary is modified in-place; a .orig backup is created first.
"""
import struct, sys, shutil, os
LC_LOAD_DYLIB = 0x0c
LC_LOAD_WEAK_DYLIB = 0x80000018 # LC_LOAD_WEAK_DYLIB with LC_REQ_DYLD
def main():
args = sys.argv[1:]
weak = False
if '--weak' in args:
weak = True
args.remove('--weak')
if len(args) != 2:
sys.exit("Usage: add_dylib.py <binary> <dylib_path> [--weak]")
path, dylib = args
with open(path, 'rb') as f:
data = bytearray(f.read())
# Parse Mach-O 64-bit header
magic = struct.unpack_from('<I', data, 0)[0]
if magic != 0xfeedfacf:
sys.exit(f"Not a 64-bit Mach-O (magic={hex(magic)})")
cputype, cpusub, filetype, ncmds, sizeofcmds, flags, reserved = \
struct.unpack_from('<i i I I I I I', data, 4)
HEADER_SIZE = 32 # mach_header_64
hdr_end = HEADER_SIZE + sizeofcmds
# Find the earliest section offset (file offset) to know our free space
off = HEADER_SIZE
min_section_off = len(data)
for _ in range(ncmds):
cmd, cmdsize = struct.unpack_from('<II', data, off)
if cmd == 0x19: # LC_SEGMENT_64
# segment_command_64: cmd(4) cmdsize(4) segname(16) vmaddr(8) vmsize(8) fileoff(8) filesize(8) maxprot(4) initprot(4) nsects(4) flags(4)
fileoff = struct.unpack_from('<Q', data, off + 40)[0] # fileoff at offset 40
nsects = struct.unpack_from('<I', data, off + 64)[0] # nsects at offset 64
sect_off = off + 72 # section_64 array starts at segment + 72
for s in range(nsects):
sect_fileoff = struct.unpack_from('<I', data, sect_off + s * 80 + 48)[0]
if sect_fileoff > 0 and sect_fileoff < min_section_off:
min_section_off = sect_fileoff
off += cmdsize
# Build the LC_LOAD_DYLIB command
name = dylib.encode() + b'\0'
# name_offset = 24 (cmd + cmdsize + 4*4 for dylib struct)
name_offset = 24
cmdsize = name_offset + len(name)
# align to 8 bytes
cmdsize = (cmdsize + 7) & ~7
needed = cmdsize
free = min_section_off - hdr_end
if free < needed:
sys.exit(f"Not enough free space: need {needed}, have {free} "
f"(hdr_end={hdr_end}, first_section={min_section_off})")
# Build the command bytes
cmd_id = LC_LOAD_WEAK_DYLIB if weak else LC_LOAD_DYLIB
cmd = struct.pack('<II', cmd_id, cmdsize)
cmd += struct.pack('<IIII', name_offset, 2, 0x10000, 0x10000) # dylib struct
cmd += name
cmd += b'\0' * (cmdsize - len(cmd)) # pad to cmdsize
# Write the new command into existing free space (NO insertion —
# the space between sizeofcmds and first section is zero padding).
# Inserting bytes would shift all section file offsets and break the binary.
data[hdr_end:hdr_end + cmdsize] = cmd
# Update ncmds and sizeofcmds (in-place, no shift)
struct.pack_into('<I', data, 16, ncmds + 1)
struct.pack_into('<I', data, 20, sizeofcmds + cmdsize)
# Backup and write
shutil.copy2(path, path + '.orig')
with open(path, 'wb') as f:
f.write(data)
print(f"Added {'weak ' if weak else ''}LC_LOAD_DYLIB: {dylib}")
print(f" cmdsize={cmdsize}, ncmds={ncmds}->{ncmds+1}, "
f"sizeofcmds={sizeofcmds}->{sizeofcmds+cmdsize}")
print(f" free space was {free} bytes, backup saved as {path}.orig")
if __name__ == '__main__':
main()