#
# ida_kernelcache/tagged_pointers.py
# Brandon Azad
#
"""ida_kernelcache.tagged_pointers

This module is responsible for processing the tagged pointers in the new iOS 12 kernelcache and
replacing them with their untagged equivalents. All found pointers are also converted into offsets.

In an alternative implementation, we could just add cross-references in IDA. However, I think this
approach is better because it is closer to what the kernelcache looks like at runtime.
"""

import idc
import idautils

import ida_utilities as idau
import kernel

_log = idau.make_log(1, __name__)

def tagged_pointer_tag(tp):
    return (tp >> 48) & 0xffff

def tagged_pointer_untag(tp):
    return tp | 0xffff000000000000

def is_tagged_pointer_format(value):
    return tagged_pointer_tag(value) != 0xffff and \
            (value & 0x0000ffff00000000) == 0x0000fff000000000

def is_tagged_pointer(value):
    return is_tagged_pointer_format(value) and \
            idau.is_mapped(tagged_pointer_untag(value), value=False)

def tagged_pointer_link(tag):
    return (tag >> 1) & ~0x3

def tagged_pointer_next(ea, tp, end=None):
    assert ea
    # First try to get the offset to the next link.
    if tp:
        link_offset = tagged_pointer_link(tagged_pointer_tag(tp))
        if link_offset:
            return ea + link_offset
        # Skip the current tagged pointer in preparation for scanning.
        ea += idau.WORD_SIZE
    # We don't have a link. Do a forward scan until we find the next tagged pointer.
    _log(3, 'Scanning for next tagged pointer')
    if end is None:
        end = idc.SegEnd(ea)
    for value, value_ea in idau.ReadWords(ea, end, step=4, addresses=True):
        if is_tagged_pointer(value):
            return value_ea
    # If we didn't find any tagged pointers at all, return None.
    return None

def untag_pointer(ea, tp):
    _log(4, 'Untagging pointer at {:x}', ea)
    idau.patch_word(ea, tagged_pointer_untag(tp))
    idc.OpOff(ea, 0, 0)

def untag_pointers_in_range(start, end):
    assert kernel.kernelcache_format == kernel.KC_12_MERGED, 'Wrong kernelcache format'
    ea, tp = start, None
    while True:
        ea = tagged_pointer_next(ea, tp, end)
        if ea is None or ea >= end:
            break
        tp = idau.read_word(ea)
        if not is_tagged_pointer(tp):
            _log(1, 'Tagged pointer traversal failed: ea={:x}, tp={:x}'.format(ea, tp))
            break
        untag_pointer(ea, tp)

def untag_pointers():
    _log(2, 'Starting tagged pointer conversion')
    for seg in idautils.Segments():
        untag_pointers_in_range(idc.SegStart(seg), idc.SegEnd(seg))
    _log(2, 'Tagged pointer conversion complete')