052.esgis.1
UPX 0.89 - 3.xx -> Markus & Laszlo ver. [ 0.76 ] <- from file.
A classic viusal C++ programe, compiled at 1998.
Headers
長文預警
本文大概有三千個字
先上圖;


使用linxue的脫殼機去掉packer,或者使用通用oep大法,upx主要是壓縮沒有加密,反正也不難。
追蹤 引用字符串找到函數結點;
int __thiscall sub_401C20(void *this) {
void *_this; // ebp@1
unsigned int len_plus1; // kr20_4@1
char *v3; // edi@1
int v4; // eax@1
int Ntimes64; // esi@3
int ii; // edi@3
int result; // eax@6
signed int j; // esi@7
unsigned int *guessPtr; // edi@7
signed int i; // eax@9
DWORD Type; // [sp+10h] [bp-608h]@1
DWORD cbData; // [sp+14h] [bp-604h]@1
HKEY phkResult; // [sp+18h] [bp-600h]@1
int guessHex; // [sp+1Ch] [bp-5FCh]@5
char v15; // [sp+20h] [bp-5F8h]@5
char v16; // [sp+24h] [bp-5F4h]@5
char v17; // [sp+28h] [bp-5F0h]@5
int C0_FF_0[2]; // [sp+2Ch] [bp-5ECh]@1
int v19; // [sp+34h] [bp-5E4h]@3
char userString[500]; // [sp+3Ch] [bp-5DCh]@1
BYTE tmp; // [sp+230h] [bp-3E8h]@1
CHAR guessString; // [sp+424h] [bp-1F4h]@1
_this = this;
memset(userString, 0, sizeof(userString));
memset(&guessString, 0, 0x1F4u);
memset(&tmp, 0, 0x1F4u);
getStrFromTextBox(this + 92, userString, 100);
getStrFromTextBox(_this + 172, &guessString, 100);
strcpy(&tmp, userString);
_strrev(&tmp); // input1 reversed
strcat(userString, &tmp); // normal+reversed str
RegOpenKeyA(HKEY_LOCAL_MACHINE, "SOFTWARE\\Microsoft\\Windows\\CurrentVersion", &phkResult);
Type = 1;
cbData = 256;
RegQueryValueExA(phkResult, "ProductID", 0, &Type, &tmp, &cbData);
Type = 1;
strcat(userString, &tmp);
cbData = 256;
RegQueryValueExA(phkResult, "RegisteredOwner", 0, &Type, &tmp, &cbData);
strcat(userString, &tmp); // 1234_4321_4321_4321
len_plus1 = strlen(userString) + 1;
setConst_4016E0(C0_FF_0);
memset(&userString[len_plus1 - 1], 0, 76u);
v3 = &userString[len_plus1 + 75];
*v3 = 0;
v3[2] = 0;
v4 = 64 - (len_plus1 & 0x3F);
userString[len_plus1 - 1] = 0x80u;
if ( v4 <= 7 )
v4 += 64;
Ntimes64 = v4 + len_plus1;
ii = 0;
for ( *(&v19 + v4 + len_plus1) = 8 * strlen(userString); ii < Ntimes64; ii += 64 )
MD5like_401700(&userString[ii], C0_FF_0); // MD5 like
//
C0_FF_0[0] = LOWORD(C0_FF_0[0]);
if ( sscanf(&guessString, "%lx%lx%lx%lx", &guessHex, &v15, &v16, &v17) == 4 ) { //
guess text like :'0x12341234 0x12341234 0x12341234 0x12341234'
j = 0;
guessPtr = &guessHex;
do {
rolN_401B90(guessPtr, 0xBADC0DE / (j++ + 0x50));
++guessPtr;
} while ( j < 3 );
i = 0;
while ( *(&guessHex + i * 4) == C0_FF_0 ) {
++i;
if ( i >= 4 )
return msgbox_4100A8("... Man, you're good enough to join CORE! ...", "Welcome", 0x40u);
}
result = msgbox_4100A8("... Better luck next time ...", "Failed", 0x30u);
} else {
result = msgbox_4100A8("... Hmmm, you don't even pass the first threshold ...", "Failed", 0x30u);
}
return result;
}
主要函數就在這個block里。
內存地址:0x19ed24
inputtext1.會操作3次得到一個4倍長度的strings1(Win10注冊表是空,直接復制3次),inputtext2以%lx的方式讀入4個,然后調用sub_401B90循環移位N次得到一個16bytes的字符串,與strings1 的類似MD5摘要結果進行比較。
input1->(like MD5 digest)——┐
cmp---(Y/N)
input2->(ROL+strange+1)___」
MD5 like func
使用PEID的Krypto 插件檢查算法會發現有且僅有md5算法,
地址塊 :dword_41B0B0;
所有的常數(F G H I)里面的都是對的,包括UPX0:setConst_4016E0函數 sets initial magic number;
但是,怎么看這個函數都不是正常的標準md5,也沒有動力去分析一個修改過的md5;
考慮到在UPX0:00401E56還and了一個0xffff,表明直接爆破md5是不可能的了;
不過可以在內存地址里得到摘要值;
對應于 "0000"的輸入,摘要值是
0019ED34 |BB A2 00 00 B8 2E 9A A3 | 42 25 88 E8 70 B8 C4 9D| ........B%..p...
這16bytes數據;
ROL strange array
這個函數是最有意思的函數;開始的時候發現它每次循環修改2 int,
c[0,1],c[1,2],c[2,3],
循環3次剛剛好全部的16bytes
查看反匯編偽代碼
char *__cdecl rolN_401B90(unsigned int *ptr, int const1)
{
char *result; // eax@1
int cnt; // ebx@1
unsigned int Lsi; // esi@1
unsigned int Hdi; // edi@1
unsigned __int64 tt0; // rax@2
int t1; // esi@2
int t2; // edi@2
unsigned __int64 tt; // rax@2
result = ptr;
cnt = const1;
Lsi = *ptr;
Hdi = ptr[1];
if ( const1 )
{
do
{
tt0 = 2 * __PAIR__(Hdi, Lsi);
LODWORD(tt0) = ((2 * Lsi) | (Hdi >> 31)) & 4;
t1 = tt0 | (Hdi >> 31);
t2 = HIDWORD(tt0);
tt = tt0 << 11;
LODWORD(tt) = t1 & 0x2000 ^ tt;
tt <<= 18;
LODWORD(tt) = t1 & 0x80000000 ^ tt;
tt *= 2i64;
Lsi = tt ^ t1;
Hdi = HIDWORD(tt) ^ t2;
--cnt;//IDA監控點
}
while ( cnt );
result = ptr;
}
*result = Lsi;
*(result + 1) = Hdi;
return result;
}
不好意思,研究了好久發現,這個代碼是錯誤的,但是只有試過才大概了解到一大堆位運算在干嘛;
在這個地方卡了好久,花時間修改導出的C代碼,發現是錯誤的計算結果;
經過匯編跟蹤比較,發現偽代碼錯誤,放棄;
用IDA dump腳本監控前幾百次運行是這樣的:
ebx, esi, edi,
0,00255F35,00000001,00000002,0000000200000001
1,00255F35,00000002,00000004,0000000400000002
2,00255F34,00000004,00000009,0000000900000004*
3,00255F33,00000008,00000012,0000001200000008
4,00255F32,00000010,00000024,0000002400000010
5,00255F31,00000020,00000048,0000004800000020
6,00255F30,00000040,00000090,0000009000000040
7,00255F2F,00000080,00000120,0000012000000080
8,00255F2E,00000100,00000240,0000024000000100
9,00255F2D,00000200,00000480,0000048000000200
A,00255F2C,00000400,00000900,0000090000000400
B,00255F2B,00000800,00001200,0000120000000800
C,00255F2A,00001000,00002400,0000240000001000
D,00255F29,00002000,00004801,0000480100002000*
E,00255F28,00004000,00009002,0000900200004000
Try Brute force
這里走歪了;
這個函數 接受 4 uint,運行后和md5like 的摘要比較,得到是否正確;
前面說到了,直接用導出的fake C code是不能用的,于是我們想要爆破的話,直接用匯編代碼來運行就好了嘛;
這里主要寫內聯匯編,把rolN_401B90 函數的匯編代碼提取出來,在vs2015里改寫接口部分,
__declspec(naked)
char* __cdecl rolN_401B90(unsigned int *Cptr, int Cconst1) {
//checked match original funcs
#define ptr 4
#define const1 8
__asm {
mov eax, [esp + ptr]
push ebx
mov ebx, [esp + 4 + const1]
push esi
mov esi, [eax]
push edi
...MORE...
RETZERO:; CODE XREF : __allshl + 3j
xor eax, eax
xor edx, edx
retn
}
#undef ptr
#undef const1
}
運行了一個測試,發現是正確的;
于是寫了一個爆破Generator function 來初始化輸入值,每10000次打個log;
結果是,原始匯編代碼運行1W次時間,4~5minutes,
如果要爆破完16bytes,需要集群了,emm.....
有沒有土豪贊助我一車GTX1080和i7 7700K 嗎
直接爆破不行,就需要分析這個匯編blocks在干嘛,這里現在看來是最最重要的;
前面調試可以看到全部是位運行,既然是數學,這里使用符號傳遞來嘗試分析;事實證明這個方法是正確的。
; char* __declspec(naked) __cdecl rolN_401B90(unsigned int *ptr, int const1)
rolN_401B90 proc near ; CODE XREF: sub_401C20+284p
;ptr = dword ptr 4
;const1 = dword ptr 8
mov eax, [esp+ptr]
push ebx
mov ebx, [esp+4+const1];cnt;
push esi
mov esi, [eax];c[0]
push edi
mov edi, [eax+4];c[1]
test ebx, ebx
jbe short loc_401C15
push ebp
loop1: ; CODE XREF: rolN_401B90+7Ej
mov ebp, edi
mov ecx, 1 ;ecx=1;
shr ebp, 1Fh
mov [esp+10h+const1], ebp;get c[1].bit31;
mov eax, esi
mov edx, edi
xor ebp, ebp ;ebp=0
call __allshl; edx,eax <<1
;eax=c[0]<<1
;edx=c[1]<<1|c[0]>>31;
mov ecx, [esp+10h+const1];ecx=c[1]>>31
or ebp, edx ; mov ebp,edx;
;ebp=c[1]<<1 | c[0]>>31;
or ecx, eax ;ecx=c[1]>>31|(c[0]<<1);
xor edx, edx ;edx=0;
mov esi, ecx ;esi=c[1]>>31|(c[0]<<1);
mov ecx, 11 ;ecx=11
mov eax, esi ;eax=c[1]>>31|(c[0]<<1);
mov edi, ebp ;edi=c[1]<<1 | c[0]>>31 ;
and eax, 4 ;eax=(c[1]>>31|(c[0]<<1)) & 4;
call __allshl
;eax=((c[1].bit31|(c[0]<<1)) & 4)<<11
;eax=((c[1]>>31 |(c[0]<<1)) & 4)<<11
;eax=((c[0]<<1) & 4)<<11
;eax=( c[0]&2 ) <<12
;edx=0
mov ecx, esi ;ecx=c[1]>>31|(c[0]<<1)
xor ebp, ebp ;ebp=0
and ecx, 2000h ;ecx=(c[1]>>31|(c[0]<<1)) & 0x2000
;ecx=(c[0]<<1) & 0x2000
xor edx, ebp ;NOP ;ebp==0
xor eax, ecx ;eax=(((c[0]<<1) & 4)<<11) ^ (c[0]<<1 & 0x2000)
;eax=( ( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000)
mov ecx, 12h ;ecx=18;
call __allshl
;eax=( (( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000)) << 18
;edx=( (( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000)) >> 14 Always edx==0
;
mov ecx, esi ;ecx=c[1]>>31|(c[0]<<1);
xor edx, ebp ;NOP,ebp==0;
and ecx, 80000000h ;ecx=c[1].bit31|(c[0]<<1) & 0x8000_0000;
;ecx=(c[0]<<1) & 0x8000_0000;
xor eax, ecx
;eax=( ((( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000))<<18 )^( (c[0]<<1) & 0x8000_0000 )
mov ecx, 1 ;ecx=1
call __allshl
;eax=(( ((( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000))<<18 )^( (c[0]<<1) & 0x8000_0000 ))<<1;
;edx=( ((( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000)) >> 14)<<1 |( (((( c[0]&2 ) <<12) ^
(c[0]<<1 & 0x2000))<<18 )^( (c[0]<<1) & 0x8000_0000 ) )>>31
xor esi, eax
;c[0]=(c[1]>>31|(c[0]<<1)) ^( ((((( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000))<<18 )^( (c[0]<<1) & 0x8000_0000 ))<<1 )
; ^ ((c[0].bit1<<31 ^ c[0].bit12<<31 ^ c[0].bit30<<31 )<<1)
;c[0]=(c[1]>>31|(c[0]<<1)) ^ 0
;c[0]= c[1]>>31|(c[0]<<1)
xor edi, edx
;c[1]=(c[1]<<1 | c[0]>>31) ^( ( ((( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000)) >> 14)<<1 |
( (((( c[0]&2 ) <<12) ^ (c[0]<<1 & 0x2000))<<18 )^( (c[0]<<1) & 0x8000_0000 ) )>>31 )
; ( c[0].bit1<<13^ c[0].bit12<<13 )>>14 <<1
;c[1]=(c[1]<<1 | c[0]>>31) ^( 0 |
(c[0].bit1<<31 ^ c[0].bit12<<31 ^ c[0].bit30<<31 )>>31)
;c[1]=(c[1]<<1 | c[0]>>31) ^(c[0].bit1 ^ c[0].bit12 ^ c[0].bit30 )
;c[1]=(c[1]<<1 | c[0]>>31) ^(c[0]>>1 ^c[0]>>12 ^c[0]>>30)&1
;c[1]= c[1]<<1 ^ (c[0]>>31 ^c[0]>>1 ^c[0]>>12 ^c[0]>>30)&1
dec ebx
jnz short loop1
mov eax, [esp+10h+ptr]
pop ebp
loc_401C15: ; CODE XREF: rolN_401B90+12j
mov [eax], esi
mov [eax+4], edi
pop edi
pop esi
pop ebx
retn
endp
__allshl proc near ; CODE XREF: rolN_401B90+29p
; rolN_401B90+46p ...
; cmp cl, 40h
; jnb short RETZERO
; cmp cl, 20h
; jnb short MORE32
shld edx, eax, cl
shl eax, cl
retn
; ---------------------------------------------------------------------------
MORE32: ; CODE XREF: __allshl+8j
mov edx, eax
xor eax, eax
and cl, 1Fh
shl edx, cl
retn
; ---------------------------------------------------------------------------
RETZERO: ; CODE XREF: __allshl+3j
xor eax, eax
xor edx, edx
retn
endp
簡單來說,
被操作的2個uint 會循環N次,
每次操作
c[1]=(c[1]<<1 | c[0]>>31) ^(c[0]>>1 ^c[0]>>12 ^c[0]>>30)&1
c[0]= c[1]>>31|(c[0]<<1)
差不多就是兩個uint32 相互循環移位,附加c[1]一個特殊的最低位XOR,條件是c[0]的c[0].bit1 ^ c[0].bit12 ^ c[0].bit30 的異或 ;
得到這個算法以后寫了個C代碼測試,驗證正確,結果又嘗試了爆破,嗯,在編譯器O2優化下 2mins/1W 計數;
不行,這個速度太慢了,于是按照邏輯寫了個純匯編的函數,手寫的主要循環體優化后只有9行匯編代碼,運行速度1mins/1W 計數,還是不夠快
柳暗花明又一村
算法已知是迭代的,普通不能爆破,那就只能找算法的特性/漏洞了,比如通項函數什么的。
但是,上面的C表述基本不能看出什么;
于是就畫了好幾個算法運行的示意圖
最后就這個看出端倪了。


可以回代運算(N+1)項得到第N項;
核心是 c[0]的n項的0~30bit在c[0]的n+1項的1~31bit, c[0]n項缺少的 31bit可以從c[0]的n+1項的31,13,2和c[1]的n+1項的最低位這四個東西異或得到;
C代碼表述
char* rolN_reverse(uint32_t* ptr, int cnt) {
uint32_t n[2], n_1[2];
n[0] = ptr[0];
n[1] = ptr[1];
printf("[-]Get: %X %X\n", n[0], n[1]);
for (int i = 0; i < cnt; i++) {
n_1[0] = (n[0] >> 1 & 0x7fffffff)|((n[1]&1 ^ n[0]>>31^ n[0]>>13^ n[0]>>2)&1)<<31;
n_1[1] = (n[1] >> 1 &0x7fffffff )|n[0]<<31;
n[0] = n_1[0];
n[1] = n_1[1];
}
printf("[-]Rev: %X %X\n", n[0], n[1]);
ptr[0] = n[0];
ptr[1] = n[1];
return ptr;
}
寫了個函數驗證,結果正確;
不考慮奇怪的MD5,
最后把注冊機(的½)寫一下就是
#define V0 0xA2BB
#define V1 0XA39A2EB8
#define V2 0XE8882542
#define V3 0X9DC4B870
#define CV0 0x255f35
#define CV1 0x24e918
#define CV2 0x2475dd
uint32_t knownArray[4] = {V0,V1,V2,V3};
rolN_reverse(knownArray + 2, CV2 );
rolN_reverse(knownArray + 1, CV1);
rolN_reverse(knownArray + 0, CV0);
for (int i = 0; i < 4; i++) {
printf(" 0x%X", knownArray);
}
/*
[-]Get: E8882542 9DC4B870
[-]Rev: D4787858 54130406
[-]Get: A39A2EB8 D4787858
[-]Rev: 8C0D43AF 50B025BA
[-]Get: A2BB 8C0D43AF
[-]Rev: 2C6B2A6D EF4CABB9
input:0000
input:0x2C6B2A6D 0xEF4CABB9 0x50B025BA 0x54130406
*/
所以,

成功完成任務;
期間各種奇奇怪怪的bug和錯誤歧途就不再描述了。。。
Summary
- 內聯匯編,x86 asm
- Z3符號引擎 不知道能不能用上幫助分析,手動分析匯編邏輯還是太慢了;
- 不要一條路走到黑,比如爆破什么的。~( ̄▽ ̄)~*
- 筆和草稿紙需常備案頭,in case of something;
- ...