Challenge 10

10 - break

As a reward for making it this far in Flare-On, we've decided to give you a break. Welcome to the land of sunshine and rainbows!

Ở bài này chúng ta có 1 file ELF. Chạy thử:

Program xuất ra “sorry i stole your input”, vậy có thể là input của ta nhập vào bằng cách nào đó đã bị đổi. Giờ ta mở file lên trong IDA.

void __cdecl __noreturn main()
{
  char buf[264]; // [esp+0h] [ebp-108h]

  puts("welcome to the land of sunshine and rainbows!");
  puts("as a reward for getting this far in FLARE-ON, we've decided to make this one soooper easy");
  putchar(10);
  printf("please enter a password friend :) ");
  buf[read(0, buf, 0xFFu) - 1] = 0;
  if ( sub_8048CDB(buf) )
    printf("hooray! the flag is: %s\n", buf);
  else
    printf("sorry, but '%s' is not correct\n", buf);
  exit(0);
}
_BOOL4 __cdecl sub_8048CDB(char *s1)
{
  return strcmp(s1, "sunsh1n3_4nd_r41nb0ws@flare-on.com") == 0;
}

Quá tuyệt vời, có luôn flag: “sunsh1n3_4nd_r41nb0ws@flare-on.com”.

Nhưng vẫn không đúng, ta vẫn bị “steal input”. Ta dùng tool strings và grep để tìm chuỗi “sorry i stole your input :)”:

Không tìm được chuỗi nào như vậy, có thể chuỗi này được build trên stack, nên không thể tìm thấy bằng tool strings.

Ở bài này mình lại quên mất 1 điều là: trước khi chạy hàm main thì program sẽ chạy các hàm Constructor. Để vào được tới hàm main, ở bài này có giải thích khá rõ. Để tóm tắt lại thì, mình để cái hình ở đây, cũng khá là dễ hiểu:

Ta xem code của hàm _init:

void __cdecl init()
{
  int v0; // esi
  int v1; // edi

  init_proc();
  v0 = &off_81A4F04 - funcs_8056364;
  if ( v0 )
  {
    v1 = 0;
    do
      funcs_8056364[v1++]();
    while ( v1 != v0 );
  }
}
.init_array:081A4EFC funcs_8056364   dd offset sub_8048CB0
.init_array:081A4EFC                 dd offset sub_8048FC5

Ở đây có 2 hàm Constructor, 1 hàm ở 0x8048CB0, 1 hàm ở 0x8048FC5. Vì hàm ở 0x8048CB0 không có gì nhiều nên ta phân tích hàm ở 0x8048FC5.

int sub_8048FC5()
{
  int v0; // eax
  int pid; // [esp+Ch] [ebp-Ch]

  setvbuf(stdout, 0, 2, 0);
  setvbuf(stdin, 0, 2, 0);
  pid = getpid();
  parrent_pid = pid;
  if ( !fork() )
  {
    sub_80490C4(pid);
    exit(0);
  }
  prctl(0x59616D61, pid, 0, 0, 0);
  nanosleep(requested_time, 0);
  v0 = nice(170);
  return printf("%s", -v0);
}

Ở hàm này, program tạo ra process con bằng fork, process con sẽ thực thi hàm sub_80490C4, ta tiếp tục đến hàm này.

int __cdecl call_ptrace_dynamic(int a1, int a2, int a3, int a4)
{
  void *handle; // ST18_4
  int (__cdecl *v5)(int, int, int, int); // ST1C_4

  handle = dlopen("libc.so.6", 1);
  v5 = (int (__cdecl *)(int, int, int, int))dlsym(handle, "ptrace");
  return v5(a1, a2, a3, a4);
}

int __cdecl sub_80490C4(__pid_t parrent_pid)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v50 = 0;
  if ( call_ptrace_dynamic(PTRACE_ATTACH, parrent_pid, 0, 0) == -1 )
  {
    v49 = sub_804BD69(parrent_pid);             // return "TracerPid" in "/proc/pid/status" if found
    if ( v49 )
    {
      if ( call_ptrace_dynamic(PTRACE_ATTACH, v49, 0, 0) == -1 )
      {
        kill(v49, SIGKILL);
        result = kill(parrent_pid, 9);
      }
      else
      {
        while ( 1 )
        {
          result = waitpid(v49, &v16, 0);
          if ( result == -1 )
            break;
          v35 = v16;
          if ( (unsigned __int8)v16 == 127 )
          {
            v36 = v16;
            v48 = (v16 & 0xFF00) >> 8;
            if ( v48 != 19 && v48 != 17 )
              call_ptrace_dynamic(7, v49, 0, v48);
            else
              call_ptrace_dynamic(7, v49, 0, 0);
          }
        }
      }
    }
      // truncated ....
  }

Hàm này nhận 1 tham số là pid của process cha. Đoạn code trên kiểm tra xem process cha có đang bị process khác trace (debug) không. Có thể đây là 1 kỹ thuật anti-debug.

else
{
  result = waitpid(parrent_pid, &stat_loc, 0);
  if ( result != -1 )
  {
    if ( call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, (int)sub_8048CDB, 0xB0F) == -1 )
      exit(0);
    signal(14, handler);
    v2 = getpid();
    create_child_and_trace_me_please_804A0B4(v2);
    v47 = (void **)&unk_81A52A0;
    unk_81A52A0 = 0;
    call_ptrace_dynamic(31, parrent_pid, 0, 0);
    // truncated ...
  }
  // truncated ...
}

Ở đoạn này, process con write giá trị (32 bit) 0xB0F vào địa chỉ 0x8048CDB của process cha, sau đó nó lại tạo ra thêm 1 process con nữa bằng hàm 0x804A0B4 (hàm này gọi tiếp hàm 0x8049C9C):

__pid_t __cdecl sub_8049C9C(__pid_t pid) // <-- this func receives 1st child's PID
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  prctl(4, 0, 0, 0, 0);
  signal(SIGINT, (__sighandler_t)1);
  signal(SIGQUIT, (__sighandler_t)1);
  signal(SIGTERM, (__sighandler_t)1);
  if ( call_ptrace_dynamic(PTRACE_ATTACH, pid, 0, 0) != -1 )
  {
    while (1)
    {
        // a lot of "if" statements here ...
    }
  }
  puts("OOPSIE WOOPSIE!! Uwu We made a mistaky wakey!!!!");
  return kill(pid, 9);
}

Sau khi process con tạo ra process con thứ 2, thì nó tiếp tục nhảy vào vòng lặp với rất nhiều câu lệnh if, switch:

while ( 1 )
{
    result = waitpid(parrent_pid, &stat_loc, 0);
    if ( result == -1 )
      break;
    v27 = stat_loc;
    if ( (unsigned __int8)stat_loc == 127 )
    {
      v28 = stat_loc;
      if ( (stat_loc & 0xFF00) >> 8 == 19 )
         call_ptrace_dynamic(31, parrent_pid, 0, 0);
      v29 = stat_loc;
      if ( (stat_loc & 0xFF00) >> 8 == 5 )
         // .. truncated
    }
    // .. truncated ....
}

Đến giờ ta có 1 số thông tin như sau:

  • Process con 1 gọi ptrace(PTRACE_ATTACH, ...) lên process cha.
  • Process con 2 gọi ptrace(PTRACE_ATTACH, ...) lên process con 1.
  • Ca 2 process con đều thực thi 1 vòng lặp while (1) với rất nhiều if , switch, và gọi rất nhiều ptrace ở trong vòng lặp đó.

PTRACE_ATTACH thường được gọi khi 1 debugger muốn debug process khác.

Mình tìm thấy trên mạng có một số bài viết rất hay về ptrace (nếu bạn chưa biết nhiều về hàm ptrace thì có thể đọc các bài dưới đây để hiểu thêm về nó):

Mình sẽ tóm tắt lại tác dụng của 1 số câu lệnh ptrace được gọi trong chương trình này ở dưới:

  • ptrace(PTRACE_ATTACH, pid, 0, 0): attach vào 1 process có pid là pid.
  • ptrace(PTRACE_POKEDATA, pid, where, val): giá trị val kiểu dữ liệu là long vào địa chỉ where của process có PID là pid.
  • ptrace(PTRACE_PEEKDATA), pid, where, 0): đọc giá trị long tại địa chỉ where của process có PID là pid.
  • ptrace(PTRACE_SETREGS, pid, 0, &regs): ghi đè thanh ghi của process có PID là pid bằng các giá trị lưu trong regs.
  • ptrace(PTRACE_CONT, pid, 0, 0): tương tự lệnh continue trong gdb.
  • ptrace(PTRACE_GETREGS, parrent_pid, 0, &regs): lấy giá trị các thanh ghi của process có PID là pid và lưu vào regs.
  • ptrace(31, ...): (ptrace(PTRACE_SYSEMU, ...)): khi gặp 1 syscall, đừng execute syscall đó mà để debugger xử lý. Tham khảo.

Đến đây ta có thể nhận ra:

  • Process 1 giống như debugger và nó điều khiển process cha.
  • Process 2 giống như debugger và nó điều khiển process con 1.

Bốn bài viết ở trên hướng dẫn cách viêt 1 debugger đơn giản, pattern của debugger này là:

while (1)
{
    reason = recv_reason(pid); // Lấy lý do process bị stop
    process(pid); // xử lý (tiếp tục chạy, hoặc step 1 lệnh, đổi thanh ghi, thoát, ...)
}

Để lấy “lí do”, ta có thể sử dụng 1 trong các hàm wait.

Ta cùng xem lại vòng lặp while (1) của process con 1:

while ( 1 )
{
    result = waitpid(parrent_pid, &stat_loc, 0);
    // ... truncated
    if ( (unsigned __int8)stat_loc == 127 ) 
    {
    	if ( (stat_loc & 0xFF00) >> 8 == 19 ) { /* ... */ } // 19 = SIGSTOP
        if ( (stat_loc & 0xFF00) >> 8 == 5 ) { /* ... */ }  // 5 = SIGTRAP
        if ( (stat_loc & 0xFF00) >> 8 == 4 ) { /*... */ }   // 4 = SIGILL
        if ( (stat_loc & 0xFF00) >> 8 == 11 ) { /* ... */ } // 11 = SIGSEGV
        if ( (stat_loc & 0xFF00) >> 8 == 2 ) { /* ... */ }  // 2 = SIGINT
        if ( (stat_loc & 0xFF00) >> 8 == 15 ) { /* ... */ } // 15 = SIGTERM
        if ( (stat_loc & 0xFF00) >> 8 == 3 ) { /* ... */ } // 3 = SIGQUIT
    }
    // ... truncated
}

Ta thấy sau khi lấy được stat_loc bằng hàm waitpid, chương trình sử dụng giá trị sau:

(stat_loc & 0xFF00) >> 8

Đoạn trên được tạo ra bởi macro WSTOPSIG, dùng để lấy “lí do” mà process dừng lại:

/*
     source: https://unix.superglobalmegacorp.com/Net2/newsrc/sys/wait.h.html
     file: sys/wait.h
*/
#define	_W_INT(w)	(*(int *)&(w))	/* convert union wait to int */
#define WSTOPSIG(x)	(_W_INT(x) >> 8)

Vậy là tùy vào “lí do” mà process con 1 sẽ điều khiển process cha như thế nào.

Trước khi đi tiếp, ta cần hiểu ý nghĩa của một số kết quả trả về bởi WSTOPSIG:

  • SIGTRAP: process dừng lại vì nó vừa gặp phải lệnh syscall.
  • SIGILL: process dừng lại vì CPU phải thực thi 1 lệnh asm không hợp lệ.
  • SIGSEGV: process dừng lại vì nó vừa truy cập vùng nhớ mà nó không được phép (vd: truy cập vùng nhớ không được phép).

Vì process con 1 điều khiển process cha thông qua việc làm “debugger”, nên mình không thể debug program này. Tuy nhiên mình có 1 cách khác để log lại các hàm được gọi, sẽ được viết ở dưới.

Việc phân tích tĩnh khá cực, vì nó rất dễ nhầm lẫn, process con 1 lại còn bị “debug” bởi process con 2 nữa nên mình hoàn toàn không biết cách nào để debug chương trình này.

Tuy nhiên có 1 kỹ thuật LD_PRELOAD dùng để hook library function trên linux, bạn đọc có thể tham khảo tại đây.

Vì program này gọi ptrace rất nhiều nên việc đầu tiên mình làm là hook hàm ptrace. Tuy nhiên, program này không gọi trực tiếp ptrace, mà nó gọi thông qua con trỏ hàm:

int __cdecl call_ptrace_dynamic(int a1, int a2, int a3, int a4)
{
    // ...
  handle = dlopen("libc.so.6", 1);
  v5 = (int (__cdecl *)(int, int, int, int))dlsym(handle, "ptrace");
  return v5(a1, a2, a3, a4);
}

Như vậy, mình không hook ptrace nữa mà mình hook dlsym:

long hooked_ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data)
{
    long r = ptrace(request, pid, addr, data);
    if ((int)request != 31)
        printf("[+] ptrace(%s, %d, %p, %p) = %lu (%lX) ('%c%c%c%c')\n", be_req(request), pid, addr,
                data, r, r, (char)r&0xFF, (char)(r>>8)&0xFF, (char)(r>>16)&0xFF, (char)(r>>24)&0xFF);
    if ((int)request == 0xC || (int)request == 0xD)
        print_regs(data);
    return r;
}

extern void *_dl_sym(void *, const char *, void *);
extern void *dlsym(void *handle, const char *name)
{
    if (!strcmp(name,"dlsym"))
        return (void*)dlsym;
    if (!strcmp(name,"ptrace"))
        return (void*)hooked_ptrace;
    return _dl_sym(handle, name, dlsym);
}

Mình đặt tên file này là “libexample.c”. Compile và hook thử:

gcc libexample.c -o libexample.so -fPIC -shared -ldl -D_GNU_SOURCE -m32 -w && LD_PRELOAD=./libexample.so ./break

Hook đã hoạt động đúng như ta muốn, ở trên ta có thể thấy chuỗi “sorry i stole …” được build bằng cách dùng ptrace(PTRACE_PEEKDATA, ...) để đọc từ process khác, đó là lí do ta không thấy chuỗi đó khi dùng strings.

Bây giờ ta bắt đầu quay lại phân tích flow của process con 1. Bắt đầu từ việc process con này viết giá trị 0xB0F vào process cha như đã nói ở trên.

call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, (int)sub_8048CDB, 0xB0F);

Trong đó, hàm sub_8048CDB:

_BOOL4 __cdecl sub_8048CDB(char *s1)
{
  return strcmp(s1, "sunsh1n3_4nd_r41nb0ws@flare-on.com") == 0;
}

Vậy là nó đã thay đổi instruction đầu tiên của hàm này (điều này cũng giải thích tại sao ta nhập “sunsh1n3_4nd_r41nb0ws@flare-on.com” mà không đúng).

Ta thử disassemble chuỗi “\x0F\x0B\x00\x00”

ud2 (ud là viết tắt của undefine), khi gặp lệnh này, process cha sẽ raise SIGILL. Ta xem tiếp ở process con 1:

 if ( (stat_loc & 0xFF00) >> 8 == SIGILL )
{
  v11 = strlen(input_81A56C0);
  SIMP_WriteProcessMemory(parrent_pid, (int)input_81A56C0, (int *)input_81A56C0, v11);
  call_ptrace_dynamic(PTRACE_GETREGS, parrent_pid, 0, (int)&regs);
  v35 = regs.esp;
  if ( call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, regs.esp + 4, (int)input_81A56C0) == -1 )
    exit(0);
  regs.eip = (int)sub_8048DCB;
  call_ptrace_dynamic(PTRACE_SETREGS, parrent_pid, 0, (int)&regs);
}

Vậy là khi process cha gặp SIGILL, process con 1 sẽ set eip = 0x8048DCB, ngoài ra còn set tham số thứ nhất (tại esp + 4) thành input mà ta nhập vào. Tóm lại nó chuyển đã chuyển sub_8048CDB(input) thành sub_8048DCB(input). Ta thử xem hàm sub_8048DCB:

int __cdecl sub_8048DCB(char *s)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v9 = strlen(s);
  argv = "rm";
  v4 = "-rf";
  v5 = "--no-preserve-root";
  v6 = "/";
  v7 = 0;
  execve(s, &argv, 0);
  --v9;
  v8 = -nice(165);
  init_buffer_804B495((int)&v2, v8);
  sub_804BABC(&v2, &unk_81A50EC);
  sub_804BABC(&v2, &unk_81A50F0);
  sub_804BABC(&v2, &unk_81A50F4);
  sub_804BABC(&v2, &unk_81A50F8);
  if ( !memcmp(s, &unk_81A50EC, 0x10u) )
  {
    memset(&unk_81A50EC, 0, 0x10u);
    result = sub_8048F05(s + 16);
  }
  else
  {
    memset(&unk_81A50EC, 0, 0x10u);
    result = 0;
  }
  return result;
}

Ở hàm này ta thấy có execvenice. Đặc biệt là v8 = -nice(165), sau đó v8 được dùng làm buffer (???). Bởi vì hàm nice return 1 số nguyên rất nhỏ, không thể nào dùng làm địa chỉ buffer được, chắc chắn là có gì đó không ổn với hàm nice này.

Như đã giải thích ở trên thì mỗi khi gặp 1 syscall, process cha sẽ raise SIGTRAP, syscall number nằm ở eax. Ở process con, nó xử lý SIGTRAP như sau:

v3 = 322423550 * (regs.orig_eax ^ 0xDEADBEEF);
// ... truncated
if ( v3 == 0xE8135594 ) { /* ... */ }
if ( v3 == 0x2499954E ) { /* ... */ }
// ... a lot of cases ...

nice có syscall number là 0x22, khi đó v3 = 0x3DFC1166:

  case 0x3DFC1166:
      v8 = (char *)get_XorEnc_Str_8056281(reg.ebx);// 0x22 -> nice
      buf = v8;
      v9 = strlen(v8);
      SIMP_WriteProcessMemory(parrent_pid, (int)&data_arr_81A52A0, (int *)buf, v9 + 1);
      free(buf);
      reg.eax = 0;
      d_ptrace(PTRACE_SETREGS, parrent_pid, 0, (int)&reg);
      break;

Như vậy, nó đã modify hàm nice, trả về 1 string tùy vào giá trị của ebx (ebx là tham số của nice).

Có rất nhiều chỗ bị modify như vậy, nên mình liệt kê tóm tắt ra bảng sau, bạn đọc có thể tự RE lại những chỗ này:

orig_eax value of v3 syscall name summary
0xD9 0xE8135594 pivot_root mov DWORD ptr [ebx], ecx
0x36 0x2499954E ioctl ?????
0x5C 0x4A51739A truncate <——————————————- magic here
0x4 0x7E85DB2A write ghi buffer ra stdout
0x22 0x3DFC1166 nice lấy string dựa vào ebx
0xB 0xF7FF4E38 execve xóa ‘\n’ trong tham số đầu tiên
0x7A 0x9C7A9D6 uname mov QWORD ptr [ebx], 0x9E3779B9C6EF3720
0x60 0x9678E7E2 get_priority (dùng bởi nice)
0x1 0xB82D3C24 exit thoát chương trình
0x98 0xC93DE012 mlockall return __pop_count(*(QWORD*)ebx)
0xF 0xAB202240 chmod xor, ror 1 vài con số ….
0x61 0x83411CE4 set_priority (dùng bởi nice)
0x3 0x91BDA628 read gọi fgets

Hàm truncate có nhiều thứ hay ho và chúng ta sẽ quay lại sau. Bây giờ ta quay lại hàm sub_8048DCB (mình đã rename 1 số thứ):

_BOOL4 __cdecl sub_8048DCB(char *inp)
{
 // ... truncated 
  len = strlen(inp);
  // truncated ...
  execve(inp, &argv, 0);                        // remove '\n' at the end
  --len;
  v8 = -nice(0xA5);                             // return a string
  j_customAES_expand_key_804B495((int)aes_ctx, v8);//
  custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50EC);
  custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50F0);
  custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50F4);
  custom_AES_enc_804BABC((BYTE *)aes_ctx, (BYTE *)&unk_81A50F8);
  if ( !memcmp(inp, &unk_81A50EC, 0x10u) )
  {
    memset(&unk_81A50EC, 0, 0x10u);
    result = sub_8048F05(inp + 16);
  }
  else
  {
    memset(&unk_81A50EC, 0, 0x10u);
    result = 0;
  }
  return result;
}

Hàm trên nhận vào input của chúng ta, loại bỏ dấu “\n” ở cuối cùng. Sau đó nó lấy 1 đoạn string bằng nice để làm key. Key này được dùng để AES decrypt 1 string khác.

Bạn đọc có thể RE thử hàm AES ở trên, nó đã bị sửa lại, block size chỉ còn 4 thay vì 16.

Sau đó, nó so sánh đoạn string vừa decrypt được với input của chúng ta. Đến đây ta hook hàm memcmp để xem nội dung của 2 tham số:

int memcmp(const void *s1, const void *s2, size_t n)
{
    int (*orig_memcmp)(const void *s1, const void *s2, size_t n) = dlsym(RTLD_NEXT, "memcmp");
    int r = orig_memcmp(s1, s2, n);
    printf("[+] memcmp s1: \"%s\", s2: \"%s\"\n", s1, s2);
    return r;
}

Và:

...
[+] memcmp s1: "AAAAAA", s2: "w3lc0mE_t0_Th3_l"
...

Vậy 16 ký tự đầu là “w3lc0mE_t0_Th3_l”. Bây giờ ta chạy lại chương trình với input “w3lc0mE_t0_Th3_lAAAAAA”:

Lần này vẫn là dòng chữ “sorry …” đó, nhưng khác với lần đầu.

Ở lần đầu, khi ta nhập input “AAAAA” thì chương trình hiện ngay dòng đó luôn, còn nếu ta nhập “w3lc0…” thì chương trình mất khoảng 1 vài phút mới hiện lên dòng chữ đó, chứng tỏ là input này có gì đó làm cho chương trình đi theo flow khác.

if ( !memcmp(inp, &unk_81A50EC, 0x10u) )
{
    memset(&unk_81A50EC, 0, 0x10u);
    result = sub_8048F05(inp + 16); // <---- go
}

Bây giờ ta phân tích hàm sub_8048F05, hàm này nhận tham số là &input[16]:

_BOOL4 __cdecl sub_8048F05(void *src)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v1 = nice(0xA4);
  s = (char *)-v1;
  v2 = strlen((const char *)-v1);
  v6 = calc_hash(0LL, (int)s, v2); // v6 = 0x674a1dea4b695809
  v5 = 40000;
  memcpy(&file, src, 0x20u);
  for ( i = 0; i < v5; i += 8 )
    sub_804C369(&file + i, v6, HIDWORD(v6), &v4); // decrypt ?
  return truncate(&file, 32) == 32;
}

Ta phân tích tiếp hàm sub_804C369:

unsigned int __cdecl sub_804C369(__mode_t *a1, int a2, int a3, const char *a4)
{
  // [COLLAPSED LOCAL DECLARATIONS. PRESS KEYPAD CTRL-"+" TO EXPAND]

  v11 = __readgsdword(0x14u);
  v6 = 0;
  sub_804C217(__PAIR__(a3, a2), 16, (int)a4); // <------- try RE this function
  v7 = *a1;
  mode = a1[1];
  v5 = 0;
  v9 = mode;
  v10 = v7 ^ chmod(a4, mode);
  v7 = mode;
  mode = v10; 
  MEMORY[0](&loc_804C3C4, &v5); // <----- here
  *a1 = mode;
  a1[1] = v7;
  return __readgsdword(0x14u) ^ v11;
}

Ở trên ta có thể thấy dòng MEMORY[0](...), đoạn đó tương đương với đoạn asm sau:

xor eax, eax
call eax

Đoạn này sẽ làm process raise SIGSEGV. Process con 1 xử lý đoạn này như sau:

if ( (stat_loc & 0xFF00) >> 8 == SIGSEGV )
{
  call_ptrace_dynamic(PTRACE_GETREGS, parrent_pid, 0, (int)&regs);
  v34 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, regs.esp, 0);
  v33 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, regs.esp + 4, 0);
  v32 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, regs.esp + 8, 0);
  v31 = call_ptrace_dynamic(PTRACE_PEEKDATA, parrent_pid, v32, 0) + 1;
  regs.esp += 4;
  if ( v31 > 15 )
  {
    regs.eip = v34;
  }
  else
  {
    regs.eip = v33;
    call_ptrace_dynamic(PTRACE_POKEDATA, parrent_pid, v32, v31);
    regs.esp += 16;
  }
  call_ptrace_dynamic(PTRACE_SETREGS, parrent_pid, 0, (int)&regs);
}

Với đoạn code trên, giả sử ta có 1 hàm f(x,y) với f == NULL (để tạo SIGSEGV), thì:

  • Nếu (*y)++ >= 16 thì return.
  • Ngược lại: set eip = x.
  • Tóm lại cái hàm f này giúp chương trình lặp 16 lần mà không cần dùng for, while, ....

Quay lại hàm sub_804C369, hàm này dùng để encrypt 1 block data gồm 8 bytes. Hàm này sử dụng vòng loop bằng SIGSEGV như giải thích ở trên, bao gồm 2 bước:

  • Expand key:
void expand_key(uint64_t key, uint32_t *ctx)
{
    uint32_t *_ctx = ctx;
    uint64_t k = key;
    for (int i = 0; i < 16; ++i)
    {
        _ctx[7] = k & 0xFFFFFFFFull;
        _ctx[19] = (k >> 32ull) & 0xFFFFFFFFull;
        _ctx[41] = pop_cnt(k) >> 1;
        int _v6 = k & 1;
        k = k >> 1;
        if (_v6)
        {
            k = k ^ 0x9E3779B9C6EF3720uLL;
        }
        _ctx = _ctx + 62;
    }
}
  • Encrypt:
uint32_t encipher_8bytes(uint32_t *a1, uint64_t key, uint32_t *ctx)
{
    uint32_t v7;
    uint32_t v8;
    uint32_t v10;
    expand_key(key, ctx);
    uint32_t *_ctx = ctx;
    uint64_t k = key;
    v7 = a1[0];
    v8 = a1[1];
    _ctx = ctx;
    for (int i = 0; i < 16; ++i)
    {
        v10 = v7 ^ ROR(v8 + _ctx[7], _ctx[41]) ^ _ctx[19];
        v7 = v8;
        v8 = v10;
        _ctx = _ctx + 62;
    }
    a1[0] = v8;
    a1[1] = v7;
    return 0;
}

Sau khi encrypt đoạn input, nó encrypt luôn gần 40000 bytes data trong process cha, cuối cùng gọi truncate(data, 32).

memcpy(&file, src, 0x20u); // src is actually &input[16]
for ( i = 0; i < v5; i += 8 )
  sub_804C369((__mode_t *)(&file + i), v6, SHIDWORD(v6), &v4); // encrypt_8bytes
return truncate(&file, 32) == 32;

Ta lại tiếp tục coi process con 1 xử lý truncate như nào:

case 0x4A51739A: // truncate (0x5C)
    SIMP_ReadProcessMemory(parrent_pid, reg.ebx, (int *)&file, 40000);
    for ( i = 0; i <= 39999 && *(_BYTE *)(i + 0x804C640); ++i )
    {
      v18[i] = *(_BYTE *)(i + 0x804C640);// file[i]
      if ( v46 == -1 && v18[i] != *(_BYTE *)(i + 0x81A5100) )
      {
        v46 = i;                // v46 = incorrect position
      }
    }
    v46 = v44(0xA4F57126, input_81A56C0, v46);
    reg.eax = v46;
    d_ptrace(PTRACE_SETREGS, parrent_pid, 0, (int)&reg);
    break;

Nó sẽ copy data vừa được encrypt lên stack của chính nó, đồng thời so sánh data đó với 1 mảng hardcode ở 0x81A5100, ở đây có 32 byte [100, 160, 96, 2, 234, 138, 135, 125, 108, 233, 124, 228, 130, 63, 45, 12, 140, 183, 181, 235, 207, 53, 79, 66, 79, 173, 43, 73, 32, 40, 124, 224].

Ta viết ngay 1 đoạn decrypt đoạn trên:

int main()
{
    uint32_t ctx[62*17];
    uint8_t data[] = { 100, 160, 96, 2, 234, 138, 135, 125, 108, 233, 124, 228, 130, 63, 45, 12, 140, 183, 181, 235, 207, 53, 79, 66, 79, 173, 43, 73, 32, 40, 124, 224 , 0};
    uint64_t key = 0x674a1dea4b695809ULL;
    for (int i = 0; i < 32; i = i + 8)
    {
        decipher_8bytes((uint32_t*)(data + i), key, ctx);
    }
    printf("%s\n", (char*)data);
    return 0;
}
4nD_0f_De4th_4nd_d3strUct1oN_4nd

Tiếp theo, nó gọi hàm v44 tạo SIGSEGV:

v46 = v44(0xA4F57126, input_81A56C0, v46);

Ta sẽ xem process con thứ 2 xử lý SIGSEGV như nào:

if ( _exitcode == SIGSEGV )             // SIGSEGV
    {
      d_ptrace(PTRACE_GETREGS, pid, 0, (int)&regs);
      v_esp_0 = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp, 0);
      v_esp_4 = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp + 4, 0);
      v_esp_8 = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp + 8, 0);
      v_esp_C = d_ptrace(PTRACE_PEEKDATA, pid, regs.esp + 0xC, 0);
      // truncated ...
      {
        switch ( v_esp_4 )
        {
          case (int)0xA4F57126:
            regs.eax = v_esp_C;
            if ( v_esp_C != -1 )            // if (there is incorrect byte)
            {
              SIMP_ReadProcessMemory(pid, v_esp_8, (int *)input_81A56C0, 62);
              if ( strncmp(s1, "@no-flare.com", 0xDu) ) // s1 is &input[48];
              {
                regs.eax = -1;
              }
            }
            break;
		// truncated ...
        }
      }
	// truncated ... 
    }

Ơ, vậy flag là “w3lc0mE_t0_Th3_l4nD_0f_De4th_4nd_d3strUct1oN_4nd@no-flare.com” à, hmm có gì đó không ổn.

Vậy là có gì đó không đúng, mình thử hook hàm strncmp, thì không thấy hàm strncmp được thực thi. Như vậy là flow của chương trình đã bị đổi.

Đoạn copy data lên stack có gì đó đáng nghi nên mình xem lại:

for ( i = 0; i <= 39999 && *(_BYTE *)(i + 0x804C640); ++i )
{
  v18[i] = *(_BYTE *)(i + 0x804C640);// v18[i] = file[i]
    // truncated ...
}
v46 = v44(0xA4F57126, input_81A56C0, v46);
// truncated ...

Trên stack, v18 là một mảng char có độ dài 16000, nhưng đoạn trên có thể copy tới tận 40000 byte -> BufferOverflow. Ta viết 1 đoạn code nhỏ để lấy đoạn data sau khi được encrypt:

int main()
{
    uint32_t ctx[62*17];
    uint64_t key = 0x674a1dea4b695809ULL;
    uint8_t* data = new uint8_t[40000];
    FILE* f = fopen("dump.bin", "rb");
    fread(data, 1, 40000, f);
    fclose(f);
    for (int i = 0; i < 40000; i = i + 8)
    {
        encipher_8bytes((uint32_t*)(data + i), key, ctx);
    }
    f = fopen("dec.bin", "wb");
    fwrite(data, 1, 40000, f);
    fclose(f);
    delete[] data;
    return 0;
}

Ở trong IDA, ta có thể tính được khoảng cách từ v44 tới đầu mảng v18 là 16164. Ta mở HxD để tới offset này:

Vậy là v44 đã bị ghi đè bởi 0x8053B70, địa chỉ này nằm trong đoạn data được encrypt, nên ta sẽ: lấy đoạn data đã được encrypt để patch lên file break gốc, sau đó bỏ vào IDA để phân tích lại hàm 0x8053B70.

# python3

if __name__ == '__main__':
    with open('break', 'rb') as f:
        data = f.read()
    with open('dec.bin', 'rb') as f:
        patch_data = f.read()
    data = data[:0x4640] + patch_data + data[0x4640+40000:]
    with open('patch', 'wb') as f:
        f.write(data)
    print ('[+] Done')

Sau khi có file mới, ta dùng IDA phân tích tiếp hàm sub_8053B70, hàm này gọi lại hàm sub_805492E(mình đã RE hàm này và rename lại hết, bạn đọc có thể tự RE lại các hàm liên quan tới bigint và xác nhận lại):

void __cdecl __noreturn sub_805492E(int a1, char *inp, int a3)
{
  // truncated ...
  v_8053B70 = sub_8053B70;
  pid = dword_81A5280;
  ptrace_8054C5C(0, dword_81A5280, PTRACE_GETREGS, &regs);
  if ( a3 != 32 )
  {
    regs.eax = -1;
    ptrace_8054C5C(0, pid, PTRACE_SETREGS, &regs);
    ptrace_8054C5C(0, pid, PTRACE_DETACH, 0);
    sysexit_80540CB(0);
  }
  zeroing_80544E2(bi_input);
  zeroing_80544E2(r32_byte);
  zeroing_80544E2(v11);
  hextoint_8054447(bi_1, (BYTE *)&word_8055BE2[-67282078] + (_DWORD)v_8053B70, 64);// d1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f (bi[1])
  hextoint_8054447(bi_2, (BYTE *)&dword_8055B60[-33641039] + (_DWORD)v_8053B70, 64);// c10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31 (bi[2])
  hextoint_8054447(bi_3, (BYTE *)&byte_8055B1F[-134564156 + (_DWORD)v_8053B70], 64);// 480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c (bi[3])
  hextoint_8054447(bi_4, (BYTE *)&byte_8055B1F[-134564156 + (_DWORD)v_8053B70], 64);// 480022d87d1823880d9e4ef56090b54001d343720dd77cbc5bc5692be948236c (bi[4])
  hextoint_8054447(bi_5, (BYTE *)&byte_8055BA1[-134564156 + (_DWORD)v_8053B70], 64);// d036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb (bi[5])
  qmemcpy(bi_input, inp + 48, 24u);
  fd = call_sysopen_805409F(0, 0, (char *)&word_8055B12[-67282078] + (_DWORD)v_8053B70);// /dev/urandom
  sysread_80540B5(0x20u, r32_byte, fd);         // read 32 byte /dev/urandom
  divmod_80543CA(r32_byte, bi_1, placeholder, a4);// divmod(r32, bi_1)
  sysclose_8054091(fd);
  assign_805422A(r32_byte, a4);                 // r32 = a4
  fast_pow_8054533(bi_2, r32_byte, (int)bi_1, v11);
  assign_805422A(r32_byte, a4);
  fast_pow_8054533(bi_4, r32_byte, (int)bi_1, v13);
  mul_bigint_80546E1((int)bi_input, (int)v11, r32_byte);
  divmod_80543CA(r32_byte, bi_1, placeholder, (BYTE *)v14);
  memset(v17, 0, sizeof(v17));
  mb_cvt_int_to_hex_8054882(v13, v17, 1024);
  memset(v17, 0, sizeof(v17));
  mb_cvt_int_to_hex_8054882(v14, v17, 1024);
  if ( cmp_big_int_8054251(bi_3, v13) || cmp_big_int_8054251(bi_5, v14) )// v13 == bi_3 && v14 == bi_5
  {
    regs.eax = -1;
    ptrace_8054C5C(0, pid, PTRACE_SETREGS, &regs);
    ptrace_8054C5C(0, pid, PTRACE_DETACH, 0);
    sysexit_80540CB(0);
  }
  inp[72] = 0;
  ptrace_readdata_8054C75(pid, (char *)dword_81A57C0, (void **)inp, (signed int)&v_8053B70, 73);
  regs.eax = 32;
  ptrace_8054C5C(0, pid, PTRACE_SETREGS, &regs);
  ptrace_8054C5C(0, pid, PTRACE_DETACH, 0);
  sysexit_80540CB(0);
}

Mẹo: bạn đọc có thể chỉ cần nhìn vào các tham số trước và sau khi thực hiện hàm để đoán xem hàm đó làm gì, thay vì phải RE.

Ta viết lại đoạn trên như sau:

r32 = rand();
a4 = r32 % bi[1];
r32 = a4;
bi[2] = (bi[2] ** r32) % (bi_1), v11 = old_bi[2];
r32 = a4;
bi[4] = (bi[4] ** r32) % (bi_1), v13 = old_bi[4];
r32 = input * v11;
v14 = r32 % bi[1];
v14 == bi[5]; <---------- condition
v14 = (input * bi[2]) % bi[1];

Về cơ bản hàm trên sẽ:

  • Chuyển input[48:48+24] sang số nguyên, little endian, ta gọi số này là x.
  • Ta phải giải (x * bi[2]) % bi[1] == bi[5]

Với phương trình \(x*y\equiv t\ (mod\ z)\), ta nhân cả 2 vế với nghịch đảo modun của \(y\), được: \(x * y * y^{-1} \equiv t * y^{-1}\ (mod\ z)\)

$\Rightarrow$ \(x \equiv t * y^{-1}\ (mod\ z)\)

Đoạn code sau tính x trong phương trình trên:

# python 3

def egcd(a, b):
    if a == 0:
        return (b, 0, 1)
    else:
        g, y, x = egcd(b % a, a)
        return (g, x - (b // a) * y, y)

def modinv(a, m):
    g, x, y = egcd(a, m)
    if g != 1:
        raise Exception('modular inverse does not exist')
    else:
        return x % m

if __name__ == '__main__':
    z = 0xd1cc3447d5a9e1e6adae92faaea8770db1fab16b1568ea13c3715f2aeba9d84f
    y = 0xc10357c7a53fa2f1ef4a5bf03a2d156039e7a57143000c8d8f45985aea41dd31
    t = 0xd036c5d4e7eda23afceffbad4e087a48762840ebb18e3d51e4146f48c04697eb
    
    r = (((modinv(y, z) * t)))
    #print (hex(r))
    g = r % z
    #print (hex(g))
    for i in range(24):
        print (chr((g >> (8*i)) & 0xFF), end = '')
    print ('')
_n0_puppi3s@flare-on.com

Vậy ta có flag ^^!

w3lc0mE_t0_Th3_l4nD_0f_De4th_4nd_d3strUct1oN_4nd_n0_puppi3s@flare-on.com

[+] Source code dùng để giải cho tất cả các bài nằm ở đây