Why IDA doesn't fold constants and how to fix it

More in Reverse engineering
- Recognize STL code easily
Recognizing STL code with ease 😐 During reverse engineering, we often encounter STL code whose analysis…
- Again CFG
CFG again 👋 A common task when extracting malware configurations at scale is obtaining function boundaries…
- Deobfuscating .NET function names manually
Deobfuscating .NET function names manually 🙌 .NET malware loves packers, obfuscation (of names, CFG, and other…
- Idea for a correlation rule in SIEM
Idea for a SIEM correlation rule 💡 Although tracking the entire attack chain described in the…
- We continue reproducing the attack from the post above 🔼
Continuing to reproduce the attack from the post above 🔼 3️⃣ Creating a public API Gateway…
Why IDA doesn’t fold constants and how to fix it 👨💻
Recently, obfuscation has been increasingly common in software where constants are replaced with arithmetic expressions. For example, this technique is present in OLLVM.
😑 What’s the problem here: some of the constants that make up the final expression are stored in memory. Because of this, IDA by default cannot properly apply constant propagation and automatically “fold” such constructs back into their original values (screenshot 1).
It’s especially unpleasant when addresses of called functions are also masked this way. In practice, this results in constructs like:
mov <reg>, <const>
add <reg>, cs:<offset_*>
call <reg>Code language: Intel x86 Assembly (x86asm)
For static analysis, this is of course an extra headache 🧱
But there’s a nuance: if you explicitly specify that the data at address cs:<offset_*> is a constant qword/dword, then during decompilation IDA will itself pull the value from memory, recalculate the expression, and fold it back into the original constant.
Before explicitly marking such data as const, it’s worth making sure that the value is truly readonly. One of the simple ways is to look at cross-references: if all references to the address are read-only, then it’s a good candidate for const (screenshot 2).
It’s also useful to filter out values that are actually pointers, so as not to break the typing (screenshot 3).
😎 All of this can be automated with a small IDAPython script (IDA 9.0+), which walks through the .data segment (or any other segment you need), finds values that are used only for reading, excludes those that look like addresses, and explicitly marks them as const byte/word/dword/qword depending on their size.
As a result, the pseudocode becomes noticeably cleaner and more readable (screenshot 4).
And here’s that IDAPython script:
import ida_bytes
import ida_segment
import ida_typeinf
import idautils
import ida_xref
import idaapi
from collections import namedtuple
PTR_SIZE = 8 if idaapi.inf_is_64bit() else 4
TypeInfo = namedtuple('SizeInfo', ['get_data', 'typename', 'badaddr'])
SIZE_TO_TYPE_INFO_MAP = {
1: (TypeInfo(ida_bytes.get_byte, "byte", 0xFF)),
2: (TypeInfo(ida_bytes.get_word, "word", 0xFFFF)),
4: (TypeInfo(ida_bytes.get_dword, "dword", 0xFFFFFFFF)),
8: (TypeInfo(ida_bytes.get_qword, "qword", 0xFFFFFFFFFFFFFFFF))
}
def apply_const_type(ea: int) -> bool:
flags = ida_bytes.get_flags(ea)
if not ida_bytes.is_data(flags):
return False
item_size = ida_bytes.get_item_size(ea)
type_info = SIZE_TO_TYPE_INFO_MAP.get(item_size)
if type_info is None:
return False
if type_info.get_data(ea) == type_info.badaddr:
return False
typeinfo_str = "const " + type_info.typename
tif = ida_typeinf.tinfo_t(typeinfo_str)
ida_bytes.clr_op_type(ea, 0)
return ida_typeinf.apply_tinfo(ea, tif, 1)
def is_addr_readonly(target_addr: int) -> bool:
flags = ida_bytes.get_full_flags(target_addr)
return ida_bytes.has_xref(flags) and \
all(ref.type == ida_xref.dr_R for ref in idautils.XrefsTo(target_addr))
def apply_const_type_if_readonly(target_addr: int) -> bool:
return apply_const_type(target_addr) if is_addr_readonly(target_addr) else False
def data_is_offset(ea:int) -> bool:
item_size = ida_bytes.get_item_size(ea)
if item_size != PTR_SIZE:
return False
type_info = SIZE_TO_TYPE_INFO_MAP.get(item_size)
if type_info is None:
return False
value = type_info.get_data(ea)
return (idaapi.getseg(value)) is not None
def main():
print("Casting readonly memory values to consts ")
print("---------------------------------")
segment_name = ".data"
seg = ida_segment.get_segm_by_name(segment_name)
if not seg:
print("No {segment_name} segment")
return
ea = seg.start_ea
fixed_consts_num = 0
while ea < seg.end_ea:
ea_size = ida_bytes.get_item_size(ea)
if ea_size <= 0:
ea += 1
continue
if not data_is_offset(ea) and apply_const_type_if_readonly(ea):
fixed_consts_num += 1
ea += ea_size
print("---------------------------------")
print(f"Done. fixed = {fixed_consts_num} consts")
print("Decompile again to view results: F5")
if __name__ == "__main__":
main()
#tips #reverse
@ptescalator (X, Max)



More in Reverse engineering
- Recognize STL code easily
Recognizing STL code with ease 😐 During reverse engineering, we often encounter STL code whose analysis…
- Again CFG
CFG again 👋 A common task when extracting malware configurations at scale is obtaining function boundaries…
- Deobfuscating .NET function names manually
Deobfuscating .NET function names manually 🙌 .NET malware loves packers, obfuscation (of names, CFG, and other…
- Idea for a correlation rule in SIEM
Idea for a SIEM correlation rule 💡 Although tracking the entire attack chain described in the…
- We continue reproducing the attack from the post above 🔼
Continuing to reproduce the attack from the post above 🔼 3️⃣ Creating a public API Gateway…







