pwndocs

pwndocs is a dummy-friendly guide to pwn.

The goal of this guide is to not only teach people the basics of pwn, but help them gain an understanding of what actually happens under the hood.

By helping to overcome the daunting initial step, hopefully people will gain the motivation and know-how to venture into more advanced/complicated topics on their own.

This blog is still heavily a work-in-progress. Stay tuned to my twitter to stay up to date with the development of this guide.

About Me

I am Elma, a university student residing in Singapore.

You can find out more about me at my blog.

This guide is purely a passion project of mine.

If you enjoy the content, it would be awesome if you could buy me a coffee!

Pwn By Example

Inspired by Solidity by Example, this section aims to ease the reader's understanding of pwn by presenting detailed walkthrough of simplified challenges.

In order to maximize your learning, I would recommend that you download and try the challenge for yourself before reading the writeup. After reading and understanding the writeup, attempt to solve it on your own from scratch without referring back to the writeup.

After all, this guide is only a means to aid in your learning. Most of the learning will only happen when you try it for yourself!

how2pwn?

At some point, everyone has their own method as to how to approach a CTF pwn challenge. However, when you're starting out, it is perfectly normal to find yourself totally lost when faced with a program and unsure as to where to begin looking or what to do.

it is okay if you find yourself stumped... (haha geddit)

A Textbook Approach to Pwn

If you already have some experience in doing Pwn challenges and are fairly confident in it, feel free to move ahead and skip this section!

If you are totally new and need a guideline on how you can best approach a challenge, this is for you. We will explore a 5 step process on how we can approach and attempt to solve a pwn challenge.

  • Running and Fuzzing the Program
  • Analyzing the Source Code
  • Understanding the Exploit
  • Writing the Exploit
  • Debug your Exploit

Let's see what each of these steps entails.

1. Running and Fuzzing the Program

Info

Fuzzing is when we provide invalid, malformed, or unexpected inputs into a system to reveal software defects and vulnerabilities. (i.e. large strings, negative integers etc.)

In short, we try to input as much nonsense as we can to break a program!

Running the program is extremely important as it helps us gain a very quick understanding of the program functionalities.

Fuzzing the program helps us to potentially find bugs in a program quickly. If we are able to crash the program, it might imply that there is a vulnerability that we can exploit!

2. Analyze the Source Code

Running the program only shows us what the program wants us to see, but we are blind to other things that the program may be doing under the hood.

We will have to analyze the code of the program to gain a better understanding of how the program works. This is known as static analysis.

  • If the program crashed in step 1, this is where you triage and find out what bug is present in the program.

  • The idea of source code analysis is to have a brief understanding on what each function does in general.

Tip

When we try to look for vulnerabilities in a source code, one tip is to look out for inputs that a program takes in. Are these inputs vulnerable? This could be our entrypoint to triggering a bug!

If the source code is not provided to us, we will have to go one step further to use a tool such as IDA/Ghidra to reverse engineer the program. Find out more here

3. Understand the Exploit

If you have reached this stage, it means that you have already found a vulnerability from the previous 2 stages.

Ask yourself some questions:

  • What kind of vulnerability is it?

  • What does this vulnerability allow us to do?

  • What is our end goal and what do we want to accomplish?

Start planning how you can make use of the vulnerability to accomplish your objective.

4. Writing the Exploit

We typically write our exploits using the pwntools python library.

pwntools is a python library that is used to easily interact with executables/remote services and also comes with many convenient functionalities specific to pwning.

You can find a pwntools guide here, but ultimately, you will only become familiar with it through practice.

5. Debugging the Exploit

Most of the time, even if we have planned out and written our exploit, it is not going to work out of the box.

A huge part about writing exploits is also the ability to debug it. Attach your debugger to your pwntools script with gdb.attach(p).

Using the debugger, why did the program crash? What instruction did it crash on?

What are you expecting to see in your debugger? (i.e. if doing ROP, you expect to see your ROP chain at the ret instruction on the top of the stack)

Does your exploit work as intended? Why not and how can we fix it?

Conclusion

Definitely, if you are a pro and have your own workflow, then you don't have to strictly follow this. This is merely a recommendation for beginners who are totally lost.

Most of the guides in this chapter will follow the structure covered above.

Return Oriented Programming

In this guide, we will attempt to solve the following challenge. I strongly encourage you to download these files and try it out for yourself/follow along.

Miscellaneous

If you are pwning and you have yet to face any weird unexplainable pitfalls/bugs so far, you are probably not pwning enough.

Sometimes we encounter random bugs that other people don't face, despite taking the exact same steps documented in any guidebook. This creates a false sense of difficulty and a barrier of entry, especially to newbies who are already struggling with the crucial concepts itself.

This section aims to document some environmental issues that we might encounter when we attempt pwn challenges (or even play CTFs for that matter).

Apart from that, weird random topics that are difficult to classify will also be parked in this section.

The Infamous MOVAPS issue

If you are attempting to write a ROP chain, and your program crashes at some weird movaps instruction, chances are that you are facing a stack alignment issue. This is also the cause of many exploits "working locally but not remotely".

Analyzing a MOVAPS crash

Let's first look at what a typical crash brought about by MOVAPS looks like in GDB.

movaps issue illustrated in GDB

As you can see, the program crashes with Segmentation Fault upon hitting the movaps instruction.

*RSP  0x7fffa09ee928 ◂— 0x0
...
<_int_malloc+2832>    movaps xmmword ptr [rsp + 0x10], xmm1

Looking at this movaps documentation, it mentions that

When the source or destination operand is a memory operand, the operand must be aligned on a 16-byte (0x10) boundary or a general-protection exception (#GP) is generated.

This basically means that our memory address that is referenced has to be a multiple of 0x10 bytes. However, in this case, our memory operand is 0x7fffa09ee928+0x10=0x7fffa09ee938 -- evidently not a multiple of 0x10.

Fixing the MOVAPS issue

In order to align our stack, we simply have to increment or decrement our RSP by 0x8. This means that as long as we push or pop a value on our stack before we call the function, we should be able to align our stack! There's so many different ways we can do this, but I will cover the two simplest way.

Add an additional ret

Assuming that you have a ROP chain, we can simply add a ret instruction/gadget before we call our function.

Tip

ret is functionally the same as pop rip.

Skip the function prologue

Assume we are trying to call a function win() that looks like this

void win() {
    system("/bin/sh");
}

The assembly for such a function will look like this:

Dump of assembler code for function win:

   // function prologue
   win+0:     endbr64
   win+4:     push   rbp
   win+5:     mov    rbp,rsp

   // load argument
   win+8:     lea    rax,[rip+0xeac]        # 0x2004
   win+15:    mov    rdi,rax

   // call system
   win+18:    call   0x1050 <system@plt>

   // function epilogue
   win+23:    nop
   win+24:    pop    rbp
   win+25:    ret

As we can see, the only relevant part of the code starts from win+8.

If we face a movaps issue, we can simply return to win+5 or win+8 directly, skipping the initial push rbp instruction.

This would offset our RSP by 8, and fix the stack alignment issue (if any).

The Decompiler