Vector35 / Vector35/binaryninja-api

BN sometimes mishandles two 32-bit arguments packed into 64-bit stack slot

Open
#8,279 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
C++
Stars
1.3k
Forks
298
Avg merge
5d 5h
Merged PRs (30d)
19

Description

Version and Platform (required):

  • Binary Ninja Version: 5.4.9875-dev (86d1e1ad)
  • Edition: Commercial
  • OS: Debian Linux
  • OS Version: unstable
  • CPU Architecture: x86_64

Bug Description:
In the Apple arm64 ABI, "Function arguments may consume slots on the stack that are not multiples of 8 bytes." Thus, for example, a series of 32-bit arguments on the stack is placed with no padding between arguments. Binary Ninja mishandles this… sometimes.

Steps To Reproduce:

Test case: test.zip

Both files (optimized.o and unoptimized.o) are built from the same source code:

extern void test(int x0, int x1, int x2, int x3, int x4, int x5, int x6, int x7, int sp0, int sp4, int sp8, int spC);

void go() {
    test(0, 1, 2, 3, 4, 5, 6, 7, 0x100, 0x104, 0x108, 0x10c);
}

To reproduce, load one of the objects, create a function for go (for some reason it is not created automatically; another bug?), and set the type of test to match the source code.

Expected behavior: Both files should show test being called with the correct arguments.
Actual behavior: unoptimized.o is correct, but optimized.o shows:

0040004c        return _test(x0: 0, x1: 1, x2: 2, x3: 3, x4: 4, x5: 5, x6: 6, x7: 7, 
0040004c            sp0: 0x10400000100, sp4: 0x10c00000108)

The difference between the binaries is that optimized.o was built with -O3 while unoptimized.o was built with -O0. As a result, optimized.o packs the arguments into 64-bit registers before storing them to the stack:

0040000c  082180d2   mov     x8, #0x108
00400010  8821c0f2   movk    x8, #0x10c, lsl #0x20  {0x10c00000108}
00400014  092080d2   mov     x9, #0x100
00400018  8920c0f2   movk    x9, #0x104, lsl #0x20  {0x10400000100}
0040001c  e92300a9   stp     x9, x8, [sp] {var_20} {var_18}  {0x10400000100}  {0x10c00000108}

Whereas optimized.o just stores the arguments one at a time:

00400010  08208052   mov     w8, #0x100
00400014  280100b9   str     w8, [x9 {var_20}]  {0x100}
00400018  88208052   mov     w8, #0x104
0040001c  280500b9   str     w8, [x9, #0x4 {var_1c}]  {0x104}
00400020  08218052   mov     w8, #0x108
00400024  280900b9   str     w8, [x9, #0x8 {var_18}]  {0x108}
00400028  88218052   mov     w8, #0x10c
0040002c  280d00b9   str     w8, [x9, #0xc {var_14}]  {0x10c}

The actual stack layout is the same. But for some reason BN treats these differently.

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by loading test.zip's optimized.o and unoptimized.o in Binary Ninja, creating go, and applying the stated type to test. Compare the generated call arguments and trace how the Apple arm64 ABI handles packed 32-bit stack arguments; done means both binaries display the twelve expected arguments, including sp0 through spC.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
reverse-engineering
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
50/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.