JonathanSalwan / Triton

Triton is a dynamic binary analysis library. Build your own program analysis tools, automate your reverse engineering, perform software verification or just emulate code.
https://triton-library.github.io
Apache License 2.0
3.4k stars 524 forks source link

Potential ARM32 Bug #1227

Closed pdamian closed 1 year ago

pdamian commented 1 year ago

Consider the following example:

#!/usr/bin/env python3
## -*- coding: utf-8 -*-
from triton import ARCH, Instruction, MODE, TritonContext

function = {
    0x41508e: b"\x4f\xf0\x04\x0c",  # mov ip,  #0x4
    0x415092: b"\xbc\xf1\xff\x0f",  # cmp.w ip, #0xff
    0x415096: b"\xd8\xbf",          # it le
    0x415098: b"\x01\x20",          # movs r0, #1
    0x41509a: b"\x00\xdc",          # bgt #0x4
    0x41509c: b"\x00\xbf",          # nop
    0x41509e: b"\x00\xbf",          # nop
}

ctx = TritonContext(ARCH.ARM32)
ctx.setMode(MODE.ALIGNED_MEMORY, True)
ctx.setThumb(True)

pc = 0x41508e
while pc in function:
    inst = Instruction(pc, function[pc])
    ctx.processing(inst)
    print(inst)
    pc = ctx.getConcreteRegisterValue(ctx.registers.pc)

When executing the above instructions without Triton, I observe the following output (the branch bgt #0x4 is not taken):

0x41508e: mov.w ip, #4
0x415092: cmp.w ip, #0xff
0x415096: it le
0x415098: movs r0, #1
0x41509a: bgt #0x41509e
0x41509c: nop
0x41509e: nop

In Triton, however, the branch is taken:

0x41508e: mov.w ip, #4
0x415092: cmp.w ip, #0xff
0x415096: it le
0x415098: movs r0, #1
0x41509a: bgt #0x41509e
0x41509e: nop

If I take the following function instead (with another cmp.w ip, #0xff added just before the branch):

function = {
    0x41508e: b"\x4f\xf0\x04\x0c",  # mov ip,  #0x4
    0x415092: b"\xbc\xf1\xff\x0f",  # cmp.w ip, #0xff
    0x415096: b"\xd8\xbf",          # it le
    0x415098: b"\x01\x20",          # movs r0, #1
    0x41509a: b"\xbc\xf1\xff\x0f",  # cmp.w ip, #0xff
    0x41509e: b"\x00\xdc",          # bgt #0x4
    0x4150a0: b"\x00\xbf",          # nop
    0x4150a2: b"\x00\xbf",          # nop
}

Triton does fall-through the branch as well (as in the case without Triton):

0x41508e: mov.w ip, #4
0x415092: cmp.w ip, #0xff
0x415096: it le
0x415098: movs r0, #1
0x41509a: cmp.w ip, #0xff
0x41509e: bgt #0x4150a2
0x4150a0: nop
0x4150a2: nop

Could it be that Triton incorrectly "clears" the comparison status or do I misunderstand something here?

cnheitman commented 1 year ago

Indeed, there seems to be an issue. Triton seems to be updating the flag incorrectly. Inside an IT block, a 16-bit instruction should not set the flag condition (with the exception of CMP, CMN and TST). I'll push a fix for this next week.

cnheitman commented 1 year ago

@pdamian I just pushed a fix for this into the dev-v1.0 branch. Could you confirm it fixes your issue?

pdamian commented 1 year ago

@cnheitman: Awesome, thanks! I can confirm that this fixes the issue.

pdamian commented 1 year ago

@cnheitman: I have another related question. Not sure though if this should be considered a Triton bug or is the intended behavior. Consider the following example:

#!/usr/bin/env python3
## -*- coding: utf-8 -*-
from triton import ARCH, Instruction, MODE, TritonContext

function = {
    0x10: b"\x4f\xf0\x00\x00",  # mov r0, #0
    0x14: b"\x4f\xf0\x01\x01",  # mov r1, #1
    0x18: b"\x88\x42",          # cmp r0, r1
    0x1a: b"\x08\xbf",          # it eq
    0x1c: b"\x4f\xf0\x02\x00",  # mov r0, #2
    0x20: b"\x00\xbf",          # nop
}

ctx = TritonContext(ARCH.ARM32)
ctx.setMode(MODE.ALIGNED_MEMORY, True)
ctx.setThumb(True)

pc = 0x10
while pc in function:
    inst = Instruction(pc, function[pc])
    ctx.processing(inst)
    print(f"{str(inst):s}")
    pc = ctx.getConcreteRegisterValue(ctx.registers.pc)
r0 = ctx.getConcreteRegisterValue(ctx.registers.r0)
print(f"r0 = {r0:d}")

The output when running above code is as follows:

0x10: mov.w r0, #0
0x14: mov.w r1, #1
0x18: cmp r0, r1
0x1a: it eq
0x1c: mov.w r0, #2
0x20: nop
r0 = 0

At the end, the value of register r0 is 0, which is correct since the instruction mov.w r0, #2 should not be executed.

My issue now is that when I step through the very same code sequence in a debugger, the mov.w r0, #2 instruction is skipped, i.e. the pc never is at 0x1c. In Triton on the other hand, a pc equals to 0x1c happens.

Is that the intended behavior of Triton that the mov.w r0, #2 instruction is processed, however does not have any effect?

Context: I am using a debugger to collect the trace of a binary. This trace I subsequently run with Triton. The described behavior leads to a de-synchronization when I compare the addresses executed by the debugger and the ones by Triton.

cnheitman commented 1 year ago

Hey @pdamian . Yes, this is the intended behavior. The way we have to process IT blocks in Triton is by utilizing the conditional mode of instruction. This means that we use the conditions of the IT block to the instructions into conditional ones. This has the side effect that you mention. For now, this is something that has to be dealt with in the instruction processing loop. Perhaps, we can find a way to make it easier to handle this in the loop to better reflect the true behavior.

pdamian commented 1 year ago

@cnheitman : OK, thanks for the clarifications. Now that I know it's the intended Triton behavior, I can handle that as well in my scripts.