昨天打完的MAR DASCTF,來復個盤~
不過就re做了3/4然后有事提前開溜了hhh,拿了drinkSomeTea和replace的三血心滿意足(蜜汁三血執念。
感覺這回的出題人好喜歡TEA啊(正好最近在整理加解密算法),就是TEA和XTEA都出了卻不帶XXTEA玩有點可惜/doge。
掃雷也復盤完了!好耶!
Reverse
drinkSomeTea
是一個邏輯超級明顯但是超——坑的題。
鑒於這是復盤,那就直擊要害吧,懶得把當時兜兜轉轉的心路歷程復述一遍了>^<。
邏輯很簡單,就是將./tea.png
(FileName=“./tea.png”)的內容以二進制形式讀入到unk_409988
中,然后以8字節為單位(v7+=8
)對其進行處理(loc_4010A0
這里),再寫入./tea.png.out
中。而題目附件給了這個最后輸出的./tea.png.out
,需要我們還原./tea.png
。
loc_4010A0
這里沒有轉函數的原因是出現了花指令干擾靜態分析,nop掉(74 03 75 01 E8改成90 90 90 90 90)再重新轉代碼轉函數即可。
然后我們就得到了sub_4010A0()
。
很明顯地可以看到是TEA加密,就是說tea.png
經過TEA加密以后得到了tea.png.out
。
a2是加密時的key,退回到上一層可以發現是dword_407030
:
但是如果拿平時的TEA解密腳本跑根本就行不通,當時多用了兩三倍的時間去調(patch掉exit,走一遍加密流程進行對比),最后才發現是int的問題,一般TEA加解密都是uint_32的(。就是這個細節浪費了超多時間TvT
最后上解密腳本,基本上把unsigned int改成int就好(改自TEA、XTEA、XXTEA加密解密算法_gsls200808的專欄-CSDN博客):
#include <stdio.h>
#include <stdint.h>
//加密函數
void encrypt (int* v, int* k) {
int v0=v[0], v1=v[1], sum=0, i; /* set up */
int delta=0x9e3779b9; /* a key schedule constant */
int k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i < 32; i++) { /* basic cycle start */
sum += delta;
v0 += ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
v1 += ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
} /* end cycle */
v[0]=v0; v[1]=v1;
}
//解密函數
void decrypt (int* v, int* k) {
int v0=v[0], v1=v[1], sum=0xC6EF3720, i; /* set up */
int delta=0x9e3779b9; /* a key schedule constant */
int k0=k[0], k1=k[1], k2=k[2], k3=k[3]; /* cache key */
for (i=0; i<32; i++) { /* basic cycle start */
v1 -= ((v0<<4) + k2) ^ (v0 + sum) ^ ((v0>>5) + k3);
v0 -= ((v1<<4) + k0) ^ (v1 + sum) ^ ((v1>>5) + k1);
sum -= delta;
} /* end cycle */
v[0]=v0; v[1]=v1;
}
int main()
{
int v[14656]={0},k[4]={0x67616C66, 0x6B61667B, 0x6C665F65, 0x7D216761};
FILE *p1 = fopen("./tea.png.out", "rb");
fread(&v, 4, 14656, p1);
fclose(p1);
for(int i=0;i<14656;i+=2){
decrypt(&v[i], k);
}
FILE *p2 = fopen("./tea.png", "wb");
fwrite(&v, 4, 14656, p2);
fclose(p2);
return 0;
}
得到./tea.png
:
得到flag:DASCTF{09066cbb91df55502e6fdc83bf84cf45}
Enjoyit-1
又一道TEA。
附件用ExEinfoPE可以看到
說明是.NET逆向,於是用ILSpy打開。
看到不尋常字符串DotfuscatorAttribute
,用搜索引擎一查可以發現是使用Dotfuscator加密混淆程序的產物(使用Dotfuscator加密混淆程序以及如何脫殼反編譯_qwsf01115的專欄-CSDN博客)。
所以根據文章指引用de4dot(可用release:Release de4dot mod · CodingGuru1989/de4dot)進行反混淆,得到Enjoyit-1-cleaned.exe
,再用ILSpy打開,就可以看到混淆前在Class0里的main函數。
這個邏輯也很簡單,無非就是輸入text正確以后運行100000秒就會輸出flag。
(當然肯定不可能這么走啊,100000s=1666.67min=27.78h,必然是等不起的。
flag產生的邏輯是先用uint_
和text
進行method_3()
的處理,然后再與array3按字節異或。
而text相當於是已知的,關鍵在 method_1()
這里。
可以看出是一個base64換表,Table在string_0這里:
於是可以先寫腳本得到text:
import base64
from binascii import *
src='yQXHyBvN3g/81gv51QXG1QTBxRr/yvXK1hC='
table='abcdefghijklmnopqrstuvwxyz0123456789+/ABCDEFGHIJKLMNOPQRSTUVWXYZ'
b64table='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
text=base64.b64decode(src.translate(str.maketrans(table,b64table)).encode())
print(text)
# text=b'combustible_oolong_tea_plz'
然后在主函數往下看,來到了第二個關鍵函數method_3()
:
顯而易見是個改了delta的XTEA加密,傳進來的uint_0是主函數的uint_,byte_0是text的前四字節。
依舊是用上面博客的XTEA腳本改了一下,得到XTEA加密后的uint_:
#include <stdio.h>
#include <stdint.h>
/* take 64 bits of data in v[0] and v[1] and 128 bits of key[0] - key[3] */
void encipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], sum=0, delta=2654435464;
for (i=0; i < num_rounds; i++) {
v0 += (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
sum += delta;
v1 += (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
}
v[0]=v0; v[1]=v1;
}
void decipher(unsigned int num_rounds, uint32_t v[2], uint32_t const key[4]) {
unsigned int i;
uint32_t v0=v[0], v1=v[1], delta=2654435464, sum=delta*num_rounds;
for (i=0; i < num_rounds; i++) {
v1 -= (((v0 << 4) ^ (v0 >> 5)) + v0) ^ (sum + key[(sum>>11) & 3]);
sum -= delta;
v0 -= (((v1 << 4) ^ (v1 >> 5)) + v1) ^ (sum + key[sum & 3]);
}
v[0]=v0; v[1]=v1;
}
int main()
{
uint32_t v[2]={288,369};
uint32_t const k[4]={0x63,0x6f,0x6d,0x62};
unsigned int r=32;
encipher(r, v, k);
printf("%8x%8x\n",v[0],v[1]);
return 0;
}
因為主函數里后續處理是把8 bit十六進制的兩個結果往str里填,所以輸出采用十六進制形式。
得到str="6308fe34b7fe6fdb"
最后進行xor處理即可,上exp:
xtea="6308fe34b7fe6fdb"
arr3=[2,5,4,13,3,84,11,4,87,3,86,3,80,7,83,3,0,4,83,94,7,84,4,0,1,83,3,84,6,83,5,80]
flag=""
for i in range(len(arr3)):
flag+=chr(arr3[i]^ord(xtea[i%len(xtea)]))
print("flag{"+flag+"}")
得到flag:flag{4645e180540ffa7a67cfa174cde105a2}
replace
u1s1不知道這個標級怎么標的,居然是比StrangeMine還高的困難,也有可能是我tcl吧(
主邏輯還是很簡單,輸入長度為24的flag並且以"flag{}"包裹(check在sub_401550()
),然后經過某些處理(sub_401AE7()
、sub_401925()
還有可能的IsDebuggerPresent()
)等於某已知字符串(check在sub_401883()
)即可。
最后check這里byte_4080E0
是輸入后經過處理存放的關鍵位置。
從頭開始捋處理函數,這里sub_401AE7()
的作用是把輸入和已知數組進行xor以后放進byte_4080E0
中。
真的有這么簡單嗎?
當然不會,困難題目誒。(從這里逆向解的話會得到假flag。
真正的奧秘在下一個函數sub_401925()
這里,看到VirtualProtectEx()
這個改內存讀寫權限的關鍵函數,DNA瞬間動了 (別什么奇怪的東西都往DNA里刻啊喂 。
這里很像之前做過的一個題(RE套路/從EASYHOOK學inline hook | c10udlnk_Log),這種函數一般是搭配WriteProcessMemory()
在運行時對內存進行修改(從而達到跳轉到某些函數的目的)。
靜態分析的話那篇blog也有講,但是為了做題方便直接動態調試走起。
因為這里有超——多的反調試,懶得一個個patch了,直接把exit(0)里的功能給patch掉(偷懶大法好。
即把這里六個字節直接patch成90 90 90 90 90 90
,全部nop掉。
變成這個樣子:
別忘了動態調試前要把patch的字節保存進exe里(Edit->Patch program->Apply patches to input file
)。
然后開調,斷點下到sub_401925()
和IsDebuggerPresent()
這里,記得要隨便輸入一個符合前兩個check的字符串。
從sub_401925()
可以大致看到修改的是IsDebuggerPresent()
的內容。
所以我們按F9直接走到IsDebuggerPresent()
這里,F7步入。
可以看到跳轉到了一個函數,而再往里走可以看到這是個被花指令處理的函數所以靜態分析down掉了。
很容易就能找到三個跟第一題相同原理的花指令(注意+2那里要nop多一個字節),所以直接patch,然后把數據按c轉成code。
最后得到反編譯結果:
void sub_4015C3()
{
int v0[128]; // [rsp+30h] [rbp-50h] BYREF
__int64 v1[5]; // [rsp+230h] [rbp+1B0h]
char *v2; // [rsp+258h] [rbp+1D8h]
int v3; // [rsp+264h] [rbp+1E4h]
int j; // [rsp+268h] [rbp+1E8h]
int i; // [rsp+26Ch] [rbp+1ECh]
VirtualProtectEx(hProcess, IsDebuggerPresent_0, 0x10ui64, 0x40u, &flNewProtect);
WriteProcessMemory(hProcess, IsDebuggerPresent_0, Destination, 0x10ui64, 0i64);
VirtualProtectEx(hProcess, IsDebuggerPresent_0, 0x10ui64, flNewProtect, 0i64);
if ( !IsDebuggerPresent() )
{
j = 0;
v3 = 0;
v1[0] = 0i64;
v1[1] = 0i64;
v1[2] = 0i64;
v1[3] = 0i64;
memcpy(v0, &unk_404020, sizeof(v0));
v2 = Str;
for ( i = 1; i <= 5; ++i )
{
for ( j = 0; j <= 23; ++j )
v2[j] = v0[(unsigned __int8)v2[j]];
}
for ( i = 0; i <= 5; ++i )
*((_DWORD *)v1 + i) = ((unsigned __int8)v2[i + 12] << 8) | ((unsigned __int8)v2[i + 6] << 16) | ((unsigned __int8)v2[i] << 24) | (unsigned __int8)v2[i + 18];
for ( i = 0; i <= 5; ++i )
sprintf(&byte_4080E0[8 * i], "%x", *((unsigned int *)v1 + i));
}
}
就是先把IsDebuggerPresent()
里的內容還原,然后在非調試情況下將輸入經過一些處理放到byte_4080E0
中。
這!才是真正的加密函數!
然后這個邏輯也很好逆啦,順着就是先在unk_404020盒里換五次,然后柵欄密碼得到最后的字符串。
寫出exp有:
from binascii import *
from hashlib import md5
ans=unhexlify("416f6b116549435c2c0f1143174339023d4d4c0f183e7828")
tmps=[0,6,12,18]
seq=[]
for i in range(6):
tmpl=[x+i for x in tmps]
seq=seq+tmpl
arr1=[0 for i in range(24)]
for i in range(24):
arr1[seq[i]]=ans[i]
box=[0x00000080, 0x00000065, 0x0000002F, 0x00000034, 0x00000012, 0x00000037, 0x0000007D, 0x00000040, 0x00000026, 0x00000016, 0x0000004B, 0x0000004D, 0x00000055, 0x00000043, 0x0000005C, 0x00000017, 0x0000003F, 0x00000069, 0x00000079, 0x00000053, 0x00000018, 0x00000002, 0x00000006, 0x00000061, 0x00000027, 0x00000008, 0x00000049, 0x0000004A, 0x00000064, 0x00000023, 0x00000056, 0x0000005B, 0x0000006F, 0x00000011, 0x0000004F, 0x00000014, 0x00000004, 0x0000001E, 0x0000005E, 0x0000002D, 0x0000002A, 0x00000032, 0x0000002B, 0x0000006C, 0x00000074, 0x00000009, 0x0000006E, 0x00000042, 0x00000070, 0x0000005A, 0x00000071, 0x0000001C, 0x0000007B, 0x0000002C, 0x00000075, 0x00000054, 0x00000030, 0x0000007E, 0x0000005F, 0x0000000E, 0x00000001, 0x00000046, 0x0000001D, 0x00000020, 0x0000003C, 0x00000066, 0x0000006B, 0x00000076, 0x00000063, 0x00000047, 0x0000006A, 0x00000029, 0x00000025, 0x0000004E, 0x00000031, 0x00000013, 0x00000050, 0x00000051, 0x00000033, 0x00000059, 0x0000001A, 0x0000005D, 0x00000044, 0x0000003E, 0x00000028, 0x0000000F, 0x00000019, 0x0000002E, 0x00000005, 0x00000062, 0x0000004C, 0x0000003A, 0x00000021, 0x00000045, 0x0000001F, 0x00000038, 0x0000007F, 0x00000057, 0x0000003D, 0x0000001B, 0x0000003B, 0x00000024, 0x00000041, 0x00000077, 0x0000006D, 0x0000007A, 0x00000052, 0x00000073, 0x00000007, 0x00000010, 0x00000035, 0x0000000A, 0x0000000D, 0x00000003, 0x0000000B, 0x00000048, 0x00000067, 0x00000015, 0x00000078, 0x0000000C, 0x00000060, 0x00000039, 0x00000036, 0x00000022, 0x0000007C, 0x00000058, 0x00000072, 0x00000068]
arr2=[0 for i in range(24)]
for i in range(5):
for j in range(24):
arr2[j]=box.index(arr1[j])
arr1=arr2
myInput=''.join(map(chr,arr1))
print(myInput)
flag=myInput.encode()
print(md5(flag).hexdigest()) # 別忘了最后提交md5
得到flag:flag{Sh1t_you_dec0d3_it}
奇怪的掃雷
這道題賽中沒怎么做,賽后復盤靜態硬剛+動態調沒搞出來,終於等到了帶掃雷玩的wp(MAR DASCTF明御攻防賽 PWN、RE Writeup - 安全客,安全資訊平台),感覺思路大概沒錯、關鍵函數也找對了,但是忽略了一個小地方:
我當時還在納悶為什么是用代碼段的數據來計算md5值,原來是為了檢測有沒有patch啊(
以及請教隊里大佬以后才發現還忽略了一個點,就是md5的update是拼接的,沒認真學md5的我一直以為是覆蓋的(慚愧,真的要好好學哈希算法。
編寫用來hook的dll文件對我來說還是有點難(tcl),就試着用自己的方法做一下好了。
(啊其實就是直接靜態硬解,從頭捋捋怎么發現函數的。
首先直接findcrypt,找到md5的常數。
通過交叉引用找到上一層。
發現這里有一個前面題目提到的花指令,照例nop掉,轉函數,看偽代碼。
感覺這是個自己寫的處理異常的函數(TopLevelExceptionFilter
這個名字引起警覺)。
然后往下看,先根據交叉引用+識別算法給MD5的一系列函數命個名,方便靜態分析。
(也可以盲找AfxMessageBox
的交叉引用,畢竟能出flag的地方一般都在MessageBox。)
更何況這題是賽后復盤,根據大家群里零零散散發出來的截圖可以判斷flag用AfxMessageBox
給出,而查AfxMessageBox的交叉引用可以發現就這里是放了變量的。
主要邏輯在:
很明顯地看到,v12裝着最后的md5(也就是flag。
整體邏輯是:
- 先把
IUnknown::operator=()
函數首地址開始的12288字節(0x402000-0x405000)放到Src
里,然后根據觸發異常時的eax、ebx、ecx、edx的值修改部分位置(Src[4101]
、Src[128]
、Src[256]
、Src[0x2000]
),最后把Src
丟進MD5Update里。 - 把
&loc_404FFE + 2
開始的12288字節(0x405000-0x408000)放到Src
里,再Src
丟進MD5Update里。 - 最后算出MD5,此MD5值即為flag。
慶幸這個MD5算法沒有被魔改,不然還得找魔改的地方(癱。
現在缺的數據就是觸發異常時的eax、ebx、ecx、edx的值了,只要找到check一切好說,可以從時間的增加入手找到操縱游戲的邏輯部分。
游戲類果斷上CE(Cheat Engine)找關鍵內存,先綁上運行的exe文件,調整掃描設置后點擊“首次掃描”。
在界面隨便點一下開始游戲(讓游戲開始計時),然后調整CE右側設置,一直點“再次掃描”(注意點擊間隔在1s以上,不然時間根本就沒動過就掃描不出來了),直到左側只剩少數地址(並且有一個地址的當前值和計時器的相同)為止。
雙擊這條地址,選中記錄,按F6
,查看改寫這個地址的位置。
可以看到
在IDA的反匯編窗口中按g
跳到這個地址(0x408803
)
再反編譯可以看到
這就是時間增加的函數。
依次查交叉引用,可以發現check應該存在於這個右鍵松開的函數里,並且可以猜是sub_403D30()
。
為什么我篤定是這個呢,因為這個函數不僅是以if框着的形式調用,而且里面還會觸發一個異常:
也就是我們常說的CC斷點
這邊的邏輯可以猜測是循環check,一旦有不對的地方就直接return 0;
,全部通過以后可觸發斷點,進而走到TopLevelExceptionFilter
並給flag。
而我們需要的正是這里的eax、ebx、ecx、edx的值。
而觀察整個check函數的匯編可以發現,最后是直接用變量給四個寄存器賦值的,並且在之前的代碼中這些變量幾乎沒有變動(具體可以自己琢磨琢磨,看目的寄存器的位置;只可意會不可言傳.jpg),所以可以通過把函數開始的jmp short loc_403D55
patch成jmp short loc_403D9E
。
這樣直接跳過循環來到關鍵位置loc_403D9E
,動態調試時即可在觸發int3斷點時看到四個寄存器的值。
然后開調,老規矩,記得調試之前要把patch的字節保存進exe里(Edit->Patch program->Apply patches to input file
)。
選高級以后隨便點個右鍵插旗子,然后放任程序走,走到斷點時有提示,關掉提示以后可以看到右上角:
四個寄存器的值也拿到了!
接下來就差內存了。因為前面靜態分析的時候我們patch了不少地方(花指令+這波跳轉),所以要拿最最開始的附件來dump。
打開最最開始的附件,選擇File->Script command
,分別在IDC下輸入以下腳本dump出兩塊內存:
static main(){
auto i,fp;
fp=fopen("./dump_0x402000","wb");
auto start=0x402000;
auto size=12288;
for(i=start;i<start+size;i++){
fputc(Byte(i),fp);
}
}
static main(){
auto i,fp;
fp=fopen("./dump_0x405000","wb");
auto start=0x405000;
auto size=12288;
for(i=start;i<start+size;i++){
fputc(Byte(i),fp);
}
}
然后編寫python3腳本,按照我們最開始分析的邏輯得到flag:
from hashlib import md5
with open("./dump_0x402000",'rb') as fp:
data_0x402000_list=list(fp.read())
with open("./dump_0x405000",'rb') as fp:
data_0x405000=fp.read()
eax=0
v9=eax//2
ebx=0x10
ecx=0x1E
edx=0x64
data_0x402000_list[4101]=ebx
data_0x402000_list[128]=ecx
data_0x402000_list[256]=edx
data_0x402000_list[0x2000]=v9
data_0x402000=b''
for b in data_0x402000_list:
data_0x402000+=b.to_bytes(1,'little')
md5=md5()
md5.update(data_0x402000)
md5.update(data_0x405000)
print(md5.hexdigest())
flag:4a468b1a17760d263b0969963e0a6c9b