CatFly【難度:1】
題目介面
下載附件,發現是dll檔案,放到linux中執行一下,執行介面如圖所示:
從上圖中可以看到兩處字串,上面的字串不斷滾動,下方字串在次數上不斷累加,猜測上方字串與flag相關。
靜態除錯
-
開啟IDA,找到main函式
方便分析,此處只貼上關鍵部分程式碼(原始碼的後半部分)
time(&timer); v13 = 1; v24 = 0LL; v23 = 0; v22 = 0; v12 = off_FA88; while ( v13 ) { if ( dword_E104 ) printf("\x1B[H"); else printf("\x1B[u"); for ( k = dword_E1EC; k < dword_E1F0; ++k ) { for ( m = dword_E1F4; m < dword_E1F8; ++m ) { if ( k <= 23 || k > 42 || m >= 0 ) { if ( m >= 0 && (unsigned int)k <= 0x3F && m <= 63 ) { v19 = off_FA20[v24][k][m]; off_FA88 = sub_6314((unsigned int)v24, k, m, (__int64)v12); } else { v19 = 44; } } else { v18 = (2 - m) % 16 / 8; if ( ((v24 >> 1) & 1) != 0 ) v18 = 1 - v18; s[128] = (__int64)",,>>&&&+++###==;;;,,"; v19 = asc_BFE3[v18 - 23 + k]; if ( !v19 ) v19 = 44; } if ( v25 ) { printf("%s", *((const char **)&unk_FCC0 + v19)); } else if ( v19 == v22 || !*((_QWORD *)&unk_FCC0 + v19) ) { printf("%s", off_FA88); } else { v22 = v19; printf("%s%s", *((const char **)&unk_FCC0 + v19), off_FA88); } } sub_65E2(1LL); } if ( dword_E100 ) { time(&time1); v11 = difftime(time1, timer); v10 = sub_63FF((unsigned int)(int)v11); for ( n = (dword_E1FC - 29 - v10) / 2; n > 0; --n ) putchar(32); dword_E1E8 += printf("\x1B[1;37mYou have nyaned for %d times!\x1B[J\x1B[0m", (unsigned int)++dword_108E0); } v22 = 0; ++v23; if ( dword_104C4 && v23 == dword_104C4 ) sub_6471(); if ( !off_FA20[++v24] ) v24 = 0LL; usleep(1000 * v27); } return 0LL;
因為flag可能與螢幕上滾動的字串有關,所以此處需要格外關注
printf
函式。從上述關鍵程式碼中,可以看到off_FA88最可能與flag有關,因為其他的printf函式列印的基本是一個確定的值。 -
檢視目標的交叉引用路徑
確定了off_FA88後,開始查詢與其相關的函式。
-
第一處是469行中的對off_FA88的賦值操作
off_FA88的值是sub_6314()函式的返回值。
-
接下來進入sub_6314()函式
函式虛擬碼如下圖所示:
其功能為:當a2!=18,a3≤4||a3>54時off_FA88不變(v12的值就是off_FA88),所以此處相當於是k=18(一次)且m∈(4,53)時會執行sub_6314函式,即迴圈執行50次。該函式的返回值是byte_104C8的地址,byte_104C8的值與dword_E120[a3-5],dword_E120[a3-5]的值又與sub_62B5()函式的返回值有關。這裡還有個sub_62E3作為邏輯判斷的條件。將此處邏輯進行還原,還原始碼如下:
for (int i = 0; i < 50; i++) { dword_E120[i] ^= sub_62B5(); }
-
檢視sub_62B5()函式
函式虛擬碼如下圖所示:
可以看到該函式的返回值與dword_E1E8相關,檢視dword_E1E8的交叉引用,可以看到只在main函式中有一次自增操作:
-
總結整個流程
-
-
還原關鍵彙編程式碼
此處使用大佬wp給的程式碼進行分析,基本都給瞭解析:
#include<stdio.h> #include<string.h> //在ida裡面點開dword_E1E8會發現初始值為0x1106 int dword_E1E8 = 0x1106; //同理,ida裡也能看到dword_E120的初始值 int dword_E120[50] = { 0x27fb, 0x27a4, 0x464e, 0x0e36, 0x7b70, 0x5e7a, 0x1a4a, 0x45c1, 0x2bdf, 0x23bd, 0x3a15, 0x5b83, 0x1e15, 0x5367, 0x50b8, 0x20ca, 0x41f5, 0x57d1, 0x7750, 0x2adf, 0x11f8, 0x09bb, 0x5724, 0x7374, 0x3ce6, 0x646e, 0x010c, 0x6e10, 0x64f4, 0x3263, 0x3137, 0x00b8, 0x229c, 0x7bcd, 0x73bd, 0x480c, 0x14db, 0x68b9, 0x5c8a, 0x1b61, 0x6c59, 0x5707, 0x09e6, 0x1fb9, 0x2ad3, 0x76d4, 0x3113, 0x7c7e, 0x11e0, 0x6c70 }; //原封不動抄下來就好了 int sub_62B5() { dword_E1E8 = 1103515245 * dword_E1E8 + 12345; return (dword_E1E8 >> 10) & 0x7FFF; } //這塊是拿來算輸出數字n需要多少個字元的。 //因為main函式中的dword_E1E8需要接收printf函式的返回值 //而printf函式的返回值是列印的字元長度 int llog(int n) { int a = 0; while (n /= 10)a++; return a; } //這個函式我沒有特別放出來,反正這裡也是照抄的 int sub_62E3(char a1) { int result; // rax if ((a1 & 0x7Fu) <= 0x7E) result = (a1 & 0x7Fu) > 0x20; else result = 0LL; return result; } int main() { //count代表那個不停自增的dword_108E0 int count = 0; while (1) { for (int i = 0; i < 50; i++) { dword_E120[i] ^= sub_62B5(); } count++; //計算printf的返回值,更改dword_E1E8,每10倍增加一個位數 dword_E1E8 += 42 + llog(count); if (count % 1000000 == 0) { printf("Count:%d\n", count); } //flag代表off_FA88 unsigned char flag[51] = { 0 }; for (int i = 0; i < 50; i++) { //根據出題人所說,出題時迴圈次數為705980581,但是線性同餘隨機數演算法出現了迴圈導致在100427942就出現了flag,若只考慮陣列的最低位元組,能在100001958得到flag // Loop: 100427942 // if((dword_E120[i] & 0xff00)){ // break; // } // Loop: 100001958 if (!sub_62E3(dword_E120[i])) { break; } flag[i] = dword_E120[i] & 0xff; } if (memcmp("CatCTF", flag, 6) == 0) { puts(flag); printf("Count:%d\n", count); break; } } }
-
執行程式碼,得到flag