XCTF - Reverse 全關卡詳解
XCTF - Reverse
目錄- XCTF - Reverse
參考:
XCTF—WriteUp
對LOWORD, HIWORD, LOBYTE, HIBYTE的理解
C++ 學習——char * ,char a[ ],char ** ,char *a[] 的區別
第一題: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反編譯
#!/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
看名字像初始化,看了程式碼也看不出什麼
banner()
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函式的邏輯大概是:
- 定義一個字元指標lpMen,往該地址寫入字串unk_409B10
- 如果經過幾個檢測debug的函式,將lpMen作為引數,呼叫生成Flag的函式
- 退出程式(這段要修改)
- 如果沒有生成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;
}
這段程式碼主要是各種運算,我們要解決一些概念問題,整個問題就迎刃而解:
-
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 };
-
_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進行異或
-
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;
}
程式邏輯
-
輸入一個字串,要求開頭是“nctf{”,字串長度是24,最後一個字元是'}'
-
對字串的每一個字元依次進行if判斷,v4只能是
{'O', 'o', '.', '0'}
的其中一個。在判斷的同時有以下操作- 會改變兩個量:v9,v10[0]。變化是加一或者減一。
- 會對v9,v10[0]的值的範圍進行一個判斷,將結果儲存在v6(v5)中
-
判斷結束後,會用
sub_400690
函式判斷v10[0], v9的值是否合法,不合法則退出程式 -
當遍歷完最後一個元素時,v5也會作為狀態判斷的標準,通過之後則退出遍歷
-
最後以一個判斷語句,判斷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;
}