2016-04-05: 细节已通知厂商并且等待厂商处理中 2016-04-08: 厂商已经确认,细节仅向厂商公开 2016-04-11: 细节向第三方安全合作伙伴开放(绿盟科技、唐朝安全巡航、无声信息) 2016-06-02: 细节向核心白帽子及相关领域专家公开 2016-06-12: 细节向普通白帽子公开 2016-06-22: 细节向实习白帽子公开 2016-07-07: 细节向公众公开
农业银行app程序中xhtml、xml、lua等脚本/资源全部加密,但是由于key明文写死在dex中,可以解密所有的加密资源和脚本,导致大量信息泄露。
农业银行xhtml、xml、lua等脚本被加密
assets目录下部分xhtml文件
xhtml被加密脚本解密流程如下各图所示:
readResFile调用readLoadcalFile
readLoacalFile调用getLocalFileInfo
getLocalFileInfo调用readResource
readResource调用readOnlineResource和readOfflineResource
readOnlineResource调用decrypt函数进行解密
decrypt调用initCipher
调用AES算法进行解密
AES算法的key和IV以明文方式写死在dex中知道算法和key之后,我们可以批量解密assets目录下所有的加密脚本
批量解密assets目录下的所有加密脚本以下为某个lua解密后的部分脚本信息
--[[ @doc 登陆密码校验 @input 控件名 @output 0 window:alert("<?cs var:title?>" .. "只允许输入数字和字母!"); 1 window:alert("<?cs var:title?>" .. "不允许只输入单一数字或字母!"); 2 window:alert("<?cs var:title?>" .. "不允许只输入连续的数字或字母!"); true 成功 测试通过]]--function public:check_login_pin(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if string.find(inputVal, "^%w+$") == nil then return 0; end; local inputString = string.lower(inputVal); local sub = string.sub(inputString, 1, 1); local gsub, _ = string.gsub(inputString, sub, ""); if gsub == "" then return 1; end; local word = "abcdefghijklmnopqrstuvwxyz"; local num = "0123456789"; local condition = string.find(word, inputString) or string.find(num, inputString) or string.find(string.reverse(word), inputString) or string.find(string.reverse(num), inputString); if condition ~= nil then return 2; end; return true;end;--[[ @doc 邮箱正则校验 @input 控件名 @output false window:alert("<?cs var:title?>" .. "格式不正确!"); true 成功 测试通过]]--function public:check_email(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if string.find(inputVal, "^%w+[%_%-%+%.%w]*%w@%w+[%_%.%w%-]*%w%.%w+[%_%.%w%-]*%w$") == nil then return false; else return true; end;end;--[[ @doc 校验数字和字母 @input 控件名 @output false window:alert("<?cs var:title?>" .. "只允许输入数字和字母!"); true 成功 测试通过]]--function public:check_number_and_word(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if string.find(inputVal, "^%w+$") == nil then return false; else return true; end;end;--[[ @doc 金额校验(不限制位数),小数点后只能有2位,整数部分不能出现0X.XX的情况 @input 控件名 @output false window:alert("<?cs var:title?>" .. "格式不正确,请重新输入!"); true 成功 测试通过]]--function public:check_money_format(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if inputVal == "" then else if string.find(inputVal, "^[1-9]%d+$") == nil and string.find(inputVal, "^%d$") == nil and string.find(inputVal, "^[1-9]%d+%.%d?%d$") == nil and string.find(inputVal, "^%d%.%d?%d$") == nil then return false; end; end; _, len = string.find(inputVal, "%d+"); return true;end;--[[ @doc 金额校验(10位) @input 控件名 @output false window:alert("<?cs var:title?>" .. "格式不正确,请重新输入!"); true 成功 测试通过]]--function public:check_amount(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if inputVal == "" then else if string.find(inputVal, "^[1-9]%d+$") == nil and string.find(inputVal, "^%d$") == nil and string.find(inputVal, "^[1-9]%d+%.%d?%d$") == nil and string.find(inputVal, "^%d%.%d?%d$") == nil then return false; end; end; _, len = string.find(inputVal, "%d+"); if len > 10 then return false; end; return true;end;--[[ @doc 金额校验(15位) @input 控件名 @output false window:alert("<?cs var:title?>" .. "输入格式不正确!"); true 成功 测试通过]]--function public:check_amount_15digits(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if inputVal == "" then else if string.find(inputVal, "^[1-9]%d+$") == nil and string.find(inputVal, "^%d$") == nil and string.find(inputVal, "^[1-9]%d+%.%d?%d$") == nil and string.find(inputVal, "^%d%.%d?%d$") == nil then return false; end; end; _, len = string.find(inputVal, "%d+"); if len > 15 then return false; end; return true;end;--[[ @doc 7位整数位金额格式判断 @input 控件名 @output false window:alert("<?cs var:title?>" .. "整数位不能多于七位,请重新输入!"); true 成功 测试通过]]--function public:check_amount_mse(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if inputVal == "" then else if string.find(inputVal, "^[1-9]%d+$") == nil and string.find(inputVal, "^%d$") == nil and string.find(inputVal, "^[1-9]%d+%.%d?%d$") == nil and string.find(inputVal, "^%d%.%d?%d$") == nil then return false; end; end; _, len = string.find(inputVal, "%d+"); if len > 7 then return false; end; return true;end;--[[ @doc 判断输入项是否为空 @input 控件名 @output false window:alert("<?cs var:title?>" .. "不能为空,请重新输入!"); true 成功 测试通过]]--function public:check_nil(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if inputVal == nil or inputVal == "" then return false; else if string.find(inputVal, "^ +$") ~= nil then return false; end; end; return true;end;--[[ @doc 校验转账 @input 控件名 @output false window:alert("<?cs var:title?>" .. "长度不能小于<?cs var:leng?>位,请重新输入!"); true 成功 测试通过]]--function public:check_min_leng(elemName,length) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if length > string.len(inputVal) then return false; end; return true;end;--[[ @do 校验长度 @input 控件名,长度 @output false window:alert("<?cs var:title?>" .. "长度须为<?cs var:leng?>位,请重新输入!"); true 成功 测试通过]]--function public:check_leng(elemName,length) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if length ~= string.len(inputVal) then return false; end; return true;end;--[[ @doc 拆分方法 @input 控件名 @output 返回table 测试通过]]--function public:split(szFullString, szSeparator) local nFindStartIndex = 1; local nSplitIndex = 1; local nSplitArray = {}; while true do local startLocation ,endLocation = string.find(szFullString,szSeparator); if startLocation == nil then nSplitArray[nSplitIndex] = szFullString; break; end; nSplitArray[nSplitIndex] = string.sub(szFullString,1,startLocation-1); nSplitIndex = nSplitIndex + 1; szFullString = string.sub(szFullString,endLocation+1,string.len(szFullString)) ; end; return nSplitArray;end;--[[ @doc 判断金额是否符合格式规则 @input 控件名 @output false window:alert("<?cs var:title?>" .. "输入格式不正确!"); true 成功]]--function public:check_int_amount(elemName) local inputElem = document:getElementsByName(elemName); local inputVal =inputElem[1]:getPropertyByName("value"); if inputVal == "" then else if string.find(inputVal, "^[1-9]%d+$") == nil then return false; end; end; return true;end;--[[ 校验输入金额 ]]-- function check_valid(number) local i, num, newnum; firstLetter = string.sub(number, 1, 1); secondLetter = string.sub(number, 2, 2); if number == "" or number == "." or tonumber(number)<0.01 then window:alert("收款金额不能为空,且金额不得小于0.01元!"); validFlag="false"; return validFlag; elseif length(number) > 17 or length(number) == 17 then window:alert("输入金额长度应小于17位"); validFlag="false"; return validFlag; elseif firstLetter=="0" and secondLetter ~="." then window:alert("输入金额开头有多余的0,请重新输入"); validFlag="false"; return validFlag; end; i = string.find(number, "%."); --获取小数点位置 if i ~= nil then --存在小数点时 num = string.sub(number, i + 3, i + 3); if num ~= "" and num ~= nil then --小数点后第三位存在 window:alert("输入的金额小数位最多为两位,请重新输入"); validFlag="false"; return validFlag; else --小数点后第三位不存在 newnum = number; end; else --不存在小数点时 newnum = number; end; return string.format("%.2f", newnum); end; --[[ @doc 拋错页面 @input json数据(responsebody中数据转换后) @output 错误页面]]--function public:throw_error_page(params) local error_code = params["return"]["error_code"]; local error_msg = params["return"]["error_msg"]; globalTable["error_info"] = {error_code=error_code,error_msg=error_msg}; local page =nil; local platform = public:get_platform_info(); local resolution = get_resolution(); --local path = "name="..utility:escapeURI("xhtml/error.xhtml"); local path = "name="..utility:escapeURI("channels/error/xhtml/error.xhtml"); if page_ewp_debug then ryt:post(nil, "test_s/get_page", path.."&platform="..platform.."&resolution="..resolution, page_callback, fun_params, false); else local response = {}; local ebank_file = "error/xhtml/error.xhtml"; if file:isExist(ebank_file) then page = file:read(ebank_file, "text"); location:replace(page); end; endend;--[[ @doc 拋错页面 @input json数据(responsebody中数据转换后) @output 弹框报错]]--function public:throw_error_box(params) local error_msg = params["return"]["error_msg"]; window:alert(error_msg); public:hideLoading();end;--[[ @doc 拋错处理(处理弹框报错和页面报错)]]--function public:throw_error(params) local error_type = params["return"]["error_type"]; if error_type == "box" then public:throw_error_box(params); else public:throw_error_page(params); end;end;--[[ @doc 拋错校验 @input json数据(responsebody中数据转换后) @out true/false]]--function public:get_verfity_result(params) local error_code = params["return"]["error_code"]; if error_code == "000000" then return true; else return false; end;end;--[[ @doc 拋错校验总入口 @input json数据(responsebody中数据,未转换成json) @out true/false]]--function public:check_verfity(params) local response_obj = json:objectFromJSON(params); local verfity_result = public:get_verfity_result(response_obj); if verfity_result == false then local error_type = response_obj["return"]["error_type"]; if error_type == "other" then return true; else return false; end; else return true; end;end;--[[ @doc 密码校验 @input pwdlist 密码表 example {pass1="123123",pass2="222222"} busiCallback 回调方法 @output 在回调方法中获取密码:example function busiCallback(params) local pass1 = params["pass1"] end;]]--function public:encry_password(pwd_list,busiCallback) local modes = {}; for key,value in pairs(pwd_list) do modes[key]="01"; end; utility:passwordEncryption(pwd_list,modes,busiCallback); end;--[[ @doc 返回各级菜单方法 @input params 非必送,存在时将会定向进行菜单返回 @该方法会清除历史的缓存 @改方法一般用于结果页面的返回,或需确定返回至菜单的返回,交易内各页面返回请调用public:back_fun() @私有方法 back_channel_default()、back_channel_directional()非请勿用、勿动]]--function back_channel_default()-- if #pageGlobalTable < 2 then --未缓存完整菜单情况下出错,默认未返回上一级菜单或页面-- public:back_fun();-- else -- 大于等于二级菜单时,进行处理 history:clear(history:length()); for key,value in ipairs(pageGlobalTable) do history:add(value); --依次加入缓存的菜单 end; location:replace(pageGlobalTable[#pageGlobalTable]); --根据当前的菜单深度进行返回,如果深度为3则会返回三级菜单 public:hideLoading();-- end;end;function back_channel_directional(params) history:clear(history:length()); for key,value in ipairs(pageGlobalTable) do if key <= params then --根据返回菜单等级将对应缓存存入历史栈,同时清空超出限制的缓存 history:add(value); --依次加入缓存的菜单 else --清空超出限制的缓存 pageGlobalTable[key]=nil; end; end; location:replace(pageGlobalTable[params]); --用户定制返回的菜单等级进行返回 public:hideLoading();end;function public:back_channel(params) if params then if params > 3 then --异常处理 当前菜单最多为三层,超过三层,将会走默认方法 back_channel_default(); elseif params > #pageGlobalTable then -- 异常处理,当前菜单大于页面缓存的菜单,则调用默认方法 back_channel_default(); else back_channel_directional(params); end; else back_channel_default(); --默认返回方法 end;end;--[[ @doc 重新进入交易 例如:public:refresh_business('fund','fund_register')" ]]--function refresh_channelCallback(params) globalTable["cardList"] = params["responseBody"]; local channelId = params["channelId"]; local menuId = params["menuId"]; local path = ""; if menuId == nil or menuId=="" then path = channelId.."/xhtml/"..channelId..".xhtml"; else path = menuId.."/"..channelId.."/xhtml/"..channelId..".xhtml"; end; public:invoke_page(path,page_callback,nil);end;--[[请求交易数据]]--function refresh_channel(menuId,channelId) public:showLoading(); local id = channelId; local trancode = ""; local params = {id=id,interface_type="flag_app_s"}; public:invoke_trancode(channelId,nil,params,refresh_channelCallback,{channelId=channelId,menuId=menuId});end;function public:refresh_business(menuId,channelId) history:clear(history:length()); for key,value in ipairs(pageGlobalTable) do history:add(value); end; public:hideLoading(); refresh_channel(menuId,channelId);end;--[[ @doc 卡号展现方法 @input 卡号 别名 @output XXXX****XXXX]]--function public:show_cards_title(cardNumber) local newCardNumber = string.sub(cardNumber,1,4).."****"..string.sub(cardNumber,string.len(cardNumber)-3); return newCardNumber;end;--[[ @doc 卡号展现方法 @input 卡号 别名 @output XXXX****XXXX或者别名]]--function public:show_cards(cardNumber,cAlias) if cAlias ~=nil and cAlias ~="" then return cAlias; else return public:show_cards_title(cardNumber); end;end;--[[ @doc 账户别名展现方法 @input 别名,账号 @output 别名或账号后六位]]--function public:show_card_alias(alias, cardNo) if alias ~=nil and alias ~="" then return alias; else return public:show_last_six_num(cardNo); end;end;--[[ @doc @input 卡号或字符串 @output 后六位]]--function public:show_last_six_num(cardNo) local len = string.len(cardNo); if len <= 6 then return cardNo; else return string.sub(cardNo,len-5,len); end;end;--[[ @doc 动态口令公共方法 1.在页面定义div控件 <div class="dypwd_div" name="dypwd_div"></div> 2.需要各自交易编写各自样式 .dypwd_label .token_dyminput .dynamic_dyminput .token_label .token_dy_label .audio_key_label @input 包含 dymPwdMedium、coord、tokenVerifyType等四项内容的表]]----[[ @私有方法 @input 创建口令卡输入]]--function create_dynamic_password_challenge(challenge_info) local coord = challenge_info["coord"]; local tokenChallengeDiv = [[<label class="dypwd_label">动态口令:</label> <input class="dypwd_input" save="false" type="text" name="dypassword" style="-wap-input-format:'N';-wap-input-required:'true'" maxleng="6" minleng="6" border="0" hold="请输入六位动态密码"/> <input type="button" class="dymbutton" value="]]..coord..[["></input>]] return tokenChallengeDiv;end;--[[ @私有方法 @input 创建token口令输入]]--function create_token_challenge(challenge_info) local coord = challenge_info["coord"]; local tokenVerifyType = challenge_info["tokenVerifyType"]; local tokenChallengeDiv =[[]]; if tokenVerifyType == "PayType" then tokenChallengeDiv = [[<label class="token_info" numlines="2">请启动K令,按“付款”键,输入交易金额]]..coord..[[,再按“确认”键,获取动态口令</label>]]; elseif tokenVerifyType == "ChallengeType" then tokenChallengeDiv = [[<label class="token_info" numlines="2">请启动K令,输入]]..coord..[[,再按“确认”键,获取动态口令</label>]]; elseif tokenVerifyType == "AccType" then tokenChallengeDiv = [[<label class="token_info" numlines="2">请启动K令,输入账号后6位数字]]..coord..[[,再按“确认”键,获取动态口令</label>]]; end; tokenChallengeDiv = tokenChallengeDiv..[[<label class="token_label">动态口令:</label> <input class="token_input" save="false" type="text" name="dypassword" style="-wap-input-format:'N';-wap-input-required:'true'" maxleng="6" minleng="6" border="0" hold="请输入六位动态密码"/>]] return tokenChallengeDiv;end;--[[ @私有方法 @input 创建音频key输入]]--function create_audio_key_challenge(challenge_info) local audioKeyChallengeDiv = [[<label class="aukey_info">请连接K宝后,点击“确定”进行操作</label> <input type="hidden" name="dypassword" value="" />]] return audioKeyChallengeDiv;end;--[[ @doc 公共方法 创建动态口令公共方法]]--function public:token_challenge(challenge_info) local dymPwdMedium = challenge_info["dymPwdMedium"]; local tokenChallengeDiv = [[]]; if dymPwdMedium == "1" then tokenChallengeDiv = create_dynamic_password_challenge(challenge_info); elseif dymPwdMedium == "3" then tokenChallengeDiv = create_token_challenge(challenge_info); elseif dymPwdMedium == "5" then tokenChallengeDiv = create_audio_key_challenge(challenge_info); end; tokenChallengeDiv = [[<div class="dypwd_div" name="dypwd_div" ><img class="challange_line" src="local:comm_line_640.png"/> ]]..tokenChallengeDiv..[[<img class="challange_line_bottom" src="local:comm_line_640.png"/></div>]]; local ctrl = document:getElementsByName("dypwd_div"); if ctrl and #ctrl > 0 then ctrl[1]:setInnerHTML(tokenChallengeDiv); end;end;function public:challenge(challenge_info) local dymPwdMedium = challenge_info["dymPwdMedium"]; local challengeDiv = ""; if dymPwdMedium == "1" then challengeDiv = [[<div class="dypwd_div">]]..create_dynamic_password_challenge(challenge_info)..[[</div>]]; elseif dymPwdMedium == "3" then challengeDiv = [[<div class="token_div">]]..create_token_challenge(challenge_info)..[[</div>]]; elseif dymPwdMedium == "5" then challengeDiv = [[<div class="aukey_div">]]..create_audio_key_challenge(challenge_info)..[[</div>]]; end; challengeDiv = [[<div class="challenge_div" name="challenge_div" >]]..challengeDiv..[[</div>]]; local ctrl = document:getElementsByName("challenge_div"); if ctrl and #ctrl > 0 then ctrl[1]:setInnerHTML(challengeDiv); end;end;--[[ @doc 获取动态口令或签名 @input dymPwdMedium、tokenVerifyType、coord @example local dymPwdMedium = "5"; local tokenVerifyType = "AccType"; local coord = "12"; local challenge_info ={dymPwdMedium=dymPwdMedium,tokenVerifyType=tokenVerifyType,coord=coord}; @output dypassword or signdata]]--function public:get_coord(challenge_info,businessCallback) local dypassword = ""; if challenge_info["dymPwdMedium"] ~= "5" then local coord = public:get_property("dypassword","value"); if coord == nil or coord == "" or coord == " " then window:alert("请输入动态口令"); public:hideLoading(); return; elseif string.len(coord) < 6 then window:alert("动态口令应该为6位数字"); public:hideLoading(); return; end; businessCallback(tostring(coord)); else audio_signData(challenge_info,businessCallback); end;end;--[[ 私有方法 @doc 音频key签名]]--function audio_signData(challenge_info,businessCallback) local coord = challenge_info["coord"]; local signedData = ""; if coord ~= nil and coord ~= "" and coord ~= " " then signedData = audiokey:sign(coord,businessCallback); else window:alert("key宝数据异常,请进入安全设备管理交易查询后,重新进行交易。") public:hideLoading(); return; end;end;function public:get_ota_version() local version = system:getInfo("appversion"); local agent = public:get_platform_info(); local ota_version ; if agent == "iphone" then ota_version = "IP-UMP-"..tostring(version).."-000000" ; else ota_version = "AD-UMP-"..tostring(version).."-080901" ; end; return ota_version;end;--[[ this function use to turn string to table input like string_to_table(":","!","erlang:456!lua:123!cs:789") output will be {erlang = "456",lua = "123" ,cs = "789"}--]]function public:string_to_table(TagKey,TagElment,String) local Table = {}; while String ~= "" do Location = string.find(String, TagElment); if Location ~= nil then Element = string.sub(String, 1, Location-1); String = string.sub(String, Location+1, string.len(String)); else Element = String; String = ""; end; KeyLocaltion = string.find(Element, TagKey); Key = string.sub(Element,1, KeyLocaltion-1); Value = string.sub(Element, KeyLocaltion+1, string.len(Element)); Table[Key] = Value; end; return Table;end;--[[ 将字符串插入table]]--function public:string_insert_table(TagKey,TagElment,String) local Table = {}; while String ~= "" do Location = string.find(String, TagElment); if Location ~= nil then Element = string.sub(String, 1, Location-1); String = string.sub(String, Location+1, string.len(String)); else Element = String; String = ""; end; KeyLocaltion = string.find(Element, TagKey); Key = string.sub(Element,1, KeyLocaltion-1); Value = string.sub(Element, KeyLocaltion+1, string.len(Element)); table.insert(Table, Value); end; return Table;end;--[[ this function use to turn table to string input like string_to_table(":", "!", {erlang = "456",lua = "123" ,cs = "789"}) output will be "erlang:456!lua:123!cs:789"--]]function public:table_to_string(TagKey,TagElment,Table) local String = ""; for Key,Value in pairs(Table) do String = String .. Key..TagKey..Value..TagElment; end; return String;end;--[[cTranCode格式转换]]--function cTranCode_to_label(cTranCode) if cTranCode == "" then return cTranCode; else local code = string.sub(cTranCode, 6, 7); if code == "1" then return "固定金额类收费"; elseif code == "2" then return "浮动金额类收费"; elseif code == "3" then return "增值服务类收费"; else return "其它"; end; end;end;
key等相关重要信息不要以明文方式写在程序中,否则很容易被全部解密。
危害等级:高
漏洞Rank:12
确认时间:2016-04-08 09:47
CNVD未复现所述情况,已经转由CNCERT直接通报给对应银行集团公司,由其后续协调网站管理部门处置.
暂无