标 题:
【原创】逆向记事本看UTF8编码判断错误
作 者: margen
时 间: 2009-11-12,16:58:32
链 接: http://bbs.pediy.com/showthread.php?t=101120
本人菜鸟,有不对的地方请各位大侠轻点拍砖,谢谢
打开记事本,输入“去”字,用ANSI方式保存。再重新打开,如果不出意外的话,看到的竟然是乱码。为了搞清楚“去”到底怎么变的身,开始了我非常不熟练的IDA+WinDbg逆向,权当是练手。
首先用UE以十六进制的方式打开txt文件,其十六进制内容竟然是 0xFEFF 0x0225。0xFEFF是UNICODE编码的签名字节,而“去”的UNICODE编码是0x53bb,此处的0x0225显然是错误的。起初怀疑是记事本在保存时,就以其他的编码当作UNICODE写入到了文件中。WinDbg载入,跟踪到notepad!SaveFile,仔细看了一下,保存的过程如下:WideCharToMultiByte( GetACP(), ... , lpEditWCharBuf, ... );然后直接WriteFile写入到txt文件中,并没有进行任何附加的操作。那么多余的0xFEFF UNICODE签字和0x0225哪儿来的呢?无奈自己手写了CreateFile、ReadFile,终于看清,txt文件里保存的确实是“去”的ANSI编码的两个字节:0xA5C8。原来UE在以16进制方式查看有问题的txt文件时,也犯了跟记事本一样的解码错误。
重新从记事本载入文件入手。从LoadFile跟踪到NpOpenDialogHookProc,一直到fDetermineFileType,终于找到了问题的根源:notepad!IsTextUTF8函数。先说说fDetermineFileType,其汇编代码不长,如下:
; int __stdcall fDetermineFileType(LPVOID lpBuffer,int cb)
_fDetermineFileType@8 proc near ; CODE XREF: NpOpenDialogHookProc(x,x,x,x)+24A p
lpBuffer = dword ptr 8
cb = dword ptr 0Ch
mov edi, edi
push ebp
mov ebp, esp
push edi
mov edi, [ebp+cb]
xor eax, eax
cmp edi, 1
jbe short loc_10023FF
push esi
mov esi, [ebp+lpBuffer]
movzx ecx, word ptr [esi]
cmp ecx, 0BBEFh
jz short loc_10023F0
cmp ecx, 0FEFFh
jz short loc_10023D7
cmp ecx, 0FFFEh
jz short loc_10023EC
push edi ; cb
push esi ; lpBuffer
call _IsInputTextUnicode@8 ; IsInputTextUnicode(x,x)
test eax, eax
jz short loc_10023DC
loc_10023D7:
xor eax, eax
inc eax
jmp short loc_10023FE
loc_10023DC:
push edi
push esi
call _IsTextUTF8@8 ; IsTextUTF8(x,x)
neg eax
sbb eax, eax
and eax, 3
jmp short loc_10023FE
loc_10023EC:
push 2
jmp short loc_10023FD
loc_10023F0:
cmp edi, 2
jbe short loc_10023FE
cmp byte ptr [esi+2], 0BFh
jnz short loc_10023FE
push 3
loc_10023FD:
pop eax
loc_10023FE:
pop esi
loc_10023FF:
pop edi
pop ebp
retn 8
_fDetermineFileType@8 endp
翻译成C代码更直观:
int __stdcall fDetermineFileType(LPVOID lpBuffer,int cb)
{
int iType = 0;
WORD wSign = 0;
if( cb <= 1 )
return 0;
wSign = *(PWORD)lpBuffer;
switch( wSign )
{
case 0xBBEF:
{
if( cb >= 3 && (PBYTE)lpBuffer[
2] == 0xBF)
iType = 3;
}
break;
case 0xFEFF:
{
iType = 1;
}
break;
case 0xFFFE:
{
iType = 2;
}
break;
default:
{
if( !IsInputTextUnicode( lpBuffer, cb ) )
{
if( IsTextUTF8( lpBuffer, cb ) )
iType = 3;
}
else
iType = 1;
}
}
return iType;
}
当记事本打开任何一个文件时,首先会调用这个函数确定这个文本文件的编码方式。对于ANSI编码的GBXXXX标准的中文字符来说,直到IsTextUTF8函数返回FALSE,Notepad.exe才会以CP_ACP的方式调用MultiByteToWideChar,直到这时中文字符才会被正确解析,也就是说,如果IsTextUTF8返回了TRUE,那么ANSI的中文字符就会被当成UTF8编码转换成Unicode再显示出来。问题的根源就是IsTextUTF8了。看MS的家伙们是怎么写的IsTextUTF8:
; __stdcall IsTextUTF8(x,x)
_IsTextUTF8@8 proc near ; CODE XREF: fDetermineFileType(x,x)
Buffer = dword ptr 8
Size = dword ptr 0Ch
mov edi, edi
push ebp
mov ebp, esp
push esi
xor esi, esi
xor ecx, ecx
inc esi
xor edx, edx
cmp [ebp+Size], ecx
jle short FALSE
Loop:
mov eax, [ebp+Buffer]
mov al, [ecx+eax]
test al, al
jns short MayBeAscii
xor esi, esi
MayBeAscii:
test edx, edx
jnz short LeftBytes
cmp al, 80h
jb short LoopContinue
BytesCount:
shl al, 1
inc edx
test al, al
js short BytesCount
dec edx
jz short FALSE
jmp short LoopContinue
LeftBytes:
and al, 0C0h
cmp al, 80h
jnz short FALSE
dec edx
LoopContinue:
inc ecx
cmp ecx, [ebp+Size]
jl short Loop
test edx, edx
ja short FALSE
test esi, esi
jz short TRUE
FALSE:
xor eax, eax
jmp short QUIT
TRUE:
xor eax, eax
inc eax
QUIT:
pop esi
pop ebp
retn 8
_IsTextUTF8@8 endp
还是翻译成C:
BOOL IsTextUTF8( LPSTR lpBuffer, int iBufSize )
{
/*
0zzzzzzz;
110yyyyy, 10zzzzzz
1110xxxx, 10yyyyyy, 10zzzzzz
11110www, 10xxxxxx, 10yyyyyy, 10zzzzzz
*/
int iLeftBytes = 0;
BOOL bUtf8 = FALSE;
if( iBufSize <= 0 )
return FALSE;
for( int i=0;i<iBufSize;i++)
{
char c = lpBuffer[i];
if( c < 0 ) //至少有一个字节最高位被置位
bUtf8 = TRUE;
if( iLeftBytes == 0 )//之前尚无UTF-8编码的字符的前导字节,或者是下个字符。
{
if( c >= 0 ) //0000 0000 - 0100 0000
continue;
do//统计出高位连续的的个数
{
c <<= 1;
iLeftBytes++;
}while( c < 0 );
iLeftBytes--; //表示本字符的剩余字节的个数;
if( iLeftBytes == 0 )//最高位是10,不能作为UTF-8编码的字符的前导字节。
return FALSE;
}
else
{
c &= 0xC0; //1100 0000
if( c != (char)0x80 )//1000 0000 对于合法的UTF-8编码,非前导字节的前两位必须为。
return 0;
else
iLeftBytes--;
}
}
if( iLeftBytes )
return FALSE;
return bUtf8;
}
根据国际相关组织的规定,UTF8用1-4个字节来表示UNICODE,对于绝对值在7F以上的字符,UTF-8用两个及以上的字节表示,其中第一个字节的高位连续的1的个数,表示“用UTF8编码表示的字符的字节数”,此字符其他的字节必须是以10开头。如某UTF8编码的字符,第一个字节为110X XXXX,那么说明表示此字符的UTF-8编码要用到两个字节,其第二个字节必须是10YY YYYY。否则就是非法的UTF8编码。
这样说来,只要其ANSI编码值为0x8yCx— 0xByCx和0x8yDx-0xByDx字符,都被会解释成UTF-8编码:
WORD wChar = 0;
char szBuf[3]={0};
for( wChar = 0x80C0; wChar < 0xBFDF; )
{
*(PWORD)szBuf = wChar;
cout<<szBuf;
if( (wChar & 0xFF) == 0xDF )//11011111
{
wChar += 0x100;
wChar &= 0xFFC0;
}
else
wChar += 1;
}
这个编码范围的汉字,绝大多数我还是不认识的,但常用的还是有不少。以至于你写个“小丫头”、“泉水”、“来去”、“千十”等常用字保存,再用记事本打开,看到的都是乱码。
至此,“去”字引起的乱码现象,终于得到了完满解决。*转载请注明来自看雪论坛@PEdiy.com