本文最后更新于:2024年7月24日 凌晨
2021 Week 2 - 01/26
0x00 DDCTF-Android Easy
接触到的第二道安卓逆向题
下载, 发现是个 zip , 但是有 apk 的目录结构, 用 jadx-gui 打开可以看到如下的结构
很明显, 重点在 FlagActivity 类里面, i()
中将p
q
两个byte数组进行一系列操作后得到解密后的byte[] bArr2
, 作为String()
的参数返回. 之后在onClickTest()
中通过将输入的字符串同i()
的返回值进行比较, 判定 flag 是否正确.
那就很显然, i()
的返回值就是正确的 flag. 把i()
运行一次, 得到返回值DDCTF-3ad60811d87c4a2dba0ef651b2d93476@didichuxing.com
, 用flag{ }
包裹提交.
flag{DDCTF-3ad60811d87c4a2dba0ef651b2d93476@didichuxing.com}
0x01 WELCOME TO JNI
“JNI是什么?”
JNI (Java Native Interface,Java本地接口 )是一种编程框架 ,使得Java虚拟机 中的Java 程序可以调用本地应用/或库,也可以被其他程序调用。 本地程序一般是用其它语言(C 、C++ 或汇编语言 等)编写的,并且被编译为基于本机硬件和操作系统的程序。
– Wikipedia
简单的来说, JNI 可以让 Java 调用其他语言的库.
用 jadx-gui 打开 apk 文件, 定位到 Main Activity --在com.reverier.xdsec_re_20200126
下面
在MainActivity
类中, 可以看到声明了一个 native 方法 - loginUtils()
, 从名字推测是 检查 flag 登陆验证, 加载了一个本地库native-lib
, 它对应的文件在/lib
下面, 对应不同的架构.
在 33 行可以看到, loginUtil()
接受了输入的字符串作为参数, 然后返回一个布尔值作为结果, 控制输出Right
和Wrong
- 这就是重点了.
从 apk 中提取出 x86 架构对应的native-lib.so
, IDA 打开, 找到对应的方法Java_com_reverier_xdsec_1re_120200126_MainActivity_loginUtils()
, 反编译如下.
第 8 行开始, v6
保存了作为参数的字符串的长度, v5
则保存了另一个字符串的长度, v4
保存了参数字符串. 第 11 行比较两个字符串的长度, 若相等则再通过strncmp()
比较.
综上, off_1FD4 + 5972
应该就指向了flag. 0x1FD4 + 5972d == 0x880
, 跳转过去, 发现果然保存着flag.
flag{welcome_to_naive_lib!}
做完了才意识到, 其实当时直接从 IDA 的 Strings window 能直接看到这个明文字符串…
0×02 Codegate CTF 2018 RedVelvet
IDA 打开, 跳转到main()
, 发现了一大串funcX()
的调用. 有点壮观(x
观察结构发现, 在 48 行, ``fgets()接受了 28 个字节的输入(包含末尾的
\n), 保存到
s中. 而
funcX()并未改变
s的值, 而是进行了一些验证, 比如
func7()`:
这 15 个funcX()
共同对s
进行了一系列的检查, 然后计算s
的 SHA256 值, 并和0a435f46288bb5a764d13fca6c901d3750cee73fd7689ce79ef6dc0ff8f380e5
比较, 确定 flag 正确与否.
所以直接用 hashcat 穷举破解理论上倒也可行
接下来就是 angr 发挥威力的时候了, 我们不需要将程序执行完, 只需要找到一个输入, 能够满足这十五个funcX()
的约束, 使程序运行到SHA256_Init()
前即可 - 对应的地址是0x401534
.
同时, 我们还需要避免进入funcX()
中的exit(1)
的分支, 以func1()
为例.
0x4009ED
和0x4009F7
就是我们不希望运行到的地方, 因为到这里说明我们的输入没有通过func1()
的检验, 执行了exit(1)
- 其他的funcX()
同理.
这样, 我们得到了期望执行到的地址与要避免的地址, 写出如下脚本.
1 2 3 4 5 6 7 8 9 10 11 12 import angr prog = angr.Project('./RedVelvet' , load_options={'auto_load_libs' : False }) state = prog.factory.entry_state() simgr = prog.factory.simgr(state) simgr.explore(find=0x00401534 ,avoid=[0x4009ED ,0x4009F7 ,0x400A3C ,0x400A46 ,0x400A9F ,0x400B01 ,0x400B5C ,0x400C05 ,0x400CAB ,0x400D51 ,0x400DD6 ,0x400E5E ,0x400F07 ,0x400FAD ,0x4105F ,0x4010E9 , 0x40119D ]) flag = simgr.found[0 ].posix.dumps(0 )print (flag)
经过漫长的运行( VMWare Ubuntu + Docker angr/angr 大概 30 分钟? ), 我们得到了如下输出 (fg
是因为我之前误以为写错了, 于是挂起去检查脚本了…)
放到源程序里检查一下, 看来没毛病.
flag{What_You_Wanna_Be?:)_la_la}
Something Else
RedVelvet依赖 1.0.0 版本的 libcrypto.so , 但是包含它的老版本的 openssl 已经过时了, 最后用apt-file
查到英伟达的nslight-system
还带这东西, 于是安装之后手动复制出来…
理论上通过 15 个funcX()
中的约束条件, 可以直接求出来满足的输入值, 就像z3
那样
如果限定输入长度与范围( ASCII 可见字符) 的话, 应当能够跑的更快, 学习中
关于原题: 暂时没找到…
2021 Week 3 - 02/10
0x00 PyDis
一道 Python 逆向题, 本来想直接用 uncompyle6 来着, 不过它目前只能支持到 3.8 …
不过我们总是可以自己手动来解决 √ 从零开始, 半天过去了
根据提示, 用 marshal
解析 pyc 文件, 提取出指令部分, 然后用 dis
反编译, 拿到字节码指令. 根据 Python文档 , 我们可以写出同义的代码, 如下.
(#
开头的是我们推测出的每行对应的源代码, 缩进一致)
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 1 0 BUILD_LIST 0 2 LOAD_CONST 0 ((178 , 184 , 185 , 191 , 182 , 165 , 174 , 191 , 129 , 183 , 187 , 176 , 129 , 169 , 191 , 167 , 163 )) 4 CALL_FINALLY 1 (to 7 ) 6 STORE_NAME 0 (magic) 2 8 LOAD_NAME 1 (input ) 10 LOAD_CONST 1 ('flag >>> ' ) 12 CALL_FUNCTION 1 14 STORE_NAME 2 (inp) 4 16 LOAD_NAME 3 (list ) 18 LOAD_NAME 2 (inp) 20 CALL_FUNCTION 1 22 STORE_NAME 4 (flag) 5 24 LOAD_NAME 5 (len ) 26 LOAD_NAME 4 (flag) 28 CALL_FUNCTION 1 30 LOAD_NAME 5 (len ) 32 LOAD_NAME 0 (magic) 34 CALL_FUNCTION 1 36 COMPARE_OP 3 (!=) 38 POP_JUMP_IF_FALSE 54 6 40 LOAD_NAME 6 (print ) 42 LOAD_CONST 2 ('qwq' ) 44 CALL_FUNCTION 1 46 POP_TOP 7 48 LOAD_NAME 7 (exit) 50 CALL_FUNCTION 0 52 POP_TOP 9 >> 54 LOAD_NAME 8 (range ) 56 LOAD_NAME 5 (len ) 58 LOAD_NAME 4 (flag) 60 CALL_FUNCTION 1 62 LOAD_CONST 3 (2 ) 64 BINARY_FLOOR_DIVIDE 66 CALL_FUNCTION 1 68 GET_ITER >> 70 FOR_ITER 54 (to 126 ) 72 STORE_NAME 9 (i) 10 74 LOAD_NAME 4 (flag) 76 LOAD_CONST 3 (2 ) 78 LOAD_NAME 9 (i) 80 BINARY_MULTIPLY 82 LOAD_CONST 4 (1 ) 84 BINARY_ADD 86 BINARY_SUBSCR 88 LOAD_NAME 4 (flag) 90 LOAD_CONST 3 (2 ) 92 LOAD_NAME 9 (i) 94 BINARY_MULTIPLY 96 BINARY_SUBSCR 98 ROT_TWO 100 LOAD_NAME 4 (flag) 102 LOAD_CONST 3 (2 ) 104 LOAD_NAME 9 (i) 106 BINARY_MULTIPLY 108 STORE_SUBSCR 110 LOAD_NAME 4 (flag) 112 LOAD_CONST 3 (2 ) 114 LOAD_NAME 9 (i) 116 BINARY_MULTIPLY 118 LOAD_CONST 4 (1 ) 120 BINARY_ADD 122 STORE_SUBSCR 124 JUMP_ABSOLUTE 70 12 >> 126 BUILD_LIST 0 128 STORE_NAME 10 (check) 14 130 LOAD_NAME 8 (range ) 132 LOAD_NAME 5 (len ) 134 LOAD_NAME 4 (flag) 136 CALL_FUNCTION 1 138 CALL_FUNCTION 1 140 GET_ITER >> 142 FOR_ITER 26 (to 170 ) 144 STORE_NAME 9 (i) 15 146 LOAD_NAME 10 (check) 148 LOAD_METHOD 11 (append) 150 LOAD_NAME 12 (ord ) 152 LOAD_NAME 4 (flag) 154 LOAD_NAME 9 (i) 156 BINARY_SUBSCR 158 CALL_FUNCTION 1 160 LOAD_CONST 5 (222 ) 162 BINARY_XOR 164 CALL_METHOD 1 166 POP_TOP 168 JUMP_ABSOLUTE 142 17 >> 170 LOAD_NAME 8 (range ) 172 LOAD_NAME 5 (len ) 174 LOAD_NAME 0 (magic) 176 CALL_FUNCTION 1 178 CALL_FUNCTION 1 180 GET_ITER >> 182 FOR_ITER 34 (to 218 ) 184 STORE_NAME 9 (i) 18 186 LOAD_NAME 10 (check) 188 LOAD_NAME 9 (i) 190 BINARY_SUBSCR 192 LOAD_NAME 0 (magic) 194 LOAD_NAME 9 (i) 196 BINARY_SUBSCR 198 COMPARE_OP 3 (!=) 200 POP_JUMP_IF_FALSE 182 19 202 LOAD_NAME 6 (print ) 204 LOAD_CONST 2 ('qwq' ) 206 CALL_FUNCTION 1 208 POP_TOP 20 210 LOAD_NAME 7 (exit) 212 CALL_FUNCTION 0 214 POP_TOP 216 JUMP_ABSOLUTE 182 22 >> 218 LOAD_NAME 6 (print ) 220 LOAD_CONST 6 ('happy new year!' ) 222 CALL_FUNCTION 1 224 POP_TOP 226 LOAD_CONST 7 (None ) 228 RETURN_VALUE
之后写个jio本解密就行了.
1 2 3 4 5 6 7 8 9 10 11 12 enc = [178 , 184 , 185 , 191 , 182 , 165 , 174 , 191 , 129 , 183 , 187 , 176 , 129 , 169 , 191 , 167 , 163 ] dec = "" for i in range (len (enc)): enc[i] = enc[i] ^ 222 for i in range (len (enc) // 2 ): dec += chr (enc[2 *i+1 ]) dec += chr (enc[2 *i])print (dec)
flag{hapi_new_ya
, 记得补上 }
0x01 FlareOn4 IgniteMe
die
查壳, 确认 32 位 pe 文件, 没壳, IDA 打开, 定位到入口点 start
函数.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 void __noreturn start () { DWORD NumberOfBytesWritten; NumberOfBytesWritten = 0 ; stdinHandle = GetStdHandle(4294967286u ); stdoutHandle = GetStdHandle(0xFFFFFFF5 ); WriteFile(stdoutHandle, aG1v3M3T3hFl4g, 0x13 u, &NumberOfBytesWritten, 0 ); praseInput(); if ( sub_401050() ) WriteFile(stdoutHandle, aG00dJ0b, 0xA u, &NumberOfBytesWritten, 0 ); else WriteFile(stdoutHandle, aN0tT00H0tRWe7r, 0x24 u, &NumberOfBytesWritten, 0 ); ExitProcess(0 ); }
GetStdHandle
函数没见过, 查了一下文档 , 是获得设备句柄, 参数决定是标准 输入/输出/错误 设备.
在第 8 行输出提示之后调用了一个函数, 推测是解析输入 - 同时确定的输入缓冲区的第一个零字节的位置作为结尾. 然后在第 10 行调用sub_401050
检验 flag,
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 int sub_401050 () { int v1; int i; unsigned int j; char key; v1 = getFirstZeroBytePos((int )prasedInput); key = sub_401000(); for ( i = v1 - 1 ; i >= 0 ; --i ) { encIn[i] = key ^ prasedInput[i]; key = prasedInput[i]; } for ( j = 0 ; j < 39 ; ++j ) { if ( encIn[j] != (unsigned __int8)enc[j] ) return 0 ; } return 1 ; }
是一个简单的循环异或加密, 不过注意是反向的, 初始的key
是0x4
. 写个jio本解密, 拿到 flag.
flag{R_y0u_H0t_3n0ugH_t0_1gn1t3@flare-on.com}
还真是晚上做的…
0x02 BUUCTF Firmware
看题目是一个路由器固件的分析, 用 binwalk 扫描一下, 得到了文件系统的位置, 也就是提取出来的120200.squashfs
文件.
用 file
命令确认了一下, 是 squashfs 文件系统, 不过不能直接用mount
挂载 - 可能是因为有 lzma 压缩了…
在网上搜索了一下, 得知 firmware-mod-kit (简称 fmk) 可以从 squashfs 提取文件, 不过在我这里总是提取失败, 或者返回成功但是啥也没提取出来… 也许是因为 fmk 从 2013 年之后没更新过吧…
最后用 unsquashfs
提取成功 (是 squashfs-tools 下的一个工具), 在 /tmp
下发现一个 backdoor
文件…还好这名字挺明显…
确认是 32 位的 ARM ELF 文件, die 检查发现 upx 壳, 脱壳之后 IDA 打开, 定位到 main
.
在前面获取 MAC 等等之后, 这个 initConnection
函数引起了我们的注意, 尤其是这里.
1 2 3 4 5 6 7 8 9 10 11 while ( 1 ) { while ( initConnection() ) { puts ("Failed to connect..." ); sleep(5u ); } }
在initConnextion
返回不为零时, 会等待五秒再重来 - 这里应该就是回连服务器了. 进去看看, 确定地址echo.byethost51.com
, 端口36667
.
MD5(echo.byethost51.com:36667) == 33a422c45d551ac6e4756f59812a954b
所以, flag{33a422c45d551ac6e4756f59812a954b}
最后, 祝大家新年快乐🍻
2021 Week 6 - 03/05
前两周因为题不会(好多知识盲区.jpg)+准备返校,只做出来了两道…不过看题解学到了很多,继续努力
0×00 Hacking with Google 2020 Beginner
已经理解了整个过程, 不过还没有拿到 flag… angr 也没有跑出来正确的结果, 可能是没能正确识别 SSE 的函数…?
跑出来是这样…
等我再研究研究, 或许直接用 z3 会比较适合?
0×01 V&N 公开赛 CSRe
看题目是混淆过的 C#, 查了一下, 可以用 detdot的修改版 反混淆, 之后再用 ILSpy 反编译.
一个类一个类地找, 很快就能发现 Class3.Main
方法, 如下.
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 using System;using System.Security.Cryptography;using System.Text;internal sealed class Class3 { public string method_0 (string string_0, string string_1 ) { string text = string .Empty; char [] array = string_0.ToCharArray(); char [] array2 = string_1.ToCharArray(); int num = ((array.Length < array2.Length) ? array.Length : array2.Length); for (int i = 0 ; i < num; i++) { text += array[i] ^ array2[i]; } return text; } public static string smethod_0 (string string_0 ) { byte [] bytes = Encoding.UTF8.GetBytes(string_0); byte [] array = SHA1.Create().ComputeHash(bytes); StringBuilder stringBuilder = new StringBuilder(); byte [] array2 = array; foreach (byte b in array2) { stringBuilder.Append(b.ToString("X2" )); } return stringBuilder.ToString(); } private static void Main (string [] args ) { if (!Class1.smethod_1()) { return ; } bool flag = true ; Class3 @class = new Class3(); string text = Console.ReadLine(); if (smethod_0("3" + text + "9" ) != "B498BFA2498E21325D1178417BEA459EB2CD28F8" ) { flag = false ; } string text2 = Console.ReadLine(); string string_ = smethod_0("re" + text2); string text3 = @class.method_0(string_, "63143B6F8007B98C53CA2149822777B3566F9241" ); for (int i = 0 ; i < text3.Length; i++) { if (text3[i] != '0' ) { flag = false ; } } if (flag) { Console.WriteLine("flag{" + text + text2 + "}" ); } } }
flag 一共有两端, 其中text
直接就是B498BFA2498E21325D1178417BEA459EB2CD28F8
的 SHA1 原文.
对于 text2
, 观察method_0
, 发现它会返回两个 string 类型参数的异或值, 而之后的 for 循环会比较异或后的字符串每个字符是否均为"0"
- 很明显, x ^ x == 0
, 所以"re" + text2
的 SHA1 值就是 63143B6F8007B98C53CA2149822777B3566F9241
.
cmd5 上查询, 得到 text = "1415"
, text2 = "turn"
, 最后得到 flag.
flag{1415turn}
0×02 Zer0pts2020 easy-strcmp
“有时候你看见的不一定是真实的”
– RX
IDA64 打开, 定位到main
函数, 发现 flag 就摆在眼前 (误
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 __int64 __fastcall main (int a1, char **a2, char **a3) { if ( a1 > 1 ) { if ( !strcmp (a2[1 ], "zer0pts{********CENSORED********}" ) ) puts ("Correct!" ); else puts ("Wrong!" ); } else { printf ("Usage: %s <FLAG>\n" , *a2); } return 0LL ; }
怎么看第五行的那东西都不能是 flag 吧
我们发现它确实调用了一个strcmp
比较argv[0]
和那东西 (就叫假 flag 吧) 的值, 但是它又确实不是 flag… 看看左侧, 发现还有两个奇怪的函数 sub_6EA
和sub_795
.
1 2 3 4 5 6 7 8 9 10 int (**sub_795())(const char *s1, const char *s2) { int (**result)(const char *, const char *); result = &strcmp ; strcmp_pointer = (__int64 (__fastcall *)(_QWORD, _QWORD))&strcmp ; off_201028 = sub_6EA; return result; }
(srecmp_pointer
是我重命名的)
1 2 3 4 5 6 7 8 9 10 11 12 13 __int64 __fastcall sub_6EA (__int64 a1, __int64 a2) { int i; int v4; int j; for ( i = 0 ; *(_BYTE *)(i + a1); ++i ) ; v4 = (i >> 3 ) + 1 ; for ( j = 0 ; j < v4; ++j ) *(_QWORD *)(8 * j + a1) -= qword_201060[j]; return strcmp_pointer(a1, a2); }
但是我们并没有在main
中看到这两个函数的调用, 看一下交叉引用, 发现这两个函数都在.init_array
段里 - 会在main
前就执行.
sub_795
会把在.got.plt
段中原先正常的strcmp
的地址替换成sub_6EA
的地址, 而sub_6EA
会把第一个参数按照每 8 个字符一组, 减去qword_201060[j]
后再和假 flag 进行真正的strcmp
…
既然这样, 写个 jio 本 - 看上去是这样.
1 2 3 4 5 6 7 8 9 10 11 12 key = [0x42 , 0x09 , 0x4A , 0x49 , 0x35 , 0x43 , 0x0A , 0x41 , 0xF0 , 0x19 , 0xE6 , 0x0B , 0xF5 , 0xF2 , 0x0E , 0x0B , 0x2B , 0x28 , 0x35 , 0x4A , 0x06 , 0x3A , 0x0A , 0x4F ] enc = "********CENSORED********" dec = "" for i in range (len (enc)): print (c % 256 , end=" " ) dec = dec + str (chr (c % 256 ))print ("" )print (dec) dec = ""
得到的结果是l3ts_m4k3^4^DDSOUR_t0d4y
, 中间部分看上去不太对… 原因在于, 原程序是将整个QWORD
作为一个整体进行加减, 但我们的 exp 是对每一个字节进行的计算, 这样进位不会影响到前一位.
进位影响的是 9 11 13 14 这几个位置, 手动加一就好.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 key = [0x42 , 0x09 , 0x4A , 0x49 , 0x35 , 0x43 , 0x0A , 0x41 , 0xF0 , 0x19 , 0xE6 , 0x0B , 0xF5 , 0xF2 , 0x0E , 0x0B , 0x2B , 0x28 , 0x35 , 0x4A , 0x06 , 0x3A , 0x0A , 0x4F ] enc = "********CENSORED********" dec = "" for i in range (len (enc)): if i in (9 , 11 , 13 , 14 ): c = ord (enc[i]) + key[i] + 1 else : c = ord (enc[i]) + key[i] print (c % 256 , end=" " ) dec = dec + str (chr (c % 256 )) print ("" )print (dec) dec = ""
zer0pts{l3ts_m4k3_4_DETOUR_t0d4y}
不过在平台上提交的时候应该是flag{l3ts_m4k3_4_DETOUR_t0d4y}