HexHive / HexHive/retrowrite

Improve X64 jump table handling

Open
#20 4 comments 0 reactions 0 assignees View on GitHub
enhancement X64
Dominant language
Python
Stars
750
Forks
87
PR merge metrics
No merged PRs in 30d

Description

Hi, thanks for your contribution and hard work! Retrowrite is amazing.

Actually, I find a small unsoundness issue in reassembly and want to set up some discussion here. It would be very appreciated if anyone can comment on this.

In short, my key insight is that: __although we do not need to distinguish numerical numbers and references/labels in PIE binaries, we still need to distinguish numerical numbers and the label offsets__.

I use the latest commit `9e2e633e9ab165681733f3255e648a62b22e6368` for reference.

## Case 1

The story begins when I got a program which behaves differently after reassembly-and-recompilation (the attached code is reduced for easy demonstration).

```c
#include

static const int t[2] = {-187, -184};

int main() {
int x;
scanf("%d", &x);
printf("%d\n", t[x]);
}
```

My basic setup is:

```bash
$ gcc --version
gcc (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0
Copyright (C) 2017 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

$ gcc poc.c

$ file a.out
a.out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=f1c5b51f8f0547a7d9e06fdc52bef7d450d34022, not stripped

$ retrowrite -s a.out a.s

$ gcc -no-pie a.s -o b.out
[*] Relocations for a section that's not loaded: .rela.dyn
[*] Relocations for a section that's not loaded: .rela.plt
[x] Could not replace value in .init_array
[x] Couldn't find valid section 200db0
[x] Couldn't find valid section 200fd8
[x] Couldn't find valid section 200fe0
[x] Couldn't find valid section 200fe8
[x] Couldn't find valid section 200ff0
[x] Couldn't find valid section 200ff8
```

After that, we can execute `a.out` and `b.out` to get the execution results.

```bash
$ ./a.out
1
-184

$ ./b.out
1
-202
```

We can see, with the same input 1, a.out prints -184 but b.out prints -202.

Originally, I think the fault is caused by the plt warnings. But after some exploration, I found it is an unsoundness issue in theory. Specifically, let's check the reassembly file a.s

```asm
...

.globl t_818
t_818: # 818 -- 820 # static const int t[2] locates here
.LC818:
.long .LC75d-.LC818
.LC81c:
.long .LC760-.LC818

...

.LC756:
leaq .LC818(%rip), %rax
.LC75d:
movl (%rdx, %rax), %eax
.LC760:
movl %eax, %esi
.LC762:
leaq .LC823(%rip), %rdi
.LC769:
movl $0, %eax
.LC76e:
callq printf@PLT

...
```

We can see, retrowrite misclassified the numerical elements in `const int t[2]` as the label offsets (e.g., .LC75d-.LC818). After compilation, these values are changed for sure.

Then I go check the code.

https://github.com/HexHive/retrowrite/blob/9e2e633e9ab165681733f3255e648a62b22e6368/librw/rw.py#L219

It seems that retrowrite uses heuristics to symbolize jump tables (which contains many label offsets). And in this case, the global `const int t[2]` satisfies the heuristics by chance, which confuses retrowrite. Unsoundness is still here and there, somehow.

With more study, I think the problem can be summarized as:

__Although we do not need to distinguish numerical numbers and references in PIE binaries, we still need to distinguish numerical numbers and the offsets between labels__. I feel these label offsets are not in the symbol/relocation table.

The aforementioned case provides an example, where the element in jump table is the offset between labels (the target and jump base). That is why we got confused here.

## Case 2

To further study the root cause, I hand-crafted a program with inline-assembly. Hope it can help.

```asm
test rdi, rdi;
je A;

mov rbx, B-A;
push rbx;
lea r8, [rip + A];

A:
pop r9;
add r8, r9;
jmp r8;

B:
mov rax, 60; # SYS_EXIT
mov rdi, 0;
syscall;
ret;
```

Let's go through the code.

```asm
test rdi, rdi;
je A;
```

The first test-je pattern is to let retrowrite know there is a basic block starting at `A`.

```asm
mov rbx, B-A;
push rbx;
lea r8, [rip + A];
```

The mov instruction is the key, which loads the __offset between labels B and A__ into rbx.
The following push instruction stores the offset into memory, and the lea instruction loads the address of label A into r8.

```asm
A:
pop r9;
add r8, r9;
jmp r8;
```

Later, we pop the offset into r9. Note that, here we use a simple push-pop pattern to simulate _the complex behaviors (including the aliasing problems) in real-world binary_.
The add instruction adds r8 and r9, which denotes _A + B - A = B_.
Then, an indirect jump leads the control flow to B.

```asm
B:
mov rax, 60; # SYS_EXIT
mov rdi, 0;
syscall;
ret;
```

B is a simple exit(0).

My basic setup is:

```bash
$ cat poc.c
int main(int argc, char **argv) {
asm volatile(
".intel_syntax noprefix\n"

"\ttest rdi, rdi;\n"
"\tje A;\n"

"\tmov rbx, B-A;\n"
"\tpush rbx;\n"
"\tlea r8, [rip + A];\n"

".global A\n"
"A:\n"
"\tpop r9;\n"
"\tadd r8, r9;\n"
"\tjmp r8;\n"

".global B\n"
"B:\n"
"\tmov rax, 60;\n"
"\tmov rdi, 0;\n"
"\tsyscall;\n"

"\tret;\n"

".att_syntax;\n"
);
}

$ gcc poc.c

$ retrowrite -s a.out a.s
[*] Relocations for a section that's not loaded: .rela.dyn
[x] Could not replace value in .init_array
[x] Couldn't find valid section 200df8
[x] Couldn't find valid section 200fd8
[x] Couldn't find valid section 200fe0
[x] Couldn't find valid section 200fe8
[x] Couldn't find valid section 200ff0
[x] Couldn't find valid section 200ff8

$ AFL_AS_FORCE_INSTRUMENT=1 ~/AFLplusplus/afl-clang -no-pie a.s -o b.out
```

Let's first check the reassembly file

```asm
...

.LC605:
testq %rdi, %rdi
.LC608:
je .L619
.LC60a:
movq $8, %rbx # Originally, it is the offset between labels B and A, misclassified as a numerical number 8
.LC611:
pushq %rbx
.LC612:
leaq (%rip), %r8
.L619:
.LC619:

...
```

We can see retrowrite left the label offset (B-A) as a constant number 8.

After instrumentation, the offset has changed, but the constant is still left here, which breaks the recompiled binary (the indirect jump target becomes invalid).

```bash
$ ./a.out

$ ./b.out
[1] 4754 segmentation fault (core dumped) ./b.out
```

The solution is to infer the label offset (B - A). However, __the traditional challenge, which is caused by sophisticated memory behaviors, is still there__.

## More

I have attached the above [files](https://github.com/HexHive/retrowrite/files/6017216/issue.tar.gz). The directory structure is:

```
- case1 # the printf case
- poc.c
- a.out
- a.s
- b.out
- case2 # the inline-asm case
- poc.c
- a.out
- a.s
- b.out
```

It seems that an [Usenix paper](https://www.usenix.org/system/files/sec20-flores-montoya.pdf) also mentions the small unsoundness issue in Section 7.1. It would be very appreciated if anyone can share some thinkings here. And also, I hope our discussion can help the development of retrowrite, which is, again, a great work for us to follow.

Thanks!

Contributor guide

No contributing guide indexed for this repository

Research direction

Start at librw/rw.py around line 219, where jump-table symbolization heuristics are applied, and reproduce the supplied case1 and case2 artifacts from issue.tar.gz. Compare the generated a.s files with the original binaries to determine how numeric constants and label offsets are classified. Done means defining and validating a way to preserve both cases without changing jump targets or data values.

Written by the indexing model from the issue text.

Assessment

Tech stack
python
Domain
reverse-engineering
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.