Repository: pwndbg/pwndbg

Issue:
"bins" command shows that small bin is empty but actually it's not
when small bin point to somewhere on heap but the content is all zero, the bins command shows that the small bin is empty, but actually it is not empty.
![default](https://user-images.githubusercontent.com/20660269/43990056-7d82ce44-9d88-11e8-8380-4a23276f2577.png)
This has made me fail to solve one chanllenge in one CTF :(


Discussion:
Hey,

Thanks for reporting this issue!

@andigena @Segflow could you take a look at this?
@Mem2019 can you attach the binary please? 
[offbyone.tar.gz](https://github.com/pwndbg/pwndbg/files/2282929/offbyone.tar.gz)
Here are the binary, the exploit that cause the problem, and the libc that is being used
bump; @Segflow have you looked at it?
@disconnect3d couldn't replicate the error. 
@Mem2019 can you please add a call to `pause()` in your script so I know exactly at what step you got that state?
last `add("A\n", 0x310)` causes the state
```c
#include <stdlib.h>
#include <memory.h>
int main()
{
	void* a1 = malloc(0xF0);
	void* a2 = malloc(0xF0);
	void* a3 = malloc(0xF0);
	void* a4 = malloc(0xE0);
	void* a5 = malloc(0xF0);
	free(a2);
	free(a4);
	malloc(0xE0); //take a4, put a2 into smallbins
	//0x100: 0x7ffff7dd1c68 (main_arena+328) —▸ 0x602100 ◂— 0x7ffff7dd1c68
	((size_t*)a2)[0]=0;//set fd to 0, cause the problem
	/*
	pwndbg> p/x main_arena->bins[0x1F]
	$9 = 0x602100
	*/
}
```
to be specific
@Segflow how is that with the example above?
[bin_at function](https://github.com/pwndbg/pwndbg/blob/dev/pwndbg/heap/ptmalloc.py#L468) gets pointer from bin[index] and returns list without it (`include_start=False`). So when bin[index]->fd is 0, it thinks that the bin is empty.

Fix may be like that:
```python
chain = pwndbg.chain.get(int(front), offset=fd_offset, hard_stop=current_base, limit=heap_chain_limit, include_start=True)
if len(chain) > 1 and chain[0] != 0:
    # check if bin[index] points to itself (is empty)
    address = int(pwndbg.memory.poi(pwndbg.typeinfo.ppvoid, chain[0] + self.chunk_key_offset('bk')))
    address &= pwndbg.arch.ptrmask
    if chain[0] == address:
        chain = [0]
return chain
```
checking 'bk' offset as in malloc function.
The change will change ordering, from:
```python
unsortedbin
all: 0x555555756000 —▸ 0x7ffff7dd3b58 (main_arena+88) —▸ 0x555555756200 ◂— 0x555555756000
pwndbg> malloc_chunk 0x7ffff7dd3b58
0x7ffff7dd3b58 {
  prev_size = 93824994338784, 
  size = 0, 
  fd = 0x555555756200, 
  bk = 0x555555756000, 
  fd_nextsize = 0x7ffff7dd3b68 <main_arena+104>, 
  bk_nextsize = 0x7ffff7dd3b68 <main_arena+104>
}

```

to
```python
unsortedbin
all: 0x555555756200 —▸ 0x555555756000 —▸ 0x7ffff7dd3b58 (main_arena+88) ◂— 0x555555756200
pwndbg> malloc_chunk 0x7ffff7dd3b58
0x7ffff7dd3b58 {
  prev_size = 93824994338784, 
  size = 0, 
  fd = 0x555555756200, 
  bk = 0x555555756000, 
  fd_nextsize = 0x7ffff7dd3b68 <main_arena+104>, 
  bk_nextsize = 0x7ffff7dd3b68 <main_arena+104>
}
```

But that shouldn't matter, I thing?

Btw, as not fast bins are doubly linked lists, maybe we should detect corruption, and if so print broken lists from backward and forward + some red info about corruption (in bins commands)?

Resolving patch:
diff --git a/pwndbg/commands/heap.py b/pwndbg/commands/heap.py
--- a/pwndbg/commands/heap.py
+++ b/pwndbg/commands/heap.py
@@ -39,25 +39,38 @@ def format_bin(bins, verbose=False, offset=None):
     result = []
     for size in bins:
         b = bins[size]
-        if isinstance(b, tuple):
-            chain, count = b
-        else:
-            chain = b
-            count = None
 
-        if not verbose and (chain == [0] and not count):
+        count, is_chain_corrupted = None, False
+
+        # fastbins consists of only single linked list
+        if len(b) == 1:  # fastbin:
+            chain_fd = b
+        # tcachebins consists of single linked list and entries count
+        elif len(b) == 2:  # tcachebin:
+            chain_fd, count = b
+        # normal bins consists of double linked list and may be corrupted (we can detect corruption)
+        else:  # normal bin
+            chain_fd, chain_bk, is_chain_corrupted = b
+
+        if not verbose and (chain_fd == [0] and not count) and not is_chain_corrupted:
             continue
 
-        formatted_chain = pwndbg.chain.format(chain[0], offset=offset)
+        formatted_chain = pwndbg.chain.format(chain_fd[0], offset=offset)
 
         if isinstance(size, int):
             size = hex(size)
 
-        if count is not None:
-            line = (message.hint(size) + message.hint(' [%3d]' % count) + ': ').ljust(13)
+        if is_chain_corrupted:
+            line = message.hint(size) + message.error(' [corrupted]') + '\n'
+            line += message.hint('FD: ') + formatted_chain + '\n'
+            line += message.hint('BK: ') + pwndbg.chain.format(chain_bk[0], offset=main_heap.chunk_key_offset('bk'))
         else:
-            line = (message.hint(size) + ': ').ljust(13)
-        line += formatted_chain
+            if count is not None:
+                line = (message.hint(size) + message.hint(' [%3d]' % count) + ': ').ljust(13)
+            else:
+                line = (message.hint(size) + ': ').ljust(13)
+            line += formatted_chain
+
         result.append(line)
 
     if not result:
diff --git a/pwndbg/commands/version.py b/pwndbg/commands/version.py
--- a/pwndbg/commands/version.py
+++ b/pwndbg/commands/version.py
@@ -189,5 +189,3 @@ def bugreport(run_browser=False):
             print(please_please_submit + github_issue_url)    
     else:
         print(please_please_submit + github_issue_url)
-
-
diff --git a/pwndbg/heap/ptmalloc.py b/pwndbg/heap/ptmalloc.py
--- a/pwndbg/heap/ptmalloc.py
+++ b/pwndbg/heap/ptmalloc.py
@@ -393,6 +393,7 @@ def fastbin_index(self, size):
 
 
     def fastbins(self, arena_addr=None):
+        """Returns: chain or None"""
         arena = self.get_arena(arena_addr)
 
         if arena is None:
@@ -414,6 +415,7 @@ def fastbins(self, arena_addr=None):
 
 
     def tcachebins(self, tcache_addr=None):
+        """Returns: tuple(chain, count) or None"""
         tcache = self.get_tcache(tcache_addr)
 
         if tcache is None:
@@ -449,6 +451,8 @@ def bin_at(self, index, arena_addr=None):
         Bin 1          - Unsorted BiN
         Bin 2 to 63    - Smallbins
         Bin 64 to 126  - Largebins
+
+        Returns: tuple(chain_from_bin_fd, chain_from_bin_bk, is_chain_corrupted) or None
         """
         index = index - 1
         arena = self.get_arena(arena_addr)
@@ -464,9 +468,24 @@ def bin_at(self, index, arena_addr=None):
 
         front, back = normal_bins[index * 2], normal_bins[index * 2 + 1]
         fd_offset   = self.chunk_key_offset('fd')
+        bk_offset   = self.chunk_key_offset('bk')
+
+        is_chain_corrupted = False
+
+        get_chain = lambda bin, offset: pwndbg.chain.get(int(bin), offset=offset, hard_stop=current_base, limit=heap_chain_limit, include_start=True)
+        chain_fd = get_chain(front, fd_offset)
+        chain_bk = get_chain(back, bk_offset)
+
+        # check if bin[index] points to itself (is empty)
+        if len(chain_fd) == len(chain_bk) == 2 and chain_fd[0] == chain_bk[0]:
+            chain_fd = [0]
+            chain_bk = [0]
+
+        # check if corrupted
+        elif chain_fd[:-1] != chain_bk[:-2][::-1] + [chain_bk[-2]]:
+            is_chain_corrupted = True
 
-        chain = pwndbg.chain.get(int(front), offset=fd_offset, hard_stop=current_base, limit=heap_chain_limit, include_start=False)
-        return chain
+        return (chain_fd, chain_bk, is_chain_corrupted)
 
 
     def unsortedbin(self, arena_addr=None):


Test patch: