夫天地者,万物之逆旅;光阴者,百代之过客。

0%

一年前写过一个自动登录的UI,不过写得特别烂。

最近由于群晖需要每天都自启并自动联网,所以重新写了这个小脚本。

由于是在服务器上用,我就没写UI。

希望大家别吐槽代码丑~

github

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
import requests, re

def Time():
import time
t = time.time()
return int(round(t*1000))

class CUMT():

def __init__(self, user, password, company):
self.user = user
self.password = password
self.company = company

if not self.check_status():
self.login()

def check_status(self):
'''
判断是否在线
简单模拟http://10.2.5.251/a41.js?version=1592541085614 中的checkStatus函数
'''
self.check_time = Time()
check_url = 'http://10.2.5.251/drcom/chkstatus?callback=dr{}'.format(self.check_time)
print('检测登录状态的网址为:', check_url)
check_html = requests.get(check_url)
#响应包形如:
#dr1592541086816({"result":1,"time":1,"flow":0,"fsele":1,"fee":0,"m46":0,"v46ip":"10.4.190.194","myv6ip":"","oltime":15552060,"olflow":4294967295,"lip":"","stime":"","etime":"","uid":"08183035@unicom","v6af":0,"v6df":0,"v46m":0,"v4ip":"10.4.190.194","v6ip":"::","AC":"08183035@unicom","ss5":"10.4.190.194","ss6":"10.2.5.251","vid":0,"ss1":"000d484c182f","ss4":"000000000000","cvid":0,"pvid":0,"hotel":0,"aolno":18020,"eport":0,"eclass":1,"zxopt":1,"NID":"","olmac":"480eec81bde0","ollm":10,"olm1":"00000000","olm2":"0000","olm3":0,"olmm":1,"olm5":0,"gid":2,"actM":1,"actt":85,"actdf":0,"actuf":0,"act6df":0,"act6uf":0,"allfm":1,"d1":0,"u1":0,"d2":0,"u2":0,"o1":0,"nd1":0,"nu1":0,"nd2":0,"nu2":0,"no1":0})
status = re.search(r'"result":(\d),', check_html.text).group(1)
if status == '1':
print('!!!目前已在线!!!')
return True
else:
print('!!!目前未在线!!!')
return False


def login(self):

# 获取登录页网址
test_url = 'http://baidu.com'
test_html = requests.get(test_url)
login_page_url = re.search(r"<script>top\.self\.location\.href='(.+?)'</script>", test_html.text).group(1)
print('登录页网址为:', login_page_url)


# 从登录页网址中获取一些变量值
wlan_user_ip = re.search(r'wlanuserip=(.+?)&', login_page_url).group(1)
wlan_user_mac = re.search(r'mac=(.+?)&', login_page_url).group(1)
wlan_ac_name = re.search(r'wlanacname=(.+?)&', login_page_url).group(1)
login_time = Time()


# 登录请求
login_url = 'http://10.2.5.251:801/eportal/?c=Portal&a=login&callback=dr{}&login_method=1&user_account={}%40{}&user_password={}&wlan_user_ip={}&wlan_user_mac={}&wlan_ac_ip=&wlan_ac_name={}&jsVersion=3.0&_={}'.format(self.check_time, self.user, self.company, self.password, wlan_user_ip, wlan_user_mac, wlan_ac_name, login_time)
print('登录请求为:', login_url)
res = requests.get(login_url)
# 响应包形如:
# dr1592542850917({"result":"1","msg":"认证成功"})
print('响应包为:', res.content.decode('utf-8'))



user = '学号'
password = '密码'
company = 'unicom'
# 中国移动cmcc 中国联通unicom 中国电信telecom

c = CUMT(user, password, company)

1592543409057

1592543422938

狗儿的胆子特别小,害怕任何疏漏、错误的出现,所有事情都想着尽可能做到万无一失。

比如硬盘有一定概率突然损坏,导致丢失数据,所以我总是以两倍以上的硬盘容量进行冷备份,同时重要数据定时向百度网盘、ondrive更新。当然我的硬盘数据同步策略可以单独写一篇博文了,本文主要说一下我的博客的更新、备份策略。

博文->本地win10

首先,本地我使用自己用PyQT写的typecho博客管理姬进行博文的更新和备份

1592097870523

其中的“全站备份”这一选项可从数据库中下载文章独立页面的博文,markdown格式的文件,同时将文章里面的网络图片下载到本地,并修改其在markdown文件中的引用。

数据库、图片->阿里云OSS

写了一个小shell脚本,用于导出数据库文件,并向阿里云OSS同步数据库文件和博客图片。

放到宝塔的定时计划里,三小时执行一次。

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
#!/bin/bash

blog_mysql_backups () {
backup_dir=/root/sh/blogDBbackup
oss_dir=oss://iyzyi-oss/blog/mysql
username=root
password=密码
database_name=blog

tool=mysqldump
date=`date +%Y-%m-%d-%H-%M-%S`

if [ ! -d $backup_dir ];
then
mkdir -p $backup_dir;
fi

$tool -u $username -p$password $database_name > $backup_dir/$database_name-$date.sql
echo "create $backup_dir/$database_name-$date.sql"

ossutil64 cp $backup_dir $oss_dir -r --update
}

blog_uploads_backups () {
ossutil64 cp /www/wwwroot/blog/usr/uploads oss://iyzyi-oss/img/uploads -r --update
}

blog_mysql_backups
blog_uploads_backups

1592099539399

阿里云OSS->本地群辉

之前入手了一台黑群晖,意外发现群辉可以同步阿里云OSS的数据,于是我设置了一下,实现了上一步同步到阿里云OSS的数据库文件和博客图片,通过阿里云OSS的中转,同时同步到本地群辉。

今天群辉还没开机,哪天开机了我再截个图吧。

typecho->hexo

typecho是动态博客,经常出现各种问题,特别是数据库,出错频率特别高,为了不影响我打比赛的时候不影响查阅以往的笔记,所以写了个typecho博文转换成hexo博文的py脚本,并做成docker镜像。做好配置后,即可每三小时更新一次hexo博客。

同时,生成静态的hexo博客后,向阿里云OSS、Github分别推送一份。最初是只向Github推送的,但是Github在国内的速度简直不忍直视。

阿里云OSS上的hexo博客:hexo.iyzyi.com

Github上的hexo博客:hexo2.iyzyi.com

1592099351589

如果我想,也可以在服务器上也开一个hexo静态站点(容器内使用hexo s,然后外网访问iyzyi.com:50010),但是完全没有必要了。

未来的方向

  • 在群辉上也搭建一个typecho博客,实现两个typecho博客之间的同步。
  • QT重写typecho博客管理姬。

拓扑图

无标题

这是我在矿大CTF逆向区做的最后一题,因为其中的内容对我而言具有学习意义,所以单独拿出来写篇博文。其他矿大CTF逆向题解请详见矿大RE(完结撒花)

1592053619784

java

c.c.a中找到tu.class:

1592053539056

红框处是反调试的代码,如果修改了apk重新打包,再次运行会终止进程。

进入sd.dfh中看一下:

1592053729337

进入jh.gfds:

1592053815578

最内侧的df.gr是一个很长的二进制字符串,外侧的函数都是解密操作。

由于变量名被混淆了,解密代码很难读懂,而且flag在程序运行中明文出现过,所以可以考虑插桩,当将二进制字符串解密出flag时,可以使用log输出flag。

删掉反调试代码

我们来到smali层:

打开tu.smali,删掉下面的代码:

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
.line 35
invoke-virtual {p0}, Lc/c/a/tu;->getApplicationInfo()Landroid/content/pm/ApplicationInfo;

move-result-object v1

iget v2, v1, Landroid/content/pm/ApplicationInfo;->flags:I

.line 36
and-int/lit8 v2, v2, 0x2

.line 35
iput v2, v1, Landroid/content/pm/ApplicationInfo;->flags:I

if-eqz v2, :cond_0

.line 37
invoke-static {}, Landroid/os/Process;->myPid()I

move-result v1

invoke-static {v1}, Landroid/os/Process;->killProcess(I)V

.line 39
:cond_0
invoke-static {}, Landroid/os/Debug;->isDebuggerConnected()Z

move-result v1

if-eqz v1, :cond_1

.line 40
invoke-static {}, Landroid/os/Process;->myPid()I

move-result v1

invoke-static {v1}, Landroid/os/Process;->killProcess(I)V

.line 42
:cond_1
invoke-virtual {p0}, Lc/c/a/tu;->isRunningInEmualtor()Z

move-result v1

if-eqz v1, :cond_2

.line 43
invoke-static {}, Landroid/os/Process;->myPid()I

move-result v1

invoke-static {v1}, Landroid/os/Process;->killProcess(I)V

.line 45
:cond_2
const-string v1, "com.example.secret"

invoke-virtual {p0, v1}, Lc/c/a/tu;->getSignature(Ljava/lang/String;)I

move-result v0

.line 49
.local v0, "sig":I
invoke-direct {p0}, Lc/c/a/tu;->checkCRC()Z

move-result v1

if-eqz v1, :cond_3

.line 50
invoke-static {}, Landroid/os/Process;->myPid()I

move-result v1

invoke-static {v1}, Landroid/os/Process;->killProcess(I)V

1592054095920

log插桩

来到sd.smali:

1592054494071

把上图的第27行删掉,不要销毁flag。并在此处添加

1
2
3
const-string v1, "iyzyi-flag"

invoke-static {v1, v0}, Landroid/util/Log;->v(Ljava/lang/String;Ljava/lang/String;)I

同时将第18行的.locals 1改为.locals 2,以增加一个临时变量。

1592059539665

编译成新的apk后,再次反编译,可以看到java代码变成了这样:

tu.class:

1592059721721

sd.class:

1592059681816

adb查看log

夜神模拟器

安装apk后,在夜神模拟器的安装目录下打开powershell,输入:

连接设备:

./adb.exe devices

输出log:

./adb.exe logcat -s iyzyi-flag:v

iyzyi-flag是log的标签。

1592060153384

base64解码后即得flag.

手机

以Honor v10为例。

多次点击版本号,进入开发者模式:

TIM图片20200613230529

进入开发人员选项,启用USB调试“仅充电”模式下允许ADB调试

tm

通过USB线和电脑连接,允许USB调试:

sdfsd

然后手机安装刚刚插桩的apk。

从网上下载adb,并在其目录下打开powershell,剩下的adb操作和夜神模拟器的操作是一样的:

1592061105576

./adb.exe logcat -c的作用是清空log缓存区。

总结

本题从难度上来说,难度不高,但是我觉得插桩这个知识点足够它作为一篇独立的博文单列出来。

前几天我在动态里说过,我的github项目中无意中泄漏了数据库密码,随即修改了密码。

但是偶然发现,时光机里面无法上传图片了,后台也是无法上传图片。

猜测是某些缓存数据(我觉得可能是由数据库密码计算出来的cookie)存放在某个目录下,或是存放在数据库中。上传图片时可能该值有关。更改数据库密码后,该值未更换成新的,导致上传失败。

做好备份工作后,我重新安装了一遍typecho,同时导入handsome主题皮肤,唯一保留的是原有的数据库文件。

然后发现上传文件仍然失败,所以可以推断这种缓存在数据库中。

typecho博客的数据库设计的很简洁,根据以前写typecho博客管理姬的经验,我推测这个可能存在的缓存值应该存在于typecho_options这个表中。

由于操作系统考试临近,所以我先将其放在一遍,今天考完试后,我进行了如下操作:

  1. 导出当前的数据库文件
  2. 新建一个typecho博客 ,导出其数据库文件
  3. 通过文本编辑器,复制第二步得到的数据库文件中的typecho_options这个表全部内容,粘贴到第一步得到的数据库文件中的typecho_options的位置(将原有typecho_options部分的数据覆盖)
  4. 将新的数据库文件导入

测试发现此时可以正常上传图片。

然后需要做的就是将handsome的配置,网站的设置重新设定。

然后测试发现我写的typecho博客管理姬无法上传博文了,这是因为typecho_options的secret这一项的值改变了,改回原来的值即可。

1592039659517

就酱~

“刚刚警察把我叫到北大本部,问我是否同意警方逮捕你。他们对我摄像了。”

她的黑眼圈很重,眼皮耷拉着,不停地颤抖,表情看起来很缺氧,好像随时想扇我一巴掌。

她是我的母亲。

她恐惧万分,健康被摧毁,因为国保和学校老师们的过分关心,过年期间病倒住院。她说她的心脏不好,血压很高,每天晚上睡不着觉。我经常在早上打开手机界面,发现她凌晨三四点在发消息,给我哭诉她的命苦,哭诉她的心惊胆战。她一直害怕某一天早上一起床,我就人间蒸发,被关在某个小黑屋子了。

她很纠结,一方面希望我能出人头地,让她脸上有光,另一方面又希望我是个傻子,这样就能被拴在她身边。

警察毁了我的家庭。我的母亲觉得我在破坏社会的稳定和和谐,“警察该把你抓起来,为民除害。”

“他们告诉我,如果你再有行动,就要我配合把你限制起来,强行隔离,连我也见不到你了。”

什么行动?

就因为我是北大马克思主义学会的成员,而这在北大就是原罪。不仅如此,我竟然还敢支持尘肺病工人维权,声援深圳佳士工人组建工会的斗争,希望重建“工友之家”来为校工说话,甚至发文章抨击学校强制同学休学,真是罪上加罪!

平日里自己和工人们同学们的活动,在他们眼里也是别有用心。可我当然“别有用心”了。我希望走进他们,了解他们的所思所想,想和他们做朋友。他们在我的眼里不是普通人,而是历史的缔造者。

社会病了,需要刮骨疗毒,因此我迫切地关心着我们的下一代、下下一代,是否还会像我们一样活着,人人可以自由发展自己的社会,怎样才能实现。

于是,便尽我所能拂去掩盖在马克思主义上的灰尘,让声音穿透鸡汤、岁月静好和星辰大海梦:同学们、工友们,我们本不该如此生活,剥削和压迫不是亘古长存。马克思主义是我们唯一的出路,携起手来,创造新生!

“你头脑简单,涉世未深,不知道社会上是怎样的,人们不想革命,社会很和谐。”

多么可笑。

2019年伊始,

湖北天门职校的学生,拉起横幅走上大街,抗议学校的坑蒙拐骗。遭遇了特警的暴力驱散。人们发出怒吼,挥向年轻学子的警棍,为什么不挥向学校和教育局?

江苏盐城响水爆炸案,为了领取亲属遗体,人们被迫签订息事宁人的赔偿条约。遇难者家属不得不在“头七”拉横幅抗议。这一切被删于无形,零星的消息透露,警察再一次暴力执法,对于“冥顽不化”的死难者家属,推推搡搡,强制拘留。

996.icu横空出世,猿人永不为奴,放下键盘,让资本家看看谁才是信息时代的主人。

尘肺病工人的维权旷日持久,劳工人士危志立锒铛入狱,他的妻子郑楚然被封杀了一切发声渠道。

不断有人被捕,不断有人遭遇事故,不断有声音被删去,而这仅仅是2019年的开端。更不用说2018年的佳士事件、塔吊工人全国大罢工、卡车司机联合维权行动、上海长宁环卫工人罢工;2017年被清理的几十万“低端人口”;2016年双鸭山罢工;2015年利得鞋厂罢工、社会公益组织遭全面打压;2014年广州大学城环卫工罢工,学生前来支持……

他们把这些沸腾的声音抹去,然后对你说,看,世界多么清净。他们不敢把真相展现给最大多数的人民,于是污蔑那些说出真相的人“居心叵测”。

“沈雨轩,你不要给脸不要脸!你以为自己很厉害是吧?我告诉你,如果你进去了,马上服软,连他们(拍摄认罪视频的人)都不如!”

我从不抱有幻想,可以躲掉今日的打击。约谈、威胁、亲情折磨、监控、休学、软禁、抓捕……只是成长的必经之路。既然下定决心在这个魔幻的现实中站稳立场,扎根工农,那就要做好一切准备。

永远不能忘记,工人兰志伟,在毛诞纪念视频中说,“有人说你已经进去过两次,现在再发声,难道还要进去第三次吗?可是,对于我们工人来说,里面和外面,又有什么区别?在里面,是狱警的折磨和侮辱,在外面,是资本家的压榨!”苟活者在沉默中灭亡,唯有反抗才能在沉默中爆发。四十年的循环若再不去打破,劳动人民何以翻身?

永远不能忘记,工人尚杨雪,在面对持盾的警察时字字铿锵,她说,“我们要站起来,堂堂正正做一回人!”

永远不能忘记,佳士工人刘鹏华,对着派出所嘶哑着吼道,“我们要尊严!我们要尊严!我们要尊严!”

永远不能忘记,那些萍水相逢的工人。塑料厂的大姐,十二小时连轴转的夜班,滚烫的机器废液,从天而降的罚款;鞋厂的未成年少年,一加班就是通宵,还要被经理揪着耳朵扯下座位,一顿臭骂;锅炉房的大哥,十年的工龄,未曾习惯的高温,微薄的薪水……他们对我说,“你对我真好,能不能不要这么早辞工”“我恨他们”“这就是命”……

但我要紧紧抓着他们的肩膀,大声地说,这不是命!

无产阶级有伟大的志向,安源路矿工人自信地唱,“世界啊,我们来创造。压迫啊,我们来解除”。

无产阶级一旦被马克思主义武装,就会迸发出巨大的力量!

社会矛盾愈演愈烈,民众反抗大势所趋。反动势力的丧钟正在敲响,觉悟的人们终将选择马克思主义作为毕生的追求。我们应该以十二分的期待和信心,为迎接光明的到来而做准备!哪怕,下一刻就是铁窗和手铐!

亲爱的朋友,当你看到这篇文章的时候,我已经被关在某个不知名的地方,但我绝不会悲观失望,更不会妥协投降。请你们毫不动摇坚强如铁,未来某一天,我一定会与你们再次相聚,为了共同的信念奋斗终生!快去工作吧,快去投入新的战斗!

安卓模拟器打开后,发现是五子棋,挺奇怪的,有个问题,flag是以什么样的算法包含在程序里的?

正常的初始化流程走一波,解压apk,拿到jar。

打开jar,第一眼就是这个:

1591456763323

啊,aes?后来才知道没用到aes.

然后发现了三个native层的函数:

1591456828771

本题主要是native层的考察,和java层基本没有关系。

来看一下native层:

随便点了一个函数,发现函数拓扑图是这样的:

1591457037591

伪代码基本上这样的:

1591457107900

因为上学期初我听过FXTI学长的python去除控制流平坦化的这一报告(记得那时我刚进入BXS,完全听不懂学长的报告内容),所以我立即反应过来,这就是*控制流平坦化 *

学长的成果:

控制流平坦化

控制流平坦化(control flow flattening)的基本思想主要是通过一个主分发器来控制程序基本块的执行流程,例如下图是正常的执行流程

img

经过控制流平坦化后的执行流程就如下图

img

deflat镜像

deflat是去除控制流平坦化的一个脚本,基于angr,详细设计思路可以前往利用符号执行去除控制流平坦化查阅。

由于angr的版本更新,原作者的脚本已经不能用了,有大佬在其原本的基础上进行维护,项目在https://github.com/cq674350529/deflat

由于angr被建议需要python virtual enviroment,所以我直接做了个docker镜像,开箱直用。

同时,原脚本每次运行只能去除一个函数的控制流平坦化,所以我添加了几行代码,使得可以一次处理多个函数的控制流平坦化。

启动容器

1
docker run -v /tmp/dist/:/root/bin --name deflat -it iyzyi/deflat /bin/bash

-v是路径映射,容器的/root/bin映射到宿主机的/tmp/dist,二进制文件放到宿主机的/tmp/dist即可在容器的/root/bin中访问到。

路径映射可以随便改。

使用

原deflat.py

1
python3 deflat.py -f /root/bin/libcalculate.so --addr 0x41FEA0

-f 是需要修改的文件,–addr是要去除控制流平坦化的函数的起始地址。

1591461097462

我修改的deflat.py

1
2
python3 deflat-more.py -f /root/bin/libcalculate.so --addr 0x41FEA0
python3 deflat-more.py -f /root/bin/libcalculate.so --addr 0x4010340 0x401070 0x401090 0x4010d0

允许多个函数的起始地址,以空格为间隔。

因为我修改的版本没有经过深度测试,不知道有没有bug,所以请优先使用原deflat.py.

注意事项

  • 运行deflat.py时,所在路径必须是默认~/deflat/flat_control_flow,因为脚本import了相对路径的am_graph:

1591459061987

  • ida中看到的函数地址是4、5位的,如0x1fea0,但是本脚本运行时提示It is being loaded with a base address of 0x400000.所以函数的起始地址要写0x41fea0

1591459301685

对于本题

1
python3 deflat-more.py -f /root/bin/libcalculate.so --addr 0x4010340 0x401070 0x401090 0x4010d0 0x4010eb 0x401100 0x402b30 0x403810 0x404790 0x4054f0 0x406890 0x4075c0 0x408ad0 0x409130 0x409150 0x409970 0x409d20 0x409f70 0x40a120 0x40a140 0x40ad90 0x40b2c0 0x40b3b0 0x40c210 0x40c960 0x40cf60 0x40d560 0x40da80 0x40e2b0 0x40f1b0 0x40fe30 0x4116a 0x411c60 0x412770 0x413930 0x414dd0 0x415f30 0x4190a0 0x41c570 0x41ca10 0x41e120 0x41fd40 0x41fdc0 0x41fdf0 0x41fea0 0x4201c0

得跑个一个小时左右才能处理完成。

去除了控制流平坦化的函数拓扑图:

1591460012261

逻辑分析:

修复好的so文件中发现了字符串:

1591460097907

所以flag是某个数据的十六进制的md5。但是这个数据是啥呢?

函数一栏里发现了一个函数(__android_log_print),作用是输出log信息,根据交叉引用来到此处:

1591460313298

推测dword_2200C就是这个数据。

Nu1L的题解中说此题就是和电脑下五子棋,每下赢电脑一次,就会更新一次dword_2200C,直到满足某个条件,此时dword_2200C就是flag中的%x这个数据。

按照这个思路的话,那么本题不需要逆向,只需要将计算dword_2200C的代码提取出来,*删掉下棋的相关代码,相当于每大循环一次就是下赢了电脑一次 *,直接正向运行即可,直到满足条件,输出结果。

sub_6890中蕴涵dword_2200C的运算:

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
int sub_6890()
{
int v0; // ebx
unsigned __int8 v1; // ah
int v2; // edx
int v4; // [esp+128h] [ebp-70h]
signed int v5; // [esp+180h] [ebp-18h]

v5 = (signed int)((sqrt((double)(1 - -8 * (dword_22008 - 334816931))) - 1.0) / 2.0 + 1.0);
dword_22008 += v5;
v4 = dword_22008 % 4;
if ( dword_22008 % 4 )
{
if ( v4 == 1 )
{
if ( !((~((unsigned __int8)~(y_11 < 10) | (unsigned __int8)~(((x_10 - 1) * x_10 & 1) == 0)) & 1 | (unsigned __int8)(~(y_11 < 10) ^ ~(((x_10 - 1) * x_10 & 1) == 0))) & 1) )
goto LABEL_20;
while ( 1 )
{
dword_2200C *= v5;
if ( ((unsigned __int8)((y_11 < 10) ^ (((x_10 - 1) * x_10 & 1) == 0)) | (y_11 < 10
&& ((x_10 - 1) * x_10 & 1) == 0)) & 1 )
break;
LABEL_20:
dword_2200C *= v5;
}
}
else
{
while ( !(((unsigned __int8)((y_11 < 10) ^ (((x_10 - 1) * x_10 & 1) == 0)) | (y_11 < 10
&& ((x_10 - 1) * x_10 & 1) == 0)) & 1) )
;
if ( v4 == 2 )
{
if ( !((~((unsigned __int8)~(y_11 < 10) | (unsigned __int8)~(((x_10 - 1) * x_10 & 1) == 0)) & 1 | (unsigned __int8)(~(y_11 < 10) ^ ~(((x_10 - 1) * x_10 & 1) == 0))) & 1) )
goto LABEL_22;
while ( 1 )
{
dword_2200C <<= v5 % 8;
v2 = (x_10 - 43 + 42) * x_10 & 1;
if ( ((unsigned __int8)((y_11 < 10) ^ (v2 == 0)) | (y_11 < 10 && v2 == 0)) & 1 )
break;
LABEL_22:
dword_2200C <<= v5 % 8;
}
}
else if ( v4 == 3 )
{
dword_2200C += dword_22008;
}
while ( !(((unsigned __int8)((y_11 < 10) ^ (((x_10 - 1) * x_10 & 1) == 0)) | (y_11 < 10
&& ((x_10 - 1) * x_10 & 1) == 0)) & 1) )
;
}
while ( !(((unsigned __int8)((y_11 < 10) ^ (((x_10 - 1) * x_10 & 1) == 0)) | (y_11 < 10
&& ((x_10 - 1) * x_10 & 1) == 0)) & 1) )
;
}
else
{
v0 = (x_10 - 81 + 80) * x_10 & 1;
if ( !((~((unsigned __int8)~(y_11 < 10) | (unsigned __int8)~(v0 == 0)) & 1 | (unsigned __int8)(~(y_11 < 10) ^ ~(v0 == 0))) & 1) )
goto LABEL_19;
while ( 1 )
{
dword_2200C = (dword_22008 & 0x99533284 | ~dword_22008 & 0x66ACCD7B) ^ (dword_2200C & 0x99533284 | ~dword_2200C & 0x66ACCD7B);
v1 = ~(((x_10 - 1) * x_10 & 1) == 0);
if ( (~((unsigned __int8)~(y_11 < 10) | v1) & 1 | (unsigned __int8)(~(y_11 < 10) ^ v1)) & 1 )
break;
LABEL_19:
dword_2200C = (dword_22008 & 0x6C814E7D | ~dword_22008 & 0x937EB182) ^ (dword_2200C & 0x6C814E7D | ~dword_2200C & 0x937EB182);
}
}
return 0;
}

删掉下棋相关的代码(while循环就是下棋的判定,我觉得,不太确定),有如下代码:(为了直观地体现删除了哪些代码,我没有用switch-case结构来优化下面的代码的嵌套if)

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
int sub_6890(){
v5 = (signed int)((sqrt((double)(1 - -8 * (dword_22008 - 334816931))) - 1.0) / 2.0 + 1.0);
dword_22008 += v5;
v4 = dword_22008 % 4;
if ( v4 )
{
if ( v4 == 1 )
{
dword_2200C *= v5;
}
else
{
if ( v4 == 2 )
{
dword_2200C <<= v5 % 8;
}
}
else if ( v4 == 3 )
{
dword_2200C += dword_22008;
}
}
else
{
dword_2200C = (dword_22008 & 0x6C814E7D | ~dword_22008 & 0x937EB182) ^ (dword_2200C & 0x6C814E7D | ~dword_2200C & 0x937EB182);
}
return 0;
}

sub_1100函数调用了上面的sub_6890,sub_1100函数代码近五百行,所以不在此处粘贴了。

对于我们求flag来说,该函数的作用主要是使用了一个死循环:

1591463116652

然后循环内部调用了sub_6890。

唯一能跳出的循环的地方是:

1591463893007

此外的其他代码与flag的求解无关,所以不用看。

然后我们把dword_2200C和dword_22008的(已知的)初值写入程序,加上循环:

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
#include <stdio.h>
#include <math.h>

int dword_22008 = 0x13F4E6A3;
int dword_2200C = 0xDEF984B1;

int sub_6890(){
int v4, v5;
v5 = (signed int)((sqrt((double)(1 - -8 * (dword_22008 - 334816931))) - 1.0) / 2.0 + 1.0);
dword_22008 += v5;
v4 = dword_22008 % 4;
if ( v4 )
{
if ( v4 == 1 )
{
dword_2200C *= v5;
}
else
{
if ( v4 == 2 )
{
dword_2200C <<= v5 % 8;
}
else if ( v4 == 3 )
{
dword_2200C += dword_22008;
}
}
}
else
{
dword_2200C = (dword_22008 & 0x6C814E7D | ~dword_22008 & 0x937EB182) ^ (dword_2200C & 0x6C814E7D | ~dword_2200C & 0x937EB182);
}
return 0;
}

int main(){
while (dword_22008 + 2003757756 < 2338579637)
{
sub_6890();
}
printf("%d",dword_2200C);
}

解出dword_2200C为955939368,取十六进制并md5后为flag.

总结

本文主要是记录控制流平坦化的去除,和我做的deflat镜像。

后半部分的算法原理,我还有些不太懂,特别是删掉下棋的代码那部分,以及如何发现的dword_2200C就是flag的%x?

ghidra反编译mips64程序

题目给出cipher程序和ciphertext密文。

ciphertext

1590989750034

最后的0a是换行符。

reafelf -h file看一下cipher程序信息:

1590987460612

mips64的程序,大端。

ida无法反编译mips64的代码,网上查到说ida的retdec插件可以反编译mips,但是我去github看了看官方介绍,似乎不支持mips64:

1590987815137

然后无意间搜索到一款由美国安全局开发的开源逆向软件,ghidra,可以反编译mips64。

安装很简单。先安装好jdk11,可以不添加环境变量。然后运行ghidraRun.bat,如果没添加环境变量的话,会询问你jdk11的安装目录(填写根目录)。然后就可以运行了。

较为详细ghidra操作的学习可以移步全面详解 Ghidra

建立一个新project,把ciphter拖进来:

1590988786740

1590988812164

最后分析好的界面如下:

1590988911168

左侧中间窗口可以查看函数。

main函数

1590989173980

flag文件中读入flag,然后调用cipher

cipher函数

1590989331736

根据变量的实际含义改一下变量名(单击变量后按L键):

1590990009193

16个字节为一组(两个ulonglong)进行分组,第17行代码的作用就是求一共几组。右移4其实就是除以16。先减一再移位最后再加一的作用是让不足一组的算作一组,代入个实际的数算一算就容易理解了。

CONCAT44()是ghidra自己定义的一个函数,因为一些汇编操作无法通过已有的c语言中的函数进行简单描述:

1590990413533

CONCATxy(x,y的值可以变)系列的函数其实就是拼接函数,比如说CONCAT31(0xaabbcc, 0xdd) = 0xaabbccdd

具体的解释可以查看官方手册:

1590990575037

依次点击Ghidra Functionality > Decompiler > Internal Decompiler Functions

1590990674396

顺便吐槽下,ghidra很新,所以很多知识很难查到,google都很难查到相关内容。不像ida,遇到的问题基本网上都很容易找到前辈们的中文总结。

第18到20行得到了两个随机数,并分别取char,并保存至param_2[0]和param_2[1]。注意这里的param_2这个数组的变量类型不确定,伪代码中用undefined来注明了。我对此处的误解导致了我一直没有成功解出flag,直到动态调试才发现错误所在。等下我会细说此处。

然后进入循环,调用cipher,每次循环处理一组数据(16个字节,2个无符号长整型)。

最后第28行开始,输出处理后的数据。

encrypt函数

1590991844076

乍一看就是一些异或和循环移位,应该没有特别难的地方。所以,等下会有个但是

阅读一边后,很容易发现一个问题,第8行声明的in_a2这个指针,在第17、18行被调用。但是,代码中没有任何对这个指针的赋值地址的行为,我们(和程序)根本不知道这个指针指向何处(如果按照伪代码的逻辑来的话)。

所以很容易想到,是不是ghidra反编译出了一点小差错。

我们来看看汇编代码:

小白的我第一次接触mips指令集,以下内容如有谬误,希望大佬们可以斧正~

首先介绍几个用到的mips的指令:

为了不引起混淆,先说一点:

emmm,因为伪代码中的变量大多都是ulonglong(64bit),我看到下文中的双字,还在纳闷这不是32位的吗?查资料才明白,不同的架构,规定的数据类型是不一致的。mips64的数据类型没搜到,应该和mips32类似吧。这和x86差异挺大的,像我这种小白很容易误解。

指令的主要任务就是对操作数进行运算,操作数有不同的类型和长度,MIPS32 提供的基本数据类型如下:
(1)位(b):长度是 1bit。
(2)字节(Byte):长度是 8bit。
(3)半字(Half Word):长度是 16bit。
(4)字(Word):长度是 32bit。
(5)双字(Double Word):长度是 64bit。
(6)此外,还有 32 位单精度浮点数、64 位双精度浮点数等。

  • LD rt, offset(base)

    从存储器中读取双字的数据到寄存器中。

    rt是寄存器,offset是偏移量,base是基址。

    1590992920412

  • SD rt, offset(base)

    把双字的数据从寄存器存储到存储器中

    rt是寄存器,offset是偏移量,base是基址。

    1590993107606

单击伪代码中第17行中的in_a2,中间的汇编自动跳转到相关的指令位置。如图所示:

1590992664047

s8表示的是栈。v0是最常使用的寄存器。

我们得知:in_a2在栈中0x08的位置,即stack[-0x8],结合划线处,我们可以判断,encrypt是有三个参数的,第三个参数就是in_a2,是一个指针。根据指令ld(load double word)判断,应该是个longlong*或者ulonglong*类型的指针

ghidra有个很有用的功能,如果你对伪代码的不满意,可以进行一定程度的修改。

在如图所示处右键,选择第一项,进行函数签名的修改。

1590996251083

手动添加第三个参数:

1590997171966

修改后:

1590997207795

到此时,目前代码中唯一有点问题的就是第14、15行的concat44()这个函数,其他的部分都是正常的c语言代码。

看不懂伪代码,直接读汇编吧。

1590999017392

伪代码中的uVar1是[s8 + 0x30]。这其实是x86汇编中的表示方式,因为大多数人更熟悉x86,所以本文中我用了这样表示方式。它意思是取栈中偏移为0x30处的值。

伪代码中uVar1是[s8 + 0x30],uVa2r是[s8 + 0x38]。

所以uVar1 = [__edflag], uVar2 = [__edflag + 0x8]

cipher函数中调用本函数时,__edflag传进来的实参是(int)input_flag + i * 0x10,即每个分组的首地址。

所以uVar1是第一个ulonglong的地址,uVar2是第二个ulonglong的地址。

(其实不用分析,靠猜也能猜出这两个变量就是分组的数据,我做题的时候也是直接猜测的,写本文的时候才开始读的这部分汇编)

其实有时候汇编代码反而比伪代码更容易读懂。

比如本函数的第22行,(uStack24 >> 8) + (uStack24 << 0x38),有点意思,其实就是一个64位的循环右移,汇编中一行就搞定了:

1590999408198

第23行的(uStack32 >> 0x3d) + uStack32 * 8就更有意思了:

1590999446775

没想到吧,参数是0x1d,但是却移了0x3d位。为什么?请从下图找答案。

关于移位指令:

1590999576926

1590999589550

加密逻辑

分析到这一步,本函数的逻辑应该很明显了啊。

我们用ror64表示64位的循环右移,可以将加密逻辑整理为:

1
2
3
4
5
6
7
8
9
10
def enc(a, b, c, d):
v32, v24 = c, d
v40 = (ror64(b, 8) + a) ^ v32
v48 = ror64(a, 0x3d) ^ v40
for i in range(0x1f):
v24 = (ror64(v24, 8) + v32) ^ i
v32 = ror64(v32, 0x3d) ^ v24
v40 = (ror64(v40, 8) + v48) ^ v32
v48 = ror64(v48, 0x3d) ^ v40
print(hex(v48), hex(v40))

a, b分别是送入的两个ulonglong数据。

c,d是和我们之前分析的encrypt函数添加的第三个参数array有关,分别是array[0]和array[1]。先下结论c和d与cipher函数中获取的两个随机数有关,什么关系,下文会细说。

一共进行0x1f轮的加密处理。

v32和v24是一组key(密钥),在每轮的加密中都会进行变换,其值与c,d,i有关。怎么看出它是key的?因为其值与密文无关,而反过来密文与它有关。

v48和v40是密文。其值与plaintext(a,b), key(c,d)有关。

*下面重点说一下c和d这俩值。 *

在cipher函数的伪代码中可以看到:

1591001061971

如果你的伪代码中看不到encrypt函数的第三个参数,请退回上文,按照步骤修改encrypt函数的signature。

可以看到c,d和随机数有关。

如果靠猜的话,我估计绝大多数人,都会猜测c = param_2[0], d = param_2[1]。是的,我也是这么猜的,这一错误的猜测使得我整整一天都在查找错误何在。

此处伪代码很难分析啊,最大的问题在于我们不知道param_2的变量类型,伪代码给出的是undefined *代表未知。再加上这么明显的暗示!

让谁来看,都会觉得是两个随机数分别强制格式转化为一个字节的数据,然后送入param_2[0]和param_2[1],这两个都是ulonglong,然后传入encrypt函数中当作key。

当然可以通过汇编来做出判断:

1591016577339

当然这对我来说是马后炮,当我动调之后才理解了这段汇编的含义。

最后我是通过动态调试了解的具体细节。这段赋值过程特别有意思,这也是我写篇文章的主要目的。文章到这里才刚刚进入主题。

动态调试(qemu+ida)

qemu运行mips64程序

要想动态调试,首先要能运行这个程序。

QEMU(quick emulator)是一款由Fabrice Bellard等人编写的免费的可执行硬件虚拟化的(hardware virtualization)开源托管虚拟机(VMM)。

其与BochsPearPC类似,但拥有高速(配合KVM),跨平台的特性。

QEMU是一个托管的虚拟机镜像,它通过动态的二进制转换,模拟CPU,并且提供一组设备模型,使它能够运行多种未修改的客户机OS,可以通过与KVM(kernel-based virtual machine开源加速器)一起使用进而接近本地速度运行虚拟机(接近真实电脑的速度)。

QEMU还可以为user-level的进程执行CPU仿真,进而允许了为一种架构编译的程序在另外一种架构上面运行(借由VMM的形式)。

说人话:通过qemu能运行mips64架构的程序

安装qemu

以下过程可能存在谬误,我试图复现环境,但是暂时没能成功,稍后会琢磨琢磨,请大家先移步https://www.cnblogs.com/WangAoBo/p/debug-arm-mips-on-linux.html

以ubuntu为例:

apt install qemu-user-static

我做题时应该是使用apt install qemu-user进行安装的,但是滚档重新安装时发现虽然能够安装成功,但是运行文件时报错:

qemu: uncaught target signal 11 (Segmentation fault) - core dumped
Segmentation fault (core dumped)

尝试了几次都不行,改用了apt install qemu-user-static

运行

静态链接的程序可以像正常程序一样运行。

如果是动态链接的程序直接运行会报错:

1591003259105

需要安装相对应的链接库,然后通过qemu运行程序,同时指明共享库(-L参数)。

以本文mips64程序为例:

查找相关库:

apt-cache search "libc6" | grep mips64

mips64el是小端,注意区分。

1591003486905

我们在列表中选择形如libc6-mips64-cross的包进行安装:

sudo apt install libc6-mips64-cross

然后运行程序,同时指定共享库的路径:

qemu-mips64-static -L /usr/mips64-linux-gnuabi64/ cipher

注意不同架构对应的qemu的命令不同,可以先输入qemu,然后下tab键查看所有命令。

输入/usr/mips64的时候也是按下tab键,有惊喜。

1591009111472

可以看到程序运行,并输出密文。(main函数中通过putchar输出密文而不是保存到文件中)

ida远程动态调试

服务端:

qemu-mips64 -L /usr/mips64-linux-gnuabi64/ -g 23946 ./cipher > out

-g在23946端口开一个gdb调试

*客户端: *

Debugger->attach->remote GDB debugger

1591010265574

填写服务端ip和端口,然后点击Debug options:

1591010662661

箭头处的两项要勾选,不然一进入调试状态,程序就执行完成并退出了。然后点击图中划线处Set specific options.

1591010729304

选择相应的架构:

1591010841046

此后点击多次ok:

1591010572414

1591010581761

1591010592015

最后进入调试:

1591010960163

很不幸,前面说过,ida无法反编译mips64。所以只能看汇编了。

程序一开始停在系统领空。

1591011179254

按上一会f8(大概三四十步)即可跳转至程序领空。

先跳出系统领空再进行地址的跳转。否则容易导致代码没加载成功:

1591011582103

在程序领空内,g直接跳转到某处都容易导致代码没加载成功。

1591011710244

此时按c转换成汇编语言时容易出现下图所示的错误,ida崩溃退出。

1591011743436

所以我们虽然通过静态能知道各个函数的地址,但也不要直接跳转,先跳出系统领空后,再按g并输入地址进行跳转。跳转时也不要直接跳,先跳到函数的起始地址,再跳这个函数内部的地址。

然后正式开始动调。之后的过程和正常题目的调试没啥区别,就是指令集不一样罢了,照着指令手册慢慢分析就行。

我们动调的目标是搞清楚前面归纳的c,d两个参数和cipher函数中生成的两个随机数之间的关系。

所以跳出系统领空后,我们先跳转到cipher函数的起始地址120000F5C,并按c分析汇编代码:

1591012406461

动调中看不到函数信息,所以可以先静态看一下rand的调用位置:

1591012506079

然后跳转到与随机数相关的位置,f8走几步,观察寄存器中值的变化:

下图寄存器v0中放的是第一个随机数:

1591014883541

取低位字节放入v1,得到单字节的0x85:

前面是ff开头应该是因为0x85>0x7f,char中算作负数,seb指令是Sign-Extend Byte,有符号扩展字节。

1591014911835

第二个操作数同理:

1591015009932

拿到单字节0xf1:

1591015096099

然后我们跳转到encrypt函数,看看c,d两个变量的值。

从cipher函数直接跳转到encrypt函数同样容易出现代码未加载的问题,我们可以先跳转到cipher中调用encrypt函数的位置,然后f7步入。

静态查看地址:

1591014526622

动调在此处f7步入:

1591014608906

ghidra可以轻松确定encrypt中我们归纳的那个c,d的传值的位置:

1591014702296

动调运行到此处:

可以看到此时c = 0x85f1 0000 0000 0000

1591015423268

继续运行:

1591015565037

所以c = char(rand1)<<56 + char(rand2)<<48d = 0

这样我们就知道了c,d的值与随机数的关系。

这个的动调太容易让程序以及ida崩溃了,不知道大佬们有没有什么方法能使得跳转地址时代码可以正常加载出来,减少错误率。

逆向逻辑分析

为了方便大家,所以把上面归纳的加密逻辑复制到此处:

1
2
3
4
5
6
7
8
9
10
def enc(a, b, c, d):
v32, v24 = c, d
v40 = (ror64(b, 8) + a) ^ v32
v48 = ror64(a, 0x3d) ^ v40
for i in range(0x1f):
v24 = (ror64(v24, 8) + v32) ^ i
v32 = ror64(v32, 0x3d) ^ v24
v40 = (ror64(v40, 8) + v48) ^ v32
v48 = ror64(v48, 0x3d) ^ v40
print(hex(v48), hex(v40))

c和d每次运行程序都不一样,但是好在有效取值在两个字节的范围内,可以爆破。

爆破时的每次尝试可以得到一组假定的key(c,d)即key(v32, v24),经过0x1f轮的迭代,可以得到最后一轮的v32, v24(上文说过,因为v32, v24只与c,d,i有关,i是迭代的轮数)

我们已知密文,即最后一轮迭代得到的的v48, v40;并可以以爆破的方式假定c,d从而求出最后一轮的v32, v24.

可以通过v48 = ror64(v48, 0x3d) ^ v40得到上一轮的v48

再通过v40 = (ror64(v40, 8) + v48) ^ v32得到上一轮的v40

再通过v32 = ror64(v32, 0x3d) ^ v24得到上一轮的v32

再通过v24 = (ror64(v24, 8) + v32) ^ i得到上一轮的v24

如此逆向迭代,可求出明文。

解密脚本

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
import struct

def solve():
#with open('ciphertext', 'rb')as f:
# enc = f.read()
enc = b'*\x00\xf8+\xe1\x1dw\xc1\xc3\xb1q\xfc#\xd5\x91\xf40\xf1\x1e\x8b\xc2\x88YW\xd5\x94\xabwB/\xebu\xe1]v\xf0Fn\x98\xb9\xb6Q\xfd\xb5]w6\xf2\n'
for i in range(0x10000):
v32, v24 = get_final_r1_r2(i*0x1000000000000, 0)
flag = b''
for k in range(len(enc)//16):
f1, f2 = decrypt(enc[k*16:k*16+16], v32, v24)
flag += struct.pack('>Q', f1) + struct.pack('>Q', f2)
if b'RCTF' in flag:
print(hex(i), flag)


def decrypt(byte16, fc, fd):
v48 = struct.unpack('>Q', byte16[:8])[0]
v40 = struct.unpack('>Q', byte16[8:])[0]
v32, v24 = fc, fd
for i in range(0x1e, -1, -1):
v48 = rol64(v48 ^ v40, 0x3d)
v40 = rol64(ull((v40 ^ v32) - v48), 8)
v32 = rol64(v32 ^ v24, 0x3d)
v24 = rol64(ull((v24 ^ i) - v32), 8)
v48 = rol64(v48 ^ v40, 0x3d)
v40 = rol64(ull((v40 ^ v32) - v48), 8)
return v48, v40


def get_final_r1_r2(c, d):
v32, v24 = c, d
for i in range(0x1f):
v24 = ull(ror64(v24, 8) + v32) ^ i
v32 = ror64(v32, 0x3d) ^ v24
return v32, v24


def rol64(value, k):
return ull(value << k) | ull(value >> (64-k))

def ror64(value, k):
return ull(value << (64-k)) | ull(value >> k)

def ull(n):
return n & 0xffffffffffffffff

solve()

#RCTF{9876235e3a9bd69cfb6b4a3dfcdbca70f3054f91}

跑两分钟出结果。

没啥要注意的,记得进行加减等操作时注意保持ulonglong的位数就好。

参考链接:

虎符ctf三道逆向中最简单的一道,比赛期间我只做出来这一道。题目对我来说挺新颖的,所以在这里也记录一下。刚写完vm的题解,有点累,所以这篇就简单点写。

只给出一个disasm.txt,部分如下:

1588254571359

查了下,是python的字节码。

pyc可以还原成py源代码,但是这个类似汇编的东西没办法还原出来(如果大佬们知道怎么还原,劳请告诉我一下)

dis模块

同时查到这个文本是怎么来的,python的dis模块。

下面有个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
def f():
s = [90, 100, 87, 109, 86, 108, 86, 105, 90, 104, 88, 102]
b = [90, 100, 87, 109, 86, 108, 86, 105, 90, 104, 88, 102]
#a = s[6:30:3]
#
#s = 'xctf{1231231}'
#a = (((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128 + ord(s[5])
#print(a)
a = map(lambda x:x+1, zip(s, b[2:5]))

import dis
dis.dis(f)

输出:

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
  2           0 LOAD_CONST               1 (90)
2 LOAD_CONST 2 (100)
4 LOAD_CONST 3 (87)
6 LOAD_CONST 4 (109)
8 LOAD_CONST 5 (86)
10 LOAD_CONST 6 (108)
12 LOAD_CONST 5 (86)
14 LOAD_CONST 7 (105)
16 LOAD_CONST 1 (90)
18 LOAD_CONST 8 (104)
20 LOAD_CONST 9 (88)
22 LOAD_CONST 10 (102)
24 BUILD_LIST 12
26 STORE_FAST 0 (s)

3 28 LOAD_CONST 1 (90)
30 LOAD_CONST 2 (100)
32 LOAD_CONST 3 (87)
34 LOAD_CONST 4 (109)
36 LOAD_CONST 5 (86)
38 LOAD_CONST 6 (108)
40 LOAD_CONST 5 (86)
42 LOAD_CONST 7 (105)
44 LOAD_CONST 1 (90)
46 LOAD_CONST 8 (104)
48 LOAD_CONST 9 (88)
50 LOAD_CONST 10 (102)
52 BUILD_LIST 12
54 STORE_FAST 1 (b)

9 56 LOAD_GLOBAL 0 (map)
58 LOAD_CONST 11 (<code object <lambda> at 0x0000019F93CB0270, file "d:\桌面\20200419 虎符ctf\text.py", line 9>)
60 LOAD_CONST 12 ('f.<locals>.<lambda>')
62 MAKE_FUNCTION 0
64 LOAD_GLOBAL 1 (zip)
66 LOAD_FAST 0 (s)
68 LOAD_FAST 1 (b)
70 LOAD_CONST 13 (2)
72 LOAD_CONST 14 (5)
74 BUILD_SLICE 2
76 BINARY_SUBSCR
78 CALL_FUNCTION 2
80 CALL_FUNCTION 2
82 STORE_FAST 2 (a)
84 LOAD_CONST 0 (None)
86 RETURN_VALUE

Disassembly of <code object <lambda> at 0x0000019F93CB0270, file "d:\桌面\20200419 虎符ctf\text.py", line 9>:
9 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (1)
4 BINARY_ADD
6 RETURN_VALUE

上面的输出的代码形式风格和题目给出的只有细微的差异(我用的是python3,而题目是python2,可能是这个原因)

然后可以去试着分析下题目给出的代码了,遇到不懂的点,可以自己写个函数,然后disasm,看看和题目中代码的差异。

分析字节码

首先是arr0,arr1,arr2这三个列表的赋值,很容易看出来。

然后有check0, check1,check2,check3四个检测函数。

check0

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
# <genexpr> line 6 of game.py

6 0 LOAD_FAST 0 '.0'
3 FOR_ITER 32 'to 38'
6 STORE_FAST 1 'x'
9 LOAD_GLOBAL 0 'ord'
12 LOAD_FAST 1 'x'
15 CALL_FUNCTION_1 1 None
18 LOAD_GLOBAL 1 'range'
21 LOAD_CONST 32
24 LOAD_CONST 128
27 CALL_FUNCTION_2 2 None
30 COMPARE_OP 6 in
33 YIELD_VALUE
34 POP_TOP
35 JUMP_BACK 3 'to 3'
38 LOAD_CONST None
41 RETURN_VALUE
//推测是判断字符ord(x) in (32, 128)

# check0 line 5 of game.py

6 0 LOAD_GLOBAL 0 'all'
3 LOAD_GENEXPR '<code_object <genexpr>>'
6 MAKE_FUNCTION_0 0 None
9 LOAD_FAST 0 's'
12 GET_ITER
13 CALL_FUNCTION_1 1 None
16 CALL_FUNCTION_1 1 None
19 RETURN_VALUE

//check0()判断字符介于(32,128)

配合着我代码中的注释就应该能看懂了吧,我就不多解释了。

check1

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
# check1 line 8 of game.py

9 0 LOAD_GLOBAL 0 'len'
3 LOAD_FAST 0 's'
6 CALL_FUNCTION_1 1 None
9 LOAD_CONST 100
12 COMPARE_OP 0 <
15 POP_JUMP_IF_FALSE 58 'to 58'
//if len(s)<100
18 LOAD_GLOBAL 0 'len'
21 LOAD_FAST 0 's'
24 CALL_FUNCTION_1 1 None
27 LOAD_GLOBAL 0 'len'
30 LOAD_FAST 0 's'
33 CALL_FUNCTION_1 1 None
36 BINARY_MULTIPLY //乘
//len(s) * len(s)
37 LOAD_CONST 777
40 BINARY_MODULO //取模
//(len(s) * len(s))%777
41 LOAD_CONST 233
44 BINARY_XOR //异或
//((len(s) * len(s))%777) ^ 233
45 LOAD_CONST 513
48 COMPARE_OP 2 ==
//if ((len(s) * len(s))%777) ^ 233 == 513
51_0 COME_FROM 15 '15'
51 POP_JUMP_IF_FALSE 58 'to 58'

10 54 LOAD_GLOBAL 1 'True'
57 RETURN_END_IF
58_0 COME_FROM 51 '51'

12 58 LOAD_GLOBAL 2 'False'
61 RETURN_VALUE
62 LOAD_CONST None
65 RETURN_VALUE

//check1()判断flag长度,经爆破得知len(flag) = 39

check1检测flag长度是否符合条件( if ((len(s) * len(s))%777) ^ 233 == 513

爆破可以求得长度为39

check2

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
# check2 line 14 of game.py

15 0 LOAD_GLOBAL 0 'ord'
3 LOAD_FAST 0 's'
6 LOAD_CONST 0
9 BINARY_SUBSCR //TOS = TOS1[TOS]
10 CALL_FUNCTION_1 1 None
13 LOAD_CONST 128
16 BINARY_MULTIPLY

//ord(s[0]) * 128

17 LOAD_GLOBAL 0 'ord'
20 LOAD_FAST 0 's'
23 LOAD_CONST 1
26 BINARY_SUBSCR
27 CALL_FUNCTION_1 1 None
30 BINARY_ADD
31 LOAD_CONST 128
34 BINARY_MULTIPLY

//(ord(s[0])*128+ord(s[1])) * 128

35 LOAD_GLOBAL 0 'ord'
38 LOAD_FAST 0 's'
41 LOAD_CONST 2
44 BINARY_SUBSCR
45 CALL_FUNCTION_1 1 None
48 BINARY_ADD
49 LOAD_CONST 128
52 BINARY_MULTIPLY

//(((ord(s[0])*128+ord(s[1]))*128) + ord(s[2])) * 128

53 LOAD_GLOBAL 0 'ord'
56 LOAD_FAST 0 's'
59 LOAD_CONST 3
62 BINARY_SUBSCR
63 CALL_FUNCTION_1 1 None
66 BINARY_ADD
67 LOAD_CONST 128
70 BINARY_MULTIPLY

//(((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2])) * 128) + ord(s[3])) * 128

71 LOAD_GLOBAL 0 'ord'
74 LOAD_FAST 0 's'
77 LOAD_CONST 4
80 BINARY_SUBSCR
81 CALL_FUNCTION_1 1 None
84 BINARY_ADD
85 LOAD_CONST 128
88 BINARY_MULTIPLY

//(((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128

89 LOAD_GLOBAL 0 'ord'
92 LOAD_FAST 0 's'
95 LOAD_CONST 5
98 BINARY_SUBSCR
99 CALL_FUNCTION_1 1 None
102 BINARY_ADD

//(((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128 + ord(s[5])

103 LOAD_CONST 3533889469877L
106 COMPARE_OP 2 ==
109 POP_JUMP_IF_FALSE 138 'to 138'
112 LOAD_GLOBAL 0 'ord'
115 LOAD_FAST 0 's'
118 LOAD_CONST -1
121 BINARY_SUBSCR
122 CALL_FUNCTION_1 1 None
125 LOAD_CONST 125
128 COMPARE_OP 2 ==
131_0 COME_FROM 109 '109'
131 POP_JUMP_IF_FALSE 138 'to 138'

16 134 LOAD_GLOBAL 1 'True'
137 RETURN_END_IF
138_0 COME_FROM 131 '131'

18 138 LOAD_GLOBAL 2 'False'
141 RETURN_VALUE
142 LOAD_CONST None
145 RETURN_VALUE

/*
if 表达式 == 3533889469877L:
if ord(s[-1]) == 125:
return True
else return false
else return false
*/
//check2()判断flag{}这个框架,以及flag{}主体的第一个字符flag[5]

check2用于判断前6个字符是否满足(((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128 + ord(s[5]) == 3533889469877,同时判断最后一个字符是否是}

只要学过数学的小学生都知道6元一次等式根本不可能求出六个未知数,所以题目可能是假设了前五个字符是flag{,由此我们可以求出flag的第6个字符,也就是flag主体部分的第一个字符。

注意,我和炜昊都被这里坑到了,看到了flag[5],以为只有5个字符,恰好是flag{,忽略了第六个字符。

这个低级的下标错误平常不会错,但是每次到这个综合性题目中总会出错,还是要注意啊。

check3

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
# check3 line 20 of game.py

21 0 LOAD_GLOBAL 0 'map'
3 LOAD_GLOBAL 1 'ord'
6 LOAD_FAST 0 's'
9 CALL_FUNCTION_2 2 None
12 STORE_FAST 1 'arr'
//e.g: map(function, iterable, ...)
//arr = map(ord, s)

22 15 LOAD_FAST 1 'arr'
18 LOAD_CONST 6
21 LOAD_CONST 30
24 LOAD_CONST 3
27 BUILD_SLICE_3 3 //切片,3个一组
30 BINARY_SUBSCR
31 STORE_FAST 2 'a'

//a = arr[6:30:3]

23 34 SETUP_LOOP 62 'to 99'
37 LOAD_GLOBAL 2 'range'
40 LOAD_GLOBAL 3 'len'
43 LOAD_FAST 2 'a'
46 CALL_FUNCTION_1 1 None
49 CALL_FUNCTION_1 1 None
52 GET_ITER
53 FOR_ITER 42 'to 98'
56 STORE_FAST 3 'i'

//类似for i in range(len(a)) ,不过这里用的是迭代器

24 59 LOAD_FAST 2 'a'
62 LOAD_FAST 3 'i'
65 BINARY_SUBSCR
66 LOAD_CONST 17684
69 BINARY_MULTIPLY
70 LOAD_CONST 372511
73 BINARY_ADD
74 LOAD_CONST 257
77 BINARY_MODULO
78 LOAD_GLOBAL 4 'arr0'
81 LOAD_FAST 3 'i'
84 BINARY_SUBSCR
85 COMPARE_OP 3 !=
88 POP_JUMP_IF_FALSE 53 'to 53'

/*
if (a[i] * 17684 + 372511) % 257 == arr0[i]:(是==,我没搞错(因为jump if false))
goto 53(继续循环)
else return flase
*/

25 91 LOAD_GLOBAL 5 'False'
94 RETURN_END_IF
95_0 COME_FROM 88 '88'
95 JUMP_BACK 53 'to 53'
98 POP_BLOCK
99_0 COME_FROM 34 '34'

26 99 LOAD_FAST 1 'arr'
102 LOAD_CONST -2
105 LOAD_CONST 33
108 LOAD_CONST -1
111 BUILD_SLICE_3 3
114 BINARY_SUBSCR
115 LOAD_CONST 5
118 BINARY_MULTIPLY
119 STORE_FAST 4 'b'

//b = arr[-2:33:-1] * 5

27 122 LOAD_GLOBAL 0 'map'
125 LOAD_LAMBDA '<code_object <lambda>>'
128 MAKE_FUNCTION_0 0 None
131 LOAD_GLOBAL 6 'zip'
134 LOAD_FAST 4 'b'
137 LOAD_FAST 1 'arr'
140 LOAD_CONST 7
143 LOAD_CONST 27
146 SLICE+3
147 CALL_FUNCTION_2 2 None
150 CALL_FUNCTION_2 2 None
153 STORE_FAST 5 'c'
//c = map(lambda x:函数体, zip(b, arr[7,27]))
//即c[i] = b[i] ^ arr[7,27][i]

28 156 LOAD_FAST 5 'c'
159 LOAD_GLOBAL 7 'arr1'
162 COMPARE_OP 3 !=
165 POP_JUMP_IF_FALSE 172 'to 172'
29 168 LOAD_GLOBAL 5 'False'
171 RETURN_END_IF
172_0 COME_FROM 165 '165'

/*
if c == arr1:
goto 172
else return false
*/

30 172 LOAD_CONST 0
175 STORE_FAST 6 'p'

31 178 SETUP_LOOP 105 'to 286'
181 LOAD_GLOBAL 2 'range'
184 LOAD_CONST 28
187 LOAD_CONST 34
190 CALL_FUNCTION_2 2 None
193 GET_ITER
194 FOR_ITER 88 'to 285'
197 STORE_FAST 3 'i'

/*
for i in range(28, 34)
*/

32 200 LOAD_FAST 1 'arr'
203 LOAD_FAST 3 'i'
206 BINARY_SUBSCR
207 LOAD_CONST 107
210 BINARY_ADD
211 LOAD_CONST 16
214 BINARY_DIVIDE
215 LOAD_CONST 77
218 BINARY_ADD

// ((arr[i] + 107)//16)+77 (除法//还是/存疑)

219 LOAD_GLOBAL 8 'arr2'
222 LOAD_FAST 6 'p'
225 BINARY_SUBSCR
226 COMPARE_OP 3 !=
229 POP_JUMP_IF_TRUE 268 'to 268'
/*
if arr2[p] != ((arr[i] + 107)//16)+77 :
return false
*/
232 LOAD_FAST 1 'arr'
235 LOAD_FAST 3 'i'
238 BINARY_SUBSCR
239 LOAD_CONST 117
242 BINARY_ADD
243 LOAD_CONST 16
246 BINARY_MODULO
247 LOAD_CONST 99
250 BINARY_ADD
251 LOAD_GLOBAL 8 'arr2'
254 LOAD_FAST 6 'p'
257 LOAD_CONST 1
260 BINARY_ADD
261 BINARY_SUBSCR
262 COMPARE_OP 3 !=
265_0 COME_FROM 229 '229'
265 POP_JUMP_IF_FALSE 272 'to 272'
/*
if (arr[i]+117)%16+99 == arr2[p+1]:
goto 272
else return false
*/

33 268 LOAD_GLOBAL 9 'false'
271 RETURN_END_IF
272_0 COME_FROM 265 '265'

34 272 LOAD_FAST 6 'p'
275 LOAD_CONST 2
278 INPLACE_ADD //TOS = TOS1 + TOS
279 STORE_FAST 6 'p'
282 JUMP_BACK 194 'to 194'
285 POP_BLOCK
286_0 COME_FROM 178 '178'

//p += 2,然后继续循环

35 286 LOAD_GLOBAL 10 'True'
289 RETURN_VALUE

整理一下,check3的逻辑是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def check3():
arr = map(ord, flag)
a = arr[6:30:3]
for i in range(len(a)):
if (a[i] * 17684 + 372511) % 257 != arr0[i]:
return False
b = arr[-2:33:-1] * 5
c = map(lambda x: x[0]^x[1], zip(b, arr[7,27]))
if c != arr1:
return False
p = 0
for i in range(28, 34):
if arr2[p] != (((arr[i]+107)//16)+77):
return False
if arr2[p+1] != ((arr[i]+117)%16+99):
return False
p += 2
return True

逆向

倒着逆,脚本如下:

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
arr0 = [249, 91, 149, 113, 16, 91, 53 ,41]
arr1 = [43, 1, 6, 69, 20, 62, 6, 44, 24, 113, 6, 35, 0 , 3, 6, 44, 20, 22, 127, 60]
arr2 = [90, 100, 87, 109, 86, 108, 86, 105, 90, 104, 88, 102]


def check3():
arr = map(ord, flag)
a = arr[6:30:3]
for i in range(len(a)):
if (a[i] * 17684 + 372511) % 257 != arr0[i]:
return False
b = arr[-2:33:-1] * 5
c = map(lambda x: x[0]^x[1], zip(b, arr[7,27]))
if c != arr1:
return False
p = 0
for i in range(28, 34):
if arr2[p] != (((arr[i]+107)//16)+77):
return False
if arr2[p+1] != ((arr[i]+117)%16+99):
return False
p += 2
return True


def solve_check1():
for i in range(100):
if ((i *i )%777) ^ 233 == 513:
print(i)

def solve_5():
for i,n in enumerate('flag{'):
flag[i] = ord(n)
flag[38] = ord('}')
sum = 0
for i in range(5):
sum = (sum + flag[i]) * 128
#print(sum)
for i in range(32,128):
if sum+i==3533889469877:
flag[5] = i

def solve_6_30_3():
for i in range(6, 30, 3):
for j in range(32, 128):
if (j * 17684 + 372511) % 257 == arr0[(i-6)//3]:
flag[i] = j
break

def solve_28_34():
p = 0
for i in range(28,34):
for j in range(32,128):
if (((j+107)//16)+77) == arr2[p] and ((j+117)%16+99) == arr2[p+1]:
flag[i] = j
break
p += 2

def solve_34_38():
flag[34] = flag[18] ^ arr1[11]
flag[35] = flag[9] ^ arr1[2]
flag[36] = flag[12] ^ arr1[5]
flag[37] = flag[15] ^ arr1[8]

def solve_7_27():
b = flag[37:33:-1]*5
for i in range(20):
flag[i+7] = b[i] ^ arr1[i]


def main():
solve_check1()
global flag
flag = [0] * 39
solve_5()
solve_6_30_3()
solve_28_34()
solve_34_38()
solve_7_27()
for i in flag:
print(chr(i),end='')

main()

注意flag[7, 27]的值需要依靠flag[34, 38]求出。

第一次接触vm这类的题型。一开始我以为vm和vmp是类似的,但是后来才知道vm指的是自己实现一套指令系统,然后在此基础上运行字节码。类似java的虚拟机。

其实也不算是第一次做vm的题,很久之前在南邮ctf平台上做过的WxyVM这道题也是vm。这题蛮简单的,以至于我一直都不知道它其实也是vm题。

不知道为什么,网上有关虎符ctf逆向的题解不多,vm这道题只找到一份题解,令则师傅的原创-虎符第三题 -vm 虚拟机逆向。令则师傅写得挺清楚的。不过,好歹是我第一次做出这种基于栈的vm题,还是写一下题解温习一下吧。

总述

题目给出了两个文件,

  • vm,解释器,解释字节码并执行,ELF程序
  • code,字节码,十六进制数据

如何运行?

linux终端内键入./vm ./code,回车后输入flag,若错误则输出n

vm的main函数:

1588243222122

VM结构体

上图21~28行的变量可以看作一个结构体,这不是写在程序里的,是等下分析33行的vm函数的代码时总结出来的。

1
2
3
4
5
6
7
8
struct VM{
dword vm_eip;
dword vm_sp; VMstruct[1]
qword* code; VMstruct + 1
qword* stack; VMstruct + 2
qword* c;
qword* d;
}VMstruct;

vm_eip是表示当前指令运行到哪里了,比如运行code文件的0x100处的指令时,vm_eip = 0x100。

vm_sp表示栈内有几个数据元素,配合下面的stack这个变量,可以表示栈顶元素(stack[sp-1])

code是code文件中的字节码

stack是一个数组,本程序通过数组来模拟了一个栈,所有的数据运算都通过这个模拟的栈进行。

c是用于存储数据的数组,本题中c存储了三组数据,c[i+0x64]存储输入的flag,c[i+0x32]存储预设的一组数据,c[i]存储flag加密后的数据。i取值均为0到0x29,含0x29(最后c[i]和c[i+0x32]相比较,相等则答对flag)

d存储两个循环变量i,j,i = d[0], j=d[1]

为什么上面的结构体c和d两个变量,我没有使用像vm_eip,stack这种明确的变量名?

因为单独分析vm程序的vm函数根本无法分析出c和d的作用。必须结合code文件中的字节码才能分析出其含义。

分析操作码

vm文件的vm函数(sub_400A50):

经典的while-switch结构选择opcode(操作码),不同的操作码模拟不同的操作。

1588244728385

将一些基础的变量改下名之后,代码中的变量和我们总结出的VM结构体有这样的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
*(_BYTE *)(code + vm_eip)		
VMstruct.vm_eip。当前eip,程序运行到的地方

*(_BYTE *)(code + vm_eip + 1)
data。表示操作数。只有单操作数指令才会出现这个变量

(signed int)VMstruct[1]
VMstruct.vm_sp。表示栈内有几个数据元素。

*((_QWORD *)VMstruct + 1)
VMstruct.code。code字节码地址。

*((_QWORD *)VMstruct + 2)
VMstruct.stack。模拟栈

*((_QWORD *)VMstruct + 3)
VMstruct.c。存储数据的数组。

*((_QWORD *)VMstruct + 4)
VMstruct.d。存储两个循环变量

分析也没啥技巧,就慢慢来,以0xc的操作码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case 0xC:                         // case 0xc : pop a; pop b; b/a;
v58 = VMstruct[1]; //VMstruct.vm_sp
v59 = *((_QWORD *)VMstruct + 2); //VMstruct.stack
v60 = v58 - 1;
v58 -= 2;
VMstruct[1] = v60;
v61 = *(_BYTE *)(v59 + v60); //pop a
VMstruct[1] = v58;
v7 = (_BYTE *)(v58 + v59);
vm_eip_1 = (unsigned __int8)*v7; //pop b
if ( !v61 )
return vm_eip_1;
VMstruct[1] = v60;
v9 = (unsigned __int16)vm_eip_1 / v61;// temp = b/a
goto LABEL_8;
//(省略一些中间代码)
LABEL_8:
*v7 = v9; //push temp
LABEL_9:
code = *((_QWORD *)VMstruct + 1);
vm_eip_1 = *VMstruct + 1;
*VMstruct = vm_eip_1; //eip+=1
goto LABEL_2;

经过漫长的分析(里面的变量繁杂,代码可读性极低,需要一点一点看),可以整理出下面的指令,我基本上是用伪汇编表示的。

本题中的指令一部分是无操作数(单字节码)的,另一部分是单操作数(双字节码,即一个操作码加上一个操作数)的。如果下面的指令中含有data这个字眼,说明该指令是单操作数的。

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
1		getchar()
2 pop a; printf(a)
3 vm_eip ++
4 push data
5 push d[data]
6 pop d[data]
7 push c[data]
8 pop c[data]
9 pop a; pop b; push b+a
0xa pop a; pop b; push b-a
0xb pop a; pop b; push b*a
0xc pop a; pop b; push b/a
0xd pop a; pop b; push b%a
0xe pop a; pop b; push b^a
0xf pop a; pop b; push b&a
0x10 pop a; pop b; push b|a
0x11 pop a; push -a;
0x12 pop a; push ~a;
0x13 pop a; pop b; if (b!=a) {vm_eip+=2} else {vm_eip+=data}
0x14 pop a; pop b; if (b==a) {vm_eip+=2} else {vm_eip+=data}
0x15 pop a; pop b; if (b<=a) {vm_eip+=2} else {vm_eip+=data}
0x16 pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=data}
0x17 pop a; pop b; if (b>=a) {vm_eip+=2} else {vm_eip+=data}
0x18 pop a; pop b; if (b>a) {vm_eip+=2} else {vm_eip+=data}
0x19 pop a; push c[a]
0x1a pop a; pop b; c[a] = b
0x1b pop a; push d[a]
0x1c pop a; pop b; d[a] = b
0x1d vm_eip += data; if (*vm_eip>0x1d) return vm_eip
0x1e return vm_eip

0x9~0x12是一些简单的算数运算,无操作数,所涉及的数据都是从栈中弹出的。

0x13~0x18是进行判断、实现程序跳转的,类似jnz,jbe等。

0x1d其实就是jmp,注意data是char类型的(*(char *)(code + vm_eip + 1)),有正有负,比如0xe8是负数。

1
2
3
import ctypes
print(ctypes.c_byte(0xe8)).value
#输出-24

0x1e是ret

下面说一些组合指令:

0x4-0x8:向c数组写入一个数据

0x1-0x8:输入一个数据,并写入c数组

0x4-0x6:为循环变量i或j(d[0]或d[1])赋值

等等。。

分析字节码

首先很容易看出前两部分:

一:内置数据的赋值

这部分数据是code文件内含的,用于和加密后的字符串作比较。

第一部分字节码就是把这些数据赋值到c[0x32+i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def part1(self):
'''
验证flag的数据,存储在c[0x32]~c[0x32+0x29]
0x0 ~ 0xa7 :
04 66 08 32 04 4E 08 33 04 A9 08 34 04 FD 08 35
04 3C 08 36 04 55 08 37 04 90 08 38 04 24 08 39
04 57 08 3A 04 F6 08 3B 04 5D 08 3C 04 B1 08 3D
04 01 08 3E 04 20 08 3F 04 81 08 40 04 FD 08 41
04 36 08 42 04 A9 08 43 04 1F 08 44 04 A1 08 45
04 0E 08 46 04 0D 08 47 04 80 08 48 04 8F 08 49
04 CE 08 4A 04 77 08 4B 04 E8 08 4C 04 23 08 4D
04 9E 08 4E 04 27 08 4F 04 60 08 50 04 2F 08 51
04 A5 08 52 04 CF 08 53 04 1B 08 54 04 BD 08 55
04 32 08 56 04 DB 08 57 04 FF 08 58 04 28 08 59
04 A4 08 5A 04 5D 08 5B
'''
self.c = [0] * 0x100
with open(r'code', 'rb')as f:
self.b = f.read()
for i in range(0xa8//4):
group = self.b[i*4:i*4+4]
data, offset = group[1], group[3]
self.c[offset] = data

二:输入flag

输入flag,数据保存至c[0x64+i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def part2(self):
'''
输入flag,存储在c[0x64]~c[0x64+0x29]
0xa8 ~ 0x125 :
01 08 64 01 08 65 01 08 66 01 08 67 01 08 68 01
08 69 01 08 6A 01 08 6B 01 08 6C 01 08 6D 01 08
6E 01 08 6F 01 08 70 01 08 71 01 08 72 01 08 73
01 08 74 01 08 75 01 08 76 01 08 77 01 08 78 01
08 79 01 08 7A 01 08 7B 01 08 7C 01 08 7D 01 08
7E 01 08 7F 01 08 80 01 08 81 01 08 82 01 08 83
01 08 84 01 08 85 01 08 86 01 08 87 01 08 88 01
08 89 01 08 8A 01 08 8B 01 08 8C 01 08 8D
'''
flag = input('请输入长度为42的字符串:')
if len(flag) != 42:
print('字符串长度不合要求')
else:
for i in range((0x126-0xa8)//3):
group = self.b[0xa8+i*3 : 0xa8+i*3+3]
data, offset = ord(flag[i]), group[2]
self.c[offset] = data

三:加密部分

翻译字节码脚本

这部分不像前两部分的指令简单,所以我先写了个翻译字节码的脚本:

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
class Translate():
def opcode(self):
'''
得到一个字节码字典
'''
dic = r'''1 getchar()
2 pop a; printf(a)
3 vm_eip ++
4 push data
5 push d[data]
6 pop d[data]
7 push c[data]
8 pop c[data]
9 pop a; pop b; push b+a
0xa pop a; pop b; push b-a
0xb pop a; pop b; push b*a
0xc pop a; pop b; push b/a
0xd pop a; pop b; push b%a
0xe pop a; pop b; push b^a
0xf pop a; pop b; push b&a
0x10 pop a; pop b; push b|a
0x11 pop a; push -a;
0x12 pop a; push ~a;
0x13 pop a; pop b; if (b!=a) {vm_eip+=2} else {vm_eip+=data}
0x14 pop a; pop b; if (b==a) {vm_eip+=2} else {vm_eip+=data}
0x15 pop a; pop b; if (b<=a) {vm_eip+=2} else {vm_eip+=data}
0x16 pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=data}
0x17 pop a; pop b; if (b>=a) {vm_eip+=2} else {vm_eip+=data}
0x18 pop a; pop b; if (b>a) {vm_eip+=2} else {vm_eip+=data}
0x19 pop a; push c[a]
0x1a pop a; pop b; c[a] = b
0x1b pop a; push d[a]
0x1c pop a; pop b; d[a] = b
0x1d vm_eip += data; if (*vm_eip>0x1d) return vm_eip
0x1e return vm_eip'''
self.opcode_dict = {}
for i in dic.split('\n'):
num, opcode = int(i.split('\t')[0], 16), i.split('\t')[1]
self.opcode_dict[num] = opcode


def translate_part_3_opcode(self):
'''
翻译字节码
'''
self.opcode()

with open(r'code', 'rb')as f:
self.b = f.read()

eip = 0x126
while eip < 0x1e8:
op = self.b[eip]
if 'data' in self.opcode_dict[op]:
#将data替换为具体的操作数
data = self.b[eip+1]
print('{0:<8}{1:<5}{2:<7}{3}'.format(hex(eip), hex(op), hex(data), self.opcode_dict[op].replace('data', hex(data))))
eip += 2
else:
print('{0:<8}{1:<12}{2}'.format(hex(eip), hex(op), self.opcode_dict[op]))
eip += 1


def __init__(self):
self.opcode()
self.translate_part_3_opcode()


t = Translate()

本脚本没有写具体跳转的指令翻译(因为本来就是个辅助分析的程序而已)

比如这条输出:

0x178 0x1d 0xbc vm_eip += 0xbc; if (*vm_eip>0x1d) return vm_eip

需要手动计算出跳转的位置,手动替换成下面这条指令:

0x178 0x1d 0xbc jmp 0x134

又比如这条:

0x18c 0x16 0x34 pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=0x34}
需要手动翻译成:

0x18c 0x16 0x34 pop a; pop b; cmp a,b; jbe 0x1c0;

分析

最终我们得到了第三部分的伪汇编指令:

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
0x126   0x4  0x0    push 0x0
0x128 0x6 0x0 pop d[0x0]

0x12a 0x5 0x0 push d[0x0]
0x12c 0x4 0x7 push 0x7
0x12e 0x16 0x56 pop a; pop b; cmp a,b; jbe 0x184;
0x130 0x4 0x0 push 0x0
0x132 0x6 0x1 pop d[0x1]

0x134 0x5 0x1 push d[0x1]
0x136 0x4 0x6 push 0x6
0x138 0x16 0x42 pop a; pop b; cmp a,b; jbe 0x17a;
0x13a 0x5 0x0 push d[0x0]
0x13c 0x4 0x6 push 0x6
0x13e 0xb pop a; pop b; push b*a
0x13f 0x5 0x1 push d[0x1]
0x141 0x9 pop a; pop b; push b+a
0x142 0x4 0x64 push 0x64
0x144 0x9 pop a; pop b; push b+a
0x145 0x19 pop a; push c[a]
0x146 0x12 pop a; push ~a;
0x147 0x5 0x0 push d[0x0]
0x149 0x5 0x1 push d[0x1]
0x14b 0x4 0x2 push 0x2
0x14d 0x9 pop a; pop b; push b+a
0x14e 0xb pop a; pop b; push b*a
0x14f 0xf pop a; pop b; push b&a
0x150 0x4 0x64 push 0x64
0x152 0x5 0x0 push d[0x0]
0x154 0x4 0x6 push 0x6
0x156 0xb pop a; pop b; push b*a
0x157 0x5 0x1 push d[0x1]
0x159 0x9 pop a; pop b; push b+a
0x15a 0x9 pop a; pop b; push b+a

0x15b 0x19 pop a; push c[a]
0x15c 0x5 0x0 push d[0x0]
0x15e 0x5 0x1 push d[0x1]
0x160 0x4 0x2 push 0x2
0x162 0x9 pop a; pop b; push b+a
0x163 0xb pop a; pop b; push b*a
0x164 0x12 pop a; push ~a;
0x165 0xf pop a; pop b; push b&a
0x166 0x10 pop a; pop b; push b|a
0x167 0x5 0x1 push d[0x1]
0x169 0x4 0x7 push 0x7
0x16b 0xb pop a; pop b; push b*a
0x16c 0x5 0x0 push d[0x0]
0x16e 0x9 pop a; pop b; push b+a
0x16f 0x1a pop a; pop b; c[a] = b
0x170 0x5 0x1 push d[0x1]
0x172 0x4 0x1 push 0x1
0x174 0x9 pop a; pop b; push b+a
0x175 0x4 0x1 push 0x1
0x177 0x1c pop a; pop b; d[a] = b
0x178 0x1d 0xbc jmp 0x134


0x17a 0x5 0x0 push d[0x0]
0x17c 0x4 0x1 push 0x1
0x17e 0x9 pop a; pop b; push b+a
0x17f 0x4 0x0 push 0x0
0x181 0x1c pop a; pop b; d[a] = b
0x182 0x1d 0xa8 jmp 0x12a
;以上为第一部分,二重循环。设输入为x,求出f(x),存储在c[0]~c[0x29]


;第一个部分循环跳出至此
0x184 0x4 0x1 push 0x1
0x186 0x6 0x0 pop d[0x0]

0x188 0x5 0x0 push d[0x0]
0x18a 0x4 0x2a push 0x2a
0x18c 0x16 0x34 pop a; pop b; cmp a,b; jbe 0x1c0;
0x18e 0x5 0x0 push d[0x0]
0x190 0x4 0x2 push 0x2
0x192 0xd pop a; pop b; push b%a
0x193 0x4 0x0 push 0x0
0x195 0x14 0xf pop a; pop b; cmp a,b; jnz 0x1a4;
0x197 0x5 0x0 push d[0x0]
0x199 0x19 pop a; push c[a]
0x19a 0x5 0x0 push d[0x0]
0x19c 0x4 0x1 push 0x1
0x19e 0xa pop a; pop b; push b-a
0x19f 0x19 pop a; push c[a]
0x1a0 0x9 pop a; pop b; push b+a
0x1a1 0x5 0x0 push d[0x0]
0x1a3 0x1a pop a; pop b; c[a] = b


0x1a4 0x5 0x0 push d[0x0]
0x1a6 0x4 0x2 push 0x2
0x1a8 0xd pop a; pop b; push b%a
0x1a9 0x4 0x1 push 0x1
0x1ab 0x14 0xb pop a; pop b; cmp a,b; jnz 0x1b6;
0x1ad 0x4 0x6b push 0x6b
0x1af 0x5 0x0 push d[0x0]
0x1b1 0x19 pop a; push c[a]
0x1b2 0xb pop a; pop b; push b*a
0x1b3 0x5 0x0 push d[0x0]
0x1b5 0x1a pop a; pop b; c[a] = b


0x1b6 0x5 0x0 push d[0x0]
0x1b8 0x4 0x1 push 0x1
0x1ba 0x9 pop a; pop b; push b+a
0x1bb 0x4 0x0 push 0x0
0x1bd 0x1c pop a; pop b; d[a] = b
0x1be 0x1d 0xca jmp 0x188
;至此为第二部分,按照下标的奇偶,分别将上一步得到的数据进一步加密


;以下为第三部分,判断c[i]是否和c[i+0x32]是否相等,相等输出y,反之输出n
0x1c0 0x4 0x0 push 0x0
0x1c2 0x6 0x0 pop d[0x0]

0x1c4 0x5 0x0 push d[0x0]
0x1c6 0x4 0x29 push 0x29
0x1c8 0x18 0x4 pop a; pop b; cmp a,b; jae 0x1cc;
0x1ca 0x1d 0x1b jmp 0x1e5
;jmp to printf('y')


0x1cc 0x5 0x0 push d[0x0]
0x1ce 0x19 pop a; push c[a]
0x1cf 0x4 0x32 push 0x32
0x1d1 0x5 0x0 push d[0x0]
0x1d3 0x9 pop a; pop b; push b+a
0x1d4 0x19 pop a; push c[a]
0x1d5 0x14 0xc pop a; pop b; cmp a,b; jnz 0x1e1;
;jmp to printf('n')

0x1d7 0x5 0x0 push d[0x0]
0x1d9 0x4 0x1 push 0x1
0x1db 0x9 pop a; pop b; push b+a
0x1dc 0x4 0x0 push 0x0
0x1de 0x1c pop a; pop b; d[a] = b
0x1df 0x1d 0xe5 jmp 0x1c4


0x1e1 0x4 0x6e push 0x6e
;printf('n')
0x1e3 0x2 pop a; printf(a)
0x1e4 0x1e return vm_eip

;printf('y')
0x1e5 0x4 0x79 push 0x79
0x1e7 0x2 pop a; printf(a)

然后有需要慢慢分析。我是手动维护了一个栈,然后手动模拟了一遍程序。

最后整理出一下加密逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def part3(self):
'''
加密flag,加密后的数据放在c[0]~c[29]
'''
for i in range(7):
for j in range(6):
#self.c[j*7+i] = ((~self.c[6*i+j+0x64]) & ((j+2)*i)) | ((self.c[6*i+j+0x64]) & (~((j+2)*i)))
self.c[j*7+i] = self.c[6*i+j+0x64] ^ ((j+2)*i)
#因为a ^ b = (~a & b) | (a & ~b),所以上面两行代码等价

for i in range(1, 0x2a):
if i % 2 == 0:
self.c[i] = (self.c[i] + self.c[i-1]) & 0xff
elif i % 2 == 1:
self.c[i] = (self.c[i] * 0x6b) & 0xff

for i in range(0x29+1):
if self.c[i] != self.c[i+0x32]:
break

if i == 0x29:
print('y')
else:
print('n')

加密部分有两步,第一步逻辑很复杂,但是可以化简(如果我单独思考,肯定没法化简出来,这个化简是令则大佬在题解中说的)。

第二部根据奇偶分别运算。

特别注意循环的起始和终止条件!!!

特别注意c和python之间变量位数的不同,注意&0xff!!!

逆向

最后逆着写出解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Decode():
def __init__(self):
import ctypes
c_0x32 = [102, 78, 169, 253, 60, 85, 144, 36, 87, 246, 93, 177, 1, 32, 129, 253, 54, 169, 31, 161, 14, 13, 128, 143, 206, 119, 232, 35, 158, 39, 96, 47, 165, 207, 27, 189, 50, 219, 255, 40, 164, 93]
c = [0] * 0x2a
c_0x64 = [0] * 0x2a

c[0] = c_0x32[0]
for i in range(1,0x2a):
if i % 2 == 0:
for j in range(0x100):
if c_0x32[i] == (j + c_0x32[i-1]) & 0xff:
c[i] = j
elif i % 2 == 1:
for j in range(0x100):
if c_0x32[i] == (j * 0x6b) & 0xff:
c[i] = j

for i in range(7):
for j in range(6):
c_0x64[6*i+j] = c[j*7+i] ^ ((j+2)*i)

print(''.join(map(chr, c_0x64)))

总体难度不是很大,第三道题我赛后搞了六七个小时才做出,不是因为难,只是因为我对c语言的指针很不熟悉,影响了对验证逻辑的理解。

RE1 签到

1587878803272

v6[i-112]v5[i]

1
2
3
4
s = list('akhb~chdaZrdaZudqduvdZvvv|')
for i in range(len(s)):
s[i] = chr((ord(s[i])-1) ^ 6)
print(''.join(s))

encrypt3

1587879014441

1587879028636

首先输入一个即将输入字符串的长度n

然后输入字符串,记为input

然后程序使用enc_flag(网上查了查,意思是encrypt flag)对input加密(说解密也行叭~)

将结果输出,记为output.

分析关键代码:

v10=input[0] ^ input[1] ^ input[2] ^ ... ^ input[n-1]

output[i] = v10 ^ enc_flag[i]

enc_flag[i]已知。

可以看出,v10与input有关,但不管input的内容是什么,v10只是个char。所以可以爆破出所有的output。

程序中没有提示flag是input还是output,但是我们求出output后就已经发现了flag了。

我觉得input没法求,毕竟只需要逐位异或满足v10是某一特定值(爆破求出是64)即可,input的可能的字符串取值空间极大。

1
2
3
4
5
6
7
8
9
10
11
enc_flag = [38,44,33,39,59,35,34,115,117,114,113,33,36,117,118,119,35,120,38,114,117,113,38,34,113,114,117,114,36,112,115,118,121,112,35,37,121,61]
for i in range(0, 0x7f):
output = ''
for j in range(len(enc_flag)):
if not(32 <= (enc_flag[j] ^ i) <= 127):
break
else:
output += chr(enc_flag[j] ^ i)
if 'flag' in output:
print('v10=%d' % i)
print(output)

RE3 (忘了本题名称了)

输入四个int,经过sub_401863()进行变换,得到4个int,并和v9~v12比较数值,相等则输入的4个int的十六进制形式就是flag.

1587880072350

sub_401863() 函数:

第一部分:求出dword_408A40[]

1587880207049

虽然参数里有input,但是函数内部没用到input。用到了main中的v15~v18.

值是固定的,所以动态调试拿到,一共32个int.

之后进入一个32次的循环,循环内部有第2,3,4,5部分。

第二部分:求出v12

1587880490649

1587880769689

函数内部是一个4次循环,每次循环得到一个byte,合起来即一个INT。

对于每个byte:

v12[j] = input[i+2][j]^input[i+1][j]^input[i+3][j]^dword_408A40[i][j],

所以v12在形式上为:v12[0]v12[1]v12[2]v12[3]

按字节异或和按int异或其实是等同的,所以简化为:

v12 = input[i+2]^input[i+1]^input[i+3]^dword_408A40[i]

第三部分:求出v11

1587880746304

1587880788680

得到v11,4个_BYTE,即INT

v11[j] = byte_404100[16*(__int8(v12[j]>>4)) + (v12[j]&0xF)]

可以归纳为v11[j] = byte_404100[v12[j]]

byte_404100为已知的长度为0xff的数组,可以使用idc脚本导出二进制流文件。

导出后(记文件名称为dump),可以使用下面的代码提取成数组:

1
2
3
4
5
6
7
8
9
10
11
import binascii

with open('dump','rb')as f:
data = f.read()
num = []
for i in range(len(data)):
d = data[i : i+1][::-1] #b'\x00\x00C\xdf'
d = str(binascii.hexlify(d))[2:-1] #000043df
d = int(d,16) #17375
num.append(d)
print(num)

代码没优化,其实不需要这么多行,先这样放在这里吧。

第四部分:求出v10

1587880914872

__ROL4__是uint32循环左移,__ROL2__是uint16循环左移,__ROL1__是uint8循环左移

__ROR4__是uint32循环右移,__ROR2__是uint16循环右移,__ROR1__是uint8循环右移

这些名称都是ida的定义,可以在ida根目录\plugins\defs.h中详细查看:

1587881337330

(我tm一开始没注意v10的计算同时包含左移和右移,我眼瞎,以为都是__ROL4__

第五部分:求a[i+4]

1587881415207

1587881426743

a[i+4][j] = a[i][j] ^ v10[j]

求四个byte,合起来一个int.

按字节异或和按int异或其实是等同的,所以简化为:

a[i+4] = a[i] ^ v10

归纳

程序输入4个int,然后通过32次循环,每次循环求出一个input[i+4]

这里的input不完全代表输入,input[0]到input[3]是输入,但是此后的input[4]到input[35]都是求出来的。

验证input[32],input[33],input[34],input[35]是否和main中的v9~v12相等。

为了不影响理解,我把input[]这个数组起个别名:数组a[]

我们把代数式整合一下:

1
2
3
4
5
6
7
v12 = a[i+1] ^ a[i+2] ^ a[i+3] ^ dword_408A40[i]

v11[j] = byte_404100[v12[j]]

v10 = __ROR4__(v11, 14) ^ __ROL4__(v11, 10) ^ v11 ^ __ROL4__(v11, 2) ^ __ROR4__(v11, 8)

a[i+4] = a[i] ^ v10

对于上面的算式,我们已知的有dword_408A40[],byte_404100[]a[32]~a[35]

我们需要求出的是a[0]~a[3]

所以先求v12,再求v11,再求v10,最后求a[i],直到倒着求出a[0]~a[3]

脚本

脚本没整理,有些臃肿,大家将就看吧。

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
'''
循环左移和循环右移
from https://blog.csdn.net/C_chuxin/article/details/83691674
我嫌弃这两函数不够简洁,自己实现了这个需求,所以此段代码作废。
def circular_shift_left(int_value,k,bit = 8):
bit_string = '{:0%db}' % bit
bin_value = bit_string.format(int_value) # 8 bit binary
bin_value = bin_value[k:] + bin_value[:k]
int_value = int(bin_value,2)
return int_value

def circular_shift_right (int_value,k,bit = 8):
bit_string = '{:0%db}' % bit
bin_value = bit_string.format(int_value) # 8 bit binary
bin_value = bin_value[-k:] + bin_value[:-k]
int_value = int(bin_value,2)
return int_value
'''

def __ROL4__(value, k):
k %= 32
return (((value << k)&0xffffffff) | ((value >> (32-k))&0xffffffff))&0xffffffff

def __ROR4__(value, k):
k %= 32
return (((value >> k)&0xffffffff) | ((value << (32-k))&0xffffffff))&0xffffffff

byte_404100 = [214, 144, 233, 254, 204, 225, 61, 183, 22, 182, 20, 194, 40, 251, 44, 5, 43, 103, 154, 118, 42, 190, 4, 195, 170, 68, 19, 38, 73, 134, 6, 153, 156, 66, 80, 244, 145, 239, 152, 122, 51, 84, 11, 67, 237, 207, 172, 98, 228, 179, 28, 169, 201, 8, 232, 149, 128, 223, 148, 250, 117, 143, 63, 166, 71, 7, 167, 252, 243, 115, 23, 186, 131, 89, 60, 25, 230, 133, 79, 168, 104, 107, 129, 178, 113, 100, 218, 139, 248, 235, 15, 75, 112, 86, 157, 53, 30, 36, 14, 94, 99, 88, 209, 162, 37, 34, 124, 59, 1, 33, 120, 135, 212, 0, 70, 87, 159, 211, 39, 82, 76, 54, 2, 231, 160, 196, 200, 158, 234, 191, 138, 210, 64, 199, 56, 181, 163, 247, 242, 206, 249, 97, 21, 161, 224, 174, 93, 164, 155, 52, 26, 85, 173, 147, 50, 48, 245, 140, 177, 227, 29, 246, 226, 46, 130, 102, 202, 96, 192, 41, 35, 171, 13, 83, 78, 111, 213, 219, 55, 69, 222, 253, 142, 47, 3, 255, 106, 114, 109, 108, 91, 81, 141, 27, 175, 146, 187, 221, 188, 127, 17, 217, 92, 65, 31, 16, 90, 216, 10, 193, 49, 136, 165, 205, 123, 189, 45, 116, 208, 18, 184, 229, 180, 176, 137, 105, 151, 74, 12, 150, 119, 126, 101, 185, 241, 9, 197, 110, 198, 132, 24, 240, 125, 236, 58, 220, 77, 32, 121, 238, 95, 62, 215, 203, 57, 72]

dword_408A40 = [0xF9,0x86,0x21,0xF1,0x61,0x2B,0x66,0x41,0x9A,0xB1,0x6A,0x5A,0x77,0x20,0xA9,0x7B,0xF4,0x60,0x73,0x36,0x61,0x0C,0x6A,0x77,0xB3,0x89,0xBB,0xB6,0x51,0x31,0x76,0x24,0x7C,0x30,0x20,0xA5,0xBD,0x4D,0x58,0xB7,0xED,0x53,0x07,0xC3,0x57,0x5B,0xE5,0x7E,0x8C,0x60,0x88,0x69,0xB7,0x95,0xD8,0x30,0xAF,0x14,0xBA,0x44,0xA1,0x95,0x44,0x10,0x28,0xB4,0x20,0xD1,0xA3,0x5F,0xB5,0x73,0x66,0x49,0x87,0xCC,0x39,0x44,0x24,0x92,0x1F,0x64,0x9E,0xE8,0x5A,0x01,0xCA,0x98,0x60,0x90,0x15,0xC7,0x2E,0xFD,0xE1,0x99,0x0C,0xD8,0x9B,0xB7,0xB0,0x15,0x21,0x1D,0xEB,0x8A,0x22,0x0E,0x81,0x0C,0x78,0xF1,0x54,0x36,0x8D,0x42,0x96,0x34,0x29,0x62,0xE5,0x72,0xCF,0x01,0x12,0xA0,0x24,0x91]
dword_408A40_2 = [0] * 32
for i in range(32):
#四个byte合并成一个int
#dword_408A40_2[i] = hex(dword_408A40[i*4+3] * 2**24 + dword_408A40[i*4+2] * 2**16 + dword_408A40[i*4+1] * 2**8 + dword_408A40[i*4])
dword_408A40_2[i] = dword_408A40[i*4+3] * 2**24 + dword_408A40[i*4+2] * 2**16 + dword_408A40[i*4+1] * 2**8 + dword_408A40[i*4]
dword_408A40 = dword_408A40_2
#print(dword_408A40)

global a
a = [0] * 36
a[35] = 3229185894
a[34] = 2011540633
a[33] = 835020779
a[32] = 1191593383


def get_v12(i):
return a[i+1] ^ a[i+2] ^ a[i+3] ^ dword_408A40[i]

def get_v11(v12):
#print(hex(v12))
v12 = hex(v12)[2:]
if len(v12)<8:
v12 = (8-len(v12))*'0' + v12
v11 = ''
for j in range(4):
v12_j = int('0x' + v12[j*2:j*2+2], 16)
v11_j = hex(byte_404100[v12_j])[2:]
if len(v11_j)<2:
v11_j = (2-len(v11_j))*'0' + v11_j
#print(byte_404100[v12_j], ' ', v12_j, ' ', v11_j)
v11 += v11_j
#v11 = '0x' + v11[6:8] + v11[4:6] + v11[2:4] + v11[0:2]
#print(v11)
#print()
return int(v11, 16)

def get_v10(v11):
#return circular_shift_right(v11, 14) ^ circular_shift_left(v11, 10) ^ v11 ^ circular_shift_left(v11, 2) ^ circular_shift_right(v11, 8)
return __ROR4__(v11, 14) ^ __ROL4__(v11, 10) ^ v11 ^ __ROL4__(v11, 2) ^ __ROR4__(v11, 8)

def get_a_i(i):
return a[i+4] ^ get_v10(get_v11(get_v12(i)))

def solve():
import ctypes
for i in range(31, -1, -1):
a[i] = get_a_i(i)
for i in range(4):
#a[i] = ctypes.c_int(a[i]).value
print(a[i])
print('或')
for i in range(4):
a[i] = ctypes.c_int(a[i]).value
print(a[i])
print('一个是int,一个是unsigned int,反正内存中存储的十六进制是相同的')

solve()

结果有两组,分别是int和uint(内存中存储的十六进制是相同的)

img