【技術分享】從popmaster理解程序分析理論部分內容

前言
本人水平有限,如有錯誤,歡迎指點。
看到作者的出題思路是抽象代碼樹和污點分析,因為之前發了幾篇程序分析理論的文章但是一直沒有實踐,所以拿這道題自己實踐一下。
污點分析在第一篇程序設計分析理論中,我寫的是語義分析,但在后面的文章中還沒有正式出現。關于抽象代碼樹,要在后面的抽象解釋才提到。
過程間分析的內容還沒有發出來,可以先看這篇文章幫助理解后面的過程間分析
我們已經介紹的只是數據流分析的部分內容,所以這篇文章首先會是根據數據流分析的過程間分析編寫一個遍歷以入口函數為起點的所有路徑的腳本實現代碼分析。
最后我們還會看一看出題人的exp。
編程思路
這一道題是一個單輸入多輸出的問題,根據之前流的理論,我們應該從輸入開始分析。同時這道題是函數的跳轉,所以我們選擇過程間分析。根據上下文敏感的想法,我們在分支處應該是調用函數,主函數暫停,執行調用函數,調用函數退出的狀態是主函數重新開始的狀態。基于這一理論,我們定義一個函數,當出現分支時繼續調用自身函數,未出現分支時,如果依舊是過程函數,還是調用自身,如果是eval輸出函數 ,那么結束并輸出pop鏈
除此之外,不少師傅在比賽時,選擇從eval開始判斷,先尋找不覆蓋變量的eval,再往上找。是使用了逆流分析的方法, 在之前的文章里,我們提到逆流分析一般是多輸入單輸出的情況,為什么這里單輸入多輸出會使用逆流分析呢,主要是只需要找到一條鏈,多輸出轉換成了單輸出,把這道題變成了單輸入單輸出的問題。當然,這并不意味著多輸出的程序不能用逆流分析的方法。只是缺少了一些過程間分析的內容。
編寫過程
類,函數,變量
函數有過程的函數和結束的eval函數
變量就是每一次要賦值的類名
這一個程序主要是跳轉比較多,每一個具體執行沒有太多需要注意的安全問題,要結合過程間分析編寫代碼。
我們之前提到過程間分析需要記錄跳轉前跳轉后狀態。在這里主要是記錄上一個類和下一個類是哪個的問題。這個時候想到樹結構能夠記錄上下同時滿足多個分支。
在眾多樹結構中,我們想到結構歸納法幫助我們簡化邏輯,在這個程序中最小單元是函數,我們用函數當作樹狀圖的元素。我們記錄每一個函數屬于哪個類便于變量賦值,把函數是eval的當作終點。
對于一個形如
class Un9h32{ public $msePmc1; public function NndvxI($HeqXE){ for($i = 0; $i < 35; $i ++){ $aKMhTe= $HeqXE; } if(method_exists($this->msePmc1, 'T0bEwD')) $this->msePmc1->T0bEwD($HeqXE); if(method_exists($this->msePmc1, 'CdFIxA')) $this->msePmc1->CdFIxA($HeqXE);
}public function gQLRqZ($cMmLT){ eval($cMmLT);
}
}
的類,我們需要記錄類名,函數,函數指向的函數/eval
1.我們要讓python讀取class后面的內容直到{ 記錄為類名
public $直到 ; 記錄為變量名
public function 直到 ( 記錄為函數名
$this->xxx-> 直到( 記錄為下一個函數名
封裝成一個以函數名為主鍵的表,
先是一個類的封裝
因為不會字符匹配所以僅僅通過位置獲取,通用性差,后面再修改
代碼如下
#coding=utf-8import os, sys, re
i = 1j = 1
class func: class_name = '' #類名 cur_function = '' #當前函數名 next_function = [] #下一個函數名 var = '' #變量名 bool_cur = False #是否是eval bool_next = False #下一個函數是否多個
with open('asd.php') as file: for content in file: if 'class' in content: class_name = content[6:12] if 'public $' in content: var_name = content[12:19] if 'public function' in content: f = func() f.cur_function = content[20:26] f.class_name = class_name f.var = var_name i = i + 1 j = 1 if 'eval' in content: f.bool_cur = True if 'method_exists' in content: f.next_function.append(content[62:68]) if j>1 : f.bool_next = True j = j + 1print(f.class_name)print(f.cur_function)print(f.var)print(f.bool_next)print(f.bool_cur)print(f.next_function[1])
拓展到多個類
由于樹狀結構本人不會,所以用寫一個文件的方式構造樹結構
#coding=utf-8import os, sys, re
i = 1j = 1m = 1
class func: class_name = '' #類名 cur_function = '' #當前函數名 next_function = [] #下一個函數名 var = '' #變量名 bool_cur = False #是否是eval bool_next = False #下一個函數是否多個
w_file=open('tree.txt','w')
#讀取記錄
with open('asd.php') as file: for content in file: if 'class' in content: class_name = content[6:12] if 'public $' in content: var_name = content[12:19] if 'public function' in content: if i > 1 : w_file.write(f.cur_function) w_file.write(' class_name ') w_file.write(f.class_name) while j > m: w_file.write(' f ') w_file.write(f.next_function[m-1]) m = m + 1
w_file.write(' var ') w_file.write(f.var) w_file.write(' bool_cur ') w_file.write(str(f.bool_cur)) w_file.write(' bool_next ') w_file.write(str(f.bool_next)) w_file.write('')
f = func() f.cur_function = content[20:26] f.class_name = class_name f.var = var_name i = i + 1
if 'eval' in content: f.bool_cur = True if 'method_exists' in content: f.next_function.append(content[62:68]) if j>1 : f.bool_next = True j = j + 1
處理數據,使數據成為鏈狀結構。
定義函數
#處理數據
def paixu(content,keyword): pre_content = content + keyword +'-->' position = tree_content.find(keyword) key_position = tree_content.find('f ',position) keyword = tree_content[key_position+2:key_position+8] post_next = tree_content.find('bool_next',position) num = tree_content[post_next+10:post_next+11] num = int(num) while num >= 1: paixu(pre_content,keyword) key_position = tree_content.find('f ',key_position+1) keyword = tree_content[key_position+2:key_position+8] num = num - 1 post_cur = tree_content.find('bool_cur',position) current = tree_content[post_cur+9:post_cur+10] current = int(current) print(pre_content) if current == 1: w_file.write(pre_content+'') print(pre_content+'')
但是python遞歸深度有限制,同時可能會導致棧溢出
僅僅針對這一道題,我們限制鏈的長度,先找到一條能夠到eval的鏈
t = 0
#處理數據
def paixu(content,keyword,t): pre_content = content + keyword +'-->' if t > 30: print(pre_content) r_file.write(pre_content+'') return 0
position = tree_content.find(keyword)key_position = tree_content.find('f ',position)keyword = tree_content[key_position+2:key_position+8] post_next = tree_content.find('bool_next',position)num = tree_content[post_next+10:post_next+11]num = int(num)while num >= 1: t = t + 1 paixu(pre_content,keyword,t) key_position = tree_content.find('f ',key_position+1) keyword = tree_content[key_position+2:key_position+8] num = num - 1post_cur = tree_content.find('bool_cur',position)current = tree_content[post_cur+9:post_cur+10]current = int(current) if current == 1: print(pre_content) r_file.write('') r_file.write(pre_content+'') r_file.write('') return 0
3.從入口開始找路徑
#coding=utf-8import os, sys, re
sys.setrecursionlimit(3000)
class func: class_name = '' #類名 cur_function = '' #當前函數名 next_function = [] #下一個函數名 var = '' #變量名 bool_cur = 0 #是否是eval bool_next = 0 #下一個函數是否多個
#讀取記錄 def read(): i = 1 j = 0 m = 1 k = 0 w_file=open('tree.txt','w') with open('class.php') as file: for content in file: if 'class' in content: class_name = content[6:12] if 'public $' in content: var_name = content[12:19] if 'public function' in content: if i > 1 : w_file.write('cur ') w_file.write(f.cur_function) w_file.write(' class_name ') w_file.write(f.class_name) while j >= m: w_file.write(' f ') w_file.write(f.next_function[m-1]) m = m + 1
w_file.write(' var ') w_file.write(f.var) w_file.write(' bool_cur ') w_file.write(str(f.bool_cur)) w_file.write(' bool_next ') w_file.write(str(f.bool_next)) w_file.write('')
k = 0 f = func() f.cur_function = content[20:26] f.class_name = class_name f.var = var_name i = i + 1 if 'eval' in content: f.bool_cur = 1 if 'method_exists' in content: f.next_function.append(content[62:68]) j = j + 1 k = k + 1 f.bool_next = k else: if '$this->' in content: next_position = content.find('$this->') true = content.find('->',next_position+7) if true != -1: f.next_function.append(content[next_position+16:next_position+22]) j = j + 1 k = k + 1 f.bool_next = k
t = 0
#處理數據
def paixu(content,keyword,t): position = tree_content.find('cur '+keyword) pre_content = content + keyword +'-->' post_cur = tree_content.find('bool_cur',position) current = tree_content[post_cur+9:post_cur+10] current = int(current) if current == 1: print('yes '+pre_content) r_file.write('yes ') r_file.write(pre_content+'') r_file.write('') return 0 if t > 30: print('no '+pre_content) r_file.write(pre_content+'no') return 0
key_position = tree_content.find('f ',position)keyword = tree_content[key_position+2:key_position+8] post_next = tree_content.find('bool_next',position)num = tree_content[post_next+10:post_next+11]num = int(num)while num >= 1: t = t + 1 paixu(pre_content,keyword,t) key_position = tree_content.find('f ',key_position+1) keyword = tree_content[key_position+2:key_position+8] num = num - 1 t = t - 1
content = ''
\#read()
r_file=open('result.txt','w')with open('tree.txt') as w_file : keyword = 'qZ5gIM' tree_content = w_file.read() position = tree_content.find(keyword) content = keyword + '-->' key_position = tree_content.find('f ',position) keyword = tree_content[key_position+2:key_position+8] post_next = tree_content.find('bool_next',position) num = tree_content[post_next+10:post_next+11] num = int(num) while num >= 1: t = t + 1 paixu(content,keyword,t) key_position = tree_content.find('f ',key_position+1) keyword = tree_content[key_position+2:key_position+8] num = num - 1
4.驗證可行
要把有覆蓋變量的eval去除掉
我們將read 讀取eval 的語句改成
if ‘eval’ in content:f.bool_cur = 1if ‘=\’’ in content[:-1]:f.bool_cur = -1
到這一步已經可以完成pop鏈查找
但是我比較好奇是不是有一條沒有拼接的鏈
所以繼續加限制
#coding=utf-8import os, sys, re ,linecache
sys.setrecursionlimit(3000)
class func: class_name = '' #類名 cur_function = '' #當前函數名 next_function = [] #下一個函數名 var = '' #變量名 bool_cur = 0 #是否是eval bool_next = 0 #下一個函數是否多個 token = 0
#讀取記錄 def read(): token = '' i = 1 j = 0 m = 1 k = 0 last_last_last_content = '' last_last_content = '' last_content = '' now_content = '' w_file=open('tree.txt','w') with open('class.php') as file: for content in file: last_last_last_content = last_last_content last_last_content = last_content last_content = now_content now_content = content if 'class' in content: class_name = content[6:12] if 'public $' in content: var_name = content[12:19] if 'public function' in content: if i > 1 : w_file.write('cur ') w_file.write(f.cur_function) w_file.write(' class_name ') w_file.write(f.class_name) while j >= m: w_file.write(' f ') w_file.write(f.next_function[m-1]) m = m + 1
w_file.write(' var ') w_file.write(f.var) w_file.write(' bool_cur ') w_file.write(str(f.bool_cur)) w_file.write(' bool_next ') w_file.write(str(f.bool_next)) w_file.write(' token ') w_file.write(str(f.token)) w_file.write('')
k = 0 f = func() f.cur_function = content[20:26] token = '' f.class_name = class_name f.var = var_name i = i + 1
if 'eval' in content: f.bool_cur = 1 if '=\'' in last_content: f.bool_cur = 2 if '.\'' in last_last_content: num_content = last_last_last_content if '>' in last_last_last_content: num1_before_position = num_content.find('if(') num1_after_position = num_content.find('>') num2_after_position = num_content.find(')') num1 = num_content[num1_before_position+3:num1_after_position] num1 = int(num1) num2 = num_content[num1_after_position+1:num2_after_position] num2 = int(num2) if num1 > num2: f.bool_cur = 2
if 'method_exists' in content: f.next_function.append(content[62:68]) j = j + 1 k = k + 1 f.bool_next = k if '=\'' in last_content: f.token = 1 if '.\'' in last_last_content: num_content = last_last_last_content if '>' in last_last_last_content: num1_before_position = num_content.find('if(') num1_after_position = num_content.find('>') num2_after_position = num_content.find(')') num1 = num_content[num1_before_position+3:num1_after_position] num1 = int(num1) num2 = num_content[num1_after_position+1:num2_after_position] num2 = int(num2) if num1 > num2: f.token = 1 else: if '$this->' in content: next_position = content.find('$this->') true = content.find('->',next_position+7) if true != -1: f.next_function.append(content[next_position+16:next_position+22]) j = j + 1 k = k + 1 f.bool_next = k if '=\'' in last_content: f.token = 1 if '.\'' in last_last_content: num_content = last_last_last_content if '>' in last_last_last_content: num1_before_position = num_content.find('if(') num1_after_position = num_content.find('>') num2_after_position = num_content.find(')') num1 = num_content[num1_before_position+3:num1_after_position] num1 = int(num1) num2 = num_content[num1_after_position+1:num2_after_position] num2 = int(num2) if num1 > num2: f.token = 1
t = 0
#處理數據
def paixu(content,keyword,t): position = tree_content.find('cur '+keyword) token_position = tree_content.find('token ',position) token = tree_content[token_position+6:token_position+7] token = int(token) if token == 1: return 0 pre_content = content + keyword +'-->' post_cur = tree_content.find('bool_cur',position) current = tree_content[post_cur+9:post_cur+10] current = int(current) if current == 2: return 0 if current == 1: print('yes '+pre_content) r_file.write('yes ') r_file.write(pre_content+'') r_file.write('') return 0 if t > 30: print('no '+pre_content) r_file.write(pre_content+'no') return 0
key_position = tree_content.find('f ',position) keyword = tree_content[key_position+2:key_position+8] post_next = tree_content.find('bool_next',position) num = tree_content[post_next+10:post_next+11] num = int(num) while num >= 1: t = t + 1 paixu(pre_content,keyword,t) key_position = tree_content.find('f ',key_position+1) keyword = tree_content[key_position+2:key_position+8] num = num - 1 t = t - 1
content = ''
# read()
r_file=open('result.txt','w')with open('tree.txt') as w_file : keyword = 'qZ5gIM' tree_content = w_file.read() position = tree_content.find(keyword) content = keyword + '-->' key_position = tree_content.find('f ',position) keyword = tree_content[key_position+2:key_position+8] post_next = tree_content.find('bool_next',position) num = tree_content[post_next+10:post_next+11] num = int(num) while num >= 1: t = t + 1 paixu(content,keyword,t) key_position = tree_content.find('f ',key_position+1) keyword = tree_content[key_position+2:key_position+8] num = num - 1
我將所有有拼接的數據全部過濾,最后結果是沒有,可能是我代碼還有問題,也有可能本身就是希望過濾拼接字符來實現pop鏈
出題人exp
接下來我們看看一下出題人的exp,筆者學歷尚淺,只是粗淺看看而已。
大家可以去看https://www.anquanke.com/post/id/244153,這是出題人寫的出題思路,exp的鏈接也在文章中
main.php
get_ast是獲取抽象代碼樹
deal_class是處理類的函數 如果是聲明變量的類名當作變量處理,如果是方法的類名當作函數
get_func_call_map是獲取函數調用關系,包括調用和被調用
get_func_tag是判斷函數是否是敏感函數
get_CFG 處理上下文敏感的函數
function_node查找函數所在節點
get_path是遍歷路徑
deal_block處理代碼塊,判斷其中變量是否被污染
run_CFG 模擬跳轉流程
run_path模擬代碼執行過程
main函數流程:
先看有沒有危險函數,對危險函數的參數判斷有沒有被污染,然后遍歷路徑看看有沒有調用可控參數的危險函數的路徑。
config.php
config.php是對一些輸入輸出進行分類,標記用途和是否可能存在安全問題。同時對某些安全問題所需要用到的方法進行整理。
class.php
class.php是對類進行定義:
func_call_info 是關于調用函數的語句的類,記錄了調用函數的語句的標簽
func_path_info 記錄執行完整程序所調用的全部函數和變量
func_call_map既記錄函數調用關系又標記函數是否敏感
relation是記錄調用關系和調用條件的類
CFG_class 是記錄上下文數據的類
function.php是處理函數參數是否是用戶可控的
visiter.php是各種情況的遍歷器,記錄進入節點前狀態,退出節點時狀態,將進入節點前狀態轉換成進入節點的狀態,將退出節點時狀態轉換成原進程繼續的狀態。
在我的理解里,visiter的各種定義應該都可以歸納成下面的式子
init(p) = l_n
final(p) = {l_x}
blocks(p) = {is^l_n,end^l_x} ∪ blocks(S)
labels(p) = {l_n,l_x} ∪ labels(S)
flow(p) = {(l_n,init(S))} ∪ flow(S) ∪ {(l,l_x) | l ∈ final(S) }
也就是過程間分析的理論原型
語義分析部分應該是使用了基于方法的分析,將特殊結構單獨分析,在最后組合起來。
其他部分不妄加評論,以上內容如有錯誤,歡迎指點。exp分析如和原作者有沖突,一切以原作者為準。
總結
我的代碼主要是用了基于語句的分析,就是遍歷所有情況,每次遍歷判斷是否危險,出題人的代碼基于方法,更加高級。
其他的關于過程間分析的上下文敏感原理是一樣的,就是我的代碼水平更差,記錄方式沒有通用性。
對于危險函數或者污染參數肯定是出題人的代碼更加完善。我的代碼沒有通用性。
希望這篇文章能夠幫助各位理解之前的程序分析理論。筆者水平有限,如有錯誤,歡迎指點。