2

When using the stack offset for a variable obtained via ida_hexrays.lvar_t.stk.get_stkoff() or ida_hexrays.vdloc_t.stkoff() the results vary between "correct", and 8 or 16 bytes out as measured either manually or as compared to the results obtained from:

id = idc.get_frame_id(here())
for member in idautils.StructMembers(id):
    offset, name, size = member

The incorrect offsets are sourced from:

func = idaapi.get_func(ea)
vu = idaapi.open_pseudocode(func.start_ea, 0)
[n.get_stkoff() for n in vu.cfunc.lvars if n.is_stk_var()]

[edit]: the difference appears to be caused by the pseudo-code vu reporting offsets relative to the minimal SPD, whilst everything else uses the post-prologue SPD (code beneath)

def GetPseudoStackOffsetCorrection(funcea):
    func = ida_funcs.get_func(funcea)
    return func.frsize + func.frregs + idc.get_spd(idc.get_min_spd_ea(func.start_ea))

[edit 2]: this alternate method works sometimes. it functions by looking at the insn located at lvar_t.defea (assumedly "defined at ea") and calculating from there. However 10% of the time lvar.defea points at a conditional jmp, so again -- very kludgy

def get_stkoff_from_lvar(lvar, debug=1):
    ea = idc.get_item_head(lvar.defea)
    func = ida_funcs.get_func(ea)
    if not func:
        return idc.BADADDR
    
    for n in range(2):
        if idc.get_operand_type(ea, n) == idc.o_displ:
            offset = idc.get_operand_value(ea, n) + func.frsize - func.fpd

            if debug:
                lvar_name = lvar.name
                sid = idc.get_frame_id(func.start_ea)
                frame_name = idc.get_member_name(sid, offset)
                print("[debug] offset:0x{:x}, lvar_name:{}, frame_name:{}"
                        .format(offset, lvar_name, frame_name))

            return offset 

[edit]: kludges aside (which I would prefer not to use) I have issues with my [...] "rename, retype and remap" (with regex) package, which I recently noted does not update the "stack" (visible in assembly) names when calling vu.rename_lvar as would normally happen when performing a rename via N

Without an accurate offset, there is no way for the code to rename the corresponding stack variable var_18 when renaming the pseudo-code variable v1 (for example).

Any solution to this general problem is welcome, though preferentially one that allows the correct stack location of pseudo-code variables would be best as I will shortly be attempting "watch-variables" via flare-emu.

These are some results from a test function written to compare the conflicting results I have been receiving, and to confirm to myself that there is no magic "just deduct func.frregs" type answer.

Function 1.

func.flags                  : FUNC_FRAME | FUNC_PURGED_OK | FUNC_SP_READY
func.frregs                 :   8
func.frsize                 :  c0
func.fpd                    :  a0
ida_frame.frame_off_retaddr :  c8
ida_frame.frame_off_lvars   :   0
ida_frame.frame_off_args    :  d0
ida_frame.frame_off_savregs :  c0

name           lvar_offset1 stk_offset 
-------------- ------------ ---------- 
range          0x30         0x28       
guide          0x38         0x30       
ImageBase      0x58         0x50       
_stack_padding 0xd8         0xd0    

Function 2

func.flags                  : FUNC_FRAME | FUNC_PURGED_OK | FUNC_SP_READY
func.frregs                 :   8
func.frsize                 : 1c0
func.fpd                    : 190
ida_frame.frame_off_retaddr : 1c8
ida_frame.frame_off_lvars   :   0
ida_frame.frame_off_args    : 1d0
ida_frame.frame_off_savregs : 1c0

name                      lvar_offset1 stk_offset 
------------------------- ------------ ---------- 
lpTopLevelExceptionFilter 0x50         0x40       
LibFileName               0x88         0x78       
Handle                    0xc8         0xb8       
lpProcName                0x110        0x100      
hObject                   0x198        0x188      
hModule                   0x1a0        0x190      

Function 3

func.flags                  : FUNC_FRAME | FUNC_PURGED_OK | FUNC_SP_READY
func.frregs                 :   8
func.frsize                 :  40
func.fpd                    :  20
ida_frame.frame_off_retaddr :  48
ida_frame.frame_off_lvars   :   0
ida_frame.frame_off_args    :  50
ida_frame.frame_off_savregs :  40

name   lvar_offset1 stk_offset 
------ ------------ ---------- 
accum1 0x20         0x20       
accum2 0x2c         0x2c 

I can provide the test code if required, though it's not small.

[edit: test code attached, warning: not pretty]

# test code: 
#     sync_lvars_to_stk(func_ea)

def get_func_flag_names(f):
    return [x for x in [k for k in dir(idc) 
            if k.startswith('FUNC_')] 
            if f.flags & getattr(idc, x)]

def _get_vu(ea, vu):
    if vu: return vu
    return idaapi.open_pseudocode(idaapi.get_func(ea).start_ea, 0)

def get_lvars(ea, vu=None):
    vu = _get_vu(ea, vu)
    return [n.get_stkoff() for n in vu.cfunc.lvars if n.is_stk_var()]

def dump_stkvars(ea=None, iteratee=None):
    def get_member_tinfo(sid, offset):
        s = ida_struct.get_struc(sid)
        m = ida_struct.get_member(s, offset)
        tif = ida_typeinf.tinfo_t()
        try:
            if ida_struct.get_member_tinfo(tif, m):
                return tif
        except TypeError:
            pass

    results = []
    sid = idc.get_frame_id(ea)
    for member in idautils.StructMembers(sid):
        o = AttrDict()
        o.offset, o.name, o.size = member
        o.mid     = idc.get_member_id(sid,    o.offset)
        o.name    = idc.get_member_name(sid,  o.offset)
        o.size    = idc.get_member_size(sid,  o.offset)
        o.flags   = idc.get_member_flag(sid,  o.offset)
        tif       = get_member_tinfo(sid,     o.offset)
        o.tifname = str(tif) if tif else ''
        o.sid     = sid
        if callable(iteratee): iteratee(o)
        results.append(o)
    return results

def indexBy(o, key):
    r = {}
    for x in o:
        r[x[key]] = x
    return r

def sync_lvars_to_stk(ea, vu=None):
    vu = _get_vu(ea, vu)
    func = idaapi.get_func(ea)
    print("\n{:28}: {}\n{:28}: {:3x}\n{:28}: {:3x}\n{:28}: {:3x}\n{:28}: {:3x}\n{:28}: {:3x}\n{:28}: {:3x}\n{:28}: {:3x}\n"
            .format(
                "func.flags",                  " | ".join(get_func_flag_names(func)),
                "func.frregs",                 func.frregs,
                "func.frsize",                 func.frsize,
                "func.fpd",                    func.fpd,
                "ida_frame.frame_off_retaddr", ida_frame.frame_off_retaddr(func),
                "ida_frame.frame_off_lvars",   ida_frame.frame_off_lvars(func),
                "ida_frame.frame_off_args",    ida_frame.frame_off_args(func),
                "ida_frame.frame_off_savregs", ida_frame.frame_off_savregs(func),
                ))

    stkvars = indexBy(dump_stkvars(ea), 'name')
    lvars = []
    if vu and func:
        stk_lvars = [(n.name, n.tif.get_size(), 
            n.location.stkoff(),
            ) for n in vu.cfunc.lvars if n.location.is_stkoff()]

        for name, size, offset in stk_lvars:
            o = AttrDict()
            o.update({
                'name': name,
                'size': size,
                'lvar_offset': offset,
            })
            lvars.append(o)
    lvars = indexBy(lvars, 'name')

    lvar_names = lvars.keys()
    for name in lvar_names:
        if name in stkvars:
            print({
                'name': name,
                'lvar_offset': lvars[name].lvar_offset,
                'stk_offset': stkvars[name].offset,
            })

class AttrDict(dict):
    def __init__(self, *args, **kwargs):
        super(AttrDict, self).__init__(*args, **kwargs)
        self.__dict__ = self

5
  • can you post function prologues (in assembly) of these three functions?
    – R4444
    Commented Apr 20, 2021 at 12:13
  • @R4444 all three are of the form push rbp sub rsp, 1C0h lea rbp, [rsp + 30h], do you want specific values? That example was from the second (most wayward) example, the working one is rsp - 30h, rbp +20h and and the "off by 8" is push rbp; sub rsp, 0C0h; lea rbp, [rsp+20h]. And yes, it was not lost on me that there is a certain co-incidental correlation with the stack size and the offset discrepancy. I can (if required) prepare a nasm-viable or ida patch with a sample function. Commented Apr 20, 2021 at 12:25
  • It is probably also worth mentioning that IDA incorrectly defines these wayward functions as "FAR" (which I have corrected) but I guess that says something about how it's viewing them. Commented Apr 20, 2021 at 12:30
  • the reason I asked this because I observed such differences when rbp or rsp relative addressing is used. in former case the offsets get measured from frame base (after "pushing" rbp) and later case, they get measured from return address.
    – R4444
    Commented Apr 20, 2021 at 12:58
  • 1
    @R4444 I believe I have tracked the cause -- the function that is out by 8 has a single push rcx .. pop rcx and the out by 16 has two pushes. So it would seem the idaapi.open_pseudocode.vu calculation for stack offset is relative to the minimum SPD whilst everything else operates based on the post-prologue SPD. Unfortunately knowing this will not make for the most robust fix. There must be an alternative for locating the stack offsets of pseudo-code variables (e.g. you've probably noticed that if you delete a variable name in Hexrays it temporarily takes on the name of the stack var). Commented Apr 20, 2021 at 22:15

2 Answers 2

2

I just found what appears to be the official version of the adjustment function for the transmigration of worthless min-spd based offsets into heaven sent frame based offsets.

cfunc.get_stkoff_delta()

Possible implementation:

def GetMinSpdAdjustment(funcea):
    func = ida_funcs.get_func(funcea)
    return 0 - (func.frsize + func.frregs + idc.get_spd(idc.get_min_spd_ea(func.start_ea)))

Example usage:

vu = idaapi.open_pseudocode(funcea, 0)
vu.cfunc.lvars[10].get_stkoff() - vu.cfunc.get_stkoff_delta()

Complete example:


# call rename_lvar(old, new, ea)

def get_pseudocode_vu(ea, vu):
    if vu:
        return vu
    
    return idaapi.open_pseudocode(ea, 0)

def label_stkvar(offset, name, ea=None, vu=None):
    sid = idc.get_frame_id(ea)
    old_name = idc.get_member_name(sid, offset)
    if old_name:
        if old_name == name:
            return old_name

        if idc.set_member_name(sid, offset, name):
            return old_name

def rename_lvar(old, new, ea, uniq=0, vu=None):
    def make_unique_name(name, taken):
        if name not in taken:
            return name
        fmt = "%s_%%i" % name
        for i in range(3, 1<<10):
            name = fmt % i
            if name not in taken:
                return name

    old = old.strip()
    new = new.strip()
    if old == new:
        return True

    vu = get_pseudocode_vu(ea, vu)
    names = [n.name for n in vu.cfunc.lvars]

    if new in names:
        if uniq:
            return False

        new = make_unique_name(new, names)

    lvars = [n for n in vu.cfunc.lvars if n.name == old]
    if lvars:
        lvar = lvars[0]
        if lvar.is_stk_var():
            offset = lvar.get_stkoff() - vu.cfunc.get_stkoff_delta()
            old_name = label_stkvar(offset, new, ea=ea, vu=vu)

        return vu.rename_lvar(lvar, new, 1)
1

Based on the information we discuss in the comments, I have an idea about supporting such cases. You can use idc.get_frame_regs_size(ea) to adjust the offset. Thus can do something like:

adjust_offset = idc.get_frame_regs_size(ea)
[n.get_stkoff()+-adjust_offset for n in vu.cfunc.lvars if n.is_stk_var()]

according to your implementation.

You might as well want to consider other endpoints like -

print(idc.get_frame_lvar_size(ea))
print(idc.get_frame_args_size(ea))
print(idc.get_frame_size(ea))

to experiment more about adjusting the offset.

I am sure there has to be a robust solution, and this may not be perfect (I believe this should work in theory). But, give it a try.

4
  • Sorry, I shouldn't have left that code in. That was something I tried in my previously mentioned attempt to find a "rule of thumb" fix, and also the reason I ended up writing the complete (with pretty columns) testing function which disproved this theory by showing that the difference can be 16 or it can be 0 -- with the same (relative to each other) values of the various frame sizes. I believe there is some misleading documentation, get_stkoff says: "Get offset of the varialbe in the stack frame." which may better read as lvars_t.find_stkvar: offset from the minimal sp (C++: sval_t) Commented Apr 21, 2021 at 1:17
  • Also whilst searching through ida_hexrays.py, I noted that block_chains_t, mop_pair_t, qvector_history_t and vivl_t also have get_stkoff methods, which may (or may not) return min-spd based results. It's quite alarming really. I could use GetPseudoStackOffsetCorrection as added to my Q in later edit, but I feel that to be very kludgy. If there were simply a way to precisely emulate <kbd>N</kbd> and rename stack and pseudo-code vars at the same time, it would at least allow cross-referencing to be sure. Understand that I am running mass renames across a literal thousand func Commented Apr 21, 2021 at 1:21
  • 1
    i think that's a good observation. I too interesting to see a robust approach to solve this. upvoting for better reach. there's one more way - stack_size = idc.get_func_attr(ea, idc.FUNCATTR_FRSIZE);[-stack_size + mem.get_soff() for mem in ida_struct.get_struc(idc.get_frame_id(ea)).members] but I guess this is equivalent to what you are currently doing
    – R4444
    Commented Apr 21, 2021 at 2:00
  • 1
    rewriting that (for added information) as [(name, -stack_size + offset) for offset, name, size in idautils.StructMembers(idc.get_frame_id(ea))] which while being an excellent example of extracting the frame-relative position of stack variables and better than my own stkzero = ida_frame.frame_off_retaddr(func) - 8 (what was I thinking) won't solve the problem. But I'll be using your line in my StackInfo function (generates a list and also a struct.unpack string for inspecting stack variables). Commented Apr 21, 2021 at 5:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.