April 25, 2022

DASCTF 复现

一月一度,DASCTF!

SU 三月春季挑战赛

easyre

脚本不贴了,都是抄IDA代码,懒得改那一坨很丑的代码了
简单说一下我遇到的问题:
1.我刚开始手动脱壳的,虽然跑不了,但是IDA可以反编译出来。后来去网上搜了搜,大部分都是脱壳机……我下了好几个脱壳机,都说没有ASPack,很迷。有没有带师有什么很nb的脱壳机,请务必告诉我
2.加密是用的一个魔改的RC4,代码逻辑是什么不管,直接复制IDA代码,简单改一下然后跑。按道理来讲这种RC4不应该管他的逻辑,直接输入然后动调取值来异或,但是我手动脱的壳可能有些问题,(看别人说要用一个工具修复导入表,但是我摆弄了半天那个程序,没搞懂怎么玩的),跑不了所以没法动调。OD调试也8太会,输入之后直接退出了,麻。
3.刚开始跑出来是乱码,因为我最开始改的python,都用的range函数,改成C为了方便都用了while循环,结果我把计数器自增的代码写到最前面了,我是什么憨批。
3.改完之后跑是能跑出来了,但是中间有几个字符是乱码。虽然已经半夜两点多,但是不知道原因我是真!的!难!受!啊!!!网上找了份WP,两个代码一起调,发现是数据类型的锅。RC4里s盒正常人都用的unsigned char类型,我写脚本也顺手写成无符号字符型了,但是由于这个题是魔改,对s盒运算的时候有一些奇怪的运算,要用int才能保证运算结果的正确性,unsigned char会溢出而损失部分运算结果……我!#%^@$@^*#

login

仍是按照PZ师傅的WP复现

这里只记录一些我想的跟PZ师傅不同或者PZ师傅没写的

端口反调试

确定调试端口是通过serv_addr.sin_port = sub_454A60(dword_4DA120);,其中sub_454A60什么也没干,而dword_4DA120是1234。对这个数字查找交叉引用可以找到:

1
2
3
4
5
6
7
8
__int64 sub_401D95()
{
__int64 result; // rax

result = (unsigned int)(dword_4DA120 + 22712);
dword_4DA120 += 22712;
return result;
}

对这个函数查找交叉引用是找不到的。那么这个函数到底在哪里被调用?去找找init函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall _libc_csu_init(unsigned int a1, __int64 a2, __int64 a3)
{
__int64 result; // rax
signed __int64 v5; // r14
__int64 i; // rbx

result = init_proc();
v5 = ((char *)&off_4D6F90 - (char *)&off_4D6F78) >> 3;
if ( v5 )
{
for ( i = 0LL; i != v5; ++i )
result = ((__int64 (__fastcall *)(_QWORD, __int64, __int64))*(&off_4D6F78 + i))(a1, a2, a3);
}
return result;
}

可以看到是把off_4D6F78开头的一个数组作为函数依次调用。一共调用了3个函数,其中第2个函数在地址401D95处,这个就是用于反调试的函数地址。

关于为什么研究这个问题,因为我在复现的时候,IDA将地址401D94识别成了此函数的起始地址,导致这个函数反编译不出来,所以dword_4DA120的交叉引用也找不到。后来去查了一下init调用的函数表,发现是IDA识别的起始地址有问题,自己手动改一下即可。

如何识别RSA算法

拿到程序发现一个符号都没有,抠得干干净净……于是先finger一把梭了

识别出来的RSA函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
_BOOL8 __fastcall RSA(__int64 a1)
{
__int64 v1; // rax
_BOOL8 result; // rax
char n[16]; // [rsp+10h] [rbp-310h] BYREF
char c[16]; // [rsp+20h] [rbp-300h] BYREF
char e[16]; // [rsp+30h] [rbp-2F0h] BYREF
char plain[26]; // [rsp+40h] [rbp-2E0h] BYREF
char v7[6]; // [rsp+5Ah] [rbp-2C6h] BYREF
char v8[64]; // [rsp+60h] [rbp-2C0h] BYREF
char v9[632]; // [rsp+A0h] [rbp-280h] BYREF
unsigned __int64 v10; // [rsp+318h] [rbp-8h]

v10 = __readfsqword(0x28u);
strcpy(
v9,
"13123058934861171416713230498081453101147538789122070079961388806126697916963123413431108069961369055630747412550900"
"23940271082784791796087035865396294828238135174112188452839936976453044650993624026229024830522655211710058472661625"
"52929639711415105186785526790332203152463777462705158539879031845129488013974521045545898037256190760663399689993089"
"10127885089547678968793196148780382182445270838659078189316664538631875879022325427220682805580410213245364855569367"
"70291915788136708567728312473287462156937990127266216202578060866957754654833327476605875578644949127700234991859897"
"1841605936268030140638579388226573929");
strcpy(v8, "By reading we enrich the mind, by conversation we polish it.");
strcpy(v7, "10001");
char_to_int(n, v9, 10LL);
v1 = sub_4057A9((__int64)v8);
char_to_int(c, v1, 16LL);
char_to_int(e, v7, 16LL);
char_to_int(plain, a1, 10LL);
_gmpz_powm(plain, plain, e, n);
result = (unsigned int)_gmpz_cmp(plain, c) == 0;
if ( __readfsqword(0x28u) != v10 )
sub_55CC60();
return result;
}

_gmpz_powm_gmpz_cmp都是finger识别出来的函数名,通过函数名就可以知道这两个函数的功能了。

Apr X FATE

Crackme

为什么我看图标第一反应是C#,离谱
好吧,最后看题解发现是MFC写的……
我感觉这玩意已经在某道题给我留下了难以磨灭的心理阴影

先跑一下,随便输个key和flag,弹窗报wrong,那么去IDA里搜字符串wrong,定位到常量段,刚好下面是success。
去查交叉引用,定位到主要加密逻辑处:
(主要变量和函数我已经修改名称)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
int __thiscall sub_1831E0(int this)
{
const void *v1; // eax
const void *v2; // eax
int result; // eax
unsigned int len_key; // [esp+18h] [ebp-230h]
size_t len_flag; // [esp+20h] [ebp-228h] BYREF
void *Buf1; // [esp+24h] [ebp-224h] BYREF
const void *v8; // [esp+28h] [ebp-220h] BYREF
BYTE *v9; // [esp+2Ch] [ebp-21Ch] BYREF
size_t dwDataLen; // [esp+30h] [ebp-218h] BYREF
size_t v11; // [esp+34h] [ebp-214h] BYREF
DWORD v12; // [esp+38h] [ebp-210h] BYREF
BYTE flag[260]; // [esp+3Ch] [ebp-20Ch] BYREF
BYTE key[260]; // [esp+140h] [ebp-108h] BYREF

CWnd::UpdateData((CWnd *)this, 1);
memset(key, 0, sizeof(key));
memset(flag, 0, sizeof(flag));
len_key = sub_183E70((void *)(this + 216));
len_flag = sub_183E70((void *)(this + 212));
dwDataLen = 0;
v11 = 0;
v12 = 0;
v1 = (const void *)sub_182590(len_key);
memmove(key, v1, len_key);
v2 = (const void *)sub_182590(len_flag);
memmove(flag, v2, len_flag);
if ( len_key != 8 && len_flag != 32 )
return print_wrong((CWnd *)this);
enc(key, len_key >> 1, 0x8003u, (int)&Buf1, (int)&dwDataLen);
enc(&key[4], len_key >> 1, 0x8004u, (int)&v8, (int)&v11);
enc(key, len_key, 0x8003u, (int)&v9, (int)&v12);
memcmp(Buf1, (const void *)(this + 220), dwDataLen);
if ( memcmp(v8, (const void *)(this + 480), v11) )
return print_wrong((CWnd *)this);
sub_1836E0(v9, v12, flag, &len_flag, 0x104u);
if ( !memcmp(flag, (const void *)(this + 740), len_flag) )
result = print_success((CWnd *)this);
else
result = print_wrong((CWnd *)this);
return result;
}

29行指明key和flag的长度分别为8和32,随后31~33行的三个enc函数分别对key[:4]key[4:]、整个key进行加密,结果分别存储在dwDataLenv11v12中。
34~35行将dwDataLenv11中的值与this指针里的两段值(this + 220)(this + 480)进行比较,错误就报wrong,表示key输入错误。
37行将v12(也就是对整个key加密的结果)和flag用函数sub_1836E0加密,38行将结果与(this + 740)比较,正确则报success。

那么整个思路就是动调去取this指针里用来比较的4段值,然后分析enc函数,逆向得到key。
再去逆向分析sub_1836E0,逆向得到flag。

先看enc函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
bool __stdcall enc(BYTE *pbData, DWORD dwDataLen, ALG_ID Algid, int a4, int a5)
{
BYTE *v6; // [esp+10h] [ebp-20h]
BOOL v7; // [esp+18h] [ebp-18h]
BYTE v8[4]; // [esp+1Ch] [ebp-14h] BYREF
DWORD pdwDataLen; // [esp+20h] [ebp-10h] BYREF
HCRYPTPROV phProv; // [esp+24h] [ebp-Ch] BYREF
HCRYPTHASH phHash; // [esp+28h] [ebp-8h] BYREF

phProv = 0;
phHash = 0;
v6 = 0;
*(_DWORD *)v8 = 0;
pdwDataLen = 0;
v7 = CryptAcquireContextA(&phProv, 0, 0, 0x18u, CRYPT_VERIFYCONTEXT);
if ( v7 )
{
v7 = CryptCreateHash(phProv, Algid, 0, 0, &phHash);
if ( v7 )
{
v7 = CryptHashData(phHash, pbData, dwDataLen, 0);
if ( v7 )
{
pdwDataLen = 4;
v7 = CryptGetHashParam(phHash, 4u, v8, &pdwDataLen, 0);
if ( v7 )
{
v6 = (BYTE *)sub_334540(*(size_t *)v8);
if ( v6 )
{
memset(v6, 0, *(size_t *)v8);
v7 = CryptGetHashParam(phHash, 2u, v6, (DWORD *)v8, 0);
if ( v7 )
{
*(_DWORD *)a4 = v6;
*(_DWORD *)a5 = *(_DWORD *)v8;
}
}
else
{
v7 = 0;
}
}
}
}
}
if ( !v7 && v6 )
sub_33453B(v6);
if ( phHash )
CryptDestroyHash(phHash);
if ( phProv )
CryptReleaseContext(phProv, 0);
return v7;
}

都是一些win32的库函数,微软官方文档里都有详细说明,就不一个一个贴了。
主要知道这是一个哈希算法,其中枚举值0x8003表示MD5,0x8004表示SHA1。也就是对key的前半段MD5,后半段SHA1,验证key的正确性,然后对整个keyMD5作为密钥来加密flag。
那么现在就是要去找这段MD5和SHA1,去在线网站查询一下。

但是实际上,这里面写的有反调试,一跑就自动退出……经过对this结构体无尽的寻找,我找到了用来验证的密文和验证过程……

密文存储:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
_DWORD *__thiscall sub_332B30(_DWORD *this, struct CWnd *a2)
{
char v4[48]; // [esp+10h] [ebp-64h] BYREF
int v5; // [esp+40h] [ebp-34h]
int v6; // [esp+44h] [ebp-30h]
int v7; // [esp+48h] [ebp-2Ch]
int v8; // [esp+4Ch] [ebp-28h]
int v9; // [esp+50h] [ebp-24h]
int v10; // [esp+58h] [ebp-1Ch]
int v11; // [esp+5Ch] [ebp-18h]
int v12; // [esp+60h] [ebp-14h]

CDialogEx::CDialogEx((CDialogEx *)this, 0x66u, a2);
*this = &CCrackmeDlg::`vftable';
sub_334120(&unk_4D8D9F);
sub_334120(&unk_4D8DA0);
sub_333AF0();
this[52] = sub_333B10(128);
v10 = 0x600C59A8;
v11 = 0xEAB1281B;
v12 = 0xF9249AE5;
v5 = 0x978C9ED5;
v6 = 0x69D8E4B4;
v7 = 0xAB129C3A;
v8 = 0x9BA449C8;
v9 = 0x579F6DCE;
v4[0] = 0x5B;
v4[1] = 0x9C;
v4[2] = 0xEE;
v4[3] = 0xB2;
v4[4] = 0x3B;
v4[5] = 0xB7;
v4[6] = 0xD7;
v4[7] = 0x34;
v4[8] = 0xF3;
v4[9] = 0x1B;
v4[10] = 0x75;
v4[11] = 0x14;
v4[12] = 0xC6;
v4[13] = 0xB2;
v4[14] = 0x1F;
v4[15] = 0xE8;
v4[16] = 0xDE;
v4[17] = 0x33;
v4[18] = 0x44;
v4[19] = 0x74;
v4[20] = 0x75;
v4[21] = 0x1B;
v4[22] = 0x47;
v4[23] = 0x6A;
v4[24] = 0xD4;
v4[25] = 0x37;
v4[26] = 0x51;
v4[27] = 0x88;
v4[28] = 0xFC;
v4[29] = 0x67;
v4[30] = 0xE6;
v4[31] = 0x60;
v4[32] = 0xDA;
v4[33] = 0xD;
v4[34] = 0x58;
v4[35] = 7;
v4[36] = 0x81;
v4[37] = 0x43;
v4[38] = 0x53;
v4[39] = 0xEA;
v4[40] = 0x7B;
v4[41] = 0x52;
v4[42] = 0x85;
v4[43] = 0x6C;
v4[44] = 0x86;
v4[45] = 0x65;
v4[46] = 0xAF;
v4[47] = 0xB4;
this[55] = 0xA7C0769F;
this[56] = v10;
this[57] = v11;
this[58] = v12;
this[120] = v5;
this[121] = v6;
this[122] = v7;
this[123] = v8;
this[124] = v9;
qmemcpy(this + 185, v4, 0x30u);
return this;
}

其中v4是用于验证的flag的密文,this里的是MD5和SHA1。

至于说怎么找到的……
我们最开始找到的主逻辑是sub_1831E0(int this),这个函数的参数只有一个this指针……那么就去找这个函数的交叉引用,双击跳转之后IDA带我们来到了这里:
.rdata:004D92AC dd offset sub_3331E0
显然这必不是一个函数 那么到底怎么调用的……
按我的理解,这应该是C++封装的类,而sub_3331E0是类里面一个成员函数,所以它调用的时候只有一个this指针。直接找交叉引用也是只有常量段里一张表,没有直接调用的……
那么怎么找,具体来说就是从这里往上翻,翻到类的顶部,然后去查交叉引用,自然能找到调用的地方。
像这个题,从这里往上翻,翻到.rdata:004D9250 unk_4D9250 db 12h可以认为是类的顶部,然后查引用,是上面的数据off_4D9248引用,再查引用,发现是一个函数sub_332E50调用了这个类……
对这个函数再查两次引用,又会跳到一个表,然后再查一次就能找到存密文的函数。
像这样不断查引用,你会找到.rdata:004D92CC ??_7CCrackmeDlg@@6B@,对这个东西的引用处就是存储密文的函数。

为什么说this里存的应该是MD5和SHA1,因为长度是这样子……
MD5长度是32字节,SHA1长度是40字节,正好前4组是MD5,后5组是SHA1,这样子……
额但是直接拿去查查不到,那么应该是对这里面的东西做了一些修改。

上面提到.rdata:004D92CC ??_7CCrackmeDlg@@6B@,这个应该是这个题的主类了(至于为什么是主类,加密的密文都在里面了怎么不是主类(逃
你会看到这个类下面有巨长的一个函数列表,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.rdata:004D92CC ??_7CCrackmeDlg@@6B@ dd offset sub_34ACAD
.rdata:004D92CC ; DATA XREF: sub_332B30+45↑o
.rdata:004D92D0 dd offset sub_332DA0
.rdata:004D92D4 dd offset ?_Alloc_proxy@_Container_base0@std@@QAEXABU_Fake_allocator@2@@Z ; std::_Container_base0::_Alloc_proxy(std::_Fake_allocator const &)
.rdata:004D92D8 dd offset unknown_libname_78 ; MFC 3.1-14.0 32bit
.rdata:004D92DC dd offset ?OnFinalRelease@CWnd@@UAEXXZ ; CWnd::OnFinalRelease(void)
.rdata:004D92E0 dd offset sub_349733
.rdata:004D92E4 dd offset sub_336FDB
.rdata:004D92E8 dd offset UserMathErrorFunction
.rdata:004D92EC dd offset UserMathErrorFunction
.rdata:004D92F0 dd offset ?GetTypeLib@CCmdTarget@@UAEJKPAPAUITypeLib@@@Z ; CCmdTarget::GetTypeLib(ulong,ITypeLib * *)
……
……
.rdata:004D9444 dd offset sub_332E60
.rdata:004D9448 dd offset nullsub_5
.rdata:004D944C dd offset ?OnOK@CDialog@@MAEXXZ ; CDialog::OnOK(void)
.rdata:004D9450 dd offset sub_343FF5
.rdata:004D9454 dd offset nullsub_1

只贴了最开始和结束部分一点点,中间还有好长一串,可以自己康康IDA(逃
当然中间大部分是一些库函数,可以忽略,实际上的函数并不多。一个一个翻sub,可以看到里面大部分是空白的,或者是实现了一些没什么用的功能(x
就这样翻,相信你必可以翻到(x
翻到最后,倒数第2个函数sub_332E60,我们会发现对MD5和SHA1那两串密文的加密逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
int __thiscall sub_332E60(LPARAM *this)
{
const CHAR *v1; // eax
HMODULE v2; // eax
HANDLE v3; // eax
FARPROC ZwSetInformationThread; // [esp+8h] [ebp-24h]
int i; // [esp+14h] [ebp-18h]
int j; // [esp+14h] [ebp-18h]
char v9[4]; // [esp+18h] [ebp-14h] BYREF
int v10; // [esp+28h] [ebp-4h]

CDialog::OnInitDialog((CDialog *)this);
if ( sub_333AC0(0) )
{
sub_331CB0(4u);
sub_332100(v9);
v10 = 0;
sub_333D40(101);
if ( !(unsigned __int8)sub_333A90(v9) )
{
sub_333A60(0x800u, 0, 0);
v1 = (const CHAR *)std::_Ptr_base<_EXCEPTION_RECORD const>::get(v9);
sub_333A60(0, 0x10u, v1);
}
v10 = -1;
sub_3329F0(v9);
}
sub_333A30(this[52], 1u);
sub_333A30(this[52], 0);
v2 = GetModuleHandleA("ntdll.dll");
ZwSetInformationThread = GetProcAddress(v2, "ZwSetInformationThread");
v3 = GetCurrentThread();
((void (__stdcall *)(HANDLE, int, _DWORD, _DWORD))ZwSetInformationThread)(v3, 17, 0, 0);
for ( i = 0; i < 16; ++i )
*((_BYTE *)this + i + 220) ^= i;
for ( j = 0; j < 20; ++j )
*((_BYTE *)this + j + 480) ^= j;
return 1;
}

可以发现是依次异或索引值,那么写个脚本解一下。

实际上31~33行是一种反调,详见CTF-wiki
当然我们前面已经知道了这个程序里必然有反调,如果对反调很熟悉,可以直接去导出表里找相应的反调试函数,然后查交叉引用,就能找到加密逻辑。因为按照经验来讲,只要不是库函数里的反调试,有反调试的地方一般存储着重要的加密逻辑 (不然在这里写反调干什么啊喂!
当然也可以找到反调函数,下断绕过,就能动态去调试获取密文值,不用静态分析了。

静态分析解的脚本如下,在线网站查询出来的字符串写在注释里。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
this_md5=[0]*4
this_md5[0] = 0xA7C0769F
this_md5[1] = 0x600C59A8
this_md5[2] = 0xEAB1281B
this_md5[3] = 0xF9249AE5
#NocT

this_sha1=[0]*5
this_sha1[0] = 0x978C9ED5
this_sha1[1] = 0x69D8E4B4
this_sha1[2] = 0xAB129C3A
this_sha1[3] = 0x9BA449C8
this_sha1[4] = 0x579F6DCE
#uRne

for i in range(4):
for j in range(4):
tmp=this_md5[i]&0xff
this_md5[i]=this_md5[i]>>8
print(hex(tmp^(j+i*4))[2:].rjust(2,'0'),end='')
print()
for i in range(5):
for j in range(4):
tmp=this_sha1[i]&0xff
this_sha1[i]=this_sha1[i]>>8
print(hex(tmp^(j+i*4))[2:].rjust(2,'0'),end='')

这样我们就获得了key。

然后是加密flag的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
bool __stdcall sub_3336E0(BYTE *pbData, DWORD dwDataLen, BYTE *a3, DWORD *pdwDataLen, DWORD dwBufLen)
{
BOOL v6; // [esp+4h] [ebp-18h]
HCRYPTKEY phKey; // [esp+Ch] [ebp-10h] BYREF
HCRYPTPROV phProv; // [esp+10h] [ebp-Ch] BYREF
HCRYPTHASH phHash; // [esp+14h] [ebp-8h] BYREF

phProv = 0;
phHash = 0;
phKey = 0;
v6 = CryptAcquireContextA(&phProv, 0, 0, 0x18u, 0xF0000000);
if ( v6 )
{
v6 = CryptCreateHash(phProv, 0x8003u, 0, 0, &phHash);
if ( v6 )
{
v6 = CryptHashData(phHash, pbData, dwDataLen, 0);
if ( v6 )
{
v6 = CryptDeriveKey(phProv, 0x660Eu, phHash, 1u, &phKey);
if ( v6 )
v6 = CryptEncrypt(phKey, 0, 1, 0, a3, pdwDataLen, dwBufLen);
}
}
}
if ( phKey )
CryptDestroyKey(phKey);
if ( phHash )
CryptDestroyHash(phHash);
if ( phProv )
CryptReleaseContext(phProv, 0);
return v6;
}

CryptDeriveKey函数
CryptEncrypt函数
ALG_ID枚举值

可以查到0x660E对应CALG_AES_128

彳亍口巴,用python写半天解密被报了一脸错……
那就抄IDA的伪代码,用cpp仿写原程序的逻辑,调用这些API函数来模拟一遍加密过程,最后调用解密函数即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <windows.h>
#include <windef.h>
#include <wincrypt.h>
#include <stdio.h>

bool sub_3336E0(BYTE* pbData, DWORD dwDataLen, BYTE* a3, DWORD* pdwDataLen, DWORD dwBufLen)
{
BOOL v6; // [esp+4h] [ebp-18h]
HCRYPTKEY phKey; // [esp+Ch] [ebp-10h] BYREF
HCRYPTPROV phProv; // [esp+10h] [ebp-Ch] BYREF
HCRYPTHASH phHash; // [esp+14h] [ebp-8h] BYREF

phProv = 0;
phHash = 0;
phKey = 0;
v6 = CryptAcquireContextA(&phProv, 0, 0, 0x18, 0xF0000000);
if (v6)
{
v6 = CryptCreateHash(phProv, 0x8003u, 0, 0, &phHash);
if (v6)
{
v6 = CryptHashData(phHash, pbData, dwDataLen, 0);
if (v6)
{
v6 = CryptDeriveKey(phProv, 0x660E, phHash, 1, &phKey);
if (v6)
v6 = CryptDecrypt(phKey, 0, 1, 0, a3, pdwDataLen);
}
}
}
if (phKey)
CryptDestroyKey(phKey);
if (phHash)
CryptDestroyHash(phHash);
if (phProv)
CryptReleaseContext(phProv, 0);
return v6;
}

int main()
{
BYTE flag[] = { 0x5B,0x9C,0xEE,0xB2,0x3B,0xB7,0xD7,0x34,0xF3,0x1B,0x75,0x14,0xC6,0xB2,0x1F,0xE8,0xDE,0x33,0x44,0x74,0x75,0x1B,0x47,0x6A,0xD4,0x37,0x51,0x88,0xFC,0x67,0xE6,0x60,0xDA,0xD,0x58,7,0x81,0x43,0x53,0xEA,0x7B,0x52,0x85,0x6C,0x86,0x65,0xAF,0xB4 };
BYTE key[] = { 0x5c,0x53,0xa4,0xa4,0x1d,0x52,0x43,0x7a,0x9f,0xa1,0xe9,0xc2,0x6c,0xa5,0x90,0x90 };
DWORD dwKeyLen = 16;
DWORD dwDataLen = 32;
DWORD* pdwDataLen = &dwDataLen;
DWORD dwBufLen = 0x104;
sub_3336E0(key, dwKeyLen, flag, pdwDataLen, dwBufLen);
return 0;
}

VS调了半天没搞明白怎么查看变量,maybe需要插件什么的,一直报a3是无效的字符串……
行吧,扔IDA里调,下断到解密完成的地方跑程序。
但是a3还是点不进去,直接去hex里跳转到ebp,然后往下翻,找到flag。

FakePica

高高兴兴脱了壳打开jadx,面对巨长的类表显然是没心情翻的,直接搜MainActivity,但是搜出来一堆奇奇怪怪的东西,什么加密也没有。
于是☁️戴上了痛苦面具,开始在巨长的类表里一个一个翻。
然而翻了N年也没翻到,☁️终于忍不了了,直接去看WP,搜WP里的类名,然鹅也还是没有。

怎么会是呢?
是啊怎么会是呢?

WP如此简单,两组AES密文直接解密,简单得让☁️怀疑人生。

主类在哪啊?你的主类到底tmd在哪啊???这个题的唯一难度难道就在于找主类吗???

☁️关了jadx冷静了一会,想起来自己刚才脱壳的时候手滑点到了另一个APP,所以那个本来只有5MB的题到☁️手上脱壳出来20多MB的文件。

原来世界上真的又这么傻逼的人啊。
确实,除了你没别人了。

☁️长叹一声,爬回去找文件。

脱壳,找到主类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class MainActivity extends ActivityC0214AppCompatActivity {
private final IvParameterSpec IV_PARAMETER_SPEC = new IvParameterSpec("0102030405060708".getBytes());
Button checkIn;
byte[] content0 = {-114, 95, -37, Byte.MAX_VALUE, -110, 113, 41, 74, 40, 73, 19, 124, -57, -88, 39, -116, -16, -75, -3, -45, -73, -6, -104, -6, -78, 121, 110, 74, -90, -47, -28, -28};
byte[] content1 = {-40, 26, 95, -49, -40, -123, 72, -90, -100, -41, 122, -4, 25, -101, -58, 116};
EditText emailInput;
String key = "picapicapicapica";
EditText passWordInput;

public String encryptIntoHexString(String data, String key2) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(1, new SecretKeySpec(key2.getBytes(), "AES"), this.IV_PARAMETER_SPEC);
return bytesConvertHexString(cipher.doFinal(Arrays.copyOf(data.getBytes(), ((data.getBytes().length / 16) + 1) * 16)));
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

public String decryptByHexString(String data, String key2) {
try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(2, new SecretKeySpec(key2.getBytes(), "AES"), this.IV_PARAMETER_SPEC);
return new String(cipher.doFinal(hexStringConvertBytes(data.toLowerCase())), "UTF-8");
} catch (Exception e) {
e.printStackTrace();
return null;
}
}

private String bytesConvertHexString(byte[] data) {
StringBuffer result = new StringBuffer();
for (byte b : data) {
String hexString = Integer.toHexString(b & 255);
result.append(hexString.length() == 1 ? "0" + hexString : hexString);
}
return result.toString().toUpperCase();
}

private byte[] hexStringConvertBytes(String data) {
int length = data.length() / 2;
byte[] result = new byte[length];
for (int i = 0; i < length; i++) {
int first = Integer.parseInt(data.substring(i * 2, (i * 2) + 1), 16);
result[i] = (byte) ((first * 16) + Integer.parseInt(data.substring((i * 2) + 1, (i * 2) + 2), 16));
}
return result;
}

AES,CBC模式,key和iv都给了,去解密就好。
虽然但是,我试了好几个解密网站,enc2的内容都比较顺利,但是enc1不是16字节倍数的。
我先截取了前16字节,解出来是picacomic@gmail.,实际内容应该是picacomic@gmail.com。我直接对picacomic@gmail.com加密,出来的数据少了一截,换成zeropadding的话出来的数据会多一截。

具体而言:

1
2
3
4
5
6
7
8
enc1
8e5fdb7f9271294a2849137cc7a8278cf0b5fdd3b7fa98fab2796e4aa6

picacomic@gmail.com NoPadding
8e5fdb7f9271294a2849137cc7a8278cf0b5fd

picacomic@gmail.com ZeroPadding
8e5fdb7f9271294a2849137cc7a8278cf0b5fdd3b7fa98fab2796e4aa6d1e4e4

嗯,反正感觉挺迷的。说是NoPadding但是多了一截,ZeroPadding又少了一截……
不过这个题取前16字节就可以解出邮箱前半部分,后半部分靠常识也知道是com了。
大概是出题人最后的温柔

奇怪的交易

有UPX壳,脱壳。
脱壳完了丢进IDA,逻辑看不懂,发现PY之类的字符串,又想起来我第一次跑这个程序跑不起来,报错是

[112] Error loading Python lib ‘/tmp/_MEImk23nz/libpython3.10.so.1.0’: dlopen: /lib/x86_64-linux-gnu/libc.so.6: version `GLIBC_2.33’ not found (required by /tmp/_MEImk23nz/libpython3.10.so.1.0)

为了能用uncompyle6,我的python版本是3.8,看到这个报错我还挺疑惑的,怎么跟python3.10扯上关系……直到后来在IDA里看到

好家伙,这一堆import 仿佛梦回抗疫CTF被树师傅的题折磨

彳亍口巴,那必然是python打包的可执行文件了,出题人qs会玩。

环境换成python3.10,pyinstxtractor还是8能用,按照这个解决:
https://github.com/extremecoders-re/pyinstxtractor/wiki/Extracting-Linux-ELF-binaries

1
2
objcopy --dump-section pydata=pydata.dump testfile.elf
python pyinstxtractor.py pydata.dump

(我把文件名改成data)

成功解包。

☁️:停一下,我宣布一件事情。
☁️:用python3.10出pyc的出题人都是屑。
☁️:track神除外。

(手动狗头保命

加文件头,读字节码,复原python代码,虽然在线网站可以用,但是3.10解出来的部分代码还是有问题,直接看容易出锅,还是得自己手翻一下。

复原python代码之后发现从cup库导了一个encrypto函数,提示是a cup of tea,实际上可以猜出来是tea加密。
密文长度115,必然不是TEA和XTEA,因为如果看过实现代码就知道,这两个加密是要求偶数对密文的,所以只能是XXTEA。

当然了,这么猜着做题确实能做出来,但是没什么意思,还是试着去找一下cup库的内容。

实际上这里用到了一个pyinstaller加密

简而言之,就是把导入的库(pyc文件)用aes加密之后压缩成pyz文件。因为这是pyinstaller里的源代码,所以加密方式必然是这样的。
先去解包pyz文件,获取cup包加密后的pyc文件,然后对这个文件解密得到cup.pyc,再反编译去看cup的源码。

解包pyz文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import tinyaes
import zlib

CRYPT_BLOCK_SIZE = 16

# 从crypt_key.pyc获取key,也可自行反编译获取
key = bytes('0000000000000tea', 'utf-8')

inf = open('cup.pyc.encrypted', 'rb') # 打开加密文件
outf = open('cup.pyc', 'wb') # 输出文件

# 按加密块大小进行读取
iv = inf.read(CRYPT_BLOCK_SIZE)

cipher = tinyaes.AES(key, iv)

# 解密
plaintext = zlib.decompress(cipher.CTR_xcrypt_buffer(inf.read()))

# 写入解密数据
outf.write(plaintext)

inf.close()
outf.close()

解密出来之后补一下头文件,然后dis自己看字节码,可以看出来是XXTEA。那么写脚本解密。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#include <stdio.h>
#include <stdint.h>


#define DELTA 0x9e3779b9
#define MX (((z>>5^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))


void xxtea(uint32_t* v, int n, uint32_t* key)
{
uint32_t y, z, sum;
unsigned p, rounds, e;

if (n > 1) // encrypt
{
rounds = 6 + 52/n;
sum = 0;
z = v[n-1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p=0; p<n-1; p++)
{
y = v[p+1];
z = v[p] += MX;
}
y = v[0];
z = v[n-1] += MX;
}
while (--rounds);
}
else if (n < -1) // decrypt
{
n = -n;
rounds = 6 + 52/n;
sum = rounds * DELTA;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p=n-1; p>0; p--)
{
z = v[p-1];
y = v[p] -= MX;
}
z = v[n-1];
y = v[0] -= MX;
sum -= DELTA;
}
while (--rounds);
}
}

int main()
{
uint32_t v[] = {3532577106, 1472742623, 3642468664, 4193500461, 2398676029, 617653972, 1474514999, 1471783658, 1012864704,
3615627536, 993855884, 438456717, 3358938551, 3906991208, 198959101, 3317190635, 3656923078, 613157871,
2398768861, 97286225, 2336972940, 1471645170, 3233163154, 583597118, 2863776301, 3183067750, 1384330715,
2929694742, 3522431804, 2181488067, 3303062236, 3825712422, 145643141, 2148976293, 2940910035, 506798154,
994590281, 2231904779, 3389770074, 2814269052, 1105937096, 1789727804, 3757028753, 2469686072, 1162286478,
680814033, 2934024098, 2162521262, 4048876895, 2121620700, 4240287315, 2391811140, 3396611602, 3091349617,
3031523010, 2486958601, 3164065171, 1285603712, 798920280, 2337813135, 4186055520, 3523024366, 1077514121,
1436444106, 2731983230, 1507202797, 500756149, 198754565, 2382448647, 880454148, 1970517398, 3217485349,
1161840191, 560498076, 1782600856, 2643721918, 1285196205, 788797746, 1195724574, 4061612551, 103427523,
2502688387, 4147162188, 617564657, 978211984, 1781482121, 2205798970, 3939973102, 3826603515, 659557668,
2582884932, 1561884856, 2217488804, 1189296962, 169145316, 2781742156, 1323893433, 824667876, 408202876,
3759637634, 4094868412, 1508996065, 162419237, 3732146944, 3083560189, 3955940127, 2393776934, 2470191468,
3620861513, 481927014, 2756226070, 3154651143, 1261069441, 2063238535, 2222237213, 101459755, 3159774417,
1721190841, 1078395785, 176506553, 3552913423, 1566142515, 1938949000, 1499289517, 3315102456, 829714860,
3843359394, 952932374, 1283577465, 2045007203, 3957761944, 3767891405, 2917089623, 3296133521, 482297421,
1734231412, 3670478932, 2575334979, 2827842737, 3413631016, 1533519803, 4008428470, 3890643173, 272960248,
317508587, 3299937500, 2440520601, 27470488, 1666674386, 1737927609, 750987808, 2385923471, 2694339191,
562925334, 2206035395};

uint32_t k[4]= {54,54,54,54};
//n的绝对值表示v的长度,取正表示加密,取负表示解密
int n = 155;

xxtea(v, -n, k);
int i;
for(i=0;i<155;i++)
{
printf("%c",v[i]>>24);
printf("%c",(v[i]>>16)&0xff);
printf("%c",(v[i]>>8)&0xff);
printf("%c",v[i]&0xff);
}

return 0;
}
//10610336534759505889607399322387179316771488492347274741918862678692508953185876570981227584004676580623553664818853686933004290078153620168054665086468417541382824708104480882577200529822968531743002301934310349005341104696887943182074473298650903541494918266823037984054778903666406545980557074219162536057146090758158128189406073809226361445046225524917089434897957301396534515964547462425719205819342172669899546965221084098690893672595962129879041507903210851706793788311452973769358455761907303633956322972510500253009083922781934406731633755418753858930476576720874219359466503538931371444470303193503733920039

然后解RSA,用yafu跑了一下,8彳亍,☁️就8会了,因为☁️8是密码👴

☁️:在逆向题里放RSA是否搞错了什么?

于是有了以下对话:

彳亍,还得是dbt神。

搜维纳攻击,随便找个脚本。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import gmpy2
from Crypto.Util.number import *
def transform(x,y): #使用辗转相处将分数 x/y 转为连分数的形式
res=[]
while y:
res.append(x//y)
x,y=y,x%y
return res

def continued_fraction(sub_res):
numerator,denominator=1,0
for i in sub_res[::-1]: #从sublist的后面往前循环
denominator,numerator=numerator,i*numerator+denominator
return denominator,numerator #得到渐进分数的分母和分子,并返回


#求解每个渐进分数
def sub_fraction(x,y):
res=transform(x,y)
res=list(map(continued_fraction,(res[0:i] for i in range(1,len(res))))) #将连分数的结果逐一截取以求渐进分数
return res

def get_pq(a,b,c): #由p+q和pq的值通过维达定理来求解p和q
par=gmpy2.isqrt(b*b-4*a*c) #由上述可得,开根号一定是整数,因为有解
x1,x2=(-b+par)//(2*a),(-b-par)//(2*a)
return x1,x2

def wienerAttack(e,n):
for (d,k) in sub_fraction(e,n): #用一个for循环来注意试探e/n的连续函数的渐进分数,直到找到一个满足条件的渐进分数
if k==0: #可能会出现连分数的第一个为0的情况,排除
continue
if (e*d-1)%k!=0: #ed=1 (mod φ(n)) 因此如果找到了d的话,(ed-1)会整除φ(n),也就是存在k使得(e*d-1)//k=φ(n)
continue

phi=(e*d-1)//k #这个结果就是 φ(n)
px,qy=get_pq(1,n-phi+1,n)
if px*qy==n:
p,q=abs(int(px)),abs(int(qy)) #可能会得到两个负数,负负得正未尝不会出现
d=gmpy2.invert(e,(p-1)*(q-1)) #求ed=1 (mod φ(n))的结果,也就是e关于 φ(n)的乘法逆元d
return d
print("该方法不适用")

c=10610336534759505889607399322387179316771488492347274741918862678692508953185876570981227584004676580623553664818853686933004290078153620168054665086468417541382824708104480882577200529822968531743002301934310349005341104696887943182074473298650903541494918266823037984054778903666406545980557074219162536057146090758158128189406073809226361445046225524917089434897957301396534515964547462425719205819342172669899546965221084098690893672595962129879041507903210851706793788311452973769358455761907303633956322972510500253009083922781934406731633755418753858930476576720874219359466503538931371444470303193503733920039
n=0x649EE967E7916A825CC9FD3320BEABF263BEAC68C080F52824A0F521EDB6B78577EC52BF1C9E78F4BB71192F9A23F1A17AA76E5979E4D953329D3CA65FB4A71DA57412B59DFD6AEDF0191C5555D3E5F582B81B5E6B23163E9889204A81AFFDF119FE25C92F4ED59BD3285BCD7AAE14824240D2E33C5A97848F4EB7AAC203DE6330D2B4D8FF61691544FBECD120F99A157B3D2F58FA51B2887A9D06CA383C44D071314A12B17928B96F03A06E959A5AFEFA0183664F52CD32B9FC72A04B45913FCB2D5D2D3A415A14F611CF1EAC2D6C785142A8E9CC41B67A6CD85001B06EDB8CA767D367E56E0AE651491BF8A8C17A38A1835DB9E4A9292B1D86D5776C98CC25
e=0x647327833ACFEF1F9C83E74E171FC300FA347D4A6769476C33DA82C95120ACB38B62B33D429206FE6E9BB0BB7AB748A1036971BEA36EC47130B749C1C9FF6FE03D0F7D9FC5346EB0E575BDFA6C530AA57CD676894FC080D2DD049AB59625F4B9C78BCFD95CDCD2793E440E26E189D251121CB6EB177FEDB596409034E8B0C5BBD9BD9342235DBB226C9170EFE347FF0FD2CFF9A1F7B647CC83E4D8F005FD7125A89251C768AFE70BDD54B88116814D5030F499BCAC4673CCCC342FB4B6AC58EA5A64546DC25912B6C430529F6A7F449FD96536DE269D1A1B015A4AC6B6E46EE19DCE8143726A6503E290E4BAE6BD78319B5878981F6CFFDB3B818209341FD68B

d=wienerAttack(e,n)
print(long_to_bytes(pow(c,d,n)))
#b'flag{You_Need_Some_Tea}'

May 出题人挑战赛

DAS就是纯纯坐牢

PZ师傅的WP视频

WER

翻导入表,找RegisterApplicationRecoveryCallback,查交叉引用,跳到sub_140001000,F5得到伪代码:

1
2
3
4
5
HRESULT sub_140001000()
{
atexit(sub_14000F4A0);
return RegisterApplicationRecoveryCallback((APPLICATION_RECOVERY_CALLBACK)sub_14000F3B0, 0i64, 0x1388u, 0);
}

显然RegisterApplicationRecoveryCallback调用的是sub_14000F3B0,那么就去看这个函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
int sub_14000F3B0()
{
__int64 v0; // rdx
int result; // eax
CHAR Text[16]; // [rsp+20h] [rbp-30h] BYREF
int v3[8]; // [rsp+30h] [rbp-20h]
BOOL pbCancelled; // [rsp+68h] [rbp+18h] BYREF

v0 = 0i64;
v3[0] = 89457413;
pbCancelled = 0;
v3[1] = 1415448324;
v3[2] = 38799109;
v3[3] = 1348424451;
v3[4] = 89346131;
v3[5] = 1431568469;
v3[6] = 33882967;
v3[7] = 1397837906;
strcpy(Text, "Correct!");
while ( (*((unsigned __int8 *)v3 + v0) ^ *((char *)&xmmword_140035C20 + v0)) == 102 )
{
if ( ++v0 >= 32 )
{
result = MessageBoxA(0i64, Text, 0i64, 0);
if ( pbCancelled )
return result;
ApplicationRecoveryInProgress(&pbCancelled);
if ( pbCancelled )
{
ApplicationRecoveryFinished(0);
return 0;
}
ApplicationRecoveryFinished(1);
return 0;
}
}
return 0;
}

可以看到,就是一组异或……

1
2
3
4
5
6
v3=[0x5550305,0x545E0704,0x2500705,0x505F5303,0x5535053,0x55540055,0x2050357,0x53515052]
for i in v3:
t=i^0x66666666
for j in range(4):
print(chr(t&0xff),end='')
t>>=8

其实我觉得吧,这种虚假控制流的题,如果是把整个执行流放在回调函数里还好说。
因为如果main函数根本没调用的话,那么回调函数里必然要进行IO操作,而IO操作必然要引用导入的库函数,所以这个题不知道RegisterApplicationRecoveryCallback的话,直接去导入表找MessageBoxA就好了。

以下来自官方WP

1.通过Windows的WER机制隐藏控制流:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Test {
public:
Test() {
atexit(OnExit);
PVOID pvParameter = NULL;
DWORD dwPingInterval = RECOVERY_DEFAULT_PING_INTERVAL;
DWORD dwFlags = 0;
HRESULT hRes = RegisterApplicationRecoveryCallback(ApplicationRecoverCallback, pvParameter, dwPingInterval, dwFlags);
}
};
void OnExit()
{
WerReportHang(GetForegroundWindow(), NULL);
TriggerException();
}

2.在类初始化时注册一个 ApplicationRecoverCallback 回调,可以在 main 函数之前完成
3.在 onExit 里调用 WerReportHang 并触发异常,使真正的逻辑执行
4.main 中的逻辑是 ECC 中的点加法,但是设置为了不可能成立,同时在 mp_read_radix 中动了手脚,能把输入保存下来。

CEF

感觉我真是把能踩的坑都踩了一遍……还是按PZ师傅的博客复现,师傅tql QAQ,我实在是太菜了。

这种生成密钥然后加密的算法,都不用管密钥怎么生成的,直接下断点去内存里dump出来就好了

round函数里起始的result是2,所以第一句异或的几个值分别是key、a1[(i+2)%4]a1[(i+2+1)%4]a1[(i+2-1)%4],也就是a1[(i+1)%4]a1[(i+2)%4]a1[(i+3)%4],但是忽略了刚开始那个人result=2的某人索引全写错了,但是看我的脚本似乎有时候还是想起来了这个问题,所以难道这是个玄学问题么
还有就是在网上乱当代码,宏定义人家后面写的BE结尾,我直接忽略,后来死活解不对,去挨个对,发现宏里面位移是反的,才突然醒悟BE是big endian(大端序)…………………………

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int __cdecl round(int a1, _DWORD *a2)
{
int result; // eax
int v3; // esi
bool v4; // zf
int v5; // [esp+Ch] [ebp-4h]

result = 2;
v5 = 32;
do
{
v3 = *a2++ ^ *(_DWORD *)(a1 + 4 * (result % 4)) ^ *(_DWORD *)(a1 + 4 * ((result + 1) % 4)) ^ *(_DWORD *)(a1 + 4 * ((result - 1) % 4));
v4 = v5-- == 1;
*(_DWORD *)(a1 + 4 * ((result + 2) % 4)) = v3 ^ *(_DWORD *)(a1 + 4 * (((_BYTE)result - 2) & 3)) ^ __ROL4__(v3, 2) ^ __ROR4__(v3, 8) ^ __ROL4__(v3, 10) ^ __ROR4__(v3, 14);
++result;
}
while ( !v4 );
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
#include <stdio.h>
#include <string.h>

unsigned char k[] = {0x7C, 0x22, 0xDB, 0xE8, 0xB9, 0x51, 0x24, 0x01, 0xDB, 0xA9,
0x08, 0xED, 0xC3, 0x65, 0x1F, 0xC9, 0x81, 0xE9, 0xD1, 0xB3,
0x34, 0x47, 0x9B, 0x31, 0x51, 0x55, 0xBA, 0xA4, 0x2D, 0xED,
0xF2, 0xD0, 0x92, 0xD6, 0x00, 0x4A, 0x30, 0xFE, 0xAE, 0xE0,
0x4A, 0xDB, 0xBC, 0x6B, 0xF1, 0xF6, 0x15, 0xC3, 0x30, 0xB0,
0xE1, 0xB4, 0xCE, 0x4A, 0x4C, 0x69, 0xD3, 0x08, 0x92, 0x47,
0x97, 0x7B, 0x8C, 0x3F, 0xA6, 0x77, 0x77, 0x74, 0xBB, 0x9B,
0xEC, 0xED, 0xC1, 0x06, 0xE5, 0xC8, 0x2A, 0xA9, 0x55, 0xB9,
0xBB, 0xFD, 0x88, 0xB3, 0xC3, 0x97, 0x46, 0x1A, 0xAA, 0x26,
0x08, 0xB1, 0x07, 0x22, 0x1F, 0xBB, 0x60, 0xCD, 0x1D, 0x29,
0xA7, 0xE3, 0xA3, 0x2B, 0xDD, 0xDF, 0x83, 0x1B, 0xD5, 0x4F,
0x4D, 0x01, 0xF3, 0x59, 0xC6, 0x80, 0x23, 0x5B, 0xB4, 0x3E,
0x66, 0x62, 0xE3, 0x43, 0x2C, 0x53, 0x22, 0xBD, 0};

unsigned int *key = (unsigned int *)k;

#ifndef GET_ULONG_LE
#define GET_ULONG_LE(n,b,i) \
{ \
(n) = ( (unsigned long) (b)[(i) ] ) \
| ( (unsigned long) (b)[(i) + 1] << 8 ) \
| ( (unsigned long) (b)[(i) + 2] << 16 ) \
| ( (unsigned long) (b)[(i) + 3] << 24 ); \
}
#endif

#ifndef PUT_ULONG_LE
#define PUT_ULONG_LE(n,b,i) \
{ \
(b)[(i) ] = (unsigned char) ( (n) ); \
(b)[(i) + 1] = (unsigned char) ( (n) >> 8 ); \
(b)[(i) + 2] = (unsigned char) ( (n) >> 16 ); \
(b)[(i) + 3] = (unsigned char) ( (n) >> 24 ); \
}
#endif

#define SHL(x,n) (((x) & 0xFFFFFFFF) << n)
#define ROTL(x,n) (SHL((x),n) | ((x) >> (32 - n)))

#define SWAP(a,b) { unsigned long t = a; a = b; b = t; t = 0; }

void sm4_one_round(unsigned char input[16], unsigned char output[16] )
{
unsigned long i = 0;
unsigned long tmp[4] = {0};
unsigned int t;

memset(tmp, 0, sizeof(tmp));
GET_ULONG_LE( tmp[0], input, 0 )
GET_ULONG_LE( tmp[1], input, 4 )
GET_ULONG_LE( tmp[2], input, 8 )
GET_ULONG_LE( tmp[3], input, 12 )
while(i < 32)
{
t = tmp[(i+1) % 4] ^ tmp[(i+2) % 4] ^ tmp[(i+3) % 4] ^ key[i];
tmp[i % 4] ^= t ^ ROTL(t,2) ^ ROTL(t,10) ^ ROTL(t,18) ^ ROTL(t,24);
i++;
}

for ( i = 0 ; i < 4; i++ )
GET_ULONG_LE(key[26 - i], input, i * 4);

PUT_ULONG_LE(tmp[3],output,0);
PUT_ULONG_LE(tmp[2],output,4);
PUT_ULONG_LE(tmp[1],output,8);
PUT_ULONG_LE(tmp[0],output,12);
}

void sm4_dec(int length, unsigned char *input,unsigned char *output)
{
while( length > 0 )
{
sm4_one_round(input, output);
input += 16;
output += 16;
length -= 16;
}

}

int main()
{
unsigned char input[] = {0x7D, 0x54, 0xCB, 0xC0, 0x74, 0xDB, 0xF5, 0xD7, 0x6F, 0xD9,
0x92, 0x1B, 0xEB, 0x28, 0x46, 0x20, 0xE5, 0xD5, 0xD3, 0x60,
0x80, 0x6D, 0x36, 0x2F, 0xB0, 0x63, 0x2F, 0x61, 0x20, 0x0F,
0xA9, 0x30};
unsigned char output[32];

for (int i = 0; i < 16; i++)
SWAP(key[i], key[31 - i]);

sm4_dec(32, input, output);

for(int i = 0; i < 32 ; i++)
printf("%c",output[i]);
}

JIT

就是按照PZ师傅的博客复现,我就不抄过来了(逃

我的x64 Native Tools Command Prompt for VS 2022目录是C:\ProgramData\Microsoft\Windows\Start Menu\Programs\Visual Studio 2022\Visual Studio Tools\VC

然后后面印度人题也不是很想看了(bushi

Dest0g3 520迎新赛

前面两题很简单就不写了

tttea

这题属实是……比赛的时候一看七十多解,想着应该很简单,但是死活解不对,调试出来的密钥也是一样的……调一下午调不出来直接开摆,后面题也没心思看了……老想着很简单,看了一下异常也没有,交叉引用也查了但是什么也没有,怎么就是懒得去翻一下导出表……真是sb……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
NTSTATUS __stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
HANDLE v3; // eax
NTSTATUS result; // eax
NTSTATUS (__stdcall *NtQueryInformationProcess)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG); // [esp+D0h] [ebp-14h]
HMODULE hModule; // [esp+DCh] [ebp-8h]

__CheckForDebuggerJustMyCode(&unk_89C015);
hModule = LoadLibraryA("Ntdll.dll");
NtQueryInformationProcess = (NTSTATUS (__stdcall *)(HANDLE, PROCESSINFOCLASS, PVOID, ULONG, PULONG))GetProcAddress(hModule, "NtQueryInformationProcess");
v3 = GetCurrentProcess();
result = NtQueryInformationProcess(v3, ProcessDebugPort, &dword_89A1FC, 4, 0);
if ( !dword_89A1FC )
{
result = 1;
*(&dword_89A000 + 6) = 0x66403319;
}
return result;
}


char *__stdcall TlsCallback_1_0(int a1, int a2, int a3)
{
char *result; // eax
signed __int8 v4; // [esp+EBh] [ebp-5h]

v4 = NtCurrentPeb()->BeingDebugged;
result = (char *)v4;
if ( !v4 )
{
result = (char *)&unk_89A014 + 4;
*((_DWORD *)&unk_89A014 + 1) ^= 0x12345678u;
}
return result;
}

内存:

1
2
3
4
5
6
7
.data:0089A000 dword_89A000    dd 636C6557h            ; DATA XREF: TlsCallback_0_0+8F↑w
.data:0089A004 aOmeToDest0g3 db 'ome_to_Dest0g3!',0
.data:0089A014 unk_89A014 db 1 ; DATA XREF: TlsCallback_1_0+33↑o
.data:0089A015 db 0
.data:0089A016 db 0
.data:0089A017 db 0
.data:0089A018 delta dd 9E3779B9h

所以为什么交叉引用查不到,因为他是用dword_89A000 + 6unk_89A014 + 1来引用到delta,所以查不到,我真的是……还是不能过分信任交叉引用…………感觉好多都查不到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
#include <stdio.h>
#include <stdint.h>

#define DELTA 0x74746561
#define MX (((z>>6^y<<2) + (y>>3^z<<4)) ^ ((sum^y) + (key[(p&3)^e] ^ z)))

void xxtea(uint32_t* v, int n, uint32_t* key)
{
uint32_t y, z, sum;
unsigned p, rounds, e;

if (n > 1) // encrypt
{
rounds = 6 + 52/n;
sum = 0;
z = v[n-1];
do
{
sum += DELTA;
e = (sum >> 2) & 3;
for (p=0; p<n-1; p++)
{
y = v[p+1];
z = v[p] += MX;
}
y = v[0];
z = v[n-1] += MX;
}
while (--rounds);
}
else if (n < -1) // decrypt
{
n = -n;
rounds = 6 + 52/n;
sum = rounds * DELTA;
y = v[0];
do
{
e = (sum >> 2) & 3;
for (p=n-1; p>0; p--)
{
z = v[p-1];
y = v[p] -= MX;
}
z = v[n-1];
y = v[0] -= MX;
sum -= DELTA;
}
while (--rounds);
}
}

int main()
{

uint8_t v0[] = {0x03, 0x23, 0x22, 0x2F, 0x36, 0x88, 0xFD, 0x43, 0x21, 0xE8,
0x5B, 0x65, 0x31, 0x1E, 0x3B, 0xA6, 0x4B, 0xB8, 0xDC, 0x88,
0x80, 0x19, 0x84, 0x6F, 0x97, 0x72, 0x21, 0x26, 0xAD, 0x64,
0xEE, 0xBB, 0x88, 0x04, 0x4D, 0x06, 0x2F, 0x26, 0xE5, 0x6B,
0x81, 0x4B, 0xF5, 0x73};
uint32_t *v = (uint32_t*)v0;

uint32_t k[4]= {0x61, 0x65, 0x74, 0x74};

int n = 11;

xxtea(v, -n, k);
for(int i = 0; i < 44; i++)
{
printf("%c", v0[i]);
}

return 0;
}
//Dest0g3{73dd38c2-9d45-4f7a-9bd0-90a1e9907c1}

EzMath

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

namespace EzMath
{
// Token: 0x02000003 RID: 3
public static class Checker
{
// Token: 0x06000005 RID: 5 RVA: 0x00002110 File Offset: 0x00000310
public static byte[] Encrypt1(byte[] a)
{
List<byte> list = new List<byte>();
for (int i = 0; i < 8; i++)
{
uint value = (uint)((ulong)utils.Unpack32(RuntimeHelpers.GetSubArray<byte>(a, new Range(4 * i, 4 * (i + 1)))) * 83987UL % (ulong)-232573883);
list.AddRange(BitConverter.GetBytes(value));
}
return list.ToArray();
}

// Token: 0x06000006 RID: 6 RVA: 0x00002174 File Offset: 0x00000374
public static byte[] Encrypt2(byte[] a)
{
List<byte> list = new List<byte>();
for (int i = 0; i < 4; i++)
{
ulong num = utils.Unpack64(RuntimeHelpers.GetSubArray<byte>(a, new Range(8 * i, 8 * (i + 1))));
ulong value = num ^ num >> 25;
list.AddRange(BitConverter.GetBytes(value));
}
return list.ToArray();
}
}
}

陇原战”疫”2021网络安全大赛

findme

验证用的函数是off_403844,此地址处只有strcmp,所以肯定是被hook了,查交叉引用,往这个地址写了off_403840,off_403840调用了sub_401866,所以主逻辑在sub_401866。
进入sub_401866的伪代码,写了个RC4,有没有魔改不知道,直接调试。断点下到第40行的for循环调试起来,输26个a进去,提取dword_403040作为enc,v6作为xor,写异或脚本。

1
2
3
4
5
6
7
enc = [0xB7,0x52,0x85,0xC1,0x90,0xE9,0x07,0xB8,0xE4,0x1A,0xC3,0xBD,0x1D,0x8E,0x85,0x46,0x00,0x21,0x44,0xAF,0xEF,0x70,0x32,0xB5,0x11,0xC6]
xor = [0x85, 0x76, 0xB0, 0xE3, 0xA5, 0xCE, 0x1D, 0x8D, 0xED, 0x4A,
0xD1, 0x83, 0x15, 0xDA, 0xBB, 0x62, 0x53, 0x1F, 0x10, 0xBA,
0xDC, 0x72, 0x3E, 0xED, 0x51, 0xDA]
for i in range(26):
print(chr(enc[i] ^ xor[i] ^ ord('a')), end='')
#SETCTF{Th1s_i5_E2_5tRcm9!}

power

印度人题他又来了……

line1247~1278:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.L84:
.word _GLOBAL_OFFSET_TABLE_-(.LPIC12+4)
.word __stack_chk_guard(GOT)
.word .LC0-(.LPIC11+4)
.word _GLOBAL_OFFSET_TABLE_-(.LPIC13+4)
.fnend
.size _ZN3aes14encryption_cbcEPcS0_, .-_ZN3aes14encryption_cbcEPcS0_
.section .rodata
.align 2
.LC2:
.ascii "input flag:\000"
.align 2
.LC3:
.ascii "1030a9254d44937bed312da03d2db9adbec5762c2eca7b5853e"
.ascii "489d2a140427b\000"
.align 2
.LC4:
.ascii "yeah, you get it!\000"
.align 2
.LC5:
.ascii "wrong!\000"
.align 2
.LC1:
.ascii "this_is_a_key!!!\000"
.text
.align 1
.global main
.syntax unified
.thumb
.thumb_func
.fpu vfpv3-d16
.type main, %function

L84指明是AES,LC3是密文,LC1是key,iv是0(不是字符0)。
CBC解密只能解出前一半flag,用ECB就可以……
flag{y0u_found_the_aes_12113112}

EasyRE_Revenge

出题人好像是批量加的花指令……
patch了老半天,但是还是恢复不了函数,没耐心了直接上手调

到IDA拖着红色的那里,后面都是很多结构一样的函数,作用就是设置了几个参数,不同就是最后mov的地址不一样,花指令在最开始的call函数前后,是一些无用数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.text:00B11817                 call    sub_B1181D
.text:00B1181C push cs
.text:00B1181D
.text:00B1181D ; =============== S U B R O U T I N E =======================================
.text:00B1181D
.text:00B1181D
.text:00B1181D sub_B1181D proc near ; CODE XREF: .text:00B11817↑p
.text:00B1181D xor ebx, ebx
.text:00B1181F xor eax, eax
.text:00B11821 xor ecx, ecx
.text:00B11823 pop ecx
.text:00B11824 add eax, 1
.text:00B11827 mov ebx, 7
.text:00B1182C mul ebx
.text:00B1182E add ecx, 20h ; ' '
.text:00B11831 add eax, 0Bh
.text:00B11834 xor eax, ebx
.text:00B11836 push ecx
.text:00B11837 mov [ebp-77h], al
.text:00B1183A retn
.text:00B1183A sub_B1181D endp
.text:00B1183A
.text:00B1183A ; ---------------------------------------------------------------------------

就不停地找E8,然后按C转为代码……到最后一个这种结构的函数结束处,下面是一个新的函数,创建函数然后反编译。
代码有点抽象,参数改成int*类型,顺眼多了……

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// positive sp value has been detected, the output may be wrong!
int __usercall sub_4C1C92@<eax>(int *a1@<ebp>)
{
for ( *(a1 - 33) = 0; *(a1 - 33) < 8; ++*(a1 - 33) )
*(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 33)) = *(_DWORD *)(a1[2] + 4 * *(a1 - 33)) ^ a1[(7 * *(a1 - 33) + 2) % 8 - 30];
for ( *(a1 - 36) = 0; *(a1 - 36) < 8; ++*(a1 - 36) )
{
*(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) ^= *(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) << 7;
*(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) ^= a1[(7 * *(a1 - 36) + 3) % 8 - 30];
*(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) ^= *(_DWORD *)(*(a1 - 3) + 4 * ((5 * *(a1 - 36) + 3) % 8));
*(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) ^= *(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) << 13;
*(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) ^= a1[(7 * *(a1 - 36) + 5) % 8 - 30];
*(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) ^= *(_DWORD *)(*(a1 - 3) + 4 * *(a1 - 36)) << 17;
}
return sub_4C1154();
}

还原算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int __usercall sub_4C1C92@<eax>(int *key@<ebp>)
{
for ( i = 0; i < 8; ++i )
tmp[i] = input[i] ^ key[(7 * i + 2) % 8];
for ( i = 0; i < 8; ++i )
{
tmp[i] ^= tmp[i] << 7;
tmp[i] ^= key[(7 * i + 3) % 8];
tmp[i] ^= (tmp + ((5 * i + 3) % 8));
tmp[i] ^= tmp[i] << 13;
tmp[i] ^= key[(7 * i + 5) % 8];
tmp[i] ^= tmp[i] << 17;
}
return sub_4C1154();
}

这个shift xor要用特殊的算法来还原,zsky神的博客有写,我就不抄过来了(逃
其他的逆回去写就行

Eat_something

……………………………………………………………………………………
梦回miniL………………………………………………………………

我要是miniL前复现过这题也不至于做不出来WebAssembly吧呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜呜

CBCTF 2022九月挑战赛

landing

这题我tm真的是………………

经过学弟提醒,我觉得自己还是要文明一些,所以谨代表我个人向出题人致以诚挚的问候……………………@#&¥%@#¥~!`%@#*&

经过一个异或0x22和+1的处理之后,对输入进行一个类似base的加密(但是并不是)。
主程序里的密文解出来是个fake flag,func2会抛出异常进入真正的执行流程。注意出题人写了4个异常,一个int一个long一个char*一个所有类型的。
会触发的异常是最后两个(但是char*的里面好像并没有写什么东西(?懒得再翻一遍了,我是懒🐶
catch里面的代码巨长无比,懒得看汇编可以把fun1的一个jmp语句patch掉让它跳转到except块里,就能反编译出来伪代码了。
except块里的代码就是存储了密文,然后对密文异或0x12,然后对input(之前在主程序里经过了一个异或0x22和+1的处理)用nothing函数进行加密。
这个nothing跟那个假flag的加密逻辑是一样的,直接解密即可
依旧对这题充满了怨念

1
2
3
4
5
6
7
8
9
10
11
12
13
input = ''
enc = 'TFdMUkFYT0xJTklOR0hBWE9MR1hOTk5OTk5OTg'
model = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
output = []

for i in range(len(enc)):
output.append(model.index(enc[i]))
for i in range(0,36,4):
input += chr(((((output[i]<<2) | ((output[i+1]>>4)&3))&0xff)-1)^0x22)
input += chr(((((output[i+1]<<4) | ((output[i+2]>>2)&0xf))&0xff)-1)^0x22)
input += chr(((((output[i+2]<<6) | (output[i+3] & 0x3f))&0xff)-1)^0x22)
input += chr(((((output[i]<<2) | ((output[i+1]>>4)&3))&0xff)-1)^0x22)
print(input)

关于本文

本文作者 云之君, 许可由 CC BY-NC 4.0.