专栏名称: 看雪学苑
致力于移动与安全研究的开发者社区,看雪学院(kanxue.com)官方微信公众帐号。
目录
相关文章推荐
学习强国  ·  月球上能上网吗?能种菜吗?最新回应! ·  2 天前  
学习强国  ·  月球上能上网吗?能种菜吗?最新回应! ·  2 天前  
结构先声  ·  践行国家战略,共建自主生态 | ... ·  3 天前  
结构先声  ·  践行国家战略,共建自主生态 | ... ·  3 天前  
黄昏即景  ·  9.24 ·  3 天前  
黄昏即景  ·  9.24 ·  3 天前  
FreeBuf  ·  2024攻防演练复盘总结 ·  6 天前  
奇果酱  ·  放弃 Windows,华为鸿蒙 PC 要来了! ·  6 天前  
奇果酱  ·  放弃 Windows,华为鸿蒙 PC 要来了! ·  6 天前  
51好读  ›  专栏  ›  看雪学苑

m0leCon CTF 2025 Teaser re Embedded encryption RISCV 对称密码

看雪学苑  · 公众号  · 互联网安全  · 2024-09-24 17:59

正文

RISCV 32位,看起来是固件的代码。




架构、硬件相关API


大部分架构、硬件相关函数看名字猜功能。

metal_gpio_get_input_pin函数百度一下,是从针脚读数据:
__inline__ int metal_gpio_get_input_pin(struct metal_gpio *gpio, int pin)
// Get the value of the GPIO pin.

ida中F5的结果(美化了一下,应该是虚函数调用)如下,其返回值类型不是int而是bool:
bool __fastcall metal_gpio_get_input_pin(int device, char pin)
{
return device && (device->func(device) & (1 << pin)) != 0;
}

调用了随机数相关的api:srand和rand,实现和我电脑上的编译器调的库不一样,这两个API的实现也在ELF中给出来了,后续直接复制ida f5的结果出来用就行。




初始化


有一个数组长度是1604,ida里设成1608显示的效果好一点。

数组初始化内容如下:
data[0:32) (32字节) = 0, 1, 2, 3, ... , 31
data[32:65) (32+1字节) = "ptm{REDACTEDREDACTEDREDACTEDRED}\x00"
data[68:324) (256字节) = 0, 1, 2, 3, ... , 255





过程分析


这里以最开始的一段打乱IP置换表的代码为例,ida里f5出来是这个样子的:

v12 = &var644[(unsigned __int8)v10 + 1604];
v13 = &var644[(unsigned __int8)v11 + 1604];
v14 = *(v12 - 1604) + *(v13 - 1604);
*(v12 - 1604) = v14;
v15 = *(v13 - 1604);
*(v13 - 1604) = v14 - v15;
*(v12 - 1604) = v15 + *(v12 - 1604) - v14;

实际上v12=var644 + v10 + 1604,这是以栈顶指针的去索引数组,可能是编译器的锅,搞出来的代码有点混乱,后续的*(v12 - 1604)等价于var644[v10]

v14 = var644[v10] + var644[v11];
var644[v10] = v14;
v15 = var644[v11];
var644[v11] = v14 - v15;
var644[v10] = v15 + var644[v10] - v14;

这段代码简单分析一下会发现,最后是把var644[v10]和var644[v11]进行了一下交换。

按上述方法分析整个main函数,发现unsigned char var644[1604]逻辑上可以分成四个数组:

_BYTE m32[32]; // &var644[0:32]
_BYTE input[33]; // &var644[32:64]
_BYTE m256[256]; // &var644[68:324]
_DWORD output[256]; // &var644[580:1604]

其中m32是IP置换表、m256是P盒映射表,input、output是缓冲区。

简单说一下,IP置换就是交换数组中元素的位置,P盒运算就是一个一对一的映射。




加密过程


main流程总结:

◆流程1:重复1337次


重复1000次

随机交换m32的两个字节


重复10000次

随机交换m256的两个字节


IP置换:i: 0 -> 31

output[i] = input[m32[i]]


IP置换:i: 0 -> 31

input[i] = output[m32[i]]


P盒运算:i: 0 -> 31

input[i] = m256[input[i]]


i: 0 -> 31

input[i] ^= rand()


◆流程2:j: 0 -> 31

i: 0 -> 255

output[i] <<= 1


output[input[j]] ^= 1


◆将output视作16x16的矩阵

◆输出
将output每行加起来,然后输出

◆输出
将output每列加起来,然后输出

流程1是个比较常规的对称密码加密过程,不再详细叙述。
加密所用的表、数据取自随机函数生成器,相当于密钥是个随机数种子。
seed取自metal_gpio_get_input_pin,是个1位的值。
流程2,实际是把input最后一轮的结果,映射到output上。
整个过程等价于output[input[j]] = 1 << (31-j)。
然后将output当成二维方阵,输出行列和。




流程2 解密


首先从行列和还原output。

易得知,output矩阵中不同两个元素不会在相同的位同时为1,即若a!=boutput[a]&output[b]==0为真。

因此,若行和为0x101,行和第0位和第8位为1,表示该行中一个元素第0位为1,一个元素第8位为1,这两个元素可以相同。即该行存在0x100、0x1或0x101,其余为0。

同理列和也可以如此分解。

记录下第0位为1的元素所在的行和列,即可知道output矩阵中该位置第0位为1。

def get_bit_idx(x):
i = 0
res = []
while x:
if x & 1:
res.append(i)
i += 1
x >>= 1
return res

rows = [0]*32
cols = [0]*32
for r in range(16):
res = get_bit_idx(row_sums[r])
for a in res:
rows[a] = r
for c in range(16):
res = get_bit_idx(col_sums[c])
for a in res:
cols[a] = c

output = [0]*256
for i in range(32):
output[rows[i]*16 + cols[i]] |= 1 << i

然后由output还原最后一轮的input。

加密过程分析得到的output[input[j]] = 1 << (31-j)
因此若(output[i] & (1 << j)) != 0,则input[31-j] = i

inputs = [0]*32
for i in range(256):
res = get_bit_idx(output[i])
for j in res:
j = 31 - j
inputs[j] = i
print(inputs)





流程1 对称密码 解密


首先seed只能是0或1,跑两次即可。

接着是对称密码解密,IP置换、P盒运算、异或运算都是可逆的。

其中IP置换的置换表、P盒运算的映射表、异或运算的值都是由rand随机获取的。

所以需要先缓存1337轮的置换表、映射表、异或数据,然后执行这三种操作的逆运算即可。

_BYTE m32s[1337 + 1][32]; // &var644[0]
_BYTE m256s[1337 + 1][256]; // &var644[68]
_BYTE xors[1337][32];
void decrypt(unsigned int seed, _BYTE* last_input) {
_BYTE input[33] = { 0, };
_DWORD output[256] = { 0, }; // &var644[580]

memcpy(input, last_input, 32);

int i, j;

_BYTE rm256[256];

// cache rand result
srand(seed);
for (i = 0; i != 32; ++i)
m32s[0][i] = i;
for (i = 0; i != 256; ++i)
m256s[0][i] = i;
for (j = 0; j < ROUNDS; j++) {
for (i = 0; i < 1000; i++) {
_BYTE a = (int)rand() % 32;
_BYTE b = (int)rand() % 32;
if (a != b)
{
_BYTE temp;
temp = m32s[j][a];
m32s[j][a] = m32s[j][b];
m32s[j][b] = temp;
}
};
for (i = 0; i < 10000; i++) {
_BYTE a = rand();
_BYTE b = rand();
if (a != b)
{
_BYTE temp;
temp = m256s[j][a];
m256s[j][a] = m256s[j][b];
m256s[j][b] = temp;
}
};
for (i = 0; i != 32; ++i)
xors[j][i] = rand();
// copy to next round
for (i = 0; i != 32; ++i)
m32s[j+1][i] = m32s[j][i];
for (i = 0; i != 256; ++i)
m256s[j+1][i] = m256s[j][i];
}

for (j = ROUNDS - 1; j >= 0; j--) {
// rbox
for (i = 0; i < 256; i++)
rm256[m256s[j][i]] = i;
// decrypt
for (i = 0; i != 32; ++i)
input[i] ^= xors[j][i];
for (i = 0; i != 32; ++i)
input[i] = rm256[input[i]];
for (i = 0; i != 32; ++i)
output[m32s[j][i]] = input[i];
for (i = 0; i != 32; ++i)
input[m32s[j][i]] = output[i];
};

// print input
printf("input = ");
for (i = 0; i < 33; i++)
printf("%u, ", input[i]);
putchar(10);
printf("input = %s\n", input);
}

int main(int argc, const char** argv, const char** envp)
{
_BYTE last_input_flag[33] = { 47, 238, 122, 29, 149, 143, 143, 247, 59, 106, 136, 53, 69, 229, 45, 255, 13, 10, 226, 239, 237, 247, 7, 100, 159, 65, 44, 193, 159, 106, 155, 236, 0 };
decrypt(0, last_input_flag);
decrypt(1, last_input_flag);
return 0;
}




看雪ID:wx_御史神风

https://bbs.kanxue.com/user-home-907036.htm

*本文为看雪论坛优秀文章,由 wx_御史神风 原创,转载请注明来自看雪社区



# 往期推荐

1、Alt-Tab Terminator注册算法逆向

2、恶意木马历险记

3、VMP源码分析:反调试与绕过方法

4、Chrome V8 issue 1486342浅析

5、Cython逆向-语言特性分析



球分享

球点赞

球在看



点击阅读原文查看更多