史萊姆論壇

返回   史萊姆論壇 > 教學文件資料庫 > 資訊系統安全備援防護技術文件
忘記密碼?
論壇說明 標記討論區已讀

歡迎您來到『史萊姆論壇』 ^___^

您目前正以訪客的身份瀏覽本論壇,訪客所擁有的權限將受到限制,您可以瀏覽本論壇大部份的版區與文章,但您將無法參與任何討論或是使用私人訊息與其他會員交流。若您希望擁有完整的使用權限,請註冊成為我們的一份子,註冊的程序十分簡單、快速,而且最重要的是--註冊是完全免費的!

請點擊這裡:『註冊成為我們的一份子!』

Google 提供的廣告


 
 
主題工具 顯示模式
舊 2006-01-19, 12:51 AM   #1
psac
榮譽會員
 
psac 的頭像
榮譽勳章
UID - 3662
在線等級: 級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時級別:30 | 在線時長:1048小時 | 升級還需:37小時
註冊日期: 2002-12-07
住址: 木柵市立動物園
文章: 17381
現金: 5253 金幣
資產: 33853 金幣
預設 軟體保護技術

軟體保護技術

1、序列號方式

(1)序列號保護機制

  數學算法一項都是密碼加密的核心,但在一般的軟體加密中,它似乎並不太為人們關心,因為大多數時候軟體加密本身實現的都是一種編程的技巧。但近幾年來隨著序列號加密程序的普及,數學算法在軟體加密中的比重似乎是越來越大了。
  我們先來看看在網路上大行其道的序列號加密的工作原理。當用戶從網路上下載某個shareware——共享軟體後,一般都有使用時間上的限制,當過了共享軟體的試用期後,你必須到這個軟體的公司去註冊後方能繼續使用。註冊程序一般是用戶把自己的私人訊息(一般主要指名字)連同信用卡號碼告訴給軟體公司,軟體公司會根據用戶的訊息計算出一個序列碼,在用戶得到這個序列碼後,按照註冊需要的步驟在軟體中輸入註冊訊息和註冊碼,其註冊訊息的合法性由軟體驗證通過後,軟體就會取消掉本身的各種限制,這種加密實現起來比較簡單,不需要額外的成本,用戶購買也非常方便,在網際網路上的軟體80%都是以這種方式來保護的。
  我們注意到軟體驗證序列號的合法性程序,其實就是驗證用戶名和序列號之間的換算關係是否正確的程序。其驗證最基本的有兩種,一種是按用戶輸入的姓名來產生註冊碼,再同用戶輸入的註冊碼比較,公式表示如下:
  序列號 = F(用戶名)
  但這種方法等於在用戶軟體中再現了軟體公司產生註冊碼的程序,實際上是非常不安全的,不論其換算程序多麼複雜,解密者只需把你的換算程序從程序中抽取出來就可以編製一個通用的註冊程序。

  另外一種是通過註冊碼來驗證用戶名的正確性,公式表示如下:
  用戶名稱 = F逆(序列號) (如ACDSEE,小樓注)
  這其實是軟體公司註冊碼計算程序的反算法,如果正向算法與反向算法不是對稱算法的話,對於解密者來說,的確有些困難,但這種算法相當不好設計。

  於是有人考慮到一下的算法:
  F1(用戶名稱) = F2(序列號)
  F1、F2是兩種完全不同的的算法,但用戶名通過F1算法的計算出的特徵字等於序列號通過F2算法計算出的特徵字,這種算法在設計上比較簡單,保密性相對以上兩種算法也要好的多。如果能夠把F1、F2算法設計成不可逆算法的話,保密性相當的好;可一旦解密者找到其中之一的反算法的話,這種算法就不安全了。一元算法的設計看來再如何努力也很難有太大的突破,那麼二元呢?

  特定值 = F(用戶名,序列號)
  這個算法看上去相當不錯,用戶名稱與序列號之間的關係不再那麼清晰了,但同時也失去了用戶名於序列號的一一對應關係,軟體開發者必須自己維護用戶名稱與序列號之間的唯一性,但這似乎不是難以辦到的事,建個資料庫就好了。當然你也可以根據這一思法把用戶名稱和序列號分為幾個部分來構造多元的算法。
  特定值 = F(用戶名1,用戶名2,...序列號1,序列號2...)

  現有的序列號加密算法大多是軟體開發者自行設計的,大部分相當簡單。而且有些算法作者雖然下了很大的功夫,效果卻往往得不到它所希望的結果。其實現在有很多現成的加密算法可以用,如RSADES,MD4,MD5,只不過這些算法是為了加密密文或密碼用的,於序列號加密多少有些不同。我在這裡試舉一例,希望有拋磚引玉的作用:
  1、在軟體程序中有一段加密過的密文S
  2、密鑰 = F(用戶名、序列號) 用上面的二元算法得到密鑰
  3、明文D = F-DES(密文S、密鑰) 用得到的密鑰來解密密文得到明文D
  4、CRC = F-CRC(明文D) 對得到的明文套用各種CRC統計
  5、檢查CRC是否正確。最好多設計幾種CRC算法,檢查多個CRC結果是否都正確
  用這種方法,在沒有一個已知正確的序列號情況下是永遠推算不出正確的序列號的。

(2)如何攻擊序列號保護

  要找到序列號,或者修改掉判斷序列號之後的跳轉指令,最重要的是要利用各種工具定位判斷序列號的程式碼段。這些常用的API包括GetDlgItemInt, GetDlgItemTextA, GetTabbedTextExtentA, GetWindowTextA, Hmemcpy (僅僅Windows 9x), lstrcmp, lstrlen, memcpy (限於NT/2000)。

1)資料約束性的秘訣
  這個概念是+ORC提出的,只限於用明文比較註冊碼的那種保護方式。在大多數序列號保護的程序中,那個真正的、正確的註冊碼或密碼(Password)會於某個時刻出現在記憶體中,當然它出現的位置是不定的,但多數情況下它會在一個範圍之內,即存放用戶輸入序列號的記憶體位址±0X90字元的地方。這是由於加密者所用工具內部的一個Windows資料傳輸的約束條件決定的。

2)Hmemcpy函數(俗稱萬能斷點)
  函數Hmemcpy是Windows9x系統的內部函數,位於KERNEL32.DLL中,它的作用是將記憶體中的一塊資料拷貝到另一個地方。由於Windows9x系統頻繁使用該函數處理各種字串,因此用它作為斷點很實用,它是Windows9x平台最常用的斷點。在Windows NT/2K中沒有這個斷點,因為其內核和Windows9x完全不同。

3)S指令
  由於S指令忽略不在記憶體中的頁面,因此你可以使用32位平面位址資料段描述符30h在整個4GB(0~FFFFFFFFh )空間搜尋,一般用在Windows9x下面。具體步驟為:先輸入姓名或假的序列號(如: 78787878),按Ctrl+D切換到SoftICE下,下搜尋指令:
  s 30:0 L ffffffff '78787878'
  會搜尋出位址:ss:ssssssss(這些位址可能不止一個),然後用bpm斷點監視搜尋到的假註冊碼,跟蹤一下程序如何處理輸入的序列號,就有可能找到正確的序列號。

4)利用消息斷點
  在處理字串方面可以利用消息斷點WM_GETTEXT和WM_COMMAND。前者用來讀取某個控件中的文本,比如拷貝編輯視窗中的序列號到程序提供的一個緩衝區裡;後者則是用來通知某個控件的父視窗的,比如當輸入序列號之後點擊OK按鈕,則該按鈕的父視窗將收到一個WM_COMMAND消息,以表明該按鈕被點擊。
  BMSG xxxx WM_GETTEXT (攔截序列號)
  BMSG xxxx WM_COMMAND (攔截OK按鈕)
  可以用SoftICE提供的HWND指令獲得視窗關鍵句的訊息,也可以利用Visual Studio中的Spy++實用工具得到相應視窗的關鍵句值,然後用BMSG設斷點攔截。例:
  BMSG 0129 WM_COMMAND
2、警告(NAG)視窗

  Nag的本義是煩人的意思。Nag視窗是軟體設計者用來不時提醒用戶購買正式版本的視窗。軟體設計者可能認為當用戶受不了試用版中的這些煩人的視窗時就會考慮購買正式版本。它可能會在程序啟動或結束時彈出來,或者在軟體執行的某個時刻隨機或定時地彈出來,確實比較煩人。

  去除警告視窗常用的三種方法是:修改程序的資源、靜態分析,動態分析。
  去除警告視窗用資源修改工具是個不錯的方法,可以將可執行文件中的警告視窗的內容改成透明、不可見,這樣就變相去除了警告視窗。
  如果是動態跟蹤偵錯,只需找到新增此視窗的程式碼,跳過即可。常用的顯示視窗的函數有MessageBoxA、MessageBoxExA、MessageBeep 、DialogBoxParamA 、ShowWindow、CreateWindowExA等。然而某些警告視窗用這些斷點不管用,就可試試利用消息設斷點,一般都應能攔截下來。


例:利用消息斷點攔截警告視窗:

  切換到SOFTICE下指令: HWND
  應看到如下的類似訊息:

Window-Handle hQueue SZ QOwner Class-Name Window-Procedure
0080 (0) 2057 32 MSGSVR32 #32711 (switch_win) 17EF:00004B6E
0084 (1) 2057 32 EXPLORER shell_trayWnd 1487:0000016C
... ... ... ... ... ...

  在這些列表中搜尋相關應用程式的視窗關鍵句。如果NAG視窗上有OK按鈕,在class name搜尋「button」。如果NAG視窗上什麼都沒有,那可試驗找出正確的關鍵句。關鍵句列表可能非常長,但通常NAG視窗的關鍵句一般在列表的前面。

註:在這裡推薦用SMU Winspector工具協助破解NAG.它能顯示你所需要的訊息:Window-Handle, Window-Class Name, Window-Text, Parent Window-Handle, Parent-Window Class Name, Parent Window-Text, Module ...

  一但找到NAG視窗的關鍵句,套用BMSG指令在Windows的消息上下斷點。現在假設NAG視窗有OK按鈕,你己找到正確的關鍵句(handle),這時下指令:

  BMSG 0084 WM_DESTROY

   0084是NAG視窗的關鍵句(handle)。這條指令是NAG視窗從螢幕上消失時,SoftICE將中斷。此時將深入到一些不認識的API函數,可按F12返回程序。需要指出,跟蹤的目的是發現NAG視窗在何處啟始化(在返回的CALL用設斷)。NAG視窗大多用Created/Destroyed類似的CALL,因此如發現這些,就可按需要跟蹤下去。

3、時間限制

(1) 定時器

  有些程序的試用版每次執行都有時間限制,例如執行10分鍾或20分鍾就停止工作,必須重新執行該程序才能正常工作。這些程序裡面自然有個定時器來統計程序執行的時間。

1)使用Settimer()

  常用的計數器是函數Settimer(),使用這個函數新增的定時器可以發出消息VM_TIMER,或者在定時期滿時使用一個回調函數。 使用這個函數會使時間延時,精度不高。

2)使用timeSetEvent()

  給Windows驅動程式最精確的週期性通知是由Windows的多媒體服務timeSetEvent()提供的。它的時間可以精確到1毫秒。

3)使用VXD

  可以使用VMM的Set_Global_time_Out()服務來迫使回調函數的幾個毫秒再執行,這就創造了一個「只有一次」的定時器。VXD可以在回調中再次使用Set_Global_time_Out()來開始下一個定時器,這樣提供了一個連續執行的定時器了。

4)其它

  GetTickCount():精度不高;
  timeGetTime(): 可以以毫秒級返回windows開始後的時間。

(2)時間限制

  一般這類保護的軟體都有時間上的限制,如試用30天等,當過了共享軟體的試用期後,就不予執行,只有向軟體作者付費註冊之後才能得到一個無時間限制的註冊版本。

  這種檔案類型程序很多,讓你有10天、20天、30天等,它們在安裝時,在你的系統某處做上時間標記,每次執行時用當前系統時間和安裝時的時間比較,判斷你還否能使用。
  如最典型的30天限制的一種情況:
  mov ecx,1E ; 把1E (30天 十進制) 放入 ecx
  mov eax,[esp+10] ; 把用過天數放到eax
  cmp eax,ecx ; 在此比較
  jl ...
  如碰到這種情況,只需把"mov eax,[esp+10]"改成"mov eax,1" 。

  要記住當前年份、月份的十六進制的一些表示方法,如:2000年的十六進制是07D0,然後用W32DASM反彙編你的程序,用搜尋字元串的方法找D007(在機器碼中位置顛倒了一下)或其它類似時間的數位,有可能會找到有價值的線索。你別小看這種方法,對那些沒怎麼防範的程序,此招很有效。
  如:一程序限定在2000年使用,可能有如下一程式碼:
  :00037805 817C2404D0070000 cmp dword ptr [esp+04], 000007D0 比較是否在2000年。

(3)與時間相關函數

1、GetSystemTime 得當前系統時間

說明:
在一個SYSTEMTIME中載入當前系統時間,這個時間採用的是「協同世界時間」(即UTC,也叫做GMT)格式。
VOID GetSystemTime(
LPSYSTEMTIME lpSystemTime // SYSTEMTIME,隨同當前時間載入的結構
);


2、GetLocalTime 得當前本機時間

VOID GetLocalTime(

LPSYSTEMTIME lpSystemTime // SYSTEMTIME,用於安裝載入本機時間的結構
);


3、SystemTimeToFileTime 根據一個FILETIME結構的內容,載入一個SYSTEMTIME結構



BOOL SystemTimeToFileTime(

CONST SYSTEMTIME * lpst, // SYSTEMTIME,包含了系統時間訊息的一個結構
LPFILETIME lpft // FILETIME,用於安裝載入文件時間的一個結構
);
返回值 :非零表示成功,零表示失敗。


4、SetTimer 新增一定時器,在指定時間內暫停



UINT SetTimer(
HWND hwnd, // 時間訊息關鍵句
UINT idtimer, // 定時器ID 標幟符
UINT uTimeout, // 暫停時間
TIMERPROC tmprc // 處理定時程序的程序入口位址
);
4、Key File保護

  Key File(註冊文件)是一種利用文件來註冊軟體的保護方式。Key File一般是一個小文件,可以是純文本文件,也可以是包含不可顯示字元的二進制文件,其內容是一些加密過或未加密的資料,其中可能有用戶名、註冊碼等訊息。文件格式則由軟體作者自己定義。試用版軟體沒有註冊文件,當用戶向作者付費註冊之後,會收到作者寄來的註冊文件,其中可能包含用戶的個人訊息。用戶只要將該檔案放入指定的目錄,就可以讓軟體成為正式版。該檔案一般是放在軟體的安裝目錄中或系統目錄下。軟體每次啟動時,從該檔案中讀取資料,然後利用某種算法進行處理,根據處理的結果判斷是否為正確的註冊文件,如果正確則以註冊版模式來執行。

(1)破解Key File一般思法

1. 最好分析Key File的工具是十六進制工具,普通的文本編輯工具不太適合。

2. 對付這類程序,你首先建立一假的Key File文件。一般的軟體容許Key File有不同的大小和檔案名,你建立的文件內容必須易讀,跟據情況調整Key File的大小和檔案名。為什麼要易讀呢?因為目標程序從Key File中讀取資料,然後進行處理,易讀有利於你分析其運算程序。

3. Key File文件在大多數情況下,是以'*.key'形式存在的。

4. Key File檔案名可用W32DASM或十六進制工具開啟程序用搜尋字元串方式確定;

5. 讀用戶手冊(有時作者可能會提到);

6. 用Filemon 這一工具,它能既時監視系統各文件的狀態,因此執行程序時,如它去讀指定檔案名的Key File時,會在Filemon顯示Key File檔案名。一但你發現Key File檔案名,就建立一假的Key File到要被crack軟體目錄下,然後去crack。

(2)Windows下破解Key File幾個常用的函數:


函數ReadFile
作用:從文件中讀出資料
參數:其中Long,非零表示成功,零表示失敗。

BOOL ReadFile(
HANDLE hFile, // Long,文件的關鍵句
LPVOID lpBuffer, // Any,用於儲存讀入資料的一個緩衝區
DWORD nNumberOfBytesToRead, //Long,要讀入的字元數
LPDWORD lpNumberOfBytesRead, // Long,從文件中實際讀入的字元數
LPOVERLAPPED lpOverlapped // address of structure for data
);


函數CreateFileA
作用:可開啟和新增文件、管道、郵槽、通信服務、設備以及控制台


HANDLE CreateFileA(

LPCTSTR lpFileName, // String,要開啟的文件的名字
DWORD dwDesiredAccess, // 允許對設備進行讀寫訪問;
DWORD dwShareMode, // 共享模式
LPSECURITY_ATTRIBUTES lpSecurityAttributes// 指向一個SECURITY_ATTRIBUTES結構的游標,定義了文件的安全特性(如果操作系統支持的)
DWORD dwCreationDistribution, // 如何新增文件
DWORD dwFlagsAndAttributes, // file attributes
HANDLE hTemplateFile //Long,如果不為零,則指定一個文件關鍵句。新文件將從這個文件中複製 增強內容
);


函數_lopen( )
作用:以二進制模式開啟指定的文件


HFILE _lopen(

LPCSTR lpPathName, // 欲開啟文件的名字
int iReadWrite // 訪問模式和共享模式常數的一個組合
);


函數FindFirstFileA( )
作用:根據檔案名搜尋文件

HANDLE FindFirstFile(

LPCTSTR lpFileName, // 欲搜尋的檔案名。可包含萬用字元,並可包含一個路徑或相對路徑名
LPWIN32_FIND_DATA lpFindFileData // WIN32_FIND_DATA,這個結構用於安裝載入與找到的文件有關的訊息。該結構可用於後續的搜尋
);

5、功能限制的程序

  這種程序一般是DEMO版或表單中部分選項是灰色。有些DEMO版本的部分功能裡面根本就沒有。而有些程序功能全有,只要註冊後就正常了。

  你使用這些DEMO程序部分被禁止的功能時,會跳出提示項,說這是DEMO版等話,它們一般都是使用MessageBox[A] 或 DialogBox[A]等函數。你可在W32DASM反彙編它,一般能找到如下字元串:"Function Not Avaible in Demo" 或 "Command Not Avaible" 或 "Can't save in Shareware/Demo"等,這些CALL會被相應的使用,可作為你破解的一指示器。

  另外,就是表單中部分選項是灰色的不能用,一般它們是通過如下兩種函數實現的:

(1)EnableMenuItem

允許、禁止或變灰指定的表單 列項
BOOL EnableMenuItem(
HMENU hMenu, // 表單關鍵句
UINT uIDEnableItem, // 表單ID,形式為:充許,禁止,或灰
UINT uEnable //表單項目旗幟
);
Returns
在ASM程式碼形式如下:
PUSH uEnable    //uEnable=0 則表單選項允許
PUSH uIDEnableItem
PUSH hWnd
CALL [KERNEL32!EnableMenuItem]

(2)EnableWindow
允許或禁止滑鼠和鍵盤控制指定視窗和 列項(禁止時表單變灰)
BOOL EnableWindow(
HWND hWnd, // 視窗關鍵句
BOOL bEnable // 允許/禁止輸入
);
Returns
如視窗以前被禁止則返回一TRUE,否則返回 FALSE。
6、CD-check


  最簡單也最一般的光碟保護就是程序在啟動時判斷光碟中的光碟上是否存在特定的文件,如果不存在則認為用戶沒有正版光碟,拒絕執行。在程序執行的程序當中一般不再檢查光碟的存在與否。Windows下的具體實現一般是這樣的:先用GetLogicalDriveStrings( )或GetLogicalDrives( )得到系統中安裝的所有驅動器的列表,然後再用GetDriveType( )檢查每一個驅動器,如果是光碟則用CreateFileA( )或FindFirstFileA( )等函數檢查特定的文件存在與否,並可能進一步地檢查文件的內容、大小、內容等。 這種光碟檢查是比較容易被破解的,解密者只要利用上述函數設斷點找到程序啟動時檢查光碟的地方,修改判斷指令就可以跳過光碟檢查。

(1)可將遊戲(或其它程序)的光碟拿出,執行遊戲,將出現一些錯誤提示,如: Please insert the - CD, or: You need the CD to play the - . 利用這提示可在W32DASM中利用串式資料參考功能搜尋相應的程式碼進行分析。

(2)相關函數

1、GetDrivetype(a) 判斷一個磁碟機的檔案類型

UINT GetDriveType(
LPCTSTR lpRootPathName // String,包含了驅動器根目錄路徑的一個字串
);

返回值
0 驅動器不能識別
1 指定的目錄不存在
2 DriveRemoveable
3 A Fixed Disk (HardDrive)
4 Remote Drive(Network)
5 Cd-Rom驅動器
6 RamDisk

如果是普通的程序,你可將EAX由5改成3即可。

注意:有些程序可能檢測光碟根目錄相關文件,CD的卷冊也可能被檢測。

2、GetLogicalDrives 判斷系統中存在哪些邏輯磁碟機字母

這函數沒有參數

返回值
這個結構中的二進制位標誌著存在哪些驅動器。其中,位0設為1表示驅動器A:存在於系統中;位1設為1表示存在B:驅動器;以次類推

3、GetLogicalDriveStrings 獲取一個字串,其中包含了當前所有邏輯磁碟機的根驅動器路徑

DWORD GetLogicalDriveStrings(

DWORD nBufferLength, // 字串的長度
LPTSTR lpBuffer   // 用於安裝載入邏輯磁碟機名稱的字串。每個名字都用一個NULL字元分隔,在最後一個名             字後面用兩個NULL表示中止(空中止)
);

返回值
安裝載入到lpBuffer的字元數量(排除空中止字元)。如緩衝區的長度不夠,不能容下路徑,則返回值就變成要求的緩衝區大小。零表示失敗。會設定GetLastError

4、GetFileAttributesA 判斷指定文件的內容

DWORD GetFileAttributes(

LPCTSTR lpFileName //指定欲獲取內容的一個文件的名字
);


5、GetFileSize 判斷文件長度

DWORD GetFileSize(

HANDLE hFile, // 文件的關鍵句
LPDWORD lpFileSizeHigh, // 指定一個長整數,用於安裝載入一個64位文件長度的頭32位。如這個長度沒有超過               2ふ32字元,則該參數可以設為NULL(變成ByVal)
);

返回值
返回文件長度。&HFFFFFFFF表示出現錯誤。注意如lpFileSizeHigh不為NULL,且結果為&HFFFFFFFF,那麼必須使用GetLastError,判斷是否實際發生了一個錯誤,因為這是一個有效的結果

6、GetLastError 針對之前使用的api函數,用這個函數取得增強錯誤訊息

返回值
由api函數決定。請參考api32.txt文件,其中列出了一系列錯誤常數;都以ERROR_前綴起頭。常用的錯誤程式碼見下表
ERROR_INVALID_HANDLE 無效的關鍵句作為一個參數傳送
ERROR_CALL_NOT_IMPLEMENTED 在win 95下使用專為win nt設計的win32 api函數
ERROR_INVALID_PARAMETER 函數中有個參數不正確

7、ReadFile 從文件中讀出資料

具體參考KEYFILE一節。

8、其它一些CDROM訊息

中斷2F是mscdex中斷,可用bpint 2f, al=0 ah=15檢測Mmscdex是否安裝。
也可試著用文件存取設斷
__________________
http://bbsimg.qianlong.com/upload/01/08/29/68/1082968_1136014649812.gif
psac 目前離線  
送花文章: 3, 收花文章: 1631 篇, 收花: 3205 次
舊 2006-01-19, 11:45 PM   #2 (permalink)
sim
註冊會員
榮譽勳章
UID - 712
在線等級: 級別:9 | 在線時長:118小時 | 升級還需:22小時級別:9 | 在線時長:118小時 | 升級還需:22小時級別:9 | 在線時長:118小時 | 升級還需:22小時級別:9 | 在線時長:118小時 | 升級還需:22小時
註冊日期: 2002-12-06
VIP期限: 2007-04
文章: 220
精華: 0
現金: 29 金幣
資產: 329 金幣
預設

感謝..................
sim 目前離線  
送花文章: 111, 收花文章: 6 篇, 收花: 9 次
 


主題工具
顯示模式

發表規則
不可以發文
不可以回覆主題
不可以上傳附加檔案
不可以編輯您的文章

論壇啟用 BB 語法
論壇啟用 表情符號
論壇啟用 [IMG] 語法
論壇禁用 HTML 語法
Trackbacks are 禁用
Pingbacks are 禁用
Refbacks are 禁用


所有時間均為台北時間。現在的時間是 08:14 AM


Powered by vBulletin® 版本 3.6.8
版權所有 ©2000 - 2024, Jelsoft Enterprises Ltd.


SEO by vBSEO 3.6.1