macho/macho-go/pkg/ios/macho/edit.go
nganhkhoa a2f9ca82e7 update shellcode
- shellcode correctly passes arguments to main
- shellcode deals with __bss section in __DATA
- remove hardcoded values
2023-06-26 15:33:15 +07:00

618 lines
18 KiB
Go

package macho
import (
"fmt"
"io"
"math/rand"
"time"
"bytes"
log "github.com/sirupsen/logrus"
. "ios-wrapper/pkg/ios"
)
// #include "fixups.h"
import "C"
func rewriteLoadcommandsWithoutCodesignature(mc *MachoContext) {
if mc.Is64bit() {
mc.file.Seek(int64(Header_size_64), io.SeekStart)
} else {
mc.file.Seek(int64(Header_size), io.SeekStart)
}
for _, cmd := range mc.commands {
if cmd.Cmd() == LC_CODE_SIGNATURE {
continue
}
log.WithFields(log.Fields{
"cmd": cmd.Cmd(),
"cmdsize": cmd.Cmdsize(),
"name": cmd.Cmdname(),
}).Trace("Rewrite Load command")
mc.file.Write(cmd.Serialize(mc))
}
}
// Remove Codesign data from Mach-O binary
// There are many caveats to this problem.
// Mostly related to how codesign-allocate works.
// Removing codesign data at the end of __LINKEDIT sement
// is probably easier than removing them at an abitrary location.
//
// Assuming the codesign data is at the end of __LINKEDIT segment.
// The binary is probably signed at the last step, which is common.
//
// CODE_SIGNATURE load commands points to the codesign data offset and size.
// __LINKEDIT section points to data offset and size.
// We have:
//
// linkedit = (section*) LC_SEGMENT.section[0] // name="__LINKEDIT"
// codesign = (linkedit_data_command*) LC_CODE_SIGNATURE
// BinarySize = { f.seek(0, SEEKEND); return f.tell() }
//
// linkedit->fileoff + linkedit->filesize == codesign->dataOff + codesign->dataSize
// linkedit->fileoff + linkedit->filesize == BinarySize
//
// To remove the codesign data, we truncate the file to remove the codesign data.
// Then we update the linkedit->filesize to linkedit->filesize - codesign->dataSize
// We also fix the header to not read the LC_CODE_SIGNATURE,
// because codesign is the last step, the last load command is LC_CODE_SIGNATURE.
// Fix header->ncmds -= 1, header->sizeofcmds -= sizeof(linkedit_data_command)
//
// There's one more caveat, as mentioned by `insert_dylib`. Codesign data is aligned
// by 0x10. So it would have some padding before codesign data. Now that we have
// removed the data, but the padding still exists and is not prefered by codesign-allocate.
// To fix this issue, **again** we assume that the previous section is the String table
// (referenced by symtab) and increase the size of the string table to the end of data.
//
// A better approach could be an extended search for section pointing to the previous data.
// But general cases are String table.
//
// Implementation warnings:
// We have two implementation designs.
// - Re-parse the load commands and edit the value at offset
// - Use parsed load commands data, edit the values, and rewrite header + load commands
func (mc *MachoContext) RemoveCodesign() bool {
if !mc.WriteEnabled() {
return false
}
linkedit := mc.FindSegment("__LINKEDIT")
if linkedit == nil {
log.Warn("The binary has no __LINKEDIT Segment")
return false
}
var codesign *LinkEdit
for _, cmd := range mc.Linkedits() {
if cmd.Cmd() == LC_CODE_SIGNATURE {
codesign = cmd
break
}
}
if codesign == nil {
log.Warn("The binary is not signed, no LC_CODE_SIGNATURE found")
return false
}
filesize := uint64(len(mc.buf))
linkedit_end := uint64(linkedit.Fileoff()) + linkedit.Filesize()
codesign_end := uint64(codesign.Dataoff() + codesign.Datasize())
// linkedit is the last item in Mach-O binary
if linkedit_end != filesize {
log.Panic("Link edit is not the last item")
return false
}
// codesign is the last in linkedit
if linkedit_end != codesign_end {
log.Panic("Code sign data is not the last item in link edit segment")
return false
}
linkedit_newsize := linkedit.Filesize() - uint64(codesign.Datasize())
if (mc.symtab.stroff+mc.symtab.strsize)%0x10 == 0 {
// codesign requires padding of 0x10
// if strtab+strsize & 0xff < 0x8, it will pad til 0x10
// when removed codesign, we truncate the file to offset at 0x8 rather at 0x10
} else {
linkedit_newsize -= 8
}
mc.file.Truncate(int64(uint64(linkedit.Fileoff()) + linkedit_newsize))
mc.symtab.strsize = uint32(linkedit.Fileoff()+linkedit_newsize) - mc.symtab.stroff
// fix linkedit section
if mc.Is64bit() {
linkedit.(*Segment64).filesize = linkedit_newsize
} else {
linkedit.(*Segment32).filesize = uint32(linkedit_newsize)
}
// rewrite load commands
mc.UpdateHeaderRemoveLcmd(codesign.Cmdsize())
rewriteLoadcommandsWithoutCodesignature(mc)
// erase old LC_CODE_SIGNATURE data
old_codesign_lcmd_offset := func() uint64 {
loadcmd_size := int64(mc.Header().sizeofcmds)
if mc.Is64bit() {
return uint64(loadcmd_size) + Header_size_64
} else {
return uint64(loadcmd_size) + Header_size
}
}()
// size of codesign = sizeof linkedit_data_command = 4 * 4
mc.file.WriteAt(make([]byte, 4*4), int64(old_codesign_lcmd_offset))
// mc.file.Seek(old_codesign_lcmd_offset, io.SeekStart)
// mc.file.Write(make([]byte, 4*4))
return true
}
func (mc *MachoContext) RemoveInitFunctions() bool {
if mc.WriteEnabled() {
for _, ptr := range mc.InitFunctions() {
if mc.Is64bit() {
mc.file.WriteAt([]byte{0, 0, 0, 0, 0, 0, 0, 0}, int64(ptr.offset))
} else {
mc.file.WriteAt([]byte{0, 0, 0, 0}, int64(ptr.offset))
}
log.WithFields(log.Fields{
"offset": fmt.Sprintf("0x%x", ptr.Offset()),
"value": fmt.Sprintf("0x%x", ptr.Value()),
}).Debug("Remove init pointer")
}
return true
}
return false
}
func (mc *MachoContext) RemoveUnnecessaryInfo() bool {
for _, command := range mc.commands {
switch command.(type) {
case *LinkEdit:
var le = command.(*LinkEdit)
if le.Cmdname() != "LC_FUNCTION_STARTS" && le.Cmdname() != "LC_DATA_IN_CODE" {
continue
}
var start int64 = int64(le.dataoff)
var end int64 = start + int64(le.datasize)
for i := start; i < end; i++ {
mc.file.WriteAt([]byte{0}, i)
}
continue
default:
continue
}
}
return false
}
func (mc *MachoContext) AddLoadCmd(lcmd LoadCommand) {
var offset uint64
payload := lcmd.Serialize(mc)
if uint64(len(payload)) > mc.PaddingSize() {
log.WithFields(log.Fields{
"cmd": lcmd,
"len(cmd)": len(payload),
"available": mc.PaddingSize(),
}).Panic("Not enough space to add load command")
}
if mc.Is64bit() {
offset = Header_size_64 + uint64(mc.header.sizeofcmds)
} else {
offset = Header_size + uint64(mc.header.sizeofcmds)
}
log.WithFields(log.Fields{
"cmd": lcmd.Cmdname(),
"size": len(payload),
"offset": offset,
}).Debug("Adding Load Command")
mc.file.WriteAt(payload, int64(offset))
mc.UpdateHeaderAddLcmd(uint32(len(payload)))
}
func (mc *MachoContext) AddDylib(dylib string) {
if mc.DylibExisted(dylib) {
log.WithFields(log.Fields{
"dylib": dylib,
}).Debug("Adding dylib but existed")
return
}
name := []byte(dylib)
name = append(name, 0)
dylib_lcmd := Dylib{
c: LoadCmd{cmd: LC_LOAD_DYLIB, cmdsize: 0},
name: name,
nameoff: 24,
timestamp: 0,
current_version: 0,
compatibility_version: 0,
}
mc.AddLoadCmd(&dylib_lcmd)
}
func (mc *MachoContext) AddRPath(rpath string) {
if mc.RPathExisted(rpath) {
log.WithFields(log.Fields{
"rpath": rpath,
}).Debug("Adding rpath but existed")
return
}
path := []byte(rpath)
path = append(path, 0)
rpath_lcmd := RPath{
c: LoadCmd{cmd: LC_RPATH, cmdsize: 0},
offset: 12,
path: path,
}
mc.AddLoadCmd(&rpath_lcmd)
}
func (mc *MachoContext) UpdateHeaderAddLcmd(size uint32) {
if mc.WriteEnabled() {
mc.header.ncmds += 1
mc.header.sizeofcmds += size
mc.file.WriteAt(mc.header.Serialize(mc), 0)
}
}
func (mc *MachoContext) UpdateHeaderRemoveLcmd(size uint32) {
if mc.WriteEnabled() {
mc.header.ncmds -= 1
mc.header.sizeofcmds -= size
mc.file.WriteAt(mc.header.Serialize(mc), 0)
}
}
func (mc *MachoContext) RemoveBindSymbols() {
if !mc.WriteEnabled() {
return
}
rand.Seed(time.Now().UnixNano())
if mc.dyldinfo == nil {
mc.removeBindSymbolsModern()
} else {
mc.removeBindSymbolsLegacy()
}
mc.ReworkForObjc()
// due to some limitations when design this tool
// we write the c code to stdout lol
for _, symbol := range mc.CollectBindSymbols() {
if symbol.Type() != "lazy" {
continue
}
if mc.dyldinfo != nil {
// for legacy resolve the opcodes can be rewritten as 0x00
mc.file.WriteAt(make([]byte, 8), int64(symbol.file_address))
} else {
// for modern resolve the opcodes must not be rewritten as 0x00
// because it contains 2 types of opcodes, BIND and REBASE
// we only fix BIND and leave REBASE intact
// However, each opcodes has a *next* field to the next opcode
// So if we want to leave the header intact (contains pointers, size)
// We should rewrite this as REBASE opcode (so no symbol lookup happens)
// and it can continue
// we can write random values, because the loader just do
// (high8 << 56 | target) - mach_header
// or something, so no symbol lookup and no error at runtime
target := rand.Int()
high8 := rand.Int()
value := C.MakeRebaseFixupOpcode(C.int(symbol.next), C.ulonglong(target), C.ulonglong(high8))
v := make([]byte, 8)
mc.byteorder.PutUint64(v, uint64(value))
mc.file.WriteAt(v, int64(symbol.file_address))
}
}
}
func (mc *MachoContext) removeBindSymbolsModern() {
// we don't mess up the chain
// we clear the imports table, and the raw opcodes
// clearing imports table disables static analysis
// clearing opcodes forces runtime manual mapping
// imports item are defined by mc.fixups.imports_format
// basic case is dyld_chained_import, 4 bytes
start := mc.fixups.dataoff
size := mc.fixups.datasize
fixups := new(Fixups)
fixups.Deserialize(mc, mc.buf[start:start+size])
start = mc.fixups.dataoff + fixups.imports_offset
size = fixups.imports_count * 4
fmt.Printf("// Erase at=0x%x size=0x%x\n", start, size)
mc.file.WriteAt(make([]byte, size), int64(start))
// string reference are at the end of this section
start = mc.fixups.dataoff + fixups.symbols_offset
size = mc.fixups.Datasize() - fixups.symbols_offset
fmt.Printf("// Erase at=0x%x size=0x%x\n", start, size)
mc.file.WriteAt(make([]byte, size), int64(start))
fixups.imports_count = 0
mc.file.WriteAt(fixups.Serialize(mc), int64(mc.fixups.dataoff))
}
func (mc *MachoContext) removeBindSymbolsLegacy() {
start := mc.dyldinfo.lazy_bind_off
size := mc.dyldinfo.lazy_bind_size
// set lazy opcodes to 0x00 == DO_BIND
// but no symbol state to bind
mc.file.WriteAt(make([]byte, size), int64(start))
}
func (mc *MachoContext) ReworkForObjc() {
text_start := 0
data_end := 0
lc_main_offset := int64(0)
ptr := int64(0)
if mc.Is64bit() {
ptr, _ = mc.file.Seek(int64(Header_size_64), io.SeekStart)
} else {
ptr, _ = mc.file.Seek(int64(Header_size), io.SeekStart)
}
for _, cmd := range mc.commands {
if cmd.Cmd() == LC_MAIN {
lc_main_offset = ptr + 8
ptr += int64(cmd.Cmdsize())
continue
}
if cmd.Cmd() != LC_SEGMENT_64 {
ptr += int64(cmd.Cmdsize())
continue
}
var segment = cmd.(*Segment64)
if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__TEXT")) == 0 {
section_ptr := ptr + 0x40 + 8
for _, section := range segment.Sections() {
if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__text")) == 0 {
text_start = int(section.Offset())
}
if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__init_offsets")) == 0 {
// mc.file.WriteAt([]byte("__init_offsetx"), section_ptr)
// edit flags to not S_MOD_INIT_FUNC
mc.file.WriteAt([]byte{0, 0, 0, 0}, section_ptr + 0x40)
}
section_ptr += 16 * 2 + 8 * 2 + 4 * 8
}
}
if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__DATA_CONST")) == 0 {
section_ptr := ptr + 0x40 + 8
for _, section := range segment.Sections() {
if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_classlist")) == 0 {
mc.file.WriteAt([]byte("__objc_classbruh"), section_ptr)
}
if bytes.Compare(bytes.Trim(section.SectName(), "\x00"), []byte("__objc_nlclslist")) == 0 {
mc.file.WriteAt([]byte("__objc_nlclsbruh"), section_ptr)
}
section_ptr += 16 * 2 + 8 * 2 + 4 * 8
}
}
if bytes.Compare(bytes.Trim(segment.SegName(), "\x00"), []byte("__DATA")) == 0 {
// end of __DATA segment, should have enough space for a pointer
// __bss section is dynamically allocated at the end to or something, hmmge
// assume that it order correctly, which it should if compiled and not modified
sections := segment.Sections()
last := sections[len(sections) - 1]
data_end = int(last.Offset()) + int(last.Size())
if (last.Offset() == 0) {
before_last := sections[len(sections) - 2]
data_end += int(before_last.Offset()) + int(before_last.Size())
}
}
ptr += int64(cmd.Cmdsize())
}
mc.file.Seek(0, io.SeekStart)
// dummy value past the end of __DATA segment (logical size),
// its physical size is still a page
// mc.file.WriteAt([]byte{0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7}, int64(0x81d8))
// we use 2 registers, x8 x9
// stack values:
// [ return address, header, argc, argv, env, apple ]
// we need to store the return address, and parameters passed to main
// we also store our header address to not calculate many times
/*
adr x8, 0
sub sp, sp, #0x30
str x30, [sp]
movz x9, #0x3d68 ; offset at this point
sub x8, x8, x9
str x8, [sp, #0x8]
str x0, [sp, #0x10]
str x1, [sp, #0x18]
str x2, [sp, #0x20]
str x3, [sp, #0x28]
movz x9, #0x81d8 ; offset to end of __DATA
add x9, x8, x9
ldr x9, [x9]
blr x9
ldr x8, [sp, #0x8]
ldr x0, [sp, #0x10]
ldr x1, [sp, #0x18]
ldr x2, [sp, #0x20]
ldr x3, [sp, #0x28]
movz x9, #0x3e3c ; offset to original main
add x9, x8, x9
blr x9
ldr x30, [sp]
add sp, sp, #0x10
ret
*/
// TODO: fix to work with offset larger than 0xffff
shellcode := []uint32{
0x10000008,
0xD100C3FF,
0xF90003FE,
0, // movz_shellcode_offset,
0xCB090108,
0xF90007E8,
0xF9000BE0,
0xF9000FE1,
0xF90013E2,
0xF90017E3,
0, // movz_data_end_offset,
0x8B090109,
0xF9400129,
0xD63F0120,
0xF94007E8,
0xF9400BE0,
0xF9400FE1,
0xF94013E2,
0xF94017E3,
0, // movz_main_offset,
0x8B090109,
0xD63F0120,
0xF94003FE,
0x910043FF,
0xD65F03C0,
}
ins_size_byte := 4
shellcode_offset := text_start - (ins_size_byte * len(shellcode))
main_offset := int(mc.entryoff)
encode_movz := func(v int) uint32 {
return uint32(uint32(v)<<5 | uint32(0x694)<<21 | uint32(0x09))
}
movz_shellcode_offset := encode_movz(shellcode_offset)
movz_main_offset := encode_movz(main_offset)
movz_data_end_offset := encode_movz(data_end)
shellcode[3] = movz_shellcode_offset
shellcode[10] = movz_data_end_offset
shellcode[19] = movz_main_offset
fmt.Printf("// shellcode_offset=%x\n", shellcode_offset)
fmt.Printf("// main_offset=%x\n", main_offset)
fmt.Printf("// data_end=%x\n", data_end)
fmt.Printf("// movz_shellcode_offset=%x\n", movz_shellcode_offset)
fmt.Printf("// movz_main_offset=%x\n", movz_main_offset)
fmt.Printf("// movz_data_end_offset=%x\n", movz_data_end_offset)
fmt.Printf("// lc_main_offset=%x\n", lc_main_offset)
offset := int64(shellcode_offset)
{
// fix main to point to our newly created shellcode
bs := make([]byte, 8)
mc.byteorder.PutUint64(bs, uint64(offset))
mc.file.WriteAt(bs, int64(lc_main_offset))
}
bs := make([]byte, 4)
for _, ins := range shellcode {
mc.byteorder.PutUint32(bs, ins)
mc.file.WriteAt(bs, offset)
offset += 4
}
}
func (mc *MachoContext) RemoveSymbolTable() {
// try to remove symtab and dysymtab
mc.removeSymtabCommand()
mc.removeDySymtabCommand()
}
func (mc *MachoContext) removeSymtabCommand() {
ptr := int64(0)
if mc.Is64bit() {
ptr, _ = mc.file.Seek(int64(Header_size_64), io.SeekStart)
} else {
ptr, _ = mc.file.Seek(int64(Header_size), io.SeekStart)
}
var symtab_fix *Symtab
for _, cmd := range mc.commands {
if cmd.Cmd() != LC_SYMTAB {
ptr += int64(cmd.Cmdsize())
continue
}
var symtab = cmd.(*Symtab)
symtab_fix = symtab
// erase strings referenced
start := int64(symtab_fix.stroff)
size := symtab_fix.strsize
fmt.Printf("// Erase at=0x%x size=0x%x\n", start, size)
mc.file.WriteAt(make([]byte, size), start)
// erase nlist64 symbol items
start = int64(symtab_fix.symoff)
size = symtab_fix.nsyms * 16
fmt.Printf("// Erase at=0x%x size=0x%x\n", start, size)
mc.file.WriteAt(make([]byte, size), start)
symtab_fix.symoff = 0
symtab_fix.nsyms = 0
symtab_fix.stroff = 0
symtab_fix.strsize = 0
mc.file.Seek(ptr, io.SeekStart)
mc.file.Write(symtab_fix.Serialize(mc))
break
}
mc.file.Seek(0, io.SeekStart)
}
func (mc *MachoContext) removeDySymtabCommand() {
ptr := int64(0)
if mc.Is64bit() {
ptr, _ = mc.file.Seek(int64(Header_size_64), io.SeekStart)
} else {
ptr, _ = mc.file.Seek(int64(Header_size), io.SeekStart)
}
for _, cmd := range mc.commands {
if cmd.Cmd() != LC_DYSYMTAB {
ptr += int64(cmd.Cmdsize())
continue
}
var dysymtab = cmd.(*DySymtab)
dysymtab_fix := dysymtab
dysymtab_fix.indirectsymoff = 0
dysymtab_fix.nindirectsyms = 0
mc.file.Seek(ptr, io.SeekStart)
mc.file.Write(dysymtab_fix.Serialize(mc))
}
}
func (mc *MachoContext) RemoveExportTrie() {
var start int64
var size int
if mc.dyldinfo != nil {
// legacy export trie
start = int64(mc.dyldinfo.export_off)
size = int(mc.dyldinfo.export_size)
mc.file.WriteAt(make([]byte, size), start)
} else if mc.exports != nil {
// modern export trie
start = int64(mc.exports.dataoff)
size = int(mc.exports.datasize)
mc.file.WriteAt(make([]byte, size), start)
} else {
// no export trie (??)
// should never occur unless this binary is modified
}
}