0CTF 2017-zer0IIvm题目分析

WriteUp 2年前 (2022) admin
729 0 0

【原理】

分析llvm的混淆插件,定位到主要函数flatten(),通过交叉引用快速定位prng_seed。通过硬编码的crc32来爆破出key的前三位

AES_PRECOMP_TE[i](x) = MixColumns(SBox(x) << 8*i)

scramble32是XorKey、SubBytes、MixColumn后跟另一个XorKey的 4 轮迭代

通过泄漏的key前三位来逐个爆破aes的key


【工具】

ida,python sage,python aes,gdb


【步骤】

在这个题中,我们拿到了一个用于 llvm 的混淆插件和一个混淆的二进制文件。该插件接受一个 16 字节的种子作为参数并将其用于内部 PRNG。我们的目标是分析二进制文件并从中恢复种子。


main函数:

.text:4004C0 main:           ; DATA XREF: _start+1D
.text:4004C0     push rbp
.text:4004C1     mov  rbp, rsp
.text:4004C4     sub  rsp, 4038Ch
.text:4004CB     mov  dword ptr [rbp-4], 0
.text:4004D2     mov  dword ptr [rbp-8], 46544330h
.text:4004D9     mov  dword ptr [rbp-0Ch], 4E52BCE7h
.text:4004E0
.text:4004E0 main_switch:       ; CODE XREF: .text:loc_7635AC
.text:4004E0     mov  eax, [rbp-0Ch]
.text:4004E3     mov  ecx, eax
.text:4004E5     sub  ecx, 8000DEEAh
.text:4004EB     mov  [rbp-10h], eax
.text:4004EE     mov  [rbp-14h], ecx
.text:4004F1     jz   loc_728049
.text:4004F7     jmp  $+5
.text:4004FC
.text:4004FC loc_4004FC:        ; CODE XREF: .text:00000000004004F7
.text:4004FC     mov  eax, [rbp-10h]
.text:4004FF     sub  eax, 8001EACAh
.text:400504     mov  [rbp-18h], eax
.text:400507     jz   loc_73E38A
.text:40050D     jmp  $+5
.text:400512
.text:400512 loc_400512:        ; CODE XREF: .text:000000000040050D
.text:400512     mov  eax, [rbp-10h]
.text:400515     sub  eax, 8003CC64h
.text:40051A     mov  [rbp-1Ch], eax
.text:40051D     jz   loc_5C0C95
.text:400523     jmp  $+5

对应伪代码:

state = 0x4E52BCE7
main_switch:
switch(state) {
    case 0x8000DEEA: goto loc_728049;
    case 0x8001EACA: goto loc_73E38A;
    case 0x8003CC64: goto loc_5C0C95;
    …
}
loc_xxxxxx:
   // do something
   state = 0x8003CC64
   goto main_switch


没得到什么比较有用的信息,来先看看混淆插件。

lib0opsPass.so

函数列表可以发现一个相当大的函数 Oops::OopsFlattening::flatten(). 通过cross-references可以迅速定位相关函数
调用 prng_seed 初始化PRNG

Oops::CryptoUtils::prng_seed(cryptoutils, &seed_string);

用PRNG生成16字节

memset(&key, 0, 0x20uLL);
LODWORD(cryptoutils_) = llvm::ManagedStatic<Oops::CryptoUtils>::operator->(&Oops::Oopscryptoutils, 0LL);
Oops::CryptoUtils::get_bytes(cryptoutils_, &key, 16);
if ( crc32(‘FTC0’, &key, 3) != 0xF9E319A6 )

hardcoded crc32 check可以很容易得到前三个字节: 179, 197, 140.
 key 用来混淆plains数组:

LODWORD(plains0) = plains[0];
LODWORD(v35) = llvm::ManagedStatic<Oops::CryptoUtils>::operator->(&Oops::Oopscryptoutils, v33);
v36 = plains0;
v37 = Oops::CryptoUtils::scramble32(v35, plains0, &key);

v60 = counter++;
plainsCUR = plains[v60];
LODWORD(v62) = llvm::ManagedStatic<Oops::CryptoUtils>::operator->(&Oops::Oopscryptoutils, v59);
v63 = Oops::CryptoUtils::scramble32(v62, plainsCUR, &key);

IDA得知数组大概大小:

.data:2345E0 ; _DWORD plains[65806]
.data:2345E0 plains          dd 0F6172961h, 0CB973739h, 904F3728h, 0DB7194B9h, 81E0B166h

gdb script :

$ cat >cmd
set confirm off
set pagination off
break *0x04004e3
commands
p/x $eax
cont
end
run

$ gdb -x cmd -n ./0llvm </dev/null >log
$ head -30 log
GNU gdb (Ubuntu 7.11.1-0ubuntu1~16.04) 7.11.1

Reading symbols from ./0llvm…(no debugging symbols found)…done.
Breakpoint 1 at 0x4004e3

Breakpoint 1, 0x00000000004004e3 in main ()
$1 = 0x4e52bce7

Breakpoint 1, 0x00000000004004e3 in main ()
$2 = 0x3ac545da

Breakpoint 1, 0x00000000004004e3 in main ()
$3 = 0xff97c58e

Breakpoint 1, 0x00000000004004e3 in main ()
$4 = 0xe83342dd

$ tail log
Breakpoint 1, 0x00000000004004e3 in main ()
$65789 = 0xf1dbf041

Breakpoint 1, 0x00000000004004e3 in main ()
$65790 = 0xdb9a21b8

Breakpoint 1, 0x00000000004004e3 in main ()
$65791 = 0xb02b5689
[Inferior 1 (process 10622) exited with code 027]
(gdb) quit

Recovering key

__int64 __fastcall Oops::CryptoUtils::scramble32(
    Oops::CryptoUtils *this, unsigned int x, const char *key)
{
  int v3; // ST20_4@1
  int v4; // ST24_4@1
  int v5; // ST20_4@1

  v3 = AES_PRECOMP_TE3[(x ^ key[3])] ^
       AES_PRECOMP_TE2[(BYTE1(x) ^ key[2])] ^
       AES_PRECOMP_TE1[((x >> 16) ^ key[1])] ^
       AES_PRECOMP_TE0[(BYTE3(x) ^ *key)];
  v4 = AES_PRECOMP_TE3[(v3 ^ key[7])] ^
       AES_PRECOMP_TE2[(BYTE1(v3) ^ key[6])] ^
       AES_PRECOMP_TE1[((v3 >> 16) ^ key[5])] ^
       AES_PRECOMP_TE0[(BYTE3(v3) ^ key[4])];
  v5 = AES_PRECOMP_TE3[(v4 ^ key[11])] ^
       AES_PRECOMP_TE2[(BYTE1(v4) ^ key[10])] ^
       AES_PRECOMP_TE1[((v4 >> 16) ^ key[9])] ^
       AES_PRECOMP_TE0[(BYTE3(v4) ^ key[8])];
  return AES_PRECOMP_TE3[(v5 ^ key[15])] ^
         AES_PRECOMP_TE2[(BYTE1(v5) ^ key[14])] ^
         AES_PRECOMP_TE1[((v5 >> 16) ^ key[13])] ^
         AES_PRECOMP_TE0[(BYTE3(v5) ^ key[12])] ^
         ((key[2] << 8) | (key[1] << 16) | (*key << 24) | key[3]);
}

aes 块. 得到S盒:

from aes import AES
A = AES()

for i in xrange(4):
    for x, t in enumerate(AES_PRECOMP_TE[i]):
        t = [BYTE3(t), BYTE2(t), BYTE1(t), BYTE0(t)]
        t2 = A.mixColumn(list(t), isInv=True)
        print t2
    print

$ python precomp.py
[99, 0, 0, 0]
[124, 0, 0, 0]
[119, 0, 0, 0]
[123, 0, 0, 0]
[242, 0, 0, 0]
[107, 0, 0, 0]

AES_PRECOMP_TE[i](x) = MixColumns(SBox(x) << 8*i)
X1 = (x1, a, b, c)
X2 = (x2, a, b, c)
X3 = (x3, a, b, c)
(X1 ⊕⊕ X2, X1 ⊕⊕ X3)

Recovering Seed

key生成算法:

// in Oops::CryptoUtils::encrypt(Oops::CryptoUtils *this, unsigned __int8 *dst, unsigned __int8 *nonce, unsigned __int8 *seed)
memset(dst, 0, 0x10uLL);
v5 = *(nonce_ + 1);
*buf = *nonce_;
*buf2 = v5;
for ( i = 0; i <= 15; ++i ) {
  seedbyte = seed_[i];
  for ( j = 0; j <= 7; ++j ) {
    if ( seedbyte & 1 ) {
      for ( k = 0; k <= 15; ++k )
        dst[k] ^= buf[k];
    }
    seedbyte = seedbyte >> 1;
    for ( l = 0; l <= 15; ++l )
      buf[l] = TABLE[buf[l]];
  }
}

TABLE是8位非线性S盒. Nonce is constant and hardcoded (note that it is increased by 1 before calling encrypt, as a big-endian number, see Oops::CryptoUtils::inc_ctr):

// in  Oops::CryptoUtils::prng_seed(struct CryptoUtils *cryptoutils, __int64 a2)
noncebuf = cryptoutils->nonce;
*noncebuf = 0xD7C59B4DFFD1E010LL;
*(noncebuf + 1) = 0x20C7C17B250E019ALL;

虽然TABLE是非线性的, 但是buf是独立更新的。buf 和 seed 却是线性相关的, 所以可以由下列脚本恢复seed:

from sage.all import *
 
from struct import pack, unpack
 
def tobin(x, n):
    return tuple(map(int, bin(x).lstrip(“0b”).rjust(n, “0”)))
 
def frombin(v):
    return int(“”.join(map(str, v)), 2 )
 
def tobinvec(v):
    return sum( [tobin(c, 8) for c in v], () )
 
PRNG_OUT = [179, 197, 140, 9, 31, 61, 9, 48, 214, 74, 172, 159, 200, 11, 185, 236]
 
TABLE = [0x0ED,0x67,0x7F,0x0F6,0x0C7,0x9A,0x24,0x12,0x0BA,0x83,0x49,0x0DB,0x13,0x0BF,0x61,0x0B0,0x0FF,0x69,0x80,0x0EC,0x0DE,0x4,0x63,0x0C4,0x96,0x73,0x1B,0x6E,0x0A6,0x9E,0x87,0x4B,0x0FC,0x10,0x2A,0x0C3,0x5C,0x2E,0x36,0x0B2,0x0DF,0x0E3,0x90,0x0FE,0x1A,0x0F,0x1C,0x84,0x1,0x15,0x3A,0x85,0x0A5,0x57,0x3F,0x6D,0x0F5,0x4A,0x0A,0x0D6,0x9F,0x64,0x0B5,0x0F7,0x8F,0x99,0x68,0x4D,0x17,0x0F9,0x0EE,0x0F0,0x3,0x6,0x4C,0x0BD,0x58,0x33,0x0A9,0x0DC,0x3C,0x0A3,0x3B,0x0D1,0x0BB,0x28,0x0F4,0x0B9,0x0CF,0x47,0x0A0,0x6A,0x0C2,0x19,0x0B,0x97,0x81,0x35,0x91,0x7C,0x5D,0x7A,0x48,0x2B,0x41,0x0D9,0x0CB,0x6F,0x56,0x8D,0x5A,0x0C5,0x3E,0x0D8,0x0C0,0x60,0x1F,0x9,0x0CA,0x7B,0x25,0x0E7,0x0AE,0x0F2,0x77,0x0FA,0x3D,0x50,0x0E2,0x4F,0x0C9,0x2C,0x53,0x45,0x0C1,0x0E9,0x46,0x0D,0x70,0x8A,0x0A1,0x0D5,0x94,0x92,0x88,0x95,0x9D,0x26,0x9B,0x0E4,0x5,0x44,0x11,0x2D,0x7,0x1E,0x0A4,0x38,0x0E1,0x0A8,0x52,0x89,0x0AF,0x40,0x72,0x0E5,0x0B4,0x7E,0x51,0x6C,0x0FB,0x76,0x62,0x0D4,0x8,0x9C,0x54,0x5B,0x75,0x29,0x0C6,0x66,0x0DA,0x0FD,0x14,0x86,0x78,0x16,0x0B6,0x8B,0x39,0x0E6,0x0B7,0x1D,0x0D3,0x18,0x0A7,0x30,0x0E8,0x23,0x37,0x7D,0x82,0x0BE,0x34,0x0C,0x55,0x0D0,0x0EF,0x0,0x0CD,0x0AC,0x0A2,0x4E,0x0B3,0x0AB,0x31,0x8E,0x21,0x0E0,0x22,0x74,0x5E,0x8C,0x32,0x0F8,0x0EB,0x2F,0x79,0x0F1,0x42,0x0C8,0x0DD,0x0CE,0x65,0x27,0x5F,0x20,0x0B8,0x0AA,0x0AD,0x71,0x6B,0x0D2,0x0EA,0x0BC,0x0E,0x0CC,0x98,0x2,0x59,0x43,0x0B1,0x93,0x0D7,0x0F3,]
 
# note 0x20… -> 0x21
nonce = pack(“<QQ”, 0xD7C59B4DFFD1E010, 0x21C7C17B250E019A)
 
cols = [map(ord, nonce)]
for i in xrange(127):
    v = [TABLE[c] for c in cols[-1]]
    cols.append(v)
cols = [vector(GF(2), tobinvec(v)) for v in cols]
target = vector(GF(2), tobinvec(PRNG_OUT))
 
m = matrix(GF(2), 128, len(cols))
for x, c in enumerate(cols):
    m.set_column(x, c)
try:
    sol = m.solve_right(target)
except ValueError:
    print “NO SOLUTION”
    quit()
 
print “SOL”, sol
res = “”
for i in xrange(0, 128, 8):
    res += chr(frombin(sol[i:i+8][::-1]))
print “flag{%s}” % res


【考点】

1、C++逆向分析

2、llvm反混淆原理

3、AES密钥爆破


0CTF 2017-zer0IIvm题目分析


原文始发于微信公众号(山石网科安全技术研究院):0CTF 2017-zer0IIvm题目分析

版权声明:admin 发表于 2022年6月7日 上午11:34。
转载请注明:0CTF 2017-zer0IIvm题目分析 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...