[UITCTF - Writeup] Pwn200, Re300

UIT CTF lần này mình kết thúc ở #7, định là sẽ không viết writeup nhưng sau khi đọc writeup 2 bài Pwn200 và Re300 của anh Lê Thanh Bình here mình quyết định viết bài này để phân tích cách giải của mình, một hướng giải khác không kém phần thú vị :D

Binary hai bài này các bạn có thể tải ở đây

#Do bài mới viết nên các bạn có góp ý hay chỉnh sửa sai xót gì các bạn có thể mail hoặc inbox cho mình để mình edit :D. Mình cũng xin cảm ơn BTC =)) vì những challenge rất thú vị hehe mấy anh vất vả rồi :D.
Pwn200 và Re300 một góc nhìn mới.

Bài pwn200 đầu tiên chương trình khai báo ba biến str1, str2, str3 trong hàm main cả ba thằng đều nằm trên stack.

  char str3; // [sp+78h] [bp-1A0h]@4
  char str2; // [sp+100h] [bp-118h]@1
  char str1; // [sp+188h] [bp-90h]@1 

Sau khi mình nhập 3 chuỗi vào, chương trình làm vài thứ nhưng không quan trọng, vuln thì rất dễ thấy. Ở cuối chương trình concat str1 với str3 lại với nhau làm tràn stack của hàm main.

void *__cdecl string_concat(const char *str1, const char *str2)
{
  void *dest; // ST14_4@1
  size_t v3; // eax@1

  dest = (void *)&str1[strlen(str1)];
  v3 = strlen(str2);
  return memcpy(dest, str2, v3);
}
Checksec trên gdb ta thấy
gdb-peda$ checksec
CANARY    : disabled
FORTIFY   : disabled
NX        : disabled
PIE       : disabled
RELRO     : disabled
gdb-peda$ 

Cách giải thông thường của bài này hay 'old-school solution' đã là qúa quen thuộc và cũng đã được trình bày trên writeup mình đưa link ở trên, đầu tiên ret về puts hoặc printf để leak một address trong .got từ đó tính address của hàm system, sau đó quay về hàm main của chương trình để nó chạy lại từ đầu. Lúc này ta đã có được địa chỉ của system nên có thể return thằng về system để lấy shell.


.text:08048700                 push    ebp
.text:08048701                 mov     ebp, esp
.text:08048703                 push    esi
.text:08048704                 sub     esp, 214h

Do địa chỉ của main có một nullbyte ở cuối. Nên hàm string_concat ở trên khi đưa chuỗi vào strlen sẽ bị sai. Cách giải quyết khá đơn giản à dời xuống một lệnh vì cái push ebp ở đầu không quan trọng
Thật sự kiểu bài này rất basic, chơi CTF nhiều gặp hoài =.= Nhưng do hôm ấy một phần bị khớp một phần do ... !@#() Mình không qua được chỗ này =.=, mình thật sự rất tiếc vì tốn khá nhiều thời gian cho bài này mà thực sự là không đáng !
Và đây là một solution khác của mình sau bao giờ mò mẫm. Nếu để ý ở trên ta thấy NX không được bật vì vậy ta có thể run được shellcode !. Nhưng mà nhét shellcode vào đâu ?

> ./string_stripped 
Wellcome To My New String Implement On C Programming
Enter your string :^Z
[2]+  Stopped                 ./string_stripped
14:19:23 ✘ Ubuntu ~/CTF/UITCTF/pwn200 
> cat /proc/4577/maps
08048000-08049000 r-xp 00000000 08:07 413848                             /home/l0stb1t/CTF/UITCTF/pwn200/string_stripped
08049000-0804a000 rwxp 00000000 08:07 413848                             /home/l0stb1t/CTF/UITCTF/pwn200/string_stripped
f757d000-f757e000 rwxp 00000000 00:00 0 
f757e000-f7726000 r-xp 00000000 08:06 665079                             /lib/i386-linux-gnu/libc-2.19.so
f7726000-f7728000 r-xp 001a8000 08:06 665079                             /lib/i386-linux-gnu/libc-2.19.so
f7728000-f7729000 rwxp 001aa000 08:06 665079                             /lib/i386-linux-gnu/libc-2.19.so
f7729000-f772c000 rwxp 00000000 00:00 0 
f774f000-f7751000 rwxp 00000000 00:00 0 
f7751000-f7753000 r--pa 00000000 00:00 0                                  [vvar]
f7753000-f7755000 r-xp 00000000 00:00 0                                  [vdso]
f7755000-f7775000 r-xp 00000000 08:06 665090                             /lib/i386-linux-gnu/ld-2.19.so
f7775000-f7776000 r-xp 0001f000 08:06 665090                             /lib/i386-linux-gnu/ld-2.19.so
f7776000-f7777000 rwxp 00020000 08:06 665090                             /lib/i386-linux-gnu/ld-2.19.so
ffa2a000-ffa4b000 rwxp 00000000 00:00 0                                  [stack]

Ở trên là memory map của chương trình. Mặc dù ASLR được bất nhưng toàn bộ file binary của chương trình được map ở một address cố định. Từ 08048000-08049000 là phân vùng .text của binary nên ta không ghi shellcode vào đó được. Từ 08049000-0804a000 ta có cả quyền write và excute do NX bị tắt nên để shellcode vào đâu trong khoảng đó cũng được miên là không có nullbyte.
Mình có chỗ nhét shellcode vào rồi bây giờ làm sao để đưa shellcode vào đó ? Rất đơn giản ta sẽ lợi dụng hàm get_string mà ban đầu chương trình dùng để nhận input từ người dùng.

size_t __cdecl get_string(int buf, int len)
{
  size_t result; // eax@1

  fgets((char *)buf, len, stdin);
  result = strlen((const char *)buf);
  *(_DWORD *)(buf + 128) = result;
  return result;
}

Stack layout lúc đó sẽ như thế này.

LOW                                                                                                          HIGH

ESP
   |
XXXX                  XXXX                             XXXX                             XXXX
get_string       shellcode addr             shellcode addr                          length

get_string là địa chỉ của hàm get_string mà ta ret về.
shellcode_addr thứ nhất dùng để return về shellcode trên memory
shellcode_addr và length thứ hai  là argument cho buf và len của hàm get_string ở trên
Thế là xong =.= done. Mình nghĩ lẽ ra bài này cờ NX phải bật, có lẽ lúc ra đề bị quên =]]. Nhưng mà cách này làm cũng hay :D
Đây là poc của mình các bạn có thể tham khảo và debug để hiểu thêm POC.

####################

Bài Re300 =.= Thật sự mình không thể hiểu được, bài nào của mình làm một phần là do BTC ra đề bị hở, với một phần là do high qúa hay sao suy nghĩ lạ đời vãi =.=. Những bạn nào tinh thần tốt thi không bị khớp thì chơi CTF rất xanh còn như mình thì bao thọt :D

Bài này cho phép mình nhập một chuỗi số bất kỳ bắt đầu là số 1, trừ số 0 và sau đó kiểm tra chuỗi số được nhập :D. Sẽ có hai lần check mình sẽ nói check 2 trước vì check hai đơn gỉan hơn

__int64 __fastcall check2(__int64 array, unsigned int size)
{
  __int64 result; // rax@7
  unsigned int i; // [sp+1Ch] [bp-4h]@3

  if ( size <= 2 )
    wrongwrong();
  for ( i = 2; ; ++i )
  {
    result = i;
    if ( i >= size )
      break;
    if ( *(_DWORD *)(4LL * i + array) != *(_DWORD *)(4LL * (i - 1) + array) + *(_DWORD *)(4LL * (i - 2) + array) )
      wrongwrong();
  }
  return result;
}

Đoạn này check xem chuỗi nhập vào có phải là chuỗi fibonacci không. Chuỗi fibonacci sẽ có tính chất là số đứng trước sẽ bằng tổng của hai số đứng sau: 1 1 2 3 5 ....

 Giờ đến check1:
__int64 __fastcall check1(__int64 array, char size)
{
  __int64 result; // rax@4
  int v3; // [sp+18h] [bp-8h]@1
  unsigned int i; // [sp+1Ch] [bp-4h]@1

  v3 = 0;
  srand(*(_DWORD *)(4LL * (unsigned __int8)size - 4 + array));
  for ( i = 0; (unsigned __int8)size > i; ++i )
    v3 += *(_DWORD *)(4LL * i + array);
  result = *(_DWORD *)(4LL * (rand() % (unsigned __int8)size) + array);
  if ( (_DWORD)result != v3 )
    wrongwrong();
  return result;
}

Đoạn này tính tổng toàn bộ chuỗi fibonacci của chúng ta. Rồi dùng số cuối cùng trong dãy làm seed để random ra một số. Lấy số đó mod với độ dài chuỗi để chọn ra một phần tử trong dãy. Nếu số đó bằng với tổng của cả dãy thì pass.
Mấu chốt để giải bài này theo ý đồ của người ra đề các bạn cũng có thể xem trong writeup ở trên của anh Bình =.= Tất cả đều nằm ở việc trigger signal của linux
 
sigaction(8, (const struct sigaction *)&s, 0LL);

Một lần nửa do độ high của mình =.= mình không hề quan tâm hay để ý gì dòng này lúc RE, mà ngồi tìm ra cách để tính chuỗi hợp lệ =]] thế mà lại tính ra :D
Đầu tiên xét tính chất dãy fibonacci mình thấy thế này nếu dãy toàn số dương: 1 1 2 3 5 ...
Càng ngày dãy sẽ càng tăng vì vậy chuyện mà tổng tất cã các phần tử trong dãy bằng một phần tử nào đó trong dãy là không thể => dãy cần có số âm.
Xét một vài dãy fibonacii có số âm:

1 -2 -1 -3 -4
1 -8 -7
1 -3 -2 -5

Dù có nhập phần tử thứ 2 là số âm thì dãy cùng sẽ càng ngày càng âm nhiều hơn,  nhìn vô ở trên thì chỉ thấy có dãy: 1 -2 -1. Nếu may mắn đi qua check1 mà random ra được phần tử thứ 2 thì thỏa. Mình hí hửng chạy đi thử nhưng mà không được =)). Từ các ý ở trên mình thấy là chả thế quái nào tìm ra được số dãy nào thỏa yêu cầu đề. Đến đây mình đã qùy lắm rồi =.= vì mình vẫn chưa để ý đến cái hàm sigaction ở trên !. Nằm trên giường định là khóc rồi thì :D ... Ánh sáng chân lý chói qua tim, mình nhớ ra trong C có tính chất tràn số !. Tức là max int trong C là 2**31-1 nếu lấy số đó cộng thêm với 1 thì nó sẽ quay sang số âm cực đại là -2**31 :D. Cụ thể như thế này:

1 2147483647 -2147483648 -1

Ồ một dãy fibonacci đang toàn là số dương nhưng mà nhờ ngôn ngữ C tự nhiên có thêm số âm :D. Nhìn vào ở trên mình thấy tổng của dãy là -1 nếu hên mang đi thử qua được check1 thì pass còn không thì mò tiếp =]]. Nhưng mà hên là nó đúng :D :D nhiều lúc hay không bằng ăn hên mà =]]. Kết qủa là solve không theo ý người ra đề nhưng mà vui :D.

Comments