未知算法0x00(huffman+lzss)
定位函数
为了分析该算法,我们来到第二个xb文件:
TH:0x0477D673(RA:0x8002013A) sceIoOpen("umd1:", 0x00000001, 00, ) = 3 |
对应的文件为:
0390656 , /PSP_GAME/USRDIR/xbdata/yumo/common.xb
用WinHex打开该xb文件,看到包内部第一个文件采用了0x00的算法
Offset 0 1 2 3 4 5 6 7 8 9 A B C D E F |
同样的在 0x2A*4和0x2A*4 + 8偏移地址设置读断点:
bpset 0x08BE4868 r |
(注:因为采用了同样的地址,第一次会停在loading.xb上,跳过这次。直到在函数参数所对应的地址上发现了我们在WinHex上看到的预期数据)
bpset 0x08BE4870 r |
这时打印一下函数的调用关系:
bpset 0x08BE4868 r |
将怀疑点放在sub_0882ac0c
确认函数功能
host0:/> bpset 0x882AC0C host0:/> memdump 0x08BE497C |
再根据caller调用时参数的传递和返回情况。确定函数原型
u8* sub_0882AC0C(u8* src, u8* dst); |
反编译过程
编写模拟代码
1: u32 sub_0882AC0C(u8* src, u8* arg1)
2: {
3: t2 = *src;
4: t8 = 0;
5: t7 = 1;
6: src++;
7: if(t2>0)
8: {
9: t1 = t7;
10: loc_882ac24:
11: v0 = *src;
12: src++;
13: t6 = v0 - 1;
14: if(v0>0)
15: {
16: t0 = t1 << t7;
17: v1 = t0 <<1;
18: a3 = t7 -1;
19: loc_882ac40:
20: t4 = t8;
21: t5 = 0;
22: t3 = a3;
23: if(t7>0)
24: {
25: loc_882ac50:
26: v0 = t4 & 1;
27: a2 = t5 << 1;
28: t5 = a2 | v0;
29: v0 = t3;
30: t4 = t4 >> 1;
31: t3 --;
32: if(v0 > 0)
33: {
34: goto loc_882ac50;
35: }
36: }
37: loc_882ac6c:
38: a2 = *src;
39: at = 1:0 ? (t5<0x400)
40: src++;
41: if(at !=0 )
42: {
43: v0 = t5 << 1;
44: v0 = arg1 + v0;
45: loc_882ac84:
46: *(u8 *)v0 = t7;
47: t5 += t0;
48: *(u8 *)(v0+1) = a2;
49: at = 1:0 ? (t5 < 0x400)
50: v0 += v1;
51: if(at != 0)
52: {
53: goto loc_882ac84;
54: }
55: }
56: loc_882ac9c:
57: at = 1:0 ? (t7 < 0xb);
58: if(at != 0)
59: {
60: t8++;
61: v0 = t6;
62: }
63: else
64: {
65: v0 = t6;
66: }
67: t6 --;
68: if(v0 > 0)
69: {
70: goto loc_882ac40;
71: }
72: }
73: loc_882acb8:
74: t7 ++;
75: at = 1:0 ? (t2 < t7);
76: t8 = t8 << 1;
77: if(at == 0)
78: {
79: goto loc_882ac24;
80: }
81: }
82: v0 = src & 1;
83: if(v0 == 0)
84: {
85: v0 = src;
86: }
87: else
88: {
89: src++;
90: v0 = src;
91: }
92: return v0;
93: }
整理以及验证
经过整理获得下面代码
1: u8* restore_table(u8* src, u8* dst)
2: {
3: u32 cnt, mask, cnt1, data, p, weight;
4: int ii, i, index;
5: u8 character;
6:
7: cnt = *src++;
8: mask = 0;
9:
10: if(cnt>0)
11: {
12: for(index = 1; index<=cnt; index++)
13: {
14: cnt1 = *src++;
15: if(cnt1>0)
16: {
17: weight = 1 << index;
18: //v1 = t0 <<1; /*unused opcode ? */
19: for(ii=0; ii<cnt1; ii++)
20: {
21: data = mask;
22: p = 0;
23: if(index>0)
24: {
25: for(i=0; i<index; i++)
26: {
27: p = (p<<1) | (data &1);
28: data = data >> 1;
29: }
30: }
31: character = *src++;
32: if(p<0x400)
33: {
34: while(p<0x400)
35: {
36: dst[p*2 + 0] = index;
37: dst[p*2 + 1] = character;
38: p+=weight;
39: //v0+=v1; /* unused opcode ? */
40: }
41: }
42: if(index < 0xb)
43: {
44: mask ++;
45: }
46: }
47: }
48: mask = mask << 1;
49: }
50: }
51: if((u32)src&1)
52: {
53: return src+1;
54: }
55: else
56: {
57: return src;
58: }
59: }
通过和lzss相同的方法对上面的代码进行验证,发现有下面几个不一样的地方:
memory check failed offset[0x4fe] dst[0xff]!=src[0x82] |
这就需要再仔细分析汇编代码,必要的时候通过PSPLink在PSP上进行验证。
对于有返回值的函数,同时也要验证返回值是否一致。
ret = src + 0x10C |
因为函数:u32 sub_0882AC0C(u8* src, u8* arg1)只是从压缩数据的头部将字典恢复出来,所以我们还需要分析他的调用者,看看是他是如何通过这个字典进行解码的。于是来到函数sub_0882ACE4。
编写主体部分模拟代码
1: void sub_0882ACE4(u8* dst, u8* src, u32 dst_len)
2: {
3: u8 table[TABLE_LEN];
4: u8* p;
5:
6: s0 = dst;
7: a0 = src;
8: s1 = len;
9: a1 = table;
10:
11: a1 = restore_table(a0, a1);
12: a2 = s0 + s1;
13: at = 1:0 ? (s0<a2);
14: a1 = v0;
15: a3 = 0;
16: t0 = 0;
17: if(at == 0)
18: {
19: return;
20: }
21:
22: at = 1:0 ? (t0<0x10);
23: loc_882ad24:
24: if(at !=0)
25: {
26: v1 = *(u16 *)a1;
27: v1 = v1 << t0;
28: a3 = a3 | v1;
29: a1+=2;
30: t0+=0x10;
31: v1 = a3 & 0xffff
32: }
33: else
34: {
35: v1 = a3 & 0xffff;
36: }
37: loc_882ad44:
38: v1 = v1 & 0x3ff;
39: v1 = v1 <<1;
40: v1 = v1+sp;
41: a0 = v1 + 0x10;
42: v1 = *(u8*)a0;
43: at = 1: 0? (v1 < 0xb);
44: if(at==0)
45: {
46: t0 = t0 - 0xa;
47: at = 1:0 ? (t0<0x10);
48: a3 = a3 >> 0xa;
49: if(at != 0)
50: {
51: v1 = *(u16*)a1;
52: v1 = v1 << t0;
53: a3 = a3 | v1;
54: a1 += 2;
55: t0 += 0x10;
56: }
57: v1 = a3 & 0xffff;
58: *(u8*)s0 = v1;
59: s0++;
60: a3 = a3 >> 8;
61: t0 = t0 - 8;
62: }
63: else
64: {
65: v1 = *(a0+1);
66: *(u8*)s0 = v1;
67: v1 = *(a0);
68: s0++;
69: a3 = a3 >> v1;
70: t0 = t0 -v1;
71: }
72: loc_882adb4:
73: v1 = 1:0 ?(s0<a2);
74: if(v1 != 0)
75: {
76: goto loc_882ad24:
77: }
78: return;
79: }
整理以及验证
经过整理过的代码:
1: void huffman_decoder(u8* dst, u8* src, u32 dst_len)
2: {
3: u8 table[2048];
4: u8 character, *end;
5: u16 code, clen, nbits;
6: u32 cache;
7:
8: end = dst + dst_len;
9:
10: memset(table, 0xff, 2048);
11: src = restore_table(src, table);
12:
13: if(dst > end)
14: {
15: return;
16: }
17:
18: cache = 0;
19: nbits = 0;
20:
21: while(dst < end)
22: {
23: if(nbits<16)
24: {
25: cache |= (*(u16*)src)<<nbits;
26: src+=2;
27: nbits+=16;
28: }
29: code = (cache & 0xffff) & 0x3ff;
30: clen = table[code*2 + 0];
31: character = table[code*2 + 1];
32:
33: if(clen > 0xb)
34: {
35: nbits -= 0xa;
36: cache = cache >> 10;
37: if(nbits<16)
38: {
39: cache |= (*(u16*)src)<<nbits;
40: src += 2;
41: nbits += 16;
42: }
43: *dst++ = (u8)(cache & 0xffff);
44: cache >>= 8;
45: nbits -= 8;
46: }
47: else
48: {
49: *dst++ = character;
50: cache >>= clen);
51: nbits -= clen);
52: }
53: }
54: return;
55: }
通过在PSPLink上的跟踪可以发现huffman_decoder返回后,在寄存器a0所对应的地址上出现的仍为压缩过的数据(最简单的判别方法是解压缩出来的数据的长度小于xb文件头中所记录的长度,即文件的原始长度),接下来又会进入lzss_decoder函数的领空,从lzss_decoder返回后,寄存器a0对应的地址上出现了最终的数据(长度和xb文件头中描述的一致)。
所以0x00为huffman_decoder + lzss_decoder 的压缩方式。(对应的编码过程是 lzss_encoder + Huffman_encoder)
算法总结
Huffman是一种变长编码的查表算法,每个字符根据其出现的频率获得对应的编码值。Huffman编码被认为是最优编码是因为可以证明huffman二叉树(将出现的字符作为2叉树上的每个节点;用其编码值作为其在2叉树上位置;将其出现的频率作为权值)是最优二叉树,是一种带权路径长度最短的二叉树。
下面的URL中对huffman编码进行描述
http://blog.csdn.net/xx_snoopy/archive/2009/11/23/4856652.aspx
代码实现我参考了下面的开源项目
http://sourceforge.net/projects/huffman/
该变种中,第一个变化点在于引入了一个编码截断长度的概念:
当某个字符对应的编码超过了11个bits的时候,就不对该字符进行编码了,记录时采用固定的识别码+该字符的方式。
第二个变化点在于他对码表增加了一个排序的动作(见函数restore_table)。这样保持和恢复码表的时候都变简洁。
未知算法0x10(huffman)
分析的过程和0x00基本一致,只是后面少了对lzss_decoder的调用。
结束语
先将这几种算法再列举一遍:
0x00 - 未知压缩算法,在压缩头和压缩数据之间,存在一个类似字典的数据块,并且不计入压缩数据大小;(Huffman + Lzss)
0x10 - 未知压缩算法,类似或等同于0x00,不能确定;(Huffman)
0x20 - lzss压缩或者相关变种,这种压缩破起来倒是很容易;(lzss)
0x30 - 未压缩,无压缩头,直接就是raw数据。(NONE)
学习是一个理论和实践不停相互强化的过程,任何真知灼见来自于自己的实践所得,所以不要轻信他人的观点;学习需要大家的相互交流,所以欢迎大家和我联系,所谓学无先后,达者为师;如果你有不同的观点,也欢迎你告诉我。
以上几句引自《论语·学而》
作为一个优秀的程序员,除了要有客观的态度,还需要淡定的心态。在这方面我还有很长的修炼之路。谨以此和大家共勉吧。