Repository: angr/angr

Issue:
CFG recovery memory region bounds are off by one
<!--
*Disclaimer:
The angr suite is maintained by a small team of volunteers.
While we cannot guarantee any timeliness for fixes and enhancements, we will do our best.
For more real-time help with angr, from us and the community, join our [Slack.](http://angr.io/invite/)*
-->
---

**Describe the bug.**
<!--
Please include a clear and concise description of what the bug is.
-->

There are a couple of issues leading to a potential off-by-one of the scanned region sizes in `CFGBase`, depending on which CLE loader backend is being used. I discovered this by comparing the CFGs created for a small segment of assembly loaded from both (a) a simple blob and (b) from an ELF. In the case of the ELF version, the last addressable byte of the memory region is not included correctly, which can lead to an incomplete CFG recovery. Here's why:

* Issue 1: Inconsistent `max_addr` property for different loader backends.
   * For the `Blob` loader in CLE, this provides an *exclusive* end address, that is, the address of the first byte after the region, `start+length`.
   * For the `ELF` backend the *inclusive* address is returned, that is, the last addressable byte in the region, `start+length-1`. 
   * `Hex` (exclusive), `PE` (inclusive).

   CLE's `Region` class docstring for `max_addr` [indicates that this value should be inclusive](https://github.com/angr/cle/blob/103190d3899dc1b7e784edab23d0276245925b7d/cle/backends/region.py#L67), ~~so it's likely that just the simple backends like `Blob` and `Hex` need to be fixed up~~. [Fix for CLE pending here.](https://github.com/angr/cle/pull/264)

* Issue 2: `CFGBase` currently treats the `max_addr` property for all object types as exclusive, which is inconsistent with what the loader backend provides as mentioned above:
   * Regions are explicitly mentioned as being tuples of `(start address, end_address)` which to me implies exclusive, but may be unclear to some readers. The docstring could  be more clear.
   * Region lengths are calculated by: `max_addr-min_addr`
   * Condition of whether an address falls within the region is: `addr < end`

   To fix this, the [`_executable_memory_regions`](https://github.com/angr/angr/blob/1a63252a6e752e22486ce78bd3fa67dcd8357514/angr/analyses/cfg/cfg_base.py#L561) method should be fixed to make it return the correct region bounds.

I plan to file PRs to fix these issues for both ~~CLE~~ ([done](https://github.com/angr/cle/pull/264)) and ~~angr~~ ([done](https://github.com/angr/angr/pull/2315)).

**Environment Information.**
<!--
Many common issues are caused by problems with the local Python environment.
Before submitting, double-check that your versions of all modules in the angr suite (angr, cle, pyvex, ...) are up to date.
Please include the output of `python -m angr.misc.bug_report` here.
-->


**To Reproduce.**
<!--
Please include *both a script to reproduce the crash, and attach the binary used, if possible*
-->

Take some sample code:
```
     1                                  bits 32
     2                                  
     3                                  block_A:
     4 00000000 B817000000              	mov eax, 23
     5 00000005 83C00A                  	add eax, 10
     6                                  
     7 00000008 39C8                    	cmp eax, ecx
     8 0000000A 7507                    	jne block_C
     9                                  
    10                                  block_B:
    11 0000000C B800000000              	mov eax, 0
    12 00000011 EB05                    	jmp block_D
    13                                  
    14                                  block_C:
    15 00000013 B801000000              	mov eax, 1
    16                                  
    17                                  block_D:
    18 00000018 C3                      	ret
```

Assemble with `nasm`:
* `nasm -o test.binary test.nasm`
* `nasm -o test.elf -f elf32 test.nasm && strip test.elf`

Testing Blob:
```
In [1]: import angr 
   ...: p = angr.Project('test.binary', main_opts={'backend':'blob', 'arch':'x86', 'entry_point':0x400000, 'base_addr':0x400000})                                                                                  

In [2]: hex(p.loader.all_objects[0].max_addr)                                                                                                                                                                      
Out[2]: '0x400019'

In [3]: p.analyses.CFGFast().model.nodes()
Out[3]: NodeView((<CFGNode 0x400000[12]>, <CFGNode 0x40000c[7]>, <CFGNode 0x400013[6]>, <CFGNode 0x400018[1]>))
```

Testing ELF:
```
$ objdump -dMintel test.elf

test.elf:     file format elf32-i386


Disassembly of section .text:

00000000 <.text>:
   0:	b8 17 00 00 00       	mov    eax,0x17
   5:	83 c0 0a             	add    eax,0xa
   8:	39 c8                	cmp    eax,ecx
   a:	75 07                	jne    0x13
   c:	b8 00 00 00 00       	mov    eax,0x0
  11:	eb 05                	jmp    0x18
  13:	b8 01 00 00 00       	mov    eax,0x1
  18:	c3                   	ret   
```

```
In [1]: import angr                                                                                                                                                                                                

In [2]: p = angr.Project('test.elf')                                                                                                                                                                               
WARNING | 2020-08-20 16:01:50,222 | cle.loader | The main binary is a position-independent executable. It is being loaded with a base address of 0x400000.

In [3]: hex(p.loader.all_objects[0].sections[1].max_addr)                                                                                                                                                          
Out[3]: '0x400018'

In [4]: p.analyses.CFGFast().model.nodes()                                                                                                                                                                         
Out[4]: NodeView((<CFGNode 0x400000[12]>, <CFGNode 0x40000c[7]>, <CFGNode 0x400013[6]>))
```

Notice that `<CFGNode 0x400018[1]>` is missing here, because the CFG job for address 0x400018 was culled after failing the region bounds check described above (see `CFGFast::_pre_job_handling`).

**Additional context.**
<!--
Add any other context about the problem here.
-->



Discussion:

Resolving patch:
diff --git a/angr/analyses/cfg/cfg_base.py b/angr/analyses/cfg/cfg_base.py
--- a/angr/analyses/cfg/cfg_base.py
+++ b/angr/analyses/cfg/cfg_base.py
@@ -582,20 +582,20 @@ def _executable_memory_regions(self, objects=None, force_segment=False):
                     # Get all executable sections
                     for section in b.sections:
                         if section.is_executable:
-                            tpl = (section.min_addr, section.max_addr)
+                            tpl = (section.min_addr, section.max_addr+1)
                             memory_regions.append(tpl)
 
                 else:
                     # Get all executable segments
                     for segment in b.segments:
                         if segment.is_executable:
-                            tpl = (segment.min_addr, segment.max_addr)
+                            tpl = (segment.min_addr, segment.max_addr+1)
                             memory_regions.append(tpl)
 
             elif isinstance(b, PE):
                 for section in b.sections:
                     if section.is_executable:
-                        tpl = (section.min_addr, section.max_addr)
+                        tpl = (section.min_addr, section.max_addr+1)
                         memory_regions.append(tpl)
 
             elif isinstance(b, MachO):
@@ -605,12 +605,12 @@ def _executable_memory_regions(self, objects=None, force_segment=False):
                         if seg.is_executable:
                             # Take all sections from this segment (MachO style)
                             for section in seg.sections:
-                                tpl = (section.min_addr, section.max_addr)
+                                tpl = (section.min_addr, section.max_addr+1)
                                 memory_regions.append(tpl)
 
             elif isinstance(b, Blob):
                 # a blob is entirely executable
-                tpl = (b.min_addr, b.max_addr)
+                tpl = (b.min_addr, b.max_addr+1)
                 memory_regions.append(tpl)
             elif isinstance(b, NamedRegion):
                 # NamedRegions have no content! Ignore
@@ -621,7 +621,7 @@ def _executable_memory_regions(self, objects=None, force_segment=False):
             else:
                 l.warning('Unsupported object format "%s". Treat it as an executable.', b.__class__.__name__)
 
-                tpl = (b.min_addr, b.max_addr)
+                tpl = (b.min_addr, b.max_addr+1)
                 memory_regions.append(tpl)
 
         if not memory_regions:


Test patch: