Thứ Năm, 16 tháng 6, 2011

DC19 Quals Retro 400 WriteUp

Retro 400 là một bài về VM khá phức tạp, vấn đề chính xoay quanh việc reverse để tìm ra cú pháp của vmcode để có thể input vào và tìm ra bug trong việc xử lí input của vm.

Bắt đầu với function được gọi sau khi start chương trình:

void __cdecl sub_80496F0(signed int a1, int a2)
{
char v2; // zf@1
signed int v3; // ecx@1
signed int v4; // ebx@1
int v5; // edi@1
char *v6; // esi@1
int v7; // [sp+0h] [bp-114h]@1
char s; // [sp+4h] [bp-110h]@1
signed int *v9; // [sp+108h] [bp-Ch]@1
__int64 v10; // [sp+114h] [bp+0h]@1

HIDWORD(v10) = &v10;
v5 = (int)"HistoryRepeatsItself\n";
v9 = &a1;
v4 = a1;
v6 = &s;
v7 = a2;
fwrite("Password: ", 1u, 0xAu, off_80D3368);
fgets(&s, 256, off_80D3360);
v3 = 22;
do
{
if ( !v3 )
break;
v2 = *v6++ == *(_BYTE *)v5++;
--v3;
}
while ( v2 );
if ( v2 )
{
sub_8048AE0(v4, v7);
while ( 1 )
sub_8048DE0();
}
exit(0);
}



Ở đây chương trình sẽ đọc và kiểm tra PassWord so với chuỗi HistoryRepeatsItself, nếu đúng sẽ gọi sub_8048AE0 với tham số là argv[1] và argv[2]. Tiếp tục với sub_8048AE0:


int __cdecl sub_8048AE0(signed int a1, int a2)
{
FILE *v2; // esi@2
int i; // edi@4
size_t v4; // eax@5
size_t v5; // ebx@5
int v6; // eax@7
int v7; // edi@8
int v8; // esi@8
void *v9; // eax@10
int v10; // ebx@10
char v11; // cl@12
int v12; // eax@15
int j; // ecx@21
int v14; // eax@22
char v15; // dl@22
char v16; // dl@23
int v17; // eax@24
int result; // eax@26
int v19; // eax@31
void *v20; // [sp+18h] [bp-420h]@4
char ptr; // [sp+1Ch] [bp-41Ch]@5
int v22; // [sp+41Ch] [bp-1Ch]@30
int v23; // [sp+420h] [bp-18h]@30
char *v24; // [sp+424h] [bp-14h]@30
int v25; // [sp+428h] [bp-10h]@30

if ( a1 <= 1 )
{
v2 = off_80D3360;
dword_80E4FC0 = 0;
dword_80E49A0 = 0;
}
else
{
dword_80E49A0 = a2 + 8;
dword_80E4FC0 = a1 - 2;
v2 = fopen(*(const char **)(a2 + 4), "r");
}
if ( !v2 )
{
perror(*(const char **)a2);
exit(0);
}
v20 = 0;
for ( i = 0; ; memcpy_0((void *)(i + v19 - v5), &ptr, v5) )
{
while ( 1 )
{
v4 = fread(&ptr, 1u, 0x400u, v2);
v5 = v4;
if ( v4 != -1 )
break;
if ( *(_DWORD *)sub_807B300() != 4 && *(_DWORD *)sub_807B300() != 35 )
goto LABEL_35;
}
if ( !v4 )
break;
i += v4;
v19 = realloc(v20, i);
if ( !v19 )
goto LABEL_35;
v20 = (void *)v19;
}
v6 = malloc1(i);
dword_80E49A8 = 0;
dword_80E4FCC = v6;
dword_80E49A4 = v6;
dword_80E49AC = v6;
if ( !v6 )
goto LABEL_35;
v7 = (int)((char *)v20 + i);
v8 = (int)v20;
while ( v8 < (unsigned int)v7 )
{
while ( 1 )
{
while ( 1 )
{
v9 = memchr((const void *)v8, 10, v7 - v8);
v10 = (int)v9;
if ( v9 )
break;
v10 = v7 - 1;
if ( v8 < (unsigned int)(v7 - 1) )
goto LABEL_12;
LABEL_29:
if ( *(_BYTE *)v8 == ' ' )
goto LABEL_30;
LABEL_21:
for ( j = v8 + 1; j < (unsigned int)v10; dword_80E49AC = v14 + 1 )
{
v14 = dword_80E49AC;
v15 = *(_BYTE *)j++;
*(_BYTE *)dword_80E49AC = v15;
}
v16 = *(_BYTE *)j;
if ( *(_BYTE *)j != '\n' )
{
v17 = dword_80E49AC;
++j;
*(_BYTE *)dword_80E49AC = v16;
dword_80E49AC = v17 + 1;
}
v8 = j + 1;
if ( j + 1 >= (unsigned int)v7 )
goto LABEL_26;
}
if ( v8 >= (unsigned int)v9 )
goto LABEL_29;
LABEL_12:
v11 = *(_BYTE *)v8;
if ( *(_BYTE *)v8 == ' ' )
break;
if ( v11 == '\t' )
break;
LABEL_14:
v8 = v10 + 1;
}
v12 = v8;
while ( 1 )
{
++v12;
if ( v12 == v10 )
break;
while ( *(_BYTE *)v12 != ' ' )
{
if ( *(_BYTE *)v12 != '\t' )
goto LABEL_14;
++v12;
if ( v12 == v10 )
goto LABEL_20;
}
}
LABEL_20:
if ( v11 != ' ' )
goto LABEL_21;
LABEL_30:
v25 = v10 - (v8 + 1);
v24 = (char *)(v8 + 1);
v8 = v10 + 1;
sub_80487B0((struc_2 *)&v24, (struc_2 *)&v22);
sub_8048A20(v22, v23);
}
LABEL_26:
result = dword_80E4FCC;
if ( dword_80E4FCC == dword_80E49AC )
LABEL_35:
exit(0);
return result;
}


Một cách cơ bản function này sẽ làm những thao tác sau:

Đọc file từ argv2 được đưa vào, đưa nội dung vào một buffer.
Đọc từng dòng thông qua việc tìm "\n" trong buffer. Đối với mỗi dòng nó sẽ tìm kiếm " "( Space ) và "\t" (Tab), nếu không có nó sẽ tiếp tục qua dòng tiếp theo.
Nếu có sẽ dùng 1 con trỏ tạm thời tăng dần lên, khi gặp một kí tự nào đó không phải space và tab thì quay lại vòng lặp bên trên để đi đến dòng tiếp theo.
Nếu đến cuối dòng mà không gặp kí tự nào khác thì sẽ kiểm tra xem kí tự đầu tiên có phải là " " hay không. Nếu đúng thì sẽ chuyển qua gọi 2 function là sub_80487B0 và sub_8048A20. Cụ thể 2 hàm này làm gì sẽ nói rõ ở phía sau.
Nếu như kí tự đầu tiên là "\t" ( tưc là khác Space ) thì chưowng trình sẽ nhảy lại lên trên để tính toán số lượng kí tự space hoặc tab và đưa vào địa chỉ dword_80E49AC.
Tổng kết lại có thể thấy cú pháp chương trình sẽ bao gồm các kí tự Space và Tab kết thúc là "\n".
Ở Function: sub_8048A20 :


nt __cdecl sub_8048A20(int AddressOfArg, int numberArg)
{
int v2; // ebx@1
void *v3; // eax@2
int result; // eax@2
int v5; // edx@2

v2 = dword_80E49A8;
if ( !(dword_80E49A8 & 0x3FF) )
{
dword_80E4FC4 = (void *)realloc(dword_80E4FC4, 12 * dword_80E49A8 + 0x3000);
if ( !dword_80E4FC4 )
{
fwrite("Out of memory!\n", 1u, 0xFu, off_80D3368);
exit(0);
}
v2 = dword_80E49A8;
}
v3 = dword_80E4FC4;
*((_DWORD *)dword_80E4FC4 + 3 * v2) = AddressOfArg;
v5 = dword_80E49AC;
*((_DWORD *)v3 + 3 * v2 + 2) = numberArg;
*((_DWORD *)v3 + 3 * v2 + 1) = v5;
result = v2 + 1;
dword_80E49A8 = v2 + 1;
return result;
}

Trong quá trình debug mình tìm ra thông số đưa vào của function này chính là địa chỉ cùa buff và số lượng các kí tự Space/Tab của dòng đó. Cho nên sau khi function sub_8048AE0 kết thúc sẽ có một vài điểm cần lưu ý:
Với file input là:
shell python -c 'print "\x09"*6+"\x20"+"\x09"+"\x20"*7+"\x20"+"\x09"*7+"\x20"+"\x09"*7+"\x0a"' > a

Tại lúc bắt đầu :0x8048de0: push ebp

Các giá trị như sau:


gdb$ x/4wx 0x080e49a4
0x80e49a4: 0x28107070 0x00000000 0x2810708f 0x00000000
gdb$ x/4wx 0x28107070
0x28107070: 0x09090909 0x20092009 0x20202020 0x09202020
gdb$ x/4wx 0x80E4FCC
0x80e4fcc: 0x28107070 0x00000000 0x00000000 0x00000000
gdb$ x/4wx 0x080e49ac
0x80e49ac: 0x2810708f 0x00000000 0x00000000 0x00000000



Cuối cùng là function để đọc và thực thi code VM:



void __cdecl sub_8048DE0()


buff[0] = dword_80E49A4;
buff[6] = dword_80E49A4 + 6;
if ( dword_80E49A4 + 6 > (unsigned int)dword_80E49AC )
{
LABEL_80:
fwrite("Illegal instruction.\n", 1u, 0x15u, off_80D3368);
exit(0);
}
buff[1] = *(_BYTE *)dword_80E49A4++;
v3 = -((buff[1] & 0x76u) < 1);
v4 = *(_BYTE *)(buff[0] + 1);
dword_80E49A4 = buff[0] + 2;
v5 = (v4 & 0x76u) >= 1 ? 16 : 0;
v6 = (*(_BYTE *)(buff[0] + 2) & 0x76u) >= 1 ? 8 : 0;
dword_80E49A4 = buff[0] + 3;
v7 = v6;
buff[3] = *(_BYTE *)(buff[0] + 3);
dword_80E49A4 = buff[0] + 4;
v9 = (buff[3] & 0x76u) < 1;
buff[4] = *(_BYTE *)(buff[0] + 4);
dword_80E49A4 = buff[0] + 5;
v11 = v5 | (*(_BYTE *)(buff[0] + 5) & 0x76) != 0 | ~v3 & 32;
LOBYTE(v11) = v7 | v11;
dword_80E49A4 = buff[6];
v2 = ((buff[4] & 0x76u) >= 1 ? 2 : 0) | ~-v9 & 4 | v11;
if ( (_BYTE)v2 <= 32u )
{
switch ( (_BYTE)v2 )
{
case 0x1B:
.................


!!!Mình đã lượt bỏ một số đoạn cho ngắn gọn lại

Qua đoạn trên có thể hiểu chương trình đang lấy địa chỉ của buff vào và lấy từng giá trị ra, byte đầu tiên sẽ dùng để kiểm tra dấu ( khi là \x09 thì xor với 0x76 sẽ ra giá trị 0 ), byte thứ 1 sẽ AND với 32, thứ 2 với 16, thứ 3 với 8, thứ 4 với 4, thứ 5 với 2 và thứ 6 với 1. Lấy giá trị này làm v2 và dùng làm OpCode cho VM.

Tổng kết lại chương trình sẽ nhận Space và Tab là giá trị 0 và 1, lấy 7byte đầu tiên để làm Opcode, 32Byte tiếp theo cho Arg1 và 32Byte cho Arg2 bởi vì function đưới đây lấy Arg cho mỗi Opcode trừ đi mỗi lần 32Byte.

int __cdecl sub_8048290()
{
if ( !dword_80E4FC8 )
exit(0);
--dword_80E4FC8;
return LODWORD(flt_80E4BC0[dword_80E4FC8]);
}


Đó là cách rr400 hiện thực. Và bug nằm ở :

case 0x1E:
v66 = sub_8048DA0();
if ( (unsigned int)v66 > 0x7F )
fwrite("Register out of range.\n", 1u, 0x17u, off_80D3368);
dword_80E49C0[v66] = sub_8048290();
return;


Function này sau khi kiểm tra index thì chỉ báo lỗi mà không exit dẫn đến phép gán luôn được thực hiện.
dword_80E49C0[v66] = sub_8048290();

Dựa vào đây chúng ta có thể "write 4bytes to anywhere"!

Không có nhận xét nào:

Đăng nhận xét