8

I am trying to write an IDAPython script that will return a list of references to a local stack-frame variable. However, I couldn't find any API that does so.

What I am trying to achieve is a code like: xrefs = get_variable_references('arg_4') that will return the results corresponding with the GUI's results:

GUI's results

Thanks in advance.

3 Answers 3

7
+50

There is one function that does this: build_stkvar_xrefs, defined in C++ but exposed via the Python SWIG bindings. IDA builds stack xrefs dynamically when you ask for it. In order to use the function, it requires a little bit of setup.

You'll need to use a few functions to get what you need:

  • get_func(ea): retrieves the func_t structure for the function at ea
  • get_frame(func_t foo): returns the struct_t structure for the function frame specified by foo
  • DecodeInstruction(ea): returns the inst_t representing instruction at ea
  • get_stkvar(op_t op, sval_t v): op is a reference to an instruction, v is the immediate value in the operand. Usually you just use op.addr. It returns a tuple, (member_t, val). member_t is a pointer to the stack variable, which is what we need. val is the same value as the soff field in the member_t for the stack var. More on this later.
  • xreflist_t(): creates a new xreflist of xreflist_entry_t
  • build_stkvar_xrefs(xreflist_t xrefs, func_t func, member_t member): fills xrefs with xreflist_entry_t's that represent the stack var xrefs given by member in func.
  • struct_t.get_member(x): You can use this method to iterate all stack variables in a frame to retrieve all member_t's. If you want to build xrefs for all stack variables, this is usually easier.

Here's an example of how this all ties together:

# 0x4012d0 is the function address
# 0x4012dc is an instruction address referencing
# a stack variable. It looks like:
# mov [ebp - 4], ecx

pFunc = get_func(0x4012d0)
pFrame = get_frame(pFunc)
inst = DecodeInstruction(0x4012dc)
op = inst[0] #first operand references stack var
pMember, val = get_stkvar(op, op.addr)
xrefs = xreflist_t()
build_stkvar_xrefs(xrefs, pFunc, pMember)
for xref in xrefs:
    print hex(xref.ea) #print xref address

# Contrived member dictionary example.
dictMem = dict()
x = 0
while(x < pFrame.memqty):
    dictMem[GetMemberName(pFrame.id, pFrame.get_member(x).soff)] = pFrame.get_member(x)
    x = x+1
# given var name you can now use the
# dictionary to grab the member_t to pass
# to build_stkvar_xrefs
pMem = dictMem["var_4"]
xrefs = xreflist_t()
build_stkvar_xrefs(xrefs, pFunc, pMem)
for xref in xrefs:
    print hex(xref.ea) #print xrefs to var_4

soff isn't a stack offset. I think it means "structure offset", and it's an offset into the frame structure so you can retrieve other bits of information. You'll need this field to use other stack variable related functions such as: SetMemberType, SetMemberName, GetMemberName, DelStrucMember, etc.

So, for a simple on the fly variable name to xref lookup, you can do something like:

def get_stack_xrefs(func_ea, var_name):
    pFunc = get_func(func_ea)
    pFrame = get_frame(pFunc)
    pMember = None
    result = []
    while(x < pFrame.memqty):
        if GetMemberName(pFrame.id, pFrame.get_member(x).soff) == var_name:
           pMember = pFrame.get_member(x)
           break; 
        x = x+1
    if pMember: 
        xrefs = xreflist_t()
        build_stkvar_xrefs(xrefs, pFunc, pMember)
        for each in xrefs:
            result.append(each.ea)
    return result

If you want more information on these functions, I recommend taking a look at the following modules from the IDA SDK documentation (in no particular order):

  • funcs.hpp
  • frame.hpp
  • struct.hpp

Reference: https://www.hex-rays.com/products/ida/support/sdkdoc/files.html

6

TL;DR: There's no simple API to achieve this, code is at the end of the answer or here.

As far as I know, there is no easy way to get the references to stack structure. It seems like calling idautils.XrefsTo(sid) where sid is the frame id (retrieved using idc.GetFrame) should work, however I couldn't get it to yield any result in my attempts.

Instead, however, you could walk over the function's instructions and calculate the offsets into the stack manually whenever a stack reference operand is encountered.

I created a gist snippet to show just that, and although I think it's quite self-explanatory I'll go over it briefly here.

First, we'll need a mapping of stack offsets to arguments, this is taken care of by the find_stack_members function, which uses the idc.GetFrame API function mentioned above to get the structure ID for the stack of a specific function. Then, it uses idautils.StructMembers API to iterate over the stack variables.

One interesting piece of logic in the find_stack_members function is that it uses the <space>r member name as the base of the stack (where the stack is once the function is entered), this is used later in find_stack_xrefs to calculate the stack offset of a variable based on the current stack delta and the operand immediate value.

The find_stack_xrefs function iterates over the instructions in the given function, and skips any instruction but those with a operand defined to reference the stack (note that arguments that reference the stack but aren't defined as such by either IDA's auto-analysis or manually will not be treated and hence will not count as a cross-reference).

If a stack offset operand exists in the instrcution, the find_stack_xrefs function will proceed to calculate it's offset using the stack's base offset (previously retrieved by find_stack_members), the current stack delta (calculated by IDA and available using the API idc.GetSpd) and the immediate taken from the Operand structure.

For convenience, I'm also including the code here:

import idc, idaapi, idautils, ida_xref

def find_stack_members(func_ea):
    members = {}
    base = None
    frame = idc.GetFrame(func_ea)
    for frame_member in idautils.StructMembers(frame):
        member_offset, member_name, _ = frame_member
        members[member_offset] = member_name
        if member_name == ' r':
            base = member_offset
    if not base:
        raise ValueError("Failed identifying the stack's base address using the return address hidden stack member")
    return members, base

def find_stack_xrefs(func_offset):
    func_ea = ida_funcs.get_func(func_offset).startEA
    members, stack_base = find_stack_members(func_ea)
    for func_item in FuncItems(func_ea):
        flags = idc.GetFlags(ea)
        stkvar = 0 if idc.isStkvar0(flags) else 1 if idc.isStkvar1(flags) else None
        if not stkvar:
            continue
        ida_ua.decode_insn(func_item)
        op = ida_ua.cmd.Operands[stkvar]
        stack_offset = op.addr + idc.GetSpd(func_item) + stack_base
        member = members[stack_offset]
        print("At offset {:x} stack member {} is referenced by operand number {}".format(func_item, member, stkvar))

if __name__ == "__main__":
    find_stack_xrefs(idc.ScreenEA())
4
  • This code doesn't work: it's outdated, it has multiple bugs and the algorithm is simply wrong. Don't anyone use it. I'll give you just a few examples: flags = idc.GetFlags(ea) is just wrong, there is no ea variable in this script. Meaning this code never actually worked. The idc.GetSpd(func_item) call is again wrong: GetSpd takes past-the-end address, not the address of the current instruction.
    – KulaGGin
    Commented Jan 26, 2022 at 8:46
  • And even after you supply a proper argument for GetSpd, the formula op.addr + idc.GetSpd(func_item) + stack_base is still wrong. The GetSpd gets the difference between the initial and current values of ESP and doesn't help to determine which stack variable is referenced and on what offset. For example, I have instruction mov [rbp+0F0h+var_18], rax at address 0x14001242A. var_18 is at offset 0x18 respectively. The past-the-end address is 0x140012431, so I do idc.GetSpd(0x140012431), and get -0x118.
    – KulaGGin
    Commented Jan 26, 2022 at 8:54
  • 1
    Then formula: op.addr + idc.GetSpd(func_item) + stack_base turns into: 0x14001242A + (-0x118) + 0x118, which equals to 0x14001242A, which is the instruction address, not the stack offset. Which makes this answer completely wrong. If you want a working solution, check out the accepted answer by @mayahustle.
    – KulaGGin
    Commented Jan 26, 2022 at 8:54
  • @KulaGGin Thanks for catching my errors! It's been a while and I'm not sure I tested the code. It was meant as a thought process snippet more than as a working solution. Thanks for pointing it out!
    – NirIzr
    Commented Jan 26, 2022 at 9:24
0

Disclaimer: I maintain the ida-minsc plugin.

Using the minsc plugin at https://github.com/arizvisa/ida-minsc, you can do something like the following. For demonstration, we start out by disassembling the current instruction containing a stack operand. (We'd normally plug in an address, by giving it a parameter).

Python>db.disasm()
'1c00feb4f: movzx eax, [rbp+57h+var_C8]'

Next we'll just display the operands for the current instruction in case we want to verify that the operand is definitely using the stack segment. This is using instruction.ops.

Python>ins.ops()
(<class 'register_t' index=0 dtype=dt_dword name='eax' position=0+32>, SegmentOffsetBaseIndexScale(segment=%ss, offset=-0x71, base=%rbp, index=None, scale=1))

We want the stack variable pointed to by the second operand, so we can use instruction.op_stack for this. This gives us a frame member of type member_t. If the operand is a structure path (as opposed to a stack variable), we could also use instruction.op_structure to get a regular structure member. For the record, both actually have the same effect when reading the operand (and only differ when modifying an operand).

Python>ins.op_stack(1)
<member '$ F1C0077F18.var_C8' index=6 offset=-0xd0 size=+0x1 typeinfo='char'>

Next, we want the references to that frame member. We can snag these using the member_t.refs() method.

Python>ins.op_stack(1).refs()
[opref_t(address=0x1c00fe9ae, opnum=0, access=access_t(w)), opref_t(address=0x1c00fea14, opnum=1, access=access_t(r)), opref_t(address=0x1c00feaed, opnum=1, access=access_t(r)), opref_t(address=0x1c00feb4f, opnum=1, access=access_t(r))]

Finally, we can iterate through the references that we snagged, disassemble the address, and then get the stack variable for those.

Python>for ref in ins.op_stack(1).refs():
Python> print(ins.op_stack(ref))
Python> print(db.disasm(ref))

<member '$ F1C0077F18.var_C8' index=6 offset=-0xd0 size=+0x1 typeinfo='char'>
1c00fe9ae: mov [rbp+57h+var_C8], 1
<member '$ F1C0077F18.var_C8' index=6 offset=-0xd0 size=+0x1 typeinfo='char'>
1c00fea14: movzx eax, [rbp+57h+var_C8]
<member '$ F1C0077F18.var_C8' index=6 offset=-0xd0 size=+0x1 typeinfo='char'>
1c00feaed: movzx eax, [rbp+57h+var_C8]
<member '$ F1C0077F18.var_C8' index=6 offset=-0xd0 size=+0x1 typeinfo='char'>
1c00feb4f: movzx eax, [rbp+57h+var_C8]

If you want to grab them from an arbitrary frame member, you can grab the frame using function.frame(ea). Once the frame structure has been fetched, we can use the members_t attribute to access individual or multiple members.

Python>func.frame()
<class 'structure' name='$ F1C0077F18' offset=-0x120 size=0x128>.

Python>func.frame().members
<class 'structure' name='$ F1C0077F18' offset=-0x120 size=0x128>
     -120+0x28                                             [None, 40]
[0]   -f8+0x8                  PLARGE_INTEGER 'Timeout'    (<class 'type'>, 8)
[1]   -f0+0x8          PEVENT_DATA_DESCRIPTOR 'var_E8'     (<class 'type'>, 8)
[2]   -e8+0x4                             int 'var_E0'     (<class 'int'>, 4)
[3]   -e4+0x4                             int 'var_DC'     (<class 'int'>, 4)
[4]   -e0+0x4                             int 'var_D8'     (<class 'int'>, 4)
      -dc+0x4                                              [None, 4]
...
      -20+0x8                                              [None, 8]
[18]  -18+0x1                            char 'var_10'     (<class 'int'>, 1)
      -17+0xf                                              [None, 15]
[19]   -8+0x8                         char[8] ' s'         [(<class 'int'>, 1), 8]
[20]    0+0x8                         char[8] ' r'         [(<class 'int'>, 1), 8]

We only want references for members that are typed (and not an argument), so we can start out by listing the results using the members_t.list method and filtering the results for the purpose of verifying things prior to collecting everything.

Python>func.frame().members.list(typed=True, args=False)
[ 0] -f8:+0x8                 PLARGE_INTEGER 'Timeout'    (<class 'type'>, 8)                                                      (flag=35500400,dt_type=30000000)
[ 1] -f0:+0x8         PEVENT_DATA_DESCRIPTOR 'var_E8'     (<class 'type'>, 8)                                                      (flag=35500400,dt_type=30000000)
[ 7] -cc:+0x10                          GUID 'ActivityId' <class 'structure' name='_GUID' offset=-0xcc size=0x10>                  (flag=60000400,dt_type=60000000,typeid=ff00000000003840)
[ 8] -b8:+0x10 struct _EVENT_DATA_DESCRIPTOR 'var_B0'     <class 'structure' name='_EVENT_DATA_DESCRIPTOR' offset=-0xb8 size=0x10> (flag=60000400,dt_type=60000000,typeid=ff0000000000df8c)
[11] -88:+0x10 struct _EVENT_DATA_DESCRIPTOR 'var_80'     <class 'structure' name='_EVENT_DATA_DESCRIPTOR' offset=-0x88 size=0x10> (flag=60000400,dt_type=60000000,typeid=ff0000000000df8c)
[14] -58:+0x10 struct _EVENT_DATA_DESCRIPTOR 'var_50'     <class 'structure' name='_EVENT_DATA_DESCRIPTOR' offset=-0x58 size=0x10> (flag=60000400,dt_type=60000000,typeid=ff0000000000df8c)

Now we can iterate through all the results, skip any references that aren't writing to the variable by checking its reference, and outputting a disassembly of the basic-block (using function.block that contains it.

Python>for m in func.frame().members(typed=True, args=False):
Python>  for ref in m.refs():
Python>    if 'w' not in ref: continue
Python>    print(func.block(ref), ref)
Python>    print(db.disasm(func.block(ref)))
Python>    print()
Python>

bounds_t(left=0x1c0077f55, right=0x1c0077f79) opref_t(address=0x1c0077f69, opnum=0, access=access_t(w))
1c0077f55: mov rcx, qword ptr cs:WPP_MAIN_CB.Queue
1c0077f5c: lea rax, WPP_887fffb207bc3747f0ee19f0a3430899_Traceguids
1c0077f63: mov r9d, 0Ah
1c0077f69: mov [rsp+110h+Timeout], rax
1c0077f6e: mov dl, 4
1c0077f70: lea r8d, [r9+5]
1c0077f74: call WPP_RECORDER_SF_

opref_t(address=0x1c00fea47, opnum=0, access=access_t(w))
bounds_t(left=0x1c00fea14, right=0x1c00fea51) opref_t(address=0x1c00fea47, opnum=0, access=access_t(w))
1c00fea14: movzx eax, [rbp+57h+var_C8]
1c00fea18: lea r8, [rbp+57h+ActivityId]
1c00fea1c: mov [rsp+110h+var_E0], eax
1c00fea20: lea rdx, dword_1C0213D87
1c00fea27: lea rax, [rsp+110h+var_E0]
1c00fea2c: mov [rbp+57h+var_88], 4
1c00fea34: mov [rbp+57h+var_90], rax
1c00fea38: xor r9d, r9d
1c00fea3b: lea rax, [rbp+57h+var_B0]
1c00fea3f: mov rcx, rdi
1c00fea42: mov [rsp+110h+var_E8], rax
1c00fea47: mov dword ptr [rsp+110h+Timeout], r13d
1c00fea4c: call _tlgWriteTransfer_EtwWriteTransfer

...

opref_t(address=0x1c0077fca, opnum=0, access=access_t(w))
bounds_t(left=0x1c0077fb4, right=0x1c0077fd8) opref_t(address=0x1c0077fca, opnum=0, access=access_t(w))
1c0077fb4: cmp cs:bool InputTraceLogging::Perf::s_userCritLoggingEnabled, r14b
1c0077fbb: lea rdi, dword_1C0246A60
1c0077fc2: mov esi, 6
1c0077fc7: xorps xmm0, xmm0
1c0077fca: movups xmmword ptr [rbp+57h+ActivityId.Data1], xmm0
1c0077fce: lea r13d, [rsi-3]
1c0077fd2: jnz loc_1C00FE9AE

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.