HanDs
NO.2

[7月漏洞公开] 农业银行App端脚本/资源解密分析详细过程 





学习中请遵循国家相关法律法规,黑客不作恶。没有网络安全就没有国家安全

本站需要登陆后才能查看

详细说明:

农业银行app程序中xhtml、xml、lua等脚本/资源全部加密,但是由于key明文写死在dex中,可以解密所有的加密资源和脚本,导致大量信息泄露。

漏洞证明:

农业银行xhtml、xml、lua等脚本被加密

1.png



assets目录下部分xhtml文件

1.png



xhtml被加密

脚本解密流程如下各图所示:

1.png



readResFile调用readLoadcalFile

1.png



readLoacalFile调用getLocalFileInfo

1.png



getLocalFileInfo调用readResource

1.png



readResource调用readOnlineResource和readOfflineResource

1.png



readOnlineResource调用decrypt函数进行解密

1.png



decrypt调用initCipher

1.png



调用AES算法进行解密

1.png



AES算法的key和IV以明文方式写死在dex中

知道算法和key之后,我们可以批量解密assets目录下所有的加密脚本

1.png



批量解密assets目录下的所有加密脚本

以下为某个lua解密后的部分脚本信息

code 区域
--[[
@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;
end
end;

--[[
@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等相关重要信息不要以明文方式写在程序中,否则很容易被全部解密。


学习中请遵守法律法规,本网站内容均来自于互联网,本网站不负担法律责任
农业 银行 A pp
#1楼
发帖时间:2016-7-9   |   查看数:0   |   回复数:0
游客组