周末队里的几个小伙伴抽空打了下今年的HCTF,最后排在第十名。以下是WP
给了一个.sys
系统驱动,ida依次点开几个函数,在sub_1400012F0
中看到一段可疑代码:
__int64 __fastcall sub_1400012F0(__int64 a1, __int64 a2)
{
__int64 v2; // rbx
__int64 v3; // rsi
unsigned __int64 v4; // rdx
int p; // ecx
__int16 *p_input; // rdi
__int64 time; // rbp
__int16 input; // dx
char map_content; // dl
CHAR *info; // rcx
v2 = a2;
if ( *(_DWORD *)(a2 + 48) >= 0 )
{
v3 = *(_QWORD *)(a2 + 24);
v4 = (unsigned __int64)(*(unsigned __int64 *)(a2 + 56) * (unsigned __int128)0xAAAAAAAAAAAAAAABui64 >> 64) >> 3;
if ( (_DWORD)v4 )
{
p = saved_p;
p_input = (__int16 *)(v3 + 2);
time = (unsigned int)v4;
while ( *(_WORD *)(v3 + 4) )
{
LABEL_30:
p_input += 6;
if ( !--time )
goto LABEL_31;
}
map[p] = '.';
input = *p_input;
if ( *p_input == 0x11 )
{
if ( p & 0xFFFFFFF0 )
{
p -= 16;
goto LABEL_13;
}
p += 0xD0;
saved_p = p;
}
if ( input != 0x1F )
goto LABEL_14;
if ( (p & 0xFFFFFFF0) == 0xD0 )
p -= 0xD0;
else
p += 16;
LABEL_13:
saved_p = p;
LABEL_14:
if ( input == 0x1E )
{
if ( p & 0xF )
--p;
else
p += 15;
saved_p = p;
}
if ( input == 0x20 )
{
if ( (p & 0xF) == 15 )
p -= 15;
else
++p;
saved_p = p;
}
map_content = map[p];
if ( map_content == '*' )
{
info = "-1s\n";
}
else
{
if ( map_content != '7' )
{
LABEL_29:
map[p] = 'o';
goto LABEL_30;
}
info = "The input is the flag!\n";
}
saved_p = 16;
DbgPrint(info);
p = saved_p;
goto LABEL_29;
}
}
LABEL_31:
if ( *(_BYTE *)(v2 + 65) )
*(_BYTE *)(*(_QWORD *)(v2 + 184) + 3i64) |= 1u;
return *(unsigned int *)(v2 + 48);
}
可以看出这是一个走迷宫游戏,迷宫可以在data段中找到:
****************
o..............*
**************.*
************...*
***********..***
**********..****
*********..*****
********..******
*******..*******
******..********
*****..*********
****..**********
****7***********
****************
从代码中看出上左下右分别对应0x11,0x1e,0x1f,0x20,搜索得到系统驱动中各个按键对应的值,即wasd,根据题目描述使用小写,得到flag:hctf{ddddddddddddddssaasasasasasasasasas}
运行程序,输出了LuckyStar!
,IDAshift+f12
找字符串引用发现在TlsCallback_0
中。
关于TlsCallback
,只需要知道它会在程序运行之前被执行就行了。
TlsCallback
开头做了一段奇怪的哈希检查,然后有一段检测进程的反调:
do
{
v9 = 0;
do
{
if ( !lstrcmpW(off_403508[v9], v8[15]) )
goto LABEL_13;
++v9;
}
while ( v9 < 6 );
v8 = (LPCWSTR *)((char *)v8 + (_DWORD)*v8);
}
while ( *v8 );
其中off_403508
中有ida,ollydbg等,随便改掉(hex view f12修改,然后edit-patch program-apply...,也可以010直接改)。
这时可以先运行程序看看,大体就是等一段时间,然后就是常规的flag check,于是我们可以继续看代码逻辑。
后面一段代码,根据一个固定种子,生成随机数来异或脱壳一个函数401780
:
srand(0x61616161u);
v11 = (char *)&loc_401780;
v12 = 0x1B8;
do
{
++v11;
result = byte_417000[rand() % 8216];
*(v11 - 1) ^= result;
--v12;
}
while ( v12 );
可以自己写程序脱壳,更简单的是直接运行程序然后dump内存(任务管理器右键创建转储文件即可),即可得到脱壳后的函数(直接搜索附近的hex可以直接定位)
观察sub_401780
,发现它又异或脱壳了一个函数:
do
*((_BYTE *)sub_4015E0 + v1++) ^= byte_417000[rand() % 8216];
while ( v1 < 383 );
同样从刚才的dump中找到函数(直接覆盖原exe的函数用ida看即可)。
可以看出,我们的输入通过sub_4015E0
函数加密,然后与一个data值做比较。
于是观察sub_4015E0
函数,根据==
,*4/3
,和一些标志性的位运算可以猜测前面的一段是base64 encode
:
len = strlen(flag);
v4 = 0;
v20 = 4 * len / 3;
if ( v20 > 0 )
{
do
{
v5 = v4 & 3;
if ( v4 & 3 )
{
v8 = flag[v2 - 1];
if ( v5 == 1 )
{
v9 = flag[v2++];
v7 = (v9 >> 4) | 16 * (v8 & 3);
}
else if ( v5 == 2 )
{
v10 = flag[v2++];
v7 = (v10 >> 6) | 4 * (v8 & 0xF);
}
else
{
v7 = v8 & 0x3F;
}
}
else
{
v6 = &flag[v2++];
v7 = *v6 >> 2;
}
enc[v4++] = base64[v7];
}
while ( v4 < v20 );
}
if ( strlen(flag) % 3 == 1 )
{
v11 = 4 * len / 3;
v12 = 16 * (flag[v2 - 1] & 3);
*(_WORD *)&enc[v20 + 1] = '==';
v13 = base64[v12];
}
else
{
if ( strlen(flag) % 3 != 2 )
goto LABEL_15;
v11 = 4 * len / 3;
v13 = base64[4 * (flag[v2 - 1] & 0xF)];
enc[v20 + 1] = '=';
}
enc[v11] = v13;
LABEL_15:
enc[strlen(enc)] = 0;
但是码表与常规base64
不同:abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/
接下来程序又生成随机数异或了base64
编码的输入:
if ( base64_len > 0 )
{
do
{
v16 = 6;
do
{
v17 = rand() % 4;
v18 = v16;
v16 -= 2;
xor_key = (_BYTE)v17 << v18;
enc[p] ^= xor_key;
}
while ( v16 > -2 );
++p;
}
while ( p < base64_len );
}
同样,随机数可以写程序得到,但是我写出来总是不对,于是同样地dump内存:
在程序中输入24个w(因为最终比较的串是32位,所以base64前是24位),在程序结束前会system(‘pause’)
,这时加密值还在栈上,同时栈上也有那串data49e6...
,搜索这个可以在附近找到加密后的输入。
用它与24个w的base64进行异或得到一串随机数,再跟那串data异或即可还原出flag的base64,base64 decode得到flag。(注意base64是自定义码表)
from string import maketrans
base='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'
diy_base='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/'
t=maketrans(base,diy_base)
t2=maketrans(diy_base, base)
def cus_base64_enc(x):
return x.encode('base64').translate(t)
def cus_base64_dec(x):
return x.translate(t2).decode('base64')
ss=[0x4c,0xb2,0x7d,0xbe,0x04,0x3a,0x06,0x27,0x94,0xc1,0xdc,0x55,0x77,0xe5,0x8d,0x81,0x85,0xa6,0xf2,0x2d,0x83,0x1e,0x58,0xdc,0x96,0x81,0x1b,0x55,0xc8,0x8a,0xb5,0x0b]
enc=[0x49, 0xE6, 0x57, 0xBD, 0x3A, 0x47, 0x11, 0x4C, 0x95, 0xBC, 0xEE, 0x32, 0x72, 0xA0, 0xF0, 0xDE, 0xAC, 0xF2, 0x83, 0x56, 0x83, 0x49, 0x6E, 0xA9, 0xA6, 0xC5, 0x67, 0x3C, 0xCA, 0xC8, 0xCC, 0x05]
inp = cus_base64_enc('w'*24)
flag=''
for i in range(32):
flag+=chr(ss[i]^ord(inp[i])^enc[i])
print cus_base64_dec(flag)
运行得到flag:
hctf{1zumi_K0nat4_Mo3}
程序给了hex,使用hex2bin可以还原出binary,首先可以看到一串字符串:
Arduino LLC Arduino Leonardo
notepad.exe44646 + ( 64094 + ( 71825 * ( ( 15873 + ( 21793 * ( 7234 + ( 17649 * ( ( 2155 + ( 74767 * ( 35392 + ( 88216 * ( 83920 + ( 16270 + ( 20151 * ( 5268 + ( 90693 * ( 82773 + ( 716 + 27377 * ( 44329 + ( 49366 * ( ( ( 38790 + ( 70247 * ( 97233 + ( 18347 + ( 22117 * ( ( ( 72576 + ( ( 47541 + ( 46975 + ( 53769 * ( 94005 + ( ( 72914 + ( 5137 + ( 87544 * 71583 + ( 20370 + ( 37968 * ( 17478 + ( ( 40532 + ( 10089 + ( 13332 * ( ( 24170 + ( 46845 * ( 16048 + 23142 * ( 31895 + ( 62386 * ( 12179 ( 94552 + ( ( ( 52918 + ( 91580 + ( ( ( 38412 + ( 91537 * ( 70 + ( 98594 * ( ( 35275 + ( 62912 * ( 4755 + ( 16737 * ( 27595 + ( ( 43551 + ( 64482 * 3550 ) ) - 21031 ) ) ) ) ) ) - 57553 ) ) - 89883 ) - 38900 ) ) ) - 19517 ) - 79082 ) ) ) ) ) ) ) ) - 70643 ) ) 55350 ) ) ) ) ) - 40301 ) ) ) ) - 83065 ) ) ) ) ) - 52460 ) ) - 49428 ) - 94686 ) ) ) ) ) ) - 1653 ) - 65217 ) ) ) - 43827 ) 66562 ) )
根据经验可以推测出是badusb
,可以参考这篇文章,题目思路基本一致。
首先搜索Arduino Leonardo
得到atmega32u4
,用IDA Atmel AVR
架构,设备设置为atmega32u4。
sub_9A8
函数中可以看出,程序首先通过win+r
快捷键打开运行窗口,然后一直重复writeln和delay,writeln的参数r25和r24即为字符串的偏移,可以猜测出0x140即为notepad.exe
的开头:
ROM:0A4E ldi r22, 0x83 ; win
ROM:0A4F ldi r24, 0x86
ROM:0A50 ldi r25, 5
ROM:0A51 call key_input
ROM:0A53 ldi r22, 0x72 ; 'r' ; r
ROM:0A54 ldi r24, 0x86
ROM:0A55 ldi r25, 5
ROM:0A56 call key_input
ROM:0A58 sts 0x58C, r1
ROM:0A5A sts 0x58D, r1
ROM:0A5C sts 0x58E, r1
ROM:0A5E sts 0x58F, r1
ROM:0A60 sts 0x590, r1
ROM:0A62 sts 0x591, r1
ROM:0A64 sts 0x58A, r1
ROM:0A66 ldi r22, 0x8A
ROM:0A67 ldi r23, 5
ROM:0A68 ldi r24, 0x86
ROM:0A69 ldi r25, 5
ROM:0A6A call key_release
ROM:0A6C ldi r22, 0xF4
ROM:0A6D ldi r23, 1
ROM:0A6E ldi r24, 0
ROM:0A6F ldi r25, 0
ROM:0A70 call delay
ROM:0A72 ldi r24, 0x40 ; '@'
ROM:0A73 ldi r25, 1
ROM:0A74 call key_writeln
ROM:0A76 ldi r22, 0xF4
ROM:0A77 ldi r23, 1
ROM:0A78 ldi r24, 0
ROM:0A79 ldi r25, 0
ROM:0A7A call delay
ROM:0A7C ldi r24, 0x4C ; 'L'
ROM:0A7D ldi r25, 1
ROM:0A7E call key_writeln
ROM:0A80 ldi r22, 0xF4
ROM:0A81 ldi r23, 1
ROM:0A82 ldi r24, 0
ROM:0A83 ldi r25, 0
ROM:0A84 call delay
懒得写idapython脚本,直接从IDA中复制伪代码,用正则把偏移拿出来(data即为notepad开始的那串字符串):
l=[0x14C ,0x153 ,0x162 ,0x177 ,0x18B,0x1A9,0x1C8,0x1D3,0x1EB,0x1FE,0x25E ,0x207,0x21C,0x227 ,0x246 ,0x261 ,0x270 ,0x28B,0x298,0x2A3,0x2B1,0x25C ,0x2BA,0x2C5,0x2D0,0x2D7,0x2F2,0x307,0x310,0x25E ,0x327 ,0x346 ,0x3DC,0x34D ,0x364 ,0x373 ,0x38F,0x3A6,0x3B3,0x3BF,0x3D0,0x3DF,0x3EF,0x400,0x44B ,0x413,0x42C ,0x43B ,0x44F ,0x452 ,0x490,0x45F ,0x46C ,0x47D ,0x48E,0x497,0x49E,0x4B5,0x4CB,0x445 ,0x445 ,0x4D6,0x44D ,0x44D ,0x494,0x4E5,0x44F]
l=map(lambda x:x-0x140, l)
flag=''
s=open('data','rb').read()
for i in range(len(l) ):
st = l[i]
ed = st+1
while ed<len(s):
if s[ed] == '\x00':
break
ed+=1
flag+=s[st:ed]
print flag
打印出来的flag是个表达式,直接python eval得到结果转hex再decode hex即可得到flag:
>>> eval('44646 + ( 64094 + ( 71825 * ( ( 15873 + ( 21793 * ( 7234 + ( 17649 * ( ( 2155 + ( 74767 * ( 35392 + ( 88216 * ( 83920 + ( 16270 + ( 20151 * ( 5268 + ( 90693 * ( 82773 + ( 716 + ( 27377 * ( 44329 + ( 49366 * ( ( ( 38790 + ( 70247 * ( 97233 + ( 18347 + ( 22117 * ( ( ( 72576 + ( ( 47541 + ( 46975 + ( 53769 * ( 94005 + ( ( 72914 + ( 5137 + ( 87544 * ( ( 71583 + ( 20370 + ( 37968 * ( 17478 + ( ( 40532 + ( 10089 + ( 13332 * ( ( 24170 + ( 46845 * ( 16048 + ( 23142 * ( 31895 + ( 62386 * ( 12179 + ( 94552 + ( ( ( 52918 + ( 91580 + ( ( ( 38412 + ( 91537 * ( 70 + ( 98594 * ( ( 35275 + ( 62912 * ( 4755 + ( 16737 * ( 27595 + ( ( 43551 + ( 64482 * 3550 ) ) - 21031 ) ) ) ) ) ) - 57553 ) ) ) ) ) - 89883 ) - 38900 ) ) ) - 19517 ) - 79082 ) ) ) ) ) ) ) ) ) - 70643 ) ) ) ) - 55350 ) ) ) ) ) - 40301 ) ) ) ) - 83065 ) ) ) ) ) - 52460 ) ) - 49428 ) - 94686 ) ) ) ) ) ) - 1653 ) - 65217 ) ) ) ) ) ) ) ) ) ) ) ) ) ) ) - 43827 ) ) ) ) ) - 66562 ) ) )')
245160825372454180181035013425094268426669928853472000168466067114757309065141074622457247656884957267064733565L
>>> hex(245160825372454180181035013425094268426669928853472000168466067114757309065141074622457247656884957267064733565)
'0x686374667b50306c3173685f4475636b5f5461737433735f44336c3163693075735f44305f555f5468316e6b3f7dL'
>>> '686374667b50306c3173685f4475636b5f5461737433735f44336c3163693075735f44305f555f5468316e6b3f7d'.decode('hex')
'hctf{P0l1sh_Duck_Tast3s_D3l1ci0us_D0_U_Th1nk?}'
PS:题目一开始放出来的时候是另外一个奇怪的式子:
notepad.exe44646 64094 71825 66562 15873 21793 7234 17649 43827 2155 74767 35392 88216 83920 16270 20151 5268 90693 82773 716 27377 44329 49366 65217 1653 38790 70247 97233 18347 22117 94686 49428 72576 52460 47541 46975 53769 94005 83065 72914 5137 87544 40301 71583 20370 37968 17478 55350 40532 10089 13332 70643 24170 46845 16048 23142 31895 62386 12179 94552 79082 19517 52918 91580 38900 89883 38412 91537 70 98594 57553 35275 62912 4755 16737 27595 21031 43551 64482 3550 *++-+*+*++-*+*++-+-+++-+-++*+*+*++-*+++-+*+++-*+++-+*+++-++-+-*++*++-+-*+*++*+*++*+*++-*+*++-*++
根据题目名称猜测是波兰式、逆波兰式一类的,但是运算数和运算符数量匹配不起来,看了一下午没想出来。
后来与作者沟通,原来要把式子里的+-
->-
,然后就能解出,不过作者最后还是换成正常表达式了2333
IDA打开,第一关判断了系统版本:
v1 = GetVersion();
return (unsigned __int8)v1 == 5 && HIBYTE(v1) == 1;
这里我的系统版本过不了,于是直接patch掉。
下一关判断了命令行参数(即flag)长度为73,然后再sub_448726
函数中对前46位做了一个check:
首先通过sub_44AFA8
对flag加密,然后在sub_44F5B0
与一个固定data做比较,大致就是data里面偶数位是opcode,奇数位是oprand,照着模拟一下即可得到flag的前半部分:
data0=[0x07, 0xE7, 0x07, 0xE4, 0x01, 0x19, 0x03, 0x50, 0x07, 0xE4, 0x01, 0x20, 0x06, 0xB7, 0x07, 0xE4, 0x01, 0x22, 0x00, 0x28, 0x00, 0x2A, 0x02, 0x54, 0x07, 0xE4, 0x01, 0x1F, 0x02, 0x50, 0x05, 0xF2, 0x04, 0xCC, 0x07, 0xE4, 0x00, 0x28, 0x06, 0xB3, 0x05, 0xF8, 0x07, 0xE4, 0x00, 0x28, 0x06, 0xB2, 0x07, 0xE4, 0x04, 0xC0, 0x00, 0x2F, 0x05, 0xF8, 0x07, 0xE4, 0x04, 0xC0, 0x00, 0x28, 0x05, 0xF0, 0x07, 0xE3, 0x00, 0x2B, 0x04, 0xC4, 0x05, 0xF6, 0x03, 0x4C, 0x04, 0xC0, 0x07, 0xE4, 0x05, 0xF6, 0x06, 0xB3, 0x01, 0x19, 0x07, 0xE3, 0x05, 0xF7, 0x01, 0x1F, 0x07, 0xE4]
s=''
for i in range(0, len(data0), 2):
x = data0[i]
x2 = data0[i+1]
if x == 0:
x2-=34
if x == 1:
x2-=19
if x == 2:
x2-=70
if x == 3:
x2-=66
if x == 4:
x2^=0xca
if x == 5:
x2^=0xfe
if x == 6:
x2^=0xbe
if x == 7:
x2^=0xef
#print x, x2, x|((x2<<3)&0x78)
s+=chr(x|((x2<<3)&0x78))
print s
后半部分写在一个驱动文件中,我的电脑无法直接运行,于是逆一下,发现主要逻辑从sub_403310
开始,大致就是开了一个奇怪的vm(找到一个类似的题),其中sub_4030b0
中的一串相当于vm code:
void vmcalls()
{
rdmsr(0x176);
invd(0x4433);
vmcall(0x30133403);
vmcall(0x3401CC01);
vmcall(0x36327A09);
vmcall(0x3300CC00);
vmcall(0x3015CC04);
vmcall(0x35289D07);
vmcall(0x3027CC06);
vmcall(0x3412CC03);
vmcall(0x3026CD06);
vmcall(0x34081F01);
vmcall(0x3311C302);
vmcall(0x3625CC05);
vmcall(0x3930CC07);
vmcall(0x37249405);
vmcall(0x34027200);
vmcall(0x39236B04);
vmcall(0x34317308);
vmcall(0x3704CC02);
invd(0x4434);
vmcall(0x38531F11);
vmcall(0x3435CC09);
vmcall(0x3842CC0A);
vmcall(0x3538CB0B);
vmcall(0x3750CC0D);
vmcall(0x3641710D);
vmcall(0x3855CC0F);
vmcall(0x3757CC10);
vmcall(0x3740000C);
vmcall(0x3147010F);
vmcall(0x3146CC0B);
vmcall(0x3743020E);
vmcall(0x36360F0A);
vmcall(0x3152CC0E);
vmcall(0x34549C12);
vmcall(0x34511110);
vmcall(0x3448CC0C);
vmcall(0x3633CC08);
invd(0x4437);
vmcall(0x3080CC17);
vmcall(0x37742C16);
vmcall(0x3271CC14);
vmcall(0x3983CC19);
vmcall(0x3482BB17);
vmcall(0x3567BC15);
vmcall(0x3188041A);
vmcall(0x3965CC12);
vmcall(0x32869C19);
vmcall(0x3785CC1A);
vmcall(0x3281CC18);
vmcall(0x3262DC14);
vmcall(0x3573CC15);
vmcall(0x37566613);
vmcall(0x3161CC11);
vmcall(0x3266CC13);
vmcall(0x39844818);
vmcall(0x3777CC16);
vmcall(0xFFEEDEAD);
}
sub_402880
函数是对应的解释函数:
int sub_402880()
{
int opcode; // [esp+4h] [ebp-Ch]
int v2; // [esp+Ch] [ebp-4h]
opcode = vmread(0x4402);
v2 = vmread(0x440C);
r4 = vmread(0x681C);
r8_ = vmread(0x681E);
r9_ = vmread(0x6802);
if ( !inited )
{
vm_init();
inited = 1;
}
switch ( opcode )
{
case 0xA:
mod_opcodes();
break;
case 0xD:
switch_opcodes();
break;
case 0x12:
vm_calc();
break;
case 0x1C:
sub_4023E0();
break;
case 0x1F:
sub_402190();
break;
case 0x20:
write_reg_r0();
break;
default:
break;
}
vmwrite(0x681E, v2 + r8_);
return vmwrite(0x681C, r4);
}
虽然不知道上面几种指令对应哪个分支,不过根据参数和逻辑大体猜测vmcall对应vm_calc,invd对应switch_opcodes,rdmsr对应mod_opcodes。(函数重命名过)
可以先看vm_calc:
void vm_calc()
{
int (*v0)(void); // ST2C_4
unsigned int eax_; // [esp+18h] [ebp-14h]
int idx; // [esp+28h] [ebp-4h]
eax_ = (unsigned int)r0 >> 24;
idx = (BYTE2(r0) & 0xF) + 9 * (((((unsigned int)r0 >> 16) & 0xFF) >> 4) & 0xF);
if ( (unsigned __int16)r0 >> 8 == 0xCC )
p_flag = (char *)&flag;
else
p_flag = (char *)&flag_rev;
if ( eax_ == op_load_[0] )
{
regs_p[idx] = *(_DWORD *)&p_flag[4 * (unsigned __int8)r0];
do_nothing(regs_p[idx], idx);
}
else if ( eax_ == op_add[0] )
{
regs_p[idx] += *(_DWORD *)&p_flag[4 * (unsigned __int8)r0];
regs_p[idx] &= 0xFFu;
do_nothing(regs_p[idx], idx);
}
else if ( eax_ == op_minus )
{
regs_p[idx] -= *(_DWORD *)&p_flag[4 * (unsigned __int8)r0];
regs_p[idx] &= 0xFFu;
do_nothing(regs_p[idx], idx);
}
else if ( eax_ ==