duangsuse::Echo
717 subscribers
4.26K photos
130 videos
583 files
6.48K links
import this:
美而不丑、明而不暗、短而不凡、长而不乱,扁平不宽,读而后码,行之天下,勿托地上天国。
异常勿吞,难过勿过,叹一真理。效率是很重要,盲目最是低效。
简明是可靠的先验,不是可靠的祭品。
知其变,守其恒,为天下式;穷其变,知不穷,得地上势。知变守恒却穷变知新,我认真理,我不认真。

技术相干订阅~
另外有 throws 闲杂频道 @dsuset
转载频道 @dsusep
极小可能会有批评zf的消息 如有不适可退出
suse小站(面向运气编程): https://WOJS.org/#/
Download Telegram
🤔继续往下分析动态字符数组栈上分配...
邪恶的 duangsuse 居然把这个依赖 DAG 收起来了... 🤔
duangsuse::Echo
邪恶的 duangsuse 居然把这个依赖 DAG 收起来了... 🤔
你们这个是个什么口令生成算法啊,你们这是雷普前端开发者们啊你们这个算法。
麻烦你们,真的太过分了你们搞这个『加密算法』干什么,我的项目每一行代码都过不了那个 lint 检查啊,它现在没法连上你们的 API,你叫我项目怎么办啊,它还事个幼儿园基本的弱智应用好不好。

你们这是什么算法啊,哼!哼!啊啊啊啊啊啊啊。你们害死我项目了谁是出要写这种东西主意的快出来你们写代码的,再不出来我 ****** 了啊!

我跟你们说你们这群 Android 开发者啊,一天到晚搞什么这些加密啊代码保护啊会害死你们的(迫真)
你们没有前途我跟你们说你们这400多号人好好写 Jawa 不好么,一天到晚 show C/C++ 有什么意思,有什么意思啊,我偶尔写一下都快被恶心死了。

麻烦你们重视一下你们的 Jack 有点发展目标好不好,一天到晚写写 Rx 写写 Vertx 写写 Databinding 不是人啊,你们一天到晚搞 C++ 不好好写 pattern 没有前途的,NDK 害死人。

#Haha #security #Android #dev 😤
coolapk_a_kdev_f.zip
178.1 KB
#security #share #tools #coolapk 🤔 还没有测试过,或许能用
🤔目前生成的 token 还不能让酷安服务器满意
2a6e2adc2897c8d8133db17c2cd3b1045834ce58-d7d5-38eb-95d5-563167a1983d0x588f16cd

(第一部分是机器代码计算的校验和(用到了 base64 和 MD5),我花了两天用 C 重写了,第二部分是 UUID,第三部分是当前 UNIX 时间的十六进制表示)

7fa737c3bc4e80f1381d3c7e1bfdd40d47b11e0b-97b6-454b-9831-ccaff8f19f020x5c6a5245

🤔 哦... 我记得看汇编的时候的确可能拼接了这个 0x, 不过因为不是用 strcat 所以我没加,加上试试
还是不行,
duangsuse::Echo
2a6e2adc2897c8d8133db17c2cd3b1045834ce58-d7d5-38eb-95d5-563167a1983d0x588f16cd (第一部分是机器代码计算的校验和(用到了 base64 和 MD5),我花了两天用 C 重写了,第二部分是 UUID,第三部分是当前 UNIX 时间的十六进制表示) 7fa737c3bc4e80f1381d3c7e1bfdd40d47b11e0b-97b6-454b-9831-ccaff8f19f020x5c6a5245 🤔 哦... 我记得看汇编的时候的确可能拼接了这个…
我用 UUIDParser 弄了一下,发现给的 UUID 其实都有区别(一个是 name based 一个是 time based)... 最讨厌的就是有时候动态分析很麻烦 🤔

我看这逻辑也挺 plain 的,继续分析,Ruby 重写一下 emmmm...

bd 就是 base64 decode(dst, src)
be 就是 base64 encode(dst, src, srclen)

me 就是 MD5 based message digest(dst, src)

r 就是 reverse string(s)

🤔 好像虽然不是很能完全理解,但是基于调试符号的帮助,重写也不是很困难,我得想想这些名字都会代表什么
像这种反汇编对编译器学习者就很有用(
== /Users/kjsolo/StudioProjects/
== CoolLibrary/app/src/main/jni/a.c

尝试尽可能还原代码原貌,利用从 DWARF 调试符号里以及反汇编分析获得的信息。

void r(char *s) // L13
{ // L14
int length = strlen(s); // L15
int c, i, j; // L16

for (i = 0, j = length - 1; i < j; i++) // L18
{
c = s[i]; // L20
s[i] = s[j]; // L21
s[j] = c; // L22
j--;
} // L24
}

void bd(char *dst, const char *src) // L27
{
BD(dst, src);
}


void me(char *dst, const char *src) // L33
{
unsigned char *digest[16]; // L35
MD5_CTX context; // L36
MI(&context);
MU(&context, src, strlen(src));
MF(digest, &context);

for (int i = 0; i <= 15; i++) {
sprintf(dst[i + i], "%02x", digest[i]);
}
}


void be(char *dst, const char *src) // L47
{
int src_len = strlen(src);

BE(dst, src, src_len);
}

虽然看起来没用(其实也没多大用,大嘘)
不过很好玩的样子(迫真)

我居然彻底还原了原项目的代码风格布局... 细思恐极
现在 duangsuse 在 Radare2 的帮助下已经能模式识别很多 GCC 4.9 汇编模式了

马上,duangsuse 将顺路教大家一些阅读 radare2 自动分析反汇编代码的技巧。 #reveng #Learn
duangsuse::Echo
🤔 Sticker
我们来对照一下这些代码和他们的汇编形式,让我们自己也来当一回 GCC 4.9: 🤔

🐔为什么要手动『编译』一些源代码呢? #Compiler #CS #PL #backend #reveng
为什么要手动分析一些反汇编代码呢?
人不是生来就是 native 编译器,所以我们要写汇编,要了解各种实体机,X86、ARM、MIPS、AVR,看各种反汇编让自己能成为编译器(虽然依然不支持向量化和某些高等编译优化诸如逃逸分析指针别名还有更高级的寄存器分配算法,哭哭)。

有时候或许做梦能梦到自己变成编译器,正在拿着一张 native 代码的草稿图纸后序遍历游走在 AST(Abstract Syntax Tree)的大森林之中... 🤔
或许我今天的任务是为一个叫 CoolLibrary 的项目以 linux-android 的格式生成 i686 机器代码
我的前端们,预处理器、分词器、解析器,他们处理项目结构,他们处理符号定义替换、展开宏、保证了各个编译单元的独立解耦... 然后他们把我需要的东西 — AST 喂给我,而我则会处理 AST,告诉他们翻译生成的程序或者哪里有错误

我走过各种个样的子树 — 有字面量、常量、变量、指针、数组、控制结构、代码块、子程序定义、子程序调用,他们有各种各样自己的属性、flag,有时候我从背包里找出图纸判断他们是否真的应该在这,有时候我会确认他们的属性是否设置得当,有时候我拿出笔记本,记录下他们的存在以便以后查看
K&R C、C89(ANSI C)、C99、C11... 这些冗长生硬的定义通通不是我的实际写照,我只是一个简单的小工具,我负责把你们的一行行容易理解的行为代码用另外一种更低级、更底层,能让实际机器开心的方式表述出来
我所认识并理解的是一门古老的『编程语言』,它的名字叫 C,源自 BCPL,而 BCPL 又源自 CPL,BCPL 已经是 20 世纪 60 年代的东西了,即使这样也没有被 2018 年的你们抛弃?

有时候我会发现森林中暗含的危险路径,打破了抽象类型系统的限制,我就会回溯停止代码生成工作,忠实地汇报错误。
有时候我能走完这个森林,并且完成有时候会一起进行优化和低级化代码的任务,生成那些被你们认为是黑盒的东西,你们认为它牢不可破,可是我知道,即便是使用我的同道 packer / obfuscator 再对我生成的程序进行处理,也永远有人能看透我翻译出的机械逻辑背后藏着的东西,因为我... 我只是个工具
如果你不知道我对你的代码做了些什么,又怎么能看到我留下的『可执行』数据背后的秘密?

如果我是编译器,管它是 JIT 即时编译器还是 AOT 编译器,这真是最吼的。 🤔🐸(尬膜)

一言不合就念诗(划掉)

== 第一个例子,简单到只包含调用的函数
void bd(char *dst, const char *src)
{
BD(dst, src);
}

== 第二个例子,普通一点的包含本地变量的函数
void be(char *dst, const char *src)
{
size_t src_len = strlen(src);

BE(dst, src, src_len);
}

== 第三个例子,一个字符串倒序程序,它包含本地变量和一个循环控制结构
void r(char *s) {
int length = strlen(s);
int c, i, j;

for (i = 0, j = length -1; i < j; i++)
{
c = s[i];
s[i] = s[j];
s[j] = s[i];
j--;
}
}

== 最后一个例子,逻辑稍微长一点的程序,同上

void me(char *dst, const char *src)
{
unsigned char *digest;
MD5_CTX context;
MI(&context);
MU(&context, src, strlen(src));
MF(digest, &context);

for (int i = 0; i <= 15; i++)
sprintf(dst[i * 2], "%02x", digest[i]);
}
duangsuse::Echo
我们来对照一下这些代码和他们的汇编形式,让我们自己也来当一回 GCC 4.9: 🤔 🐔为什么要手动『编译』一些源代码呢? #Compiler #CS #PL #backend #reveng 为什么要手动分析一些反汇编代码呢? 人不是生来就是 native 编译器,所以我们要写汇编,要了解各种实体机,X86、ARM、MIPS、AVR,看各种反汇编让自己能成为编译器(虽然依然不支持向量化和某些高等编译优化诸如逃逸分析指针别名还有更高级的寄存器分配算法,哭哭)。 有时候或许做梦能梦到自己变成编译器,正在拿着一张…
前端的解析(这里是广义的,就是说输入源代码字符流输出 AST)比较机械化,而且和重点无关,不讲。

我们来看看这几个小例子编译之后(在 liba.so 里)是什么样的(反汇编 & 自动化分析由 Radare 2 提供 <3) 🤔

void bd(char *dst, const char *src)

sym.bd (int arg_8h, int arg_ch, int arg_14h);

arg int arg_8h @ ebp+0x8
arg int arg_ch @ ebp+0xc
var int var_4h @ esp+0x4
arg int arg_14h @ esp+0x14

push ebp
mov ebp, esp
push ebx
lea esp, [esp - 0x14]
call sym.__x86.get_pc_thunk.bx
add ebx, 0x23fb
mov eax, dword [arg_ch]
mov dword [var_4h], eax
mov eax, dword [arg_8h]
mov dword [esp], eax
call sym.BD
lea esp, [arg_14h]
pop ebx
pop ebp
ret

首先我们这里是逆向分析练习,所以还是有源代码的,但是那只是为了方便起见,这里我假设你猜到了 bd 是 base64 decode 的意思,然后其他的都不知道

为 r2 自动分析出的东西起个好名字是很重要的,因为很少有人能把那么多 arg_ch / arg_8h 这样基于偏移量的名字记好,分析程序逻辑的时候你需要知道哪是哪(当然 r2 有时候也会根据调用的函数自动推导出一些名字出来,但那很
差)

在这个函数 bd 里,我们可以直接一步一步向下走(其实阅读时有很多的技巧,比如可以由结果往参数从后向前看,但这都是自己积累出来的,别人没法喂给你)

== push ebp

考虑一下你是怎么调用别人的函数的:

+ mov eax, 0xfa ; 15(0xf) * 16(16 ^ 1) + 10(a)
+ mov ebx, dword [ebp+0xc] ; your arg2
+ mov dword [esp+0x4], ebx ; func arg2
+ mov dword [esp], eax ; func arg1
+ call func


1. 啥是 0xfa,啥是 fah
0xfa 就是 16 进制 fa、fah(hex) 也是 16 进制 fa
btw. 0b101010 就是二进制 101010 / 0o01234567 则是八进制 01234567

十六进制有 0123456789abcdef 这 16 个数位、二进制有 01 这两个数位
进制换算是高中数学的基础内容... 🤔 当然,就 RE (逆向工程)来说,好像这也不是非会不可的,建议还是了解一下
不然也可以用 irb 这样的计算器(大雾)帮你换算 #Ruby (不会告诉你们我把 Ruby 当计算器用)

2. mov 是啥?
mov 是 x86 里的数据转送之令,后面你看到的 mov 都是带两个参数的『二元指令』『二地址指令』mov
mov dst, src 它负责把指定数据对象从 src 复制到 dst

= mov ebp, esp 可以理解为 ebp = esp
= mov dword [esp+4], eax 可以理解为把寄存器 eax 的值存到 [esp+4] 这一个双字(64 位)的内存地址里去
= mov eax, dword [ebp+8] 可以理解为把 [ebp+8] 这一个双字的数据转送(放)到 eax 寄存器里

== lea 又是啥
理解成 mov 就行,实际上很多时候使用偏移量的 mov 和 lea 都可以视作一样的
如果你是那种要知道茴香豆的茴字有几种写法的人,这里,page 327

== push 是啥 call 又是啥
push 1 就是让栈指针 esp 递减 sizeof(1),然后 mov [esp], 1
call
就是先 push eip,然后再把 eip(程序计数器)指向某个存储器地址

push 3
push 2
push 1
call addup_3
add esp, 0xc

push eax
push "%s\n"
call printf

ret
则反 call 其道行之,它 poppush 的对应操作)出 return addresss 然后设置 IP(指令指针)
= add / sub 是啥
add eax, 1 <=> eax += 1
sub eax, 1
<=> eax -= 1

{顺便,我们可以分析一下参数到底是怎么传的:}

3. 汇编是啥
一句话:汇编是机器代码的助记符
所以说自动反编译现在一些高大上编译器生成的代码几乎不能看,但是汇编却往往很值得一看(当然之中也存在 false disassembly 这种干扰反汇编器的东西,不过现在逆向工程有了自动化分析辅助已经可以自动检测出很多反逆向工程的『花指令』了,所以说汇编还是很诚实的)
有些简单的汇编甚至可以被类似扫描器的算法线性弄成机器代码,可见他们有多相似

汇编是一种低级语言,你很少能看到有某种汇编存在这高级一点的结构(比如嵌套代码块)
汇编的结构可以认为是线性的,很像基于指令流的虚拟机实际执行时代码的样子

比如(伪代码)

if (jzm !is null) {
println "🐸";
}

可以用一种比较通用的 AST 结构表述

(IfBranch (Not
(Is (Variable "jzm") (null)))) {
(FunCall "println") { StringLiteral("🐸") }
}

翻译成等价的汇编逻辑,就变成这样的了

0 getstatic "jzm"
1 is? null
2 not
3 branchnot 7
4 ldstr "🐸"
5 getstatic "println"
6 call
7 halt

汇编就是现代电子计算机底层一点的通用语言(当然是特指某种汇编,比如 arm 对 arm, x86 对 x86,有时候同一个处理器 family 族还会存在不同的指令支持差异,这也就是 i386 不能安装 i686 的一些软件包的原因)

4. ebx / esp / ebp / eax 又是啥
X86 是个基于寄存器(而不是类似 JVM 的栈)而运作的解释器,比如,假设我们要打印 0 到 101 这 102 个数,那么程序可以这么写:

X86 里一般会用到的寄存器有这些:
eax ebx ecx edx
esi edi
esp ebp
eip

他们真正的名字是去掉前面那个 e 的,也就是说 ax、bx、cx(regiester)、dx(register)...
不过 X86 不是上面那么搞的,因为 80386 之前 Intel 也弄过其他的处理器,所以,AX 在 386 机上面表示某通用寄存器的低位(类似 256 这个十进制数的个位 6),用以和之前的代码兼容....

== bit / byte / halfword / word / doubleword 是什么
电子计算机计算机内部使用二进制(虽然好像再看低一点就成数字电路里的电位高低了)来表示数字
一个 bit 是一个二进制数位。一个 byte 是 8 个 bit、一个 halfword(半字)是 2 个 byte、一个 word (字)是四个 byte、一个 doublebyte(双字)是八个 byte


早些时候,很多电子计算机包括一些嵌入式设备啊/// 游戏机啊 //// 都是 16 位甚至 8 位的
这意味着至少他们计算的精度和容量很不行(比如,如果让 8 个二进制位保存有符号的数字的话,最多能保存 2 ** (8 - 1) 个有效数字,也就才 128 位,不多不多,刚刚好连现在很多聊天软件消息列表的长度都保存不了

80306 机是 32 位的,而 x64 就是扩展到默认 64 位的 386 机而已,感谢上帝吧(迫真)
要不然 AS 这种很吃内存的 IDE 不使用 PAE(物理地址扩展)只有 4G 内存用起来.... 呃
duangsuse::Echo
前端的解析(这里是广义的,就是说输入源代码字符流输出 AST)比较机械化,而且和重点无关,不讲。 我们来看看这几个小例子编译之后(在 liba.so 里)是什么样的(反汇编 & 自动化分析由 Radare 2 提供 <3) 🤔 void bd(char *dst, const char *src) sym.bd (int arg_8h, int arg_ch, int arg_14h); arg int arg_8h @ ebp+0x8 arg int arg_ch @ ebp+0xc var int…
== 顺便,我们可以分析一下参数到底是怎么传的:
bd 函数里,有一些代码是用来维护调用栈的
push ebp
mov ebp, esp
...
pop ebp
ret

|top(esp)| - |base(ebp)| <- 假设这是我们的机器栈,一个方格是一个内存单元,其中 (.) 的数目为单元的字节数
+ push ebp
|t| |link| |b|
+ mov ebp, esp
|tb| |link|
然后我们要调用一个函数,怎么办?
printf("Hello, %s", "world\n");

void printf(const char *fmt, ...);

假设我们为第一个参数分配地址(在只读的 .rodata 段,这里比较 naive,只假设它的地址是 0x23333333
递减我们的栈(有时候也可以 push,不过这里方便学习就先上好看点的)
sub esp, sizeof(char *) ; = 4 (byte)
|t| |....| |b| |link|
注意中间那个,我们分配(就是保证某个空间存在并且由某对象独有)出了一段新的栈空间,它的大小是一个 size_t (void *) 指针的大小
然后我们还得再来一次,为第一个参数分配空间
sub esp,
4

|t| |....| |....| |b| |link|
好了,那现在参数的空间分配完了,子程序 printf 应该可以使用(它的)ebp+0x8 访问第一个参数, ebp+0xc 访问它的第二个参数了(ebp+0x0 是 link,也就是 push ebp 的上一层栈基地址,ebp+0x4 又是 return address)

初始化这些空间:
lea [esp+4], "World\n"
lea [esp], 0x23333333 ; "Hello, %s"

|t| |*"Hell...| |*"Worl...| |b| |link|
然后执行调用
call printf

|t| |ret_addr| |*"Hell...| |*"Worl...| |b| |link|
子程序会设置自己的栈帧
push ebp
mov ebp, esp
<t> <b> |link->c| |ret_addr| |*"Hell...| |*"Worl...| |$$c| |link| |ret_addr| ...
最后它做完自己的事情之后
pop ebp
<t> |ret_addr| |*"Hell...| |*"Worl...| <b> |link| |ret_addr| ...
ret
<t> |*"Hell...| |*"Worl...| <b> |link| |ret_addr| ...
又回到你自己的程序,然后我们得做一件事情 — 收拾调用完 printf 的现场
add esp, 2*sizeof(void *)
|*"Hell...| |*"Worl...| <t> <b> |link| |ret_addr| ...
一切又回到从前。

以上是现在比较通用的 CDEF 调用标准,当然还有 stdcall 这种 M$ 的标准... 🤔
目前我们逆向工程需要用的是 CDEF,你们记得怎么通过静态分析找出函数参数(很简单的模式)就可以了
duangsuse::Echo
前端的解析(这里是广义的,就是说输入源代码字符流输出 AST)比较机械化,而且和重点无关,不讲。 我们来看看这几个小例子编译之后(在 liba.so 里)是什么样的(反汇编 & 自动化分析由 Radare 2 提供 <3) 🤔 void bd(char *dst, const char *src) sym.bd (int arg_8h, int arg_ch, int arg_14h); arg int arg_8h @ ebp+0x8 arg int arg_ch @ ebp+0xc var int…
那么我们得认识一个新的编译器,起名叫做 DCC (duangsuse C Compiler) 🐱

这个编译器会 walk bd 的 AST Tree,生成代码,然后我们会把这些代码和 GCC 4.9 的代码进行比对(跑

首先,DCC 的前端会解析源代码,得出 AST Tree

(subroutine "bd"
(argtypes
(ptr (prim "char")) (constptr (prim "char")))
(argnames "dst" "src")
(body
(call "BD" (var "dst") (var "src"))))

这是他得出的树 🐱

然后,DCC 会 Walk (Travelse)这个 Tree,生成机器代码:

Ct: toplevel
walk-toplevel (subroutine "bd"
bd:
push ebp
mov ebp, esp

Ct: subroutine be(char *dst, char *src)
walk-block walk-basicblock (body

walk-stmt (call "BD" (var "dst") (var "src"))

arg1 = emit-var emit-arg : alloc_gen_reg
  mov ebx, [ebp+0x8]
emit-push arg1
push ebx

arg2 = emit-var emit-arg : alloc_gen_reg
mov ecx, [ebp+0xc]
emit-push arg2
push ecx

emit-call
call BD
add esp, size_ptr
add esp, size_ptr

pop ebp
ret

最后 蠢萌(删除)的 DCC 会回溯收起 code stack,得出最终的代码:

bd:
push ebp
mov ebp, esp

mov ebx, [ebp+0x8]
push ebx
mov ecx, [ebp+0xc]
push ecx
call BD

add esp, 4
add esp, 4
pop ebp
ret
duangsuse::Echo
像这种反汇编对编译器学习者就很有用(
教大家个招:

一段简单的 C 代码反汇编可能就是四五条指令(甚至更多!)所以完全看完,很多时候不现实(除了本身业务代码很少,而且依赖的外部库代码又可以找到源码的情况)
所以除了有调试行号的情况之外,还可以这么做:

+ 我知道一点关于参数的信息,给参数起名字
+ 利用 r2 的自动分析寻找引用,给所有参数的引用传递性地起名字
+ 检查标记所有 call 的参数

+ 注意 r2 的控制流程图分析,注意 jump* 指令
+ 分析调用参数的时候熟悉一些模式,诸如哪些地址常放第几个参数,这个编译器经常怎么初始化参数,是 push 还是偏移量
+ 经常用鼠标划,找引用、重命名
+ 善于忽视:一些可能无关业务逻辑的代码,打个 flag 然后直接无视(比如 GCC 4.x 的动态栈帧大小 sub esp, * 分配,flexible array size)
+ 善于猜测,比如 printf* 函数的格式化字符串类型标记,比如某些函数原来可能是做什么的,可以少走不少弯路
+ 善于计算,诸如 struct 的 member 偏移量在尤其是 JNI 分析里有很大作用,有些清奇的编译器会用某些寄存器存储其他程序段的基址,知道一个对象的偏移要能根据 static dump 的结果算出另外一个的
+ 善于反查:数据反查逻辑是个好方法,很多时候文本搜索就够了
+ 善于寻找资料和工具:GItHub 是个好地方,很多时候被逆向的程序使用的库代码都能在上面 code search 出来
+ 善于模式识别:诸如 ary[i] 这种被翻译成 *(ary + i) 的表达式要能快速模式识别出来
+ 记忆类型:能记住本地变量和参数的类型
+ 看见其他层的东西:逆向工程必须要有基本的编程功底,能从高级代码里看到底层的逻辑、数据结构和更高层的东西,比如类型、组件、服务
不能像老谭的 C 教程一样,不要只知道『函数要先定义才能使用』、数组索引不能是负的,要看到里面的数据类型、数据长度、栈帧、分配、低地址高低址
这是逆向工程的基本,也只有有这种知识才在逆向工程中有进步的空间(因为很多时候,栈帧本地可能分配一些静态大小的数组,偏移量可能是要你自己去算的)

+ Skip 掉一些编译器生成的无关代码,比如 stack protector 的 prolog - epilog 始末码

现在逆向工程根本不赚什么钱,而且由于它的特殊性(比如对一些泛向 CS 知识的依赖),会逆向的基本都是些真正的爱好者(和计算机科学的也一样)
Happy reversing, have a good night :)
找了半天反汇编库不少,但有很多库根本没有能用的前端,这个也一样,得自己写代码来用他们,真的很难受

而且好像反汇编都比较难... 因为的确是有引用的问题存在,我再想想...