1. 程式人生 > 其它 >XCTF - Reverse 全關卡詳解

XCTF - Reverse 全關卡詳解

XCTF - Reverse

目錄

參考:

XCTF—WriteUp

關於彙編跳轉指令的說明

Visual C++ 32 位和 64 位編譯器可識別型別

對LOWORD, HIWORD, LOBYTE, HIBYTE的理解

高位元組和低位元組

C強制型別轉換總結

C++指標型別間強制轉換

C++ 學習——char * ,char a[ ],char ** ,char *a[] 的區別

dw、db、dd

第一題:simple-unpack

0x01.查殼和程式的詳細資訊

照著套路扔到PEID中檢視資訊

無果,想起可能是linux的ELF可執行檔案,扔到exeinfo中,

發現有upx殼。

注:windows下的檔案是PE檔案,Linux/Unix下的檔案是ELF檔案

0x02.UPX 脫殼

upx -d 即可對upx殼進行脫殼

0x03.載入IDA

還是從main函式開始分析,結果我們再右側發現了意外驚喜

執行程式,輸入我們看到的flag:flag{Upx_1s_n0t_a_d3liv3r_c0mp4ny}

本writeup參考來自:吉林省信睿網路,https://blog.csdn.net/xiao__1bai/article/details/119395006

第二題: logmein

0x01.查殼和檢視程式的詳細資訊

發現程式是一個ELF檔案,將其放入Linux環境中進行分析

發現程式是64位的,使用靜態分析工具IDA進行分析

0x02.IDA

從main函式開始分析,使用F5檢視虛擬碼

發現main函式的整個運算邏輯

先是,將指定字串複製到v8

s是使用者輸入的字串,先進行比較長度,如果長度比v8小,則進入sub_4007c0函式

可以看出輸出字串Incorrect password,然後,退出

如果長度大於或等與v8則進入下面的迴圈

看到判斷如果輸入的字串和經過運算後的後字串不等,則進入sub_4007c0,輸出Incorrect password,

如果想得,則進入sub_4007f0函式

證明輸入的字串就是flag

接下來寫指令碼

0x03.Write EXP

我們的目標是計算

v8 = ":\"AL_RT^L*.?+6/46";
v7 = 28537194573619560LL;
v6 = 7;

for ( i = 0; i < strlen(s); ++i )
  {
  		s[i] = (char)(*((_BYTE *)&v7 + i % v6) ^ v8[i]) )
  }

首先要理解這個表示式:

(_BYTE *)&v7是位元組型指標,其中BYTE可以理解為unsigned char, 一個位元組儲存8位無符號數,儲存的數值範圍為0-255。而一開始v7是一個long long 型別的整數,因此這裡需要將其轉換為字串。(_BYTE *)&v7相當於將v7轉化為字元陣列(串)的首元素的地址,str_v7[0]

然後(_BYTE *)&v7 + i % v6 // v6 = 7,i = 0,1,...則依次代表v7的前v6個元素的地址,自然*((_BYTE *)&v7 + i % v6)就代表迴圈遍歷字元陣列v7的前v6個元素,也就是str_v7[i % v6]

然後的異或,轉換型別,應該不必多說,只是需要注意不同環境下實現方式的不同。

接下來將詳細探討如何將long long 型v7轉化為字串

方法一:手動轉換

方法二:010editior

先將v7轉換為16進位制,然後在010editior裡按CTRL + shift + v,然後再想辦法翻轉

為什麼要翻轉呢?

先將v7的值轉化為16進位制。v7=0x65626d61726168。

如圖所示,x86架構,v7在棧中是小端儲存,即位元組序是little-endian,所以v7的高位資料放在高址,低位陣列放在低址。

最後是將計算開始那個表示式

方法一:利用python

v6 = 7
v7 = "harambe"
v8 = ":\"AL_RT^L*.?+6/46"

for i in range(len(v8)):
	x = chr(ord(v7[i % v6]) ^ ord(v8[i]))
	print(x, end = '')
    
# ord():是將字串轉換為ascii格式,為了方便運算
# chr():是將ascii轉換為字串

方法二:利用C語言(最優雅,簡單,甚至不用管v7的字串是什麼)

#include<stdio.h>
#include<string.h>
#include<windows.h>	//這標頭檔案和(BYTE*)是第一次見,學著吧

const char v8[] = ":\"AL_RT^L*.?+6/46";
const long long int v6 = 7;	
// 注意由於v6和v7要一起進行運算,因此v6也得開long long 
const long long v7 = 0x65626D61726168LL;

int main(void){
	
	for(int i = 0; i < strlen(v8); i ++ ){
		char x = (char)(*((BYTE *)&v7 + i % v6) ^ v8[i]);
		printf("%c", x);
	} 
	
	return 0;
}

參考文章:

https://blog.csdn.net/qq_43394612/article/details/84839170

XCTF - Writeup

第三題:insanity

思路一:

IDA後shift + F12

思路二:

檢視虛擬碼,發現是先設定隨機數再輸出strs裡面的一個字串,於是可以雙擊strs看看裡面都有什麼

第四題:getit

0x01.查殼


是linux的檔案。沒加殼

0x02.拖入ida

中間對部分生成了一個字串,然後將它寫入檔案中,經過四個f函式,最後又把檔案刪掉了,因此我們什麼都看不到。

百度可知,那幾個f函式的作用是把原來的資料覆蓋掉。

思路一:在四個f函式之前設定斷點,檢視相關資料。

0x03:GDB:我們這時候通過IDA檢視彙編程式碼

然後我們向下追蹤,追蹤到for迴圈的位置,因為,flag是在這裡存入檔案的,所以,我們可以在記憶體中找到正要儲存的字串

我們將滑鼠指向strlen(),在下面可以看到彙編所在的地址,然後我們根據大概的地址去看彙編程式碼

可以看到這是呼叫strlen()函式的彙編指令

我們通過上一個圖片,可以知道經過for()的判斷條件後,還要進行一步fseek函式,所以,根據彙編程式碼,可以確定jnb loc_4008B5就是fseek()函式,那麼,mov eax,[rbp+var_3C]肯定就是最後要得到的flag了

0x04.GDB:這裡我們用linux下的動態除錯工具gdb進行動態除錯

這裡介紹一下,對gdb進行強化的兩個工具peda和pwndbg,這兩個工具可以強化視覺效果,可以更加清楚的顯示堆疊,記憶體,寄存機的情況

先載入程式

然後,用b 下斷點

然後,執行 R

這裡我們可以看出,程式停止在0x400832的位置,然後,要被移動的字串在RDX的位置

注:

這裡介紹一下一下RDX,RDX存的是i/0指標,0x6010e0,這個位置存的字串是最後的flag:SharifCTF{b70c59275fcfa8aebf2d5911223c6589}

以為這裡涉及的是程式讀寫函式,所以涉及的就是i/o指標

另外也可以就直接在strlen處設定斷掉,效果如下:

所以我們能得到最後的flag: SharifCTF{b70c59275fcfa8aebf2d5911223c6589}

本writeup參考來自:吉林省信睿網路

思路二:用程式碼實現虛擬碼中的生成字串的功能

#include<stdio.h>

int main(void)
{
	char s[] = "c61b68366edeb7bdce3c6820314b7498";
	int v6 = 0;
	while ( v6 < strlen(s) )
	{
		int v3;
		if ( v6 & 1 )
			v3 = 1;
		else
			v3 = -1;
		char x = s[v6] + v3;
		v6 ++ ;
		printf("%c", x);
	}

	return 0;
}

結果為:b70c59275fcfa8aebf2d5911223c6589

然後再和SharifCTF{}進行拼接

(這是在IDA中,通過shift + F12發現的)。

值得注意的是 t 中存放的是harictf{??????????????????}. 開始t字串初始化是'0x53',即S,可能有某種奇怪的規則吧。

第五題:python-trade

0x01.下載附件

注:

python檔案在被import執行的時候會在同目錄下編譯一個pyc的檔案(為了下次快速載入),這個檔案可以和py檔案一樣使用,但無法閱讀和修改;python工具支援將pyc檔案反編譯為py檔案(可能會存在部分檔案無法反編譯)。

支援的python版本:1.0、 1.1、 1.3、 1.4、 1.5、 1.6、 2.0、 2.1、 2.2、 2.3、 2.4、 2.5、 2.6、 2.7、 3.0、 3.1、 3.2、 3.3、 3.4、 3.5、 3.6、 3.7、 3.8、 3.9、 3.10。

0x02.線上Python反編譯

https://tool.lu/pyc/

#!/usr/bin/env python
# visit https://tool.lu/pyc/ for more information
# Version: Python 2.7

import base64


def encode(message):
    s = ""
    for i in message:
        x = ord(i) ^ 32
        x = x + 16
        s += chr(x)
    return base64.b64encode(s)


correct = "XlNkVmtUI1MgXWBZXCFeKY+AaXNt"
flag = ""
print "Input flag:"
flag = raw_input()
if encode(flag) == correct:
    print "correct"
else:
    print "wrong"

這是生成的py檔案

然後,對這個檔案的運算邏輯進行逆向

0x03.寫EXP

#!/usr/bin/env python

import base64

ori = "XlNkVmtUI1MgXWBZXCFeKY+AaXNt"
ori = base64.b64decode(ori)

ans = ""

for i in ori:
    ans += chr((ord(i) - 16) ^ 32)

print(ans)

先對字串進行b64decode,然後,再進行xor運算得到最後的flag:nctf{d3c0mpil1n9_PyC}

本writeup參考來自:吉林省信睿網路

第六題:re1

思路一:IDA靜態分析

IDA F5檢視虛擬碼

這是整個main函式的運算邏輯

可以看到一個關鍵的字串,print(aFlag),那麼證明這就是輸入正確flag,然後,會輸出aFlag證明你的flag正確,然後,繼續往上分析,可以看到v3的值,是由strcmp()決定的,比較v5和輸入的字串,如果一樣就會進入後面的if判斷,所以,我們繼續往上分析,看看哪裡又涉及v5,可以看到開頭的_mm_storeu_si128(),對其進行分析發現它類似於memset(),將xmmword_413E34的值賦值給v5,所以,我們可以得到正確的flag應該在xmmword_413E34中,然後,我們雙擊413E34進行跟進

可以看到一堆十六進位制的數

這時,我們使用IDA的另一個功能 R ,能夠將十進位制的數轉換為字串。

根據小端儲存的規則可以得到flag

但是不知道為什麼不同IDA分析出來的虛擬碼差別比較大,本題我是用IDA pro7.6做的,而IDA pro 6.8則不行,連main函式都找不到,奇怪。

思路二:Ollydbg中文搜尋

這種思路我覺得比較碰巧,類似的用010editor開啟也能看到類似的字串,不過OD可能更容易發現一點

但是不知道為什麼IDA的shift + F12 看不到

第七題:game(TODO)

第八題:Hello,CTF

0x01.執行程式

輸入正確的flag,才會顯示正確

0x02.查殼

是32位的程式,並且是Microsoft Visual C++編譯,而且沒有加殼

0x03.IDA

照舊,依舊先從main開始分析,然後,對main函式進行F5檢視虛擬碼

int __cdecl main(int argc, const char **argv, const char **envp)
{
  int i; // ebx
  char v4; // al
  int result; // eax
  int v6; // [esp+0h] [ebp-70h]
  int v7; // [esp+0h] [ebp-70h]
  char Buffer[2]; // [esp+12h] [ebp-5Eh] BYREF
  char v9[20]; // [esp+14h] [ebp-5Ch] BYREF
  char v10[32]; // [esp+28h] [ebp-48h] BYREF
  __int16 v11; // [esp+48h] [ebp-28h]
  char v12; // [esp+4Ah] [ebp-26h]
  char v13[36]; // [esp+4Ch] [ebp-24h] BYREF

  strcpy(v13, "437261636b4d654a757374466f7246756e");// 將字串複製到v13的位置
  while ( 1 )
  {
    memset(v10, 0, sizeof(v10));
    v11 = 0;
    v12 = 0;
    sub_40134B(aPleaseInputYou, v6);
    scanf("%s", v9);            // 輸入一個字串
    if ( strlen(v9) > 0x11 )    // 輸入的字串長度不能大於17(0x11)
      break;
    for ( i = 0; i < 17; ++i )
    {
      v4 = v9[i];
      if ( !v4 )
        break;
      sprintf(Buffer, "%x", v4);// Buffer的定義:char Buffer[2];猜測是將v4以十六進位制的形式輸出到Buffer中
      strcat(v10, Buffer);      // 拼接v10, Buffer
    }
    if ( !strcmp(v10, v13) )    // 最後,進行比較,看輸入的字串是否和v10的字串相等,如果相等,則說明輸入了正確的flag
      sub_40134B(aSuccess, v7);
    else
      sub_40134B(aWrong, v7);
  }
  sub_40134B(aWrong, v7);
  result = --Stream._cnt;
  if ( Stream._cnt < 0 )
    return _filbuf(&Stream);
  ++Stream._ptr;
  return result;
}

0x04.將字串轉換為十六進位制

得到了最後的flag是:CrackMeJustForFun

第九題:open - source

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[]) {
    if (argc != 4) {
    	printf("what?\n");
    	//exit(1);
    }

    unsigned int first = atoi(argv[1]);	// argv[1] = 51966
    if (first != 0xcafe) {
    	printf("you are wrong, sorry.\n");
    	exit(2);
    }

    unsigned int second = atoi(argv[2]);
    if (second % 5 == 3 || second % 17 != 8) {
    	printf("ha, you won't get it!\n");
    	exit(3);
    }

    if (strcmp("h4cky0u", argv[3])) {	// argv[3] = h4cky0u
    	printf("so close, dude!\n");
    	exit(4);
    }

    printf("Brr wrrr grr\n");

    unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv[3]) - 1615810207;

    printf("Get your key: ");
    printf("%x\n", hash);
    return 0;
}

逐個分析即可。

#include<stdio.h>
#include<string.h>

int main(void){
	unsigned int first = 51966;	// 0xcafe(16) →51966(10)  
	
	unsigned int second; 		// 25
	for (unsigned int i = 0; ;i ++ ){
		if (i % 5 != 3 && i % 17 == 8){
			second = i;
			// printf("%d\n", (second % 17) * 11); // 88 
			break;
		}
	}
	char argv[10] = "h4cky0u";
	unsigned int hash = first * 31337 + (second % 17) * 11 + strlen(argv) - 1615810207;
    printf("Get your key: ");
    printf("%x\n", hash);
	return 0;
} 

第十題:no-strings-attached

法一:動態分析

0x01.查殼和檢視程式的詳細資訊

說明程式是ELF檔案,32位

這個軟體要放在Linux下執行,值得注意的是,我的Linux是64位的,一開始顯示找不到檔案,原因是沒有安裝32位的編譯環境,在網上查詢相關教程後才能執行。

0x02.使用靜態分析工具IDA進行分析

int __cdecl main(int argc, const char **argv, const char **envp)
{
  setlocale(6, &locale);
  banner();
  prompt_authentication();
  authenticate();
  return 0;
}

一個一個分析

setlocale

看名字像初始化,看了程式碼也看不出什麼

int banner()
{
  unsigned int v0; // eax

  v0 = time(0);
  srand(v0);
  wprintf((int)&unk_80488B0);
  rand();
  return wprintf((int)&unk_8048960);
}

出現了wprintf()函式,我們可以康康輸出了什麼

雙擊&unk_80488B0(其它地址也是)

可以看到一個一個的字母,我們用IDA python來輸出,執行以下指令碼

// IDA python 檢視 wprintf();的內容
addr = here();
ans = ""
for i in range(50):
    ans += get_bytes(addr + i * 4, 1).decode(errors='ignore')
print(ans)

可以得到banner()中輸出了"Welcome to cyber malware control software.","Currently tracking %d bots worldwide"

prompt_authentication();

同理可以發現,這個函式輸出了“Please enter authentication details:”

還沒有一些實質性的操作

authenticate()

void authenticate()
{
  int ws[8192]; // [esp+1Ch] [ebp-800Ch] BYREF
  wchar_t *s2; // [esp+801Ch] [ebp-Ch]

  s2 = decrypt((wchar_t *)&s, (wchar_t *)&dword_8048A90);//decrypt:加密
  if ( fgetws(ws, 0x2000, stdin) )	// 從標準輸入流輸入
  {
    ws[wcslen(ws) - 1] = 0;			// 謎之操作
    if ( !wcscmp(ws, s2) )			// 對比字串
      wprintf((int)&unk_8048B44);	// Success! Welcome back!
    else
      wprintf((int)&unk_8048BA4);	// Access denied!
  }
  free(s2);
}

分析可得,這段程式碼是將我們的輸入與s2進行對比,因此我們不妨先把s2找出來

0x03.GDB動態除錯

首先我們在authenticate()中檢視decrypt函式

它應該往eax寫入了返回結果,我們待會重點關注。

用GDB除錯,我在藍色的地方設定了斷點,然後執行,最後檢視eax暫存器(以字串形式輸出)

gdb hhh
b *0x08048728
r
x/6sw $eax

最後一步詳見:【原創】GDB之examine命令(即x命令)詳解

得到flag:9447{you_are_an_international_myster

法二:靜態分析

s2 = decrypt((wchar_t *)&s, (wchar_t *)&dword_8048A90);知,我們可以分析s與dword_8048A90這兩個陣列的值,然後模擬一遍decrypt()函式,則有可能得到答案

字串s

指令碼法:67;zqxcfsgbes`kqxjspdxnppdpdn{vxjs{

Shift + E法

另一個字串

注意:兩個引數的型別都是wchar_t 型別(長度16位或32位,本機32位,4位元組)由於有大量的0,所以不能用char型別的陣列,否則讀到第三位直接結束。此外,刪除後面4 個位元組的0,因為字串的結尾預設加0。

ps: wchar_t是C/C++的字元型別,是一種擴充套件的儲存方式。wchar_t型別主要用在國際化程式的實現中,但它不等同於unicode編碼。unicode編碼的字元一般以wchar_t型別儲存。——百度百科

#include<stdio.h>
#include<string.h>

int main(void)
{
	unsigned char s[] =
	{
		0x3A, 0x14, 0x00, 0x00, 0x36, 0x14, 0x00, 0x00, 0x37, 0x14,
		0x00, 0x00, 0x3B, 0x14, 0x00, 0x00, 0x80, 0x14, 0x00, 0x00,
		0x7A, 0x14, 0x00, 0x00, 0x71, 0x14, 0x00, 0x00, 0x78, 0x14,
		0x00, 0x00, 0x63, 0x14, 0x00, 0x00, 0x66, 0x14, 0x00, 0x00,
		0x73, 0x14, 0x00, 0x00, 0x67, 0x14, 0x00, 0x00, 0x62, 0x14,
		0x00, 0x00, 0x65, 0x14, 0x00, 0x00, 0x73, 0x14, 0x00, 0x00,
		0x60, 0x14, 0x00, 0x00, 0x6B, 0x14, 0x00, 0x00, 0x71, 0x14,
		0x00, 0x00, 0x78, 0x14, 0x00, 0x00, 0x6A, 0x14, 0x00, 0x00,
		0x73, 0x14, 0x00, 0x00, 0x70, 0x14, 0x00, 0x00, 0x64, 0x14,
		0x00, 0x00, 0x78, 0x14, 0x00, 0x00, 0x6E, 0x14, 0x00, 0x00,
		0x70, 0x14, 0x00, 0x00, 0x70, 0x14, 0x00, 0x00, 0x64, 0x14,
		0x00, 0x00, 0x70, 0x14, 0x00, 0x00, 0x64, 0x14, 0x00, 0x00,
		0x6E, 0x14, 0x00, 0x00, 0x7B, 0x14, 0x00, 0x00, 0x76, 0x14,
		0x00, 0x00, 0x78, 0x14, 0x00, 0x00, 0x6A, 0x14, 0x00, 0x00,
		0x73, 0x14, 0x00, 0x00, 0x7B, 0x14, 0x00, 0x00, 0x80, 0x14,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x53, 0x00, 0x00, 0x00,
		0x75, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x63, 0x00,
		0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x73, 0x00, 0x00, 0x00,
		0x73, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x20, 0x00,
		0x00, 0x00, 0x57, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00,
		0x6C, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x6F, 0x00,
		0x00, 0x00, 0x6D, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00,
		0x20, 0x00, 0x00, 0x00, 0x62, 0x00, 0x00, 0x00, 0x61, 0x00,
		0x00, 0x00, 0x63, 0x00, 0x00, 0x00, 0x6B, 0x00, 0x00, 0x00,
		0x21, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x41, 0x00, 0x00, 0x00, 0x63, 0x00, 0x00, 0x00,
		0x63, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x73, 0x00,
		0x00, 0x00, 0x73, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x00,
		0x64, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00, 0x6E, 0x00,
		0x00, 0x00, 0x69, 0x00, 0x00, 0x00, 0x65, 0x00, 0x00, 0x00,
		0x64, 0x00, 0x00, 0x00, 0x21, 0x00, 0x00, 0x00, 0x0A, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x01, 0x1B, 0x03, 0x3B, 0x50, 0x00, 0x00, 0x00, 0x09, 0x00,
		0x00, 0x00, 0x88, 0xF8, 0xFF, 0xFF, 0x6C, 0x00, 0x00, 0x00,
		0x1C, 0xFA, 0xFF, 0xFF, 0x90, 0x00, 0x00, 0x00, 0x5B, 0xFA,
		0xFF, 0xFF, 0xB0, 0x00, 0x00, 0x00, 0x70, 0xFA, 0xFF, 0xFF,
		0xD0, 0x00, 0x00, 0x00, 0x20, 0xFB, 0xFF, 0xFF, 0xF4, 0x00,
		0x00, 0x00, 0xC1, 0xFB, 0xFF, 0xFF, 0x14, 0x01, 0x00, 0x00,
		0xF8, 0xFB, 0xFF, 0xFF, 0x34, 0x01, 0x00, 0x00, 0x68, 0xFC,
		0xFF, 0xFF, 0x70, 0x01, 0x00, 0x00, 0x6A, 0xFC, 0xFF, 0xFF,
		0x84, 0x01, 0x00, 0x00
	};

	unsigned char a2[] =
	{
		0x01, 0x14, 0x00, 0x00, 0x02, 0x14, 0x00, 0x00, 0x03, 0x14,
		0x00, 0x00, 0x04, 0x14, 0x00, 0x00, 0x05, 0x14, 0x00, 0x00,
	};

	int v4 = 0;
	int ans[1000] = {};
	for (int i = 0; i < 150; i ++ ){
		ans[i] = s[i] - a2[i % 20];		 // 兩個迴圈可以簡化成一個
		if(ans[i]) printf("%c", ans[i]); // 不加if,會出現大量空格 
	}
	return 0;
}

執行得到flag:9447{you_are_an_international_myster

本文參考:XCTF-writeup

第十一題 :csaw2013reversing2

0x01:查殼

0x02: 思路一:靜態分析

這裡我用不同版本的IDA會生成不同的反彙編程式碼,其中v6的更容易讀懂,這裡我們選擇v6,但是二者要結合在一起看。

main函式的反彙編程式碼

int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // ecx@1
  LPVOID lpMem; // [sp+8h] [bp-Ch]@1				// 在v7中,它是char *型別的
  HANDLE hHeap; // [sp+10h] [bp-4h]@1

  hHeap = HeapCreate(0x40000u, 0, 0);
  lpMem = HeapAlloc(hHeap, 8u, MaxCount + 1);		// 看不懂,應該是賦一個初始值
  memcpy_s(lpMem, MaxCount, &unk_409B10, MaxCount);	// 類似strcpy,將字串unk_409B10到地址lpMem處
  if ( sub_40102A() || IsDebuggerPresent() )	// 看名字可以猜測,有程式碼防止Debugger,動態除錯要注意,我們靜態除錯就不管了 
  {
    __debugbreak();								// 看名字可以猜測,執行到這裡就會退出 
    sub_401000(v3 + 4, (int)lpMem);				// 這是一段被“保護”的程式碼,可以猜測是生成flag的函式,另外第一個引數沒有起作用
    ExitProcess(0xFFFFFFFF);					// 看名字可以猜測,執行到這裡也會退出
  }
  MessageBoxA(0, (LPCSTR)lpMem + 1, "Flag", 2u);// 結合名字可以知道,應該是輸出Flag
  HeapFree(hHeap, 0, lpMem);
  HeapDestroy(hHeap);
  ExitProcess(0);
}

main函式的邏輯大概是:

  1. 定義一個字元指標lpMen,往該地址寫入字串unk_409B10
  2. 如果經過幾個檢測debug的函式,將lpMen作為引數,呼叫生成Flag的函式
  3. 退出程式(這段要修改)
  4. 如果沒有生成flag,就輸出flag

很明顯,如果是動態除錯,那就得各種繞過,但我們現在只要關注生成flag的函式即可。

sub_401000(v3 + 4, (int)lpMem)函式的反彙編程式碼

unsigned int __fastcall sub_401000(int a1, int a2)	// 傳入兩個整數型引數 
{
  int v2; // esi
  unsigned int v3; // eax
  unsigned int v4; // ecx
  unsigned int result; // eax

  v2 = dword_409B38;	// 給v2賦值一個dword值的地址 
  v3 = a2 + 1 + strlen((const char *)(a2 + 1)) + 1;
  v4 = 0;
  result = ((strlen((const char *)(a2 + 1))) >> 2) + 1;
  if ( result )			// 結合下文猜測result是控制範圍的 
  {
    do
      *(_DWORD *)(a2 + 4 * v4++) ^= v2;			// 最重要的一行 
    while ( v4 < result );
  }
  return result;
}

這段程式碼主要是各種運算,我們要解決一些概念問題,整個問題就迎刃而解:

  1. lpMem的值的問題

    傳入的第二個引數是a2 = (int)lpMem,但是lpMem是一個地址,而地址那段程式碼又看不懂,不知道lpMem具體是什麼。而程式碼中又要用到a2,這該怎麼處理?

    經過分析可以發現,lpMem的值不會影響我們解題,因為其中可能造成影響的a2被消去了,下面具體分析涉及到lpMem的語句。

    首先是lpMem的初始化,第一行我們看不懂,但是經查閱,第二行的作用類似strcpy,將地址unk_409B10處開始的字串複製到地址lpMem處,複製的元素是SourceSize個

    lpMem = (CHAR *)HeapAlloc(hHeap, 8u, SourceSize + 1);
    memcpy_s(lpMem, SourceSize, &unk_409B10, SourceSize);
    

    然後在傳入引數時,令a2 = (int)lpMem

    具體使用a2時,有三處。

    第一處:v3 = a2 + 1 + strlen((const char *)(a2 + 1)) + 1;

    第二處:result = ((v3 - (a2 + 2)) >> 2) + 1;

    第三處:*(_DWORD *)(a2 + 4 * v4++) ^= v2;

    這裡有兩種用法,一種是把a2當作整數,另一種又把a2變成了指標。

    對於後者,這樣的操作等價於對一個字串進行操作,與具體的值無關。

    對於前者,我們可以發現它隻影響v3和result的取值。對於v3,我們無法得知具體的值,後面只有求result時要用到。那我們就具體分析result,我們驚訝地發現,計算result時,a2被消掉了,因此我們可以計算出result的值,其它地方也用不到v3,至此我們可以放心的說a2對我們不會造成影響。

    所以我們可以直接定義一個字元陣列:

    	unsigned char lpMem[] =	//lpMem
    	{
    		0xBB, 0xCC, 0xA0, 0xBC, 0xDC, 0xD1, 0xBE, 0xB8, 0xCD, 0xCF,
    		0xBE, 0xAE, 0xD2, 0xC4, 0xAB, 0x82, 0xD2, 0xD9, 0x93, 0xB3,
    		0xD4, 0xDE, 0x93, 0xA9, 0xD3, 0xCB, 0xB8, 0x82, 0xD3, 0xCB,
    		0xBE, 0xB9, 0x9A, 0xD7, 0xCC, 0xDD
    	};
    
  2. _DWORD型別,理解*(_DWORD *)(a2 + 4 * v4++) ^= v2;

    經查閱,它的定義是

    A dword, which is short for "double word," is a data type definition that is specific to Microsoft Windows. When defined in the file windows.h, a dword is an unsigned, 32-bit unit of data. It can contain an integer value in the range 0 through 4,294,967,295.

    它佔有相當4個位元組/char的空間,這段程式碼的含義應該是對a2所指向的地址處的Dword型別進行操作,即和v2所指向的Dword(v2被定義為int,但作用卻是指標)進行異或。

    具體來說,v2處的定義是v2 = dword_409B38;,這就是一個dword的地址,點進去可以看到它的值是0DDCCAABBh或者寫成c程式碼unsigned char v2[] ={ 0xBB, 0xAA, 0xCC, 0xDD };

    然後是異或,兩個Dword怎麼異或?

    應該可以使用windows.h來進行相關操作。

    這裡我們可以把dword當作4個char來操作,即讓我們上面定義的lpMem陣列的每一個元素,輪換著和v2進行異或

  3. result的值的問題

    int a2 = (int)lpMem;
    int v3 = a2 + 1 + strlen(lpMem + 1) + 1;
    int result = ((v3 - (a2 + 2)) >> 2) + 1;
    printf("\n%u", result);
    

    計算可得result是9,也就是遍歷一遍lpMem,所以對於char型的lpMem,範圍就是36

0x03: 編寫指令碼生成flag

於是最終程式碼如下

#include<stdio.h>
int main(void)
{
	unsigned char lpMem[] =	//lpMem
	{
		0xBB, 0xCC, 0xA0, 0xBC, 0xDC, 0xD1, 0xBE, 0xB8, 0xCD, 0xCF,
		0xBE, 0xAE, 0xD2, 0xC4, 0xAB, 0x82, 0xD2, 0xD9, 0x93, 0xB3,
		0xD4, 0xDE, 0x93, 0xA9, 0xD3, 0xCB, 0xB8, 0x82, 0xD3, 0xCB,
		0xBE, 0xB9, 0x9A, 0xD7, 0xCC, 0xDD
	};
	unsigned char v2[] =
	{
		0xBB, 0xAA, 0xCC, 0xDD
	};
	unsigned char ans[100];
	for (int i = 0; i < 36 ; i ++ )
	{
		ans[i] = (lpMem[i]) ^ v2[i % 4];
		printf("%c", ans[i]);
	}
	return 0;
}

輸出是 flag{reversing_is_not_that_hard!}

其它思路

參考文章:https://blog.csdn.net/weixin_43784056/article/details/103655968

第十二題:maze

0x01.查殼和詳細資訊

可以看到程式是ELF檔案,64位

0x02:IDA靜態分析

拖入IDA,F5檢視main函式

p.s.右鍵數字,可以把數字轉換成字元

__int64 __fastcall main(int a1, char **a2, char **a3)
{
  __int64 v3; // rbx
  int v4; // eax
  char v5; // bp
  char v6; // al
  const char *v7; // rdi
  unsigned int v9; // [rsp+0h] [rbp-28h] BYREF
  int v10[9]; // [rsp+4h] [rbp-24h] BYREF

  v10[0] = 0;
  v9 = 0;
  puts("Input flag:");
  scanf("%s", &s1); // s1 是我們輸入的字串,也就是flag了
  if ( strlen(&s1) != 24 || strncmp(&s1, "nctf{", 5uLL) || *(&byte_6010BF + 24) != '}' ) // 開頭是“nctf{”,字串長度是24,減去nctf{},還剩18個字元
  {
LABEL_22:
    puts("Wrong flag!");
    exit(-1);
  }
  v3 = 5LL;
  if ( strlen(&s1) - 1 > 5 )    // 長度得大於6
  {
    while ( 1 )
    {
      v4 = *(&s1 + v3);         // 即'{'之後的字元,轉換為整數(ASCII)
      v5 = 0;
      if ( v4 > 78 )            // N(78)
      {
        if ( (unsigned __int8)v4 == 79 )    // 'O'
        {
          v6 = sub_400650(v10); // int v1 = (*a1)--; return v1 > 0; 返回v10指向的值是否為正,並且讓v10指向的元素減一
          goto LABEL_14;
        }
        if ( (unsigned __int8)v4 == 111 )   // 'o'
        {
          v6 = sub_400660(v10); // int v1 = *a1 + 1; *a1 = v1; return v1 < 8; 讓v10指向的值加一,然後判斷其是否小於8
          goto LABEL_14;
        }
      }
      else
      {
        if ( (unsigned __int8)v4 == 46 )    // '.'
        {
          v6 = sub_400670(&v9); // int v1 = (*a1)--; return v1 > 0; 返回v9是否為正,並且讓v9減一
          goto LABEL_14;
        }
        if ( (unsigned __int8)v4 == 48 )    // '0'
        {
          v6 = sub_400680(&v9); // int v1 = *a1 + 1; *a1 = v1; return v1 < 8; 先讓v9加一,然後再判斷其是否小於8
LABEL_14:
          v5 = v6;
        }
      }
      
      if ( !(unsigned __int8)sub_400690(asc_601060, (unsigned int)v10[0], v9) )	// 狀態檢測
        goto LABEL_22;
      
      if ( ++v3 >= strlen(&s1) - 1 )
      {
        if ( v5 )
          break;
LABEL_20:
        v7 = "Wrong flag!";
        goto LABEL_21;
      }
    }
  }
  if ( asc_601060[8 * v9 + v10[0]] != '#' )  // 8 * v9 + v10[0] 得是 36
    goto LABEL_20;
  v7 = "Congratulations!";
LABEL_21:
  puts(v7);
  return 0LL;
}

程式邏輯

  1. 輸入一個字串,要求開頭是“nctf{”,字串長度是24,最後一個字元是'}'

  2. 對字串的每一個字元依次進行if判斷,v4只能是{'O', 'o', '.', '0'}的其中一個。在判斷的同時有以下操作

    1. 會改變兩個量:v9,v10[0]。變化是加一或者減一。
    2. 會對v9,v10[0]的值的範圍進行一個判斷,將結果儲存在v6(v5)中
  3. 判斷結束後,會用sub_400690函式判斷v10[0], v9的值是否合法,不合法則退出程式

  4. 當遍歷完最後一個元素時,v5也會作為狀態判斷的標準,通過之後則退出遍歷

  5. 最後以一個判斷語句,判斷v9,v10[0]是否符合要求,符合則輸出Congratulations,說明我們輸入了正確的flag。

其中sub_400690函式的作用是,判斷陣列asc_601060的特定元素,是否為指定元素

0x03.模擬程式碼

#include<iostream>
using namespace std;

const char pos[5] = {'O', 'o', '.', '0'}; 
const int len = 18;
char ans[19];

unsigned char a1[] =
{
  0x20, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 0x20, 
  0x20, 0x20, 0x2A, 0x20, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x20, 
  0x2A, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x20, 0x20, 0x2A, 0x20, 
  0x2A, 0x2A, 0x2A, 0x20, 0x20, 0x2A, 0x23, 0x20, 0x20, 0x2A, 
  0x2A, 0x2A, 0x20, 0x2A, 0x2A, 0x2A, 0x20, 0x2A, 0x2A, 0x2A, 
  0x20, 0x20, 0x20, 0x20, 0x20, 0x2A, 0x2A, 0x2A, 0x2A, 0x2A, 
  0x2A, 0x2A, 0x2A, 0x2A, 0x00
};

bool check (unsigned char* a1, int a2, int a3)
{
  char result; // rax
  result = *(unsigned char *)(a1 + a2 + 8LL * a3);
  bool x = ((result == 32) || (result == 35));    // 32 是空格(0x20),35 是'#'(0x23)
  return x;
}

void dfs(int n, int v9, int v10){
	if (n == len){
		if (v10 <= 0 || v10 >= 8 || v9 <= 0 || v9 >= 8) return;
		if (8 * v9 + v10 != 36) return;
		
		for (int i = 0; i < len; i ++ )	printf("%c", ans[i]);
		printf("\n");
		return;
	}
	if (!check(a1, v10, v9)) return;
	
	ans[n] = pos[0];
	dfs(n + 1, v9, v10 - 1);
	ans[n] = pos[1];
	dfs(n + 1, v9, v10 + 1);
	ans[n] = pos[2];
	dfs(n + 1, v9 - 1, v10);
	ans[n] = pos[3];
	dfs(n + 1, v9 + 1, v10);
    /*
   		意識到這是個迷宮之後,可以將z程式碼修改如下
    	const char dx[4] = {0, 0, -1, 1};
		const char dy[4] = {-1, 1, 0, 0};
    	for (int i = 0; i < 4; i ++ ){
		ans[n] = pos[i];
		dfs(n + 1, v9 + dx[i], v10 + dy[i]);
	}
    */
	return;
}

int main(void){
	dfs(0, 0, 0);
	return 0;
}