Saturday, August 8, 2015

[WhiteHat contest 10] Re400 writeup

Trap, trap everywhere.....
Thực sự đối với bài này mình cũng không biết nên đánh giá khó hay dễ. Dễ vì thực ra thuật toán check flag 'thực sự' rất đơn giản (chỉ vài dòng code là ra). Khó vì nếu đi sai 1 hướng thì sẽ mất rất nhiều thời gian để thoát ra khỏi mớ bòng bong. Do đi sai đường nên mình phải mất tới 2 ngày mới solved được nó.
Bài này sử dụng khá nhiều kỹ thuật anti debug và nên dùng song song cả ida và ollydbg 2 để debug. (Vì sao lại là ollydbg2 mà không phải là v1 mình sẽ nói ở bên dưới). Đồng thời do bài này nếu giải thích kỹ sẽ rất dài nên một số đoạn mình sẽ bỏ qua (thực ra là do lười writeup kỹ). :D. Chỉ tập chung chính vào cách check key của bài này.
Lòng vòng nhiều quá, bắt đầu nào.
Load vào IDA, ngay ở đoạn đầu tiên của hàm main:
if ( argc > 0 && !_stricmp("Reverse", *argv) )
  {
    sub_D31A90();
    return 0;
  }
 Mục đích của đoạn này là sẽ chạy sub_D31A90() nếu tên tham số argv = "Reverse". Khi chạy một file thì tham số truyền vào argv sẽ là đường dẫn đến file đó, trong khi argv ở đây lại không có đường dẫn? Có 1 cách để làm việc này, ở ngay đoạn tiếp theo
if ( !sub_D31870() )
  {
    if ( !CreateProcessA(&Filename, "Reverse", 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInformation) )
      printf("CreatePrcess Error");
    v5 = 0;
    do
    {
      byte_D41044[v5] ^= 0x11u;
      ++v5;
    }
 Bỏ qua sub_D31870() đoạn này chỉ nhằm mục đích xem file đã đang chạy hay chưa.
CreateProcessA(&Filename, "Reverse", 0, 0, 0, 0, 0, 0, &StartupInfo, &ProcessInformation) 
Hàm này sẽ sinh ra một process mới,argv = "Reverse".  Và tất nhiên process này sẽ run
sub_D31A90(); Check sâu vào trong hàm này, khá dài dòng nhưng mục đích chính là process mới tạo ra sẽ lắng nghe trên cổng 8888 localhost, sau khi nhận được dữ liệu sẽ tiến hành check. Để capture các gói tin loopback bạn có thể dùng công cụ Rawcap.
Antidebug: Trong hàm này thực hiện 2 kỹ thuật antidebug: GetTickCount và CheckRemoteDebuggerPresent (các bạn hãy hỏi anh gồ để biết thêm thông tin về 2 kỹ thuật này, riêng đối với CheckRemoteDebuggerPresent thì chỉ có tác dụng khi sử dụng win XP).
Khi debug file này bằng IDA và ollydbg1 sẽ không quan sát và debug được process con mới tạo ra, ollydbg2 thì đã hỗ trợ sẵn (Vào options-> debugging->events, check chọn Debug child process). Sau hàm createprocessA một cửa sổ ollydbg mới sẽ xuất hiện
Trở về với process chính của chương trình:
scanf("%500[^\n]s", byte_D41B90);
 if ( v6 >= 7 && !strncmp(byte_D41B90, "FLAG{", 5u) && byte_D41B8F[v6] == '}' )
    {
      memcpy(&byte_D41D88, &unk_D41B95, v6 - 6);
      if ( strlen(&byte_D41D88) <= 40 )

     {
         v8 = sub_D31470();
-> Nhập vào key với định dạng: FLAG{xxxx....}, len <=40, chỉ copy nguyên 'xxxx...' (mình tạm gọi là S) vào byte_D41D88, nếu đúng định dạng thì sẽ chạy tiếp func sub_D31470() để kiểm tra. Func này mục đích là kiểm tra các ký tự bên trong cặp {} phải nằm từ [0-9], và [A-F] [a-f]
if ( v9 == (strlen(&byte_D41D88) >> 1) - 1 && *(dword_D41B54 + v9) != '_' )
S được chia thành từng cặp 2 ký tự. Cặp ký tự cuối của S =5F
if ( v10 == 2596 )
dword_D41B50 = sub_D31960();
 v13 = CreateThread(0, 0, StartAddress, 0, 0, 0);

WaitForSingleObject(v13, 0xFFFFFFFF);
Ví dụ S = 011F -> v10 = 0x01 + 0x1F
sub_D31960(): Từ cặp trong S sẽ được xor với len(S)/2
 v13 = CreateThread(0, 0, StartAddress, 0, 0, 0);
-> Trong StartAddress chứa 2 kỹ thuật antidebug nữa, mời các bạn đọc thêm phần openprocess và Parent processes ở đây
if ( _stricmp(&byte_D41D88, "Whitehat_Contest_Is_Amazing") && sub_D31700() )
Trap1 -> đoạn này không có tác dụng gì, chỉ có mục đích đánh lừa
if ( dword_D42174 != dword_D41F7C && sub_D31730(strlen(&byte_D41D88)) )
Trap2 -> khi không bị debug thì dword_D42174 == dword_D41F7C và việc kiểm tra sub_D31730 là vô nghĩa
 CreateThread(0, 0, sub_D319A0, &Parameter, 0, 0);
Tạo ra một thread mới, đầu vào chính là S (sau khi đã xor )
Sub_D319A0:
 v1 = lpThreadParameter;
  operator new(8u);
  WSAStartup(0x202u, &WSAData);
  v2 = socket(2, 1, 6);
  name.sa_family = 2;
  *&name.sa_data[2] = inet_addr("127.0.0.1");
  *&name.sa_data[0] = htons(0x22B8u);
  if ( connect(v2, &name, 16) )
  {
    do
    {
      Sleep(0x1388u);
      connect(v2, &name, 16);
    }
    while ( connect(v2, &name, 16) );
    v1 = lpThreadParameter;
  }
  send(v2, *v1, *(v1 + 1), 0);
Connect tới cổng 8888 của localhost và send S lên đó.
Process được tạo ra ngay đầu tiên sẽ xử lý S. Khá dài dòng và khá nhiều trap nữa (với mục đích giả vờ check key).
Tương tự tại process chính cũng có khá nhiều đoạn giả check key (xuất hiện các string giả).
When you have eliminated the impossible whatever remains, however improbable, must be the truth. - Shelock Homes.
Và cuối cùng đây là thuật toán tạo key.
final =[0x41,0x17,0x25,0xC7,0xCB,0x55,0x5F,0x9B,0xC7,0x7C,0xC2,0xD6,0x7C,0x12,0xAF,0x80]
key =[0]*16
key[15] = 0x4F
flag ="FLAG{"
for i in xrange(len(key)-2,-1,-1):
    key[i] = final[i] ^ key[i+1]
key = list(map((lambda x:x^0x10),key))
for i in key:
    flag +=hex(i)[2:]
flag += "}"
print flag.upper()


 

Monday, May 11, 2015

ASIS CTF 2015 (Re 125 + 150 points)

Giải này download được 2 file, submit được bài 150, đang làm bài 125 thì hết giờ :(
2 bài này theo đánh giá của mình là vừa tầm với số điểm (tuy nhiên có lẽ 2 bài nên đổi điểm số cho nhau thì hay hơn).
Vì là bài basic nên mình sẽ không đi sâu vào thuật toán mà chỉ nêu về các bước cơ bản khi re để tìm ra flag 2 bài này. Phần thuật toán sẽ được comment trong code.
Ok. let's start.
Download files here
1. Re 125 points (dark_aba92f5882a156452b18b895c722cea6)
- Sau khi download file dark_aba92f5882a156452b18b895c722cea6 về tiến hành view nó bằng 1 trình soạn thảo bất kỳ sẽ thấy vài ký tự ở gần đầu file là '7zXZ' -> đây là file nén xz. Trên linux sẽ giải nén bằng command sau:
  • mv dark_aba92f5882a156452b18b895c722cea6 dark.xz
  • tar -xvf dark.xz
- Sau khi giải nén sẽ thấy trong folder dark có 2 file (dark + flag.enc), như một thói quen ngó xem thông tin về các file này bằng command file
  •  file dark
dark: ELF 64-bit LSB  executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.26, BuildID[sha1]=b3fd37b76503bda914d483041798fb9ec88ab929, stripped
  • file flag.enc
flag.enc: data
File dark là file chúng ta cần re (ELF 64 bit, bắt buộc phải có máy ảo 64bit mới chạy được), flag.enc là 1 file data bình thường. Tiến hành chạy thử file dark xem có chuyện gì xảy ra không:
  • chmod +x dark
  • ./dark
Usage: ./dark inputfile outputfile
Nhìn thông báo chúng ta có thể dự đoán đầu vào file dark là 1 file input, dark sẽ đọc nội dung trong file này sau đó xử lý và xuất ra file outputfile cho chúng ta -> flag.enc ở trên chính là outputfile, nhiệm vụ chúng ta là phải tìm lại nội dung inputfile.
- Ok. Tạo thử inputfile với nội dung 100 ký tự 'a' xem outputfile sẽ ra sao:
  • python -c 'print "a"*100' > inputfile
  • ./dark inputfile outputfile
thử mở outputfile bằng 1 trình soạn thảo bất kỳ thì thấy toàn ký tự giun dế :(, và hơn nữa số lượng ký tự khá lớn (so với file đầu vào chỉ có 100 ký tự 'a'), thử đếm xem file này bao nhiêu ký tự nhé:
  • wc -c outputfile
30215 outputfile
Output sinh ra tới 30215 ký tự. wth
- Tiến hành phân tích dark bằng ida (mình đang dùng 6.6 pro + hex-rays)  remote debug, bạn có thể đọc lại bài này để biết cách cấu hình remote debug
Sau khi load file bằng ida, thử tab string (shift + F12) xem có 'tử huyệt' gì không. Thấy 1 dòng 'khả nghi' sau:
  • .rodata:0000000000400A20 00000020 C Usage: %s inputfile outputfile\n
Click trực tiếp vào dòng đó:
  • .rodata:0000000000400A20 format db 'Usage: %s inputfile outputfile',0Ah,0
  • .rodata:0000000000400A20                                         ; DATA XREF: sub_400715+2F o
sub_400715 có tham chiếu đến string này. Click trực tiếp vào nó sau đó bấm F5 để hex-rays sinh mã Pseudocode cho chúng ta. Bước tiếp theo là quá trình thử và phân tích nên mình không nêu kỹ ở đây. Ở bên dưới là đoạn pseudocode mình đã comment đầy đủ:
 if ( a1 == 3 ) -> Kiểm tra số lượng tham số đầu vào
  {
    stream = fopen(*(v7 + 8), "r"); -> mở file input để đọc
    v21 = fopen(*(v7 + 16), "wb"); -> mở file output để ghi
    v20 = 30215; -> :D nhìn quen quen, nếu để ý kỹ sẽ nhớ là con số này đã xuất hiện rồi
    v19 = 16; 
    v18 = 30214LL;
    v2 = alloca(30224LL); -> xin cấp phát 1 vùng nhớ khá lớn (v2)
    ptr = &v5;
    v16 = 30214LL;
    v6 = 16LL;
    v3 = alloca(30224LL); -> cấp phát vùng nhớ thứ 2 (v3)
    v15 = &v5;
    fread(&v5, 1uLL, 30215uLL, stream); -> đọc 30215 char lưu vào v5 (tạm gọi là mảng input)
    for ( i = 0; v20 / v19 > i; ++i ) -> i =0;30215/16 >i;++i
    {
      for ( j = 0; j < v19; ++j )       -> j chạy từ 0 đến 15 (v19=16)
      {
        v14 = *(ptr + v19 * (i + 1) - j - 1);   -> v14 = input[16*(i+1)-j-1]
        sprintf(&s, "%02x", v14);
        nptr = v12;
        v10 = s;
        v13 = strtol(&nptr, 0LL, 16);
        *(v15 + v19 * i + j) = i * i ^ j * j ^ v13; ----> một vài thao tác xử lý thằng v14, sau đó lưu vào 1 ví trí khác (v15)
      }
    }
    fwrite(v15, 1uLL, v20, v21); -> sau 2 vòng lặp này thì tiến hành ghi ra file output với đầu vào là  mảng v15
    fclose(v21);
    fclose(stream);
  }
  else
  {
    printf("Usage: %s inputfile outputfile\n", *v7);
  }
  return 0LL;
}
Sau khi đã hiểu thuật toán, chúng ta tiến hành decrypt. File sau, sau khi decrypt xong, mở file input bằng 1 trình soạn thảo chúng ta sẽ dễ dàng nhận thấy nó là 1 file pdf. Đổi tên file và gom flag nào.
2. Re 150 points keylead (nhờ khả năng lươn lẹo + suy nghĩ đơn giản mà bài này solve trong 15', bravo :v)
Công đoạn giải nén lấy file đề bài tương tự như bài trên. Đây là mã pseudo code:
Về nội dung thì bài này nhiệm vụ của bạn là phải canh gõ 1 character vào (ký tự nào cũng được), chương trình sẽ lấy thời gian tại thời điểm sau khi đọc được ký tự gõ vào để sinh random 5 số , bạn phải canh me sau cho 5 số chương trình sinh ra là 3,1,3,3,7.
puts("hi all ----------------------");
  puts("Welcome to dice game!");
  puts("You have to roll 5 dices and get 3, 1, 3, 3, 7 in order.");
  puts("Press enter to roll.");
  v0 = getchar(); -> đọc ký tự
  v1 = time(0LL);
  srand(v1); -> seed random dựa vào thời điểm nhận được ký tự
  v8 = time(0LL); -> đo lại time
  v2 = rand() % 6 + 1; -> số thứ 1
  v7 = rand() % 6 + 1;
  v6 = rand() % 6 + 1;
  v5 = rand() % 6 + 1;
  v4 = rand() % 6 + 1;  -> số cuối cùng, nếu để ý kỹ bạn sẽ thấy rằng v4 không bao giờ =7 được :v
  printf("You rolled %d, %d, %d, %d, %d.\n", v2, v7, v6, v5, v4);
  if ( v2 != 3 ) 
    goto LABEL_20;
  if ( time(0LL) - v8 > 2 )
  {
    puts("No cheat!");
    return 0xFFFFFFFFLL;
  }
  if ( v7 != 1 )
    goto LABEL_20;
  if ( time(0LL) - v8 > 2 )
  {
    puts("No cheat!");
    return 0xFFFFFFFFLL;
  }
  if ( v6 != 3 )
    goto LABEL_20;
  if ( time(0LL) - v8 > 2 )
  {
    puts("No cheat!");
    return 0xFFFFFFFFLL;
  }
  if ( v5 != 3 )
    goto LABEL_20;
  if ( time(0LL) - v8 > 2 )
  {
    puts("No cheat!");
    return 0xFFFFFFFFLL;
  }
  if ( v4 != 7 )
  {
LABEL_20:
    puts("You DID NOT roll as I said!");
    puts("Bye bye~");
    result = 0xFFFFFFFFLL;
  }
  else if ( time(0LL) - v8 <= 2 )
  {
    puts("You rolled as I said! I'll give you the flag.");
    sub_4006B6();
    result = 0LL;
  }
  else
  {
    puts("No cheat!");
    result = 0xFFFFFFFFLL;
  }
  return result;
}

Với nhận xét 4 không bao giờ =7 được -> chúng ta sẽ không bao giờ canh me được thời gian để gõ.
-> Chỉ còn khả năng chỉnh sửa các số mà chương trình đã sinh ra cho chúng ta trực tiếp trong bộ nhớ (sau đoạn rand()%6).
Bằng khả năng mò mẫm, sau khi chỉnh sửa xong 1 cái flag xinh đẹp sẽ hiện ra. :D. 
P/s: for you, not for me.