Skip to content

Latest commit

 

History

History
193 lines (125 loc) · 11 KB

tips.md

File metadata and controls

193 lines (125 loc) · 11 KB

Tips and Tricks

  • Finding string offsets for ret2libc attack

strings -a -t x /lib32/libc-2.24.so | grep /bin/sh

  • The ever forgetful test instruction

The TEST instruction performs a bitwise AND on two operands. The flags SF(Sign Flag a.k.a. Negative Flag), ZF(Zero Flag), PF(Parity Flag) are modified while the result of the AND is discarded. The OF(Overflow Flag) and CF(Carry Flag) flags are set to 0, while AF(Auxillary Carry Flag) flag is undefined. There are 9 different opcodes for the TEST instruction depending on the type and size of the operands. It can compare 8-bit, 16-bit, 32-bit or 64-bit values. It can also compare registers, immediate values and register indirect values.

The Sign Flag(Negative Flag) is set to the most significant bit of the result of the AND. In two's complement, a number with most significant bit = 1 is negative. If the result is 0, the Zero Flag is set to 1, otherwise 0. The Parity Flag is set to the bitwise XNOR of the least significant BYTE of the result i.e. if the number of 1s in the least significant byte is even, then the PF is set to 1 else 0.

  • Branch coverage v/s Statement coverage

Unless we consider goto as a branch, branch coverage doesn't imply statement coverage and vice-versa.

If we consider goto as a branch, then branch coverage implies statement coverage but not vice-versa(Even if we take simple statements).(Note that if we have a goto statement, then one branch will never be covered so we'll never have full branch coverage in case of a goto statement. See the following example:

if(x)
  then S1;
 
S2;

So if x=1, then every statement gets covered but not every branch(the false branch).

Take a look at this presentation. Note that the numbers on the left show the order of execution for a certain input.

Note: Path coverage >= Branch coverage >= Statement coverage

  • Compound statement

if(x){
  S1;
  S2;
  S3;
}

This whole is a single compuound statement while S1, S2, S3 are simple statements.

  • Shellshock

export x='() { :;}; echo $(/bin/cat /home/shellshock/flag)'

The pattern () { :;}; is the key!

Also see this stackoverflow question and this video.

  • Common Bugs

    • Broken input functions

      • scanf("%s") - Doesn't limit the size of the input, so you can overflow basically anything. It also taken in null bytes, only stops at whitespace. The problem is with the format string "%s" and not the scanf function per se. If you use "%100s" it will take in 100 characters only.
      • gets([char*]) - Here the problem is the function itself. It doesn't allow adding string length, so NEVER use this!
    • String length mismatch

      • Constant length inconsistencies -
      • strlen() vs counted length - Say you have a #define Counted_Length 10 i.e. you know a string is 10 characters in length. But "null byte injection"! If you use strlen() somewhere and the string has a null byte then you will get a different value.
      • Counted length contradictions - This can be thought of with respect to the heartbleed bug. This occurs when you supply the length of a string at two different places but you don't check if the two values are equal.
    • Off by One errors

      • 1-index/0-index - Remember to use the correct values in your for loops. First element of list is list[0]; Last element of a list is list[length-1], not list[length]. List[length] will give you the data in memory just after the list. Someone may have place some malicious data here.
    • Numeric errors

      • negative negative 2^31 == negative 2^31 - Think of how -2^31 is represented in memory(It's all 1s. negating it means taking it's two's complement which is negative 2^31). You can bypass checks like if(x == -x and x!=0)!
      • Up-casting signed numeric types, e.g. char -> int - Consider the following code:
      char input_data[20];
      char transformation[256];
      
      for(int i = 0; i < 20; i++) {
        input_data[i] = transformation[input_data[i]];
      }
      

      Now here you may assume that input_data being a char will be converted into an integer b/w 0 and 255. But this is not true. I am not sure if this is false all the time though. But to be safe you should use unsigned char in such cases where you think you'll be using it as an index later.

      • failing to account for overflow - assuming y > 0 => x + y > x is wrong since x+y may overflow the buffer and you might end up getting a smaller value than x.
      • floating point: Failing to account for inf and nan
      • floating point: Assuming x + 1 != x
    • Order of operations errors

      • assignment expressions and ternary statements have extremely high precedence
      • Operator Priority (See mistake challenge on pwnable.kr)
    • User-controlled format strings

      • format string exploitation is a really strange and very powerful black magic. Gives you the power to read and write to memory at an address of your choice!
      • printf(variable) should be a huge red flag - Always pass the format string as the first argument to printf!
    • Use after free

      • failure to clean up freed pointers
      • type confusion
    • Improper sanitization, injection

      • Quotation breakout
      • Null bytes
      • Fail-check sanitization escape
      • Decoding after sanitization
      • Generally, using non-constant command strings is extremely dangerous
    • Failure to check error conditions

      • malloc returns NULL on error - There is no crash at malloc but you may try to dereference the pointer somewhere. So whenever malloc-ing, do check if NULL is returned.
      • scanf returns number of filled out parameters - This is a fun one.
      • functions with buffers as outparams will leave the buffer unchanged on error, return -1
      • failure to check returned length (assuming maximum) can be a memory leak
  • Bypassing / filter

To Bypass / when PATH variable is empty, use cd ..;cd ../; $(pwd). $(pwd) in root directory gives /

  • Finding GOT table address

objdump -R [binary] | grep [function_name]

  • Finding offset of a function in a library

readelf -s [path/to/library.so] | grep [function_name]

  • Getting executable from assembly

Assembling to object code

nasm -f [output_file_format] [assembly_file.asm]

Linking

ld -o [outfile_name] [object_file.o]

  • Extracting shellcode from executables

(Note that null bytes might be stripped in objdump)

for i in `objdump -d print_flag | tr '\t' ' ' | tr ' ' '\n' | egrep '^[0-9a-f]{2}$' ` ; do echo -n "\x$i" ; done

  • Good to know stuff about x64 syscall

X86-64 system calls use syscall instruction. This instruction saves return address to rcx, and after that it loads rip from IA32_LSTAR MSR. I.e. rcx is immediately destroyed by syscall. This is the reason why rcx had to be replaced for system call ABI.

This same syscall instruction also saves rflags into r11, and then masks rflags using IA32_FMASK MSR. This is why r11 isn't saved by the kernel.

So, these changes reflect how the syscall mechanism works. This is why the kernel is forced to declare rcx and r11 as not saved and even can't use them for parameter passing.

According to System V X86-64 ABI, function calls in the applications use the following sequence of registers to pass integer arguments:

rdi, rsi, rdx, rcx, r8, r9

But system call arguments (other than syscall number) are passed in another sequence of registers:

rdi, rsi, rdx, r10, r8, r9

Why is rcx replaced by r10?

For x86, the order of registers for a syscall is as follow:

ebx, ecx, edx, esi, edi

eax stores the syscall number in both cases.

Sometimes executables maybe compressed with UPX leading to its behaviour not being what you expect it to. Use strings [executable] | grep UPX to see if there is UPX compression! Also see the flag challenge on pwnable.kr

  • Note: x64 provides a new rip-relative addressing mode. Instructions that refer to a single constant address are encoded as offsets from rip. For example, the mov rax, [addr] instruction moves 8 bytes beginning at addr + rip to rax.

  • Shellcode to jump to an address

    • It's not as easy as it sounds, since there are many different opcodes for jmp, each with different syntax for the address. The opcode changes based on whether you want to jump to an address relative to the current address of the shellcode or to an absolute address.
    • From my share of experience, I believe that the best way to jump to an absolute address is to avoid the jmp instruction altogether and rather use the following assembly:
    push [Absolute address]
    ret
    
    • For relative addresses, depending on the magnitude of difference between the current and destination address, there are different opcodes. The most commonly used is E9. With this you can make a jump of as large as a 32-bit value can be. A common mistake is to use E9 08 01 to jump 264 bytes from the current instructions, but this offset is actually from the next instruction; so in this case you would want to use E9 05 01. Basically destination_address - current_address - sizeof("E9 xx xx xx xx") is how much you want to jump.
    • Reference for syntax and opcodes
  • To write 4 bytes, 2 bytes, 1 byte using format string vulnerability use %n, %hn, %hhn respectively.

  • When executing a binary, whether a given page is executable or not can be inferred from the contents of the special file /proc/$$/maps where $$ is the process ID.

  • When using IDA with wine in MacOS, Command key maps to Alt and not the option key.

  • leal in AT&T sytax means the same thing as lea in Intel syntax.

Awesome way to send hybrid hex input to a binary on the go

while read -r line; do echo -e $line; done | ./my_binary

Now you'll be able to give input like \x41l33t\x41 which normally interacting with the binary. This will be read by the bash shell, converted to Al33tA and then sent to the binary! Try it out!